Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Contribute to GitLab
Sign in
Toggle navigation
M
MBetterc
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Mbetter
MBetterc
Commits
03d2e1c8
Commit
03d2e1c8
authored
May 14, 2026
by
Your Name
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fix settlement and report sync accounting
parent
b57de374
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
205 additions
and
114 deletions
+205
-114
client.py
mbetterclient/api_client/client.py
+5
-4
games_thread.py
mbetterclient/core/games_thread.py
+68
-110
test_reports_sync_summary.py
test_reports_sync_summary.py
+48
-0
test_settlement_consistency.py
test_settlement_consistency.py
+84
-0
No files found.
mbetterclient/api_client/client.py
View file @
03d2e1c8
...
@@ -1746,6 +1746,7 @@ class ReportsSyncResponseHandler(ResponseHandler):
...
@@ -1746,6 +1746,7 @@ class ReportsSyncResponseHandler(ResponseHandler):
total_payin
=
0.0
total_payin
=
0.0
total_payout
=
0.0
total_payout
=
0.0
total_bets
=
0
total_bets
=
0
unique_match_ids
=
set
()
for
bet
in
bets
:
for
bet
in
bets
:
# Ensure bet_details are loaded (handle lazy loading)
# Ensure bet_details are loaded (handle lazy loading)
...
@@ -1756,15 +1757,15 @@ class ReportsSyncResponseHandler(ResponseHandler):
...
@@ -1756,15 +1757,15 @@ class ReportsSyncResponseHandler(ResponseHandler):
if
bet_details
:
if
bet_details
:
total_payin
+=
sum
(
d
.
amount
for
d
in
bet_details
)
total_payin
+=
sum
(
d
.
amount
for
d
in
bet_details
)
total_bets
+=
len
(
bet_details
)
total_bets
+=
len
(
bet_details
)
# Add win amounts for winning bets (result is 'win', not 'won')
total_payout
+=
sum
(
d
.
win_amount
for
d
in
bet_details
if
d
.
result
==
'win'
)
total_payout
+=
sum
(
d
.
win_amount
for
d
in
bet_details
if
d
.
result
==
'win'
)
unique_match_ids
.
update
(
d
.
match_id
for
d
in
bet_details
if
getattr
(
d
,
'match_id'
,
None
)
is
not
None
)
return
{
return
{
'total_payin'
:
float
(
total_payin
),
'total_payin'
:
float
(
total_payin
),
'total_payout'
:
float
(
total_payout
),
'total_payout'
:
float
(
total_payout
),
'net_profit'
:
float
(
total_payin
-
total_payout
),
'net_profit'
:
float
(
total_payin
-
total_payout
),
'total_bets'
:
total_bets
,
'total_bets'
:
total_bets
,
'total_matches'
:
len
(
stat
s
)
'total_matches'
:
len
(
unique_match_id
s
)
}
}
def
_calculate_backoff_time
(
self
,
retry_count
:
int
)
->
int
:
def
_calculate_backoff_time
(
self
,
retry_count
:
int
)
->
int
:
...
@@ -3183,4 +3184,4 @@ class APIClient(ThreadedComponent):
...
@@ -3183,4 +3184,4 @@ class APIClient(ThreadedComponent):
return
shlex
.
join
(
cmd_parts
)
return
shlex
.
join
(
cmd_parts
)
except
AttributeError
:
except
AttributeError
:
# Fallback for Python < 3.8
# Fallback for Python < 3.8
return
' '
.
join
(
shlex
.
quote
(
arg
)
for
arg
in
cmd_parts
)
return
' '
.
join
(
shlex
.
quote
(
arg
)
for
arg
in
cmd_parts
)
\ No newline at end of file
mbetterclient/core/games_thread.py
View file @
03d2e1c8
...
@@ -999,65 +999,75 @@ class GamesThread(ThreadedComponent):
...
@@ -999,65 +999,75 @@ class GamesThread(ThreadedComponent):
session: Database session
session: Database session
"""
"""
try
:
try
:
# Get all pending bets for this match
self
.
_settle_match_bets_from_match_state
(
match
,
session
,
pending_only
=
True
)
pending_bets
=
session
.
query
(
BetDetailModel
)
.
filter
(
except
Exception
as
e
:
BetDetailModel
.
match_id
==
match
.
id
,
logger
.
error
(
f
"Failed to resolve bets for match {match.id}: {e}"
)
BetDetailModel
.
result
==
'pending'
import
traceback
)
.
all
()
logger
.
error
(
f
"Traceback: {traceback.format_exc()}"
)
if
not
pending_bets
:
def
_settle_match_bets_from_match_state
(
self
,
match
:
MatchModel
,
session
,
pending_only
:
bool
=
True
):
logger
.
debug
(
f
"No pending bets to resolve for match {match.id}"
)
"""Canonical settlement for one completed match using stored match state.
return
This is the single recovery/finalization path used after the primary
logger
.
info
(
f
"🔍 Resolving {len(pending_bets)} pending bets for match {match.id}"
)
extraction flow has already persisted `match.result`, `match.winning_outcomes`,
and `match.under_over_result`.
# Determine winning outcomes
"""
try
:
query
=
session
.
query
(
BetDetailModel
)
.
filter
(
BetDetailModel
.
match_id
==
match
.
id
)
if
pending_only
:
query
=
query
.
filter
(
BetDetailModel
.
result
==
'pending'
)
else
:
query
=
query
.
filter
(
BetDetailModel
.
result
!=
'cancelled'
)
bet_rows
=
query
.
all
()
if
not
bet_rows
:
logger
.
debug
(
f
"No bet rows to settle for match {match.id}"
)
return
(
0
,
0
)
winning_outcomes
=
[]
winning_outcomes
=
[]
if
match
.
winning_outcomes
:
if
match
.
winning_outcomes
:
try
:
try
:
winning_outcomes
=
json
.
loads
(
match
.
winning_outcomes
)
if
isinstance
(
match
.
winning_outcomes
,
str
):
except
(
json
.
JSONDecodeError
,
TypeError
):
winning_outcomes
=
json
.
loads
(
match
.
winning_outcomes
)
pass
elif
isinstance
(
match
.
winning_outcomes
,
list
):
winning_outcomes
=
list
(
match
.
winning_outcomes
)
# Add under_over_result if set
except
(
json
.
JSONDecodeError
,
TypeError
)
as
exc
:
if
match
.
under_over_result
:
logger
.
warning
(
f
"Failed to parse winning_outcomes for match {match.id}: {exc}"
)
winning_outcomes
.
append
(
match
.
under_over_result
)
if
match
.
result
and
match
.
result
not
in
winning_outcomes
:
# Add main result if set
if
match
.
result
:
winning_outcomes
.
append
(
match
.
result
)
winning_outcomes
.
append
(
match
.
result
)
logger
.
info
(
f
"🔍 Winning outcomes for match {match.id}: {winning_outcomes}"
)
under_over_outcome
=
match
.
under_over_result
winning_set
=
{
outcome
for
outcome
in
winning_outcomes
if
outcome
and
outcome
not
in
[
'UNDER'
,
'OVER'
]}
logger
.
info
(
f
"🔍 Canonical settlement for match {match.id}: under_over_result={under_over_outcome}, "
f
"result={match.result}, result_winners={sorted(winning_set)}"
)
resolved_win
=
0
resolved_win
=
0
resolved_lost
=
0
resolved_lost
=
0
for
bet
in
bet_rows
:
for
bet
in
pending_bets
:
is_under_over_bet
=
bet
.
outcome
in
[
'UNDER'
,
'OVER'
]
if
bet
.
outcome
in
winning_outcomes
:
is_winner
=
(
bet
.
outcome
==
under_over_outcome
)
if
is_under_over_bet
else
(
bet
.
outcome
in
winning_set
)
# Winner
try
:
if
is_winner
:
coefficient
=
self
.
_get_outcome_coefficient_for_bet
(
match
.
id
,
bet
.
outcome
,
session
)
coefficient
=
self
.
_get_outcome_coefficient
(
match
.
id
,
bet
.
outcome
,
session
)
bet
.
result
=
'win'
bet
.
result
=
'win'
bet
.
win_amount
=
bet
.
amount
*
coefficient
bet
.
win_amount
=
bet
.
amount
*
coefficient
resolved_win
+=
1
resolved_win
+=
1
logger
.
info
(
f
"✅ Bet {bet.id} ({bet.outcome}) resolved as WIN with amount {bet.win_amount:.2f}"
)
logger
.
info
(
f
"✅ Settled bet {bet.id} ({bet.outcome}) as WIN with amount {bet.win_amount:.2f}"
)
except
Exception
as
coeff_e
:
logger
.
error
(
f
"Failed to get coefficient for {bet.outcome}: {coeff_e}"
)
bet
.
result
=
'win'
bet
.
win_amount
=
bet
.
amount
# Fallback to 1x coefficient
resolved_win
+=
1
else
:
else
:
# Loser
bet
.
result
=
'lost'
bet
.
result
=
'lost'
bet
.
win_amount
=
0.0
resolved_lost
+=
1
resolved_lost
+=
1
logger
.
info
(
f
"❌ Bet {bet.id} ({bet.outcome}) resolved as LOST"
)
logger
.
info
(
f
"❌ Settled bet {bet.id} ({bet.outcome}) as LOST"
)
logger
.
info
(
f
"✅ Resolved {len(pending_bets)} bets for match {match.id}: {resolved_win} wins, {resolved_lost} losses"
)
logger
.
info
(
f
"✅ Settled {len(bet_rows)} bets for match {match.id}: {resolved_win} wins, {resolved_lost} losses"
)
return
(
resolved_win
,
resolved_lost
)
except
Exception
as
e
:
except
Exception
as
exc
:
logger
.
error
(
f
"Failed to resolve bets for match {match.id}: {e}"
)
logger
.
error
(
f
"Failed canonical settlement for match {match.id}: {exc}"
)
import
traceback
raise
logger
.
error
(
f
"Traceback: {traceback.format_exc()}"
)
def
_get_outcome_coefficient_for_bet
(
self
,
match_id
:
int
,
outcome
:
str
,
session
)
->
float
:
def
_get_outcome_coefficient_for_bet
(
self
,
match_id
:
int
,
outcome
:
str
,
session
)
->
float
:
"""Get coefficient for a specific outcome from match outcomes.
"""Get coefficient for a specific outcome from match outcomes.
...
@@ -1197,17 +1207,16 @@ class GamesThread(ThreadedComponent):
...
@@ -1197,17 +1207,16 @@ class GamesThread(ThreadedComponent):
try
:
try
:
session
=
self
.
db_manager
.
get_session
()
session
=
self
.
db_manager
.
get_session
()
try
:
try
:
# Check for any remaining pending bets
pending_count
=
session
.
query
(
BetDetailModel
)
.
filter
(
pending_bets
=
session
.
query
(
BetDetailModel
)
.
filter
(
BetDetailModel
.
match_id
==
match_id
,
BetDetailModel
.
match_id
==
match_id
,
BetDetailModel
.
result
==
'pending'
BetDetailModel
.
result
==
'pending'
)
.
all
()
)
.
count
()
if
not
pending_
bets
:
if
not
pending_
count
:
logger
.
debug
(
f
"All bets already resolved for match {match_id}"
)
logger
.
debug
(
f
"All bets already resolved for match {match_id}"
)
return
return
logger
.
warning
(
f
"⚠️ [SAFETY NET] Found {
len(pending_bets)
} pending bets for completed match {match_id} - forcing resolution"
)
logger
.
warning
(
f
"⚠️ [SAFETY NET] Found {
pending_count
} pending bets for completed match {match_id} - forcing resolution"
)
# Get match details for resolution
# Get match details for resolution
match
=
session
.
query
(
MatchModel
)
.
filter_by
(
id
=
match_id
)
.
first
()
match
=
session
.
query
(
MatchModel
)
.
filter_by
(
id
=
match_id
)
.
first
()
...
@@ -1215,60 +1224,9 @@ class GamesThread(ThreadedComponent):
...
@@ -1215,60 +1224,9 @@ class GamesThread(ThreadedComponent):
logger
.
error
(
f
"[SAFETY NET] Match {match_id} not found - cannot resolve bets"
)
logger
.
error
(
f
"[SAFETY NET] Match {match_id} not found - cannot resolve bets"
)
return
return
# Parse winning outcomes from match
resolved_win
,
resolved_lost
=
self
.
_settle_match_bets_from_match_state
(
match
,
session
,
pending_only
=
True
)
winning_outcomes
=
[]
if
match
.
winning_outcomes
:
try
:
winning_outcomes
=
json
.
loads
(
match
.
winning_outcomes
)
logger
.
info
(
f
"[SAFETY NET] Match winning outcomes: {winning_outcomes}"
)
except
(
json
.
JSONDecodeError
,
TypeError
)
as
e
:
logger
.
warning
(
f
"[SAFETY NET] Failed to parse winning_outcomes: {e}"
)
# Get under/over result
under_over_result
=
match
.
under_over_result
if
match
else
None
logger
.
info
(
f
"[SAFETY NET] Match under_over_result: {under_over_result}"
)
# Resolve each pending bet
resolved_win
=
0
resolved_lost
=
0
for
bet
in
pending_bets
:
is_winner
=
False
# Check if bet outcome is in winning outcomes
if
bet
.
outcome
in
winning_outcomes
:
is_winner
=
True
logger
.
info
(
f
"[SAFETY NET] Bet {bet.id} ({bet.outcome}) matches winning outcome"
)
# Check if bet is the under/over winner
elif
bet
.
outcome
==
under_over_result
:
is_winner
=
True
logger
.
info
(
f
"[SAFETY NET] Bet {bet.id} ({bet.outcome}) matches under_over_result"
)
# Check if bet outcome matches the main result
elif
bet
.
outcome
==
result
:
is_winner
=
True
logger
.
info
(
f
"[SAFETY NET] Bet {bet.id} ({bet.outcome}) matches main result"
)
if
is_winner
:
# Winner - calculate win amount
try
:
coefficient
=
self
.
_get_outcome_coefficient
(
match_id
,
bet
.
outcome
,
session
)
bet
.
result
=
'win'
bet
.
win_amount
=
bet
.
amount
*
coefficient
resolved_win
+=
1
logger
.
info
(
f
"✅ [SAFETY NET] Force-resolved bet {bet.id} ({bet.outcome}) to WIN with amount {bet.win_amount:.2f}"
)
except
Exception
as
coeff_e
:
logger
.
error
(
f
"[SAFETY NET] Failed to get coefficient for {bet.outcome}: {coeff_e}"
)
bet
.
result
=
'win'
bet
.
win_amount
=
bet
.
amount
# Fallback to 1x coefficient
resolved_win
+=
1
else
:
# Loser
bet
.
result
=
'lost'
resolved_lost
+=
1
logger
.
info
(
f
"❌ [SAFETY NET] Force-resolved bet {bet.id} ({bet.outcome}) to LOST"
)
session
.
commit
()
session
.
commit
()
logger
.
info
(
f
"✅ [SAFETY NET] Force-resolved {
len(pending_bets)
} pending bets for match {match_id}: {resolved_win} wins, {resolved_lost} losses"
)
logger
.
info
(
f
"✅ [SAFETY NET] Force-resolved {
pending_count
} pending bets for match {match_id}: {resolved_win} wins, {resolved_lost} losses"
)
finally
:
finally
:
session
.
close
()
session
.
close
()
...
@@ -4205,8 +4163,8 @@ class GamesThread(ThreadedComponent):
...
@@ -4205,8 +4163,8 @@ class GamesThread(ThreadedComponent):
logger
.
warning
(
f
"Match {match_id} not found for statistics collection"
)
logger
.
warning
(
f
"Match {match_id} not found for statistics collection"
)
return
return
# Store the accumulated shortfall value a
t the time of match completion
# Store the accumulated shortfall value a
fter this match has already been applied
#
This historical value will be used in reports instead of the current global value
#
to the global persistent adjustment record.
accumulated_shortfall
=
self
.
_get_global_redistribution_adjustment
(
session
)
accumulated_shortfall
=
self
.
_get_global_redistribution_adjustment
(
session
)
match
.
accumulated_shortfall
=
accumulated_shortfall
match
.
accumulated_shortfall
=
accumulated_shortfall
logger
.
info
(
f
"💰 [SHORTFALL TRACKING] Stored accumulated shortfall {accumulated_shortfall:.2f} in match {match_id} at completion time"
)
logger
.
info
(
f
"💰 [SHORTFALL TRACKING] Stored accumulated shortfall {accumulated_shortfall:.2f} in match {match_id} at completion time"
)
...
...
test_reports_sync_summary.py
0 → 100644
View file @
03d2e1c8
from
pathlib
import
Path
from
types
import
SimpleNamespace
from
mbetterclient.api_client.client
import
ReportsSyncResponseHandler
from
mbetterclient.core.message_bus
import
MessageBus
from
mbetterclient.database.manager
import
DatabaseManager
from
mbetterclient.database.models
import
BetDetailModel
,
BetModel
def
_build_handler
(
tmp_path
:
Path
):
db_path
=
tmp_path
/
"reports_sync_summary.db"
db_manager
=
DatabaseManager
(
str
(
db_path
))
assert
db_manager
.
initialize
()
handler
=
ReportsSyncResponseHandler
(
db_manager
=
db_manager
,
user_data_dir
=
str
(
tmp_path
),
api_client
=
SimpleNamespace
(
settings
=
SimpleNamespace
(
rustdesk_id
=
None
)),
message_bus
=
MessageBus
(),
)
return
handler
,
db_manager
def
test_calculate_summary_uses_settled_rows_and_unique_matches
(
tmp_path
):
handler
,
db_manager
=
_build_handler
(
tmp_path
)
session
=
db_manager
.
get_session
()
try
:
bet
=
BetModel
(
uuid
=
'bet-1'
,
fixture_id
=
'fixture-1'
,
paid
=
True
,
paid_out
=
False
)
session
.
add
(
bet
)
session
.
flush
()
session
.
add_all
([
BetDetailModel
(
bet_id
=
'bet-1'
,
match_id
=
10
,
match_number
=
1
,
outcome
=
'UNDER'
,
amount
=
10.0
,
win_amount
=
20.0
,
result
=
'win'
),
BetDetailModel
(
bet_id
=
'bet-1'
,
match_id
=
10
,
match_number
=
1
,
outcome
=
'OVER'
,
amount
=
15.0
,
win_amount
=
0.0
,
result
=
'lost'
),
BetDetailModel
(
bet_id
=
'bet-1'
,
match_id
=
11
,
match_number
=
2
,
outcome
=
'WIN1'
,
amount
=
25.0
,
win_amount
=
75.0
,
result
=
'win'
),
BetDetailModel
(
bet_id
=
'bet-1'
,
match_id
=
11
,
match_number
=
2
,
outcome
=
'KO1'
,
amount
=
5.0
,
win_amount
=
0.0
,
result
=
'cancelled'
),
])
session
.
commit
()
loaded_bet
=
session
.
query
(
BetModel
)
.
filter_by
(
uuid
=
'bet-1'
)
.
first
()
summary
=
handler
.
_calculate_summary
([
loaded_bet
],
stats
=
[])
assert
summary
[
'total_payin'
]
==
50.0
assert
summary
[
'total_payout'
]
==
95.0
assert
summary
[
'net_profit'
]
==
-
45.0
assert
summary
[
'total_bets'
]
==
3
assert
summary
[
'total_matches'
]
==
2
finally
:
session
.
close
()
db_manager
.
close
()
test_settlement_consistency.py
0 → 100644
View file @
03d2e1c8
from
pathlib
import
Path
from
mbetterclient.core.games_thread
import
GamesThread
from
mbetterclient.core.message_bus
import
MessageBus
from
mbetterclient.database.manager
import
DatabaseManager
from
mbetterclient.database.models
import
BetDetailModel
,
MatchModel
,
MatchOutcomeModel
def
_build_games_thread
(
tmp_path
:
Path
):
db_path
=
tmp_path
/
"settlement_consistency.db"
db_manager
=
DatabaseManager
(
str
(
db_path
))
assert
db_manager
.
initialize
()
message_bus
=
MessageBus
()
games_thread
=
GamesThread
(
"test_games_thread"
,
message_bus
,
db_manager
)
return
games_thread
,
db_manager
def
_seed_match
(
session
):
match
=
MatchModel
(
match_number
=
1
,
fighter1_township
=
'A'
,
fighter2_township
=
'B'
,
venue_kampala_township
=
'Kampala'
,
filename
=
'fixture.txt'
,
file_sha1sum
=
'fixture-sha1'
,
fixture_id
=
'fixture-1'
,
active_status
=
True
,
status
=
'done'
,
result
=
'WIN1'
,
under_over_result
=
'UNDER'
,
winning_outcomes
=
'["WIN1", "KO1"]'
,
)
session
.
add
(
match
)
session
.
flush
()
session
.
add_all
([
MatchOutcomeModel
(
match_id
=
match
.
id
,
column_name
=
'UNDER'
,
float_value
=
2.0
),
MatchOutcomeModel
(
match_id
=
match
.
id
,
column_name
=
'OVER'
,
float_value
=
3.0
),
MatchOutcomeModel
(
match_id
=
match
.
id
,
column_name
=
'WIN1'
,
float_value
=
4.0
),
MatchOutcomeModel
(
match_id
=
match
.
id
,
column_name
=
'KO1'
,
float_value
=
5.0
),
MatchOutcomeModel
(
match_id
=
match
.
id
,
column_name
=
'WIN2'
,
float_value
=
6.0
),
])
session
.
add_all
([
BetDetailModel
(
bet_id
=
'bet-under'
,
match_id
=
match
.
id
,
match_number
=
1
,
outcome
=
'UNDER'
,
amount
=
10.0
,
result
=
'pending'
),
BetDetailModel
(
bet_id
=
'bet-over'
,
match_id
=
match
.
id
,
match_number
=
1
,
outcome
=
'OVER'
,
amount
=
11.0
,
result
=
'pending'
),
BetDetailModel
(
bet_id
=
'bet-win1'
,
match_id
=
match
.
id
,
match_number
=
1
,
outcome
=
'WIN1'
,
amount
=
12.0
,
result
=
'pending'
),
BetDetailModel
(
bet_id
=
'bet-ko1'
,
match_id
=
match
.
id
,
match_number
=
1
,
outcome
=
'KO1'
,
amount
=
13.0
,
result
=
'pending'
),
BetDetailModel
(
bet_id
=
'bet-win2'
,
match_id
=
match
.
id
,
match_number
=
1
,
outcome
=
'WIN2'
,
amount
=
14.0
,
result
=
'pending'
),
])
session
.
commit
()
return
match
.
id
def
test_ensure_all_bets_resolved_settles_two_extraction_channels
(
tmp_path
):
games_thread
,
db_manager
=
_build_games_thread
(
tmp_path
)
session
=
db_manager
.
get_session
()
try
:
match_id
=
_seed_match
(
session
)
finally
:
session
.
close
()
games_thread
.
_ensure_all_bets_resolved
(
match_id
,
'WIN1'
)
session
=
db_manager
.
get_session
()
try
:
rows
=
{
row
.
outcome
:
row
for
row
in
session
.
query
(
BetDetailModel
)
.
filter_by
(
match_id
=
match_id
)
.
all
()
}
assert
rows
[
'UNDER'
]
.
result
==
'win'
assert
rows
[
'UNDER'
]
.
win_amount
==
20.0
assert
rows
[
'OVER'
]
.
result
==
'lost'
assert
rows
[
'OVER'
]
.
win_amount
==
0.0
assert
rows
[
'WIN1'
]
.
result
==
'win'
assert
rows
[
'WIN1'
]
.
win_amount
==
48.0
assert
rows
[
'KO1'
]
.
result
==
'win'
assert
rows
[
'KO1'
]
.
win_amount
==
65.0
assert
rows
[
'WIN2'
]
.
result
==
'lost'
assert
rows
[
'WIN2'
]
.
win_amount
==
0.0
assert
session
.
query
(
BetDetailModel
)
.
filter_by
(
match_id
=
match_id
,
result
=
'pending'
)
.
count
()
==
0
finally
:
session
.
close
()
db_manager
.
close
()
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment