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
c00f9cdb
Commit
c00f9cdb
authored
Jan 27, 2026
by
Stefy Lanza (nextime / spora )
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Balabce globalized
parent
a1afa146
Changes
10
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
361 additions
and
139 deletions
+361
-139
client.py
mbetterclient/api_client/client.py
+143
-26
games_thread.py
mbetterclient/core/games_thread.py
+81
-64
match_timer.py
mbetterclient/core/match_timer.py
+7
-5
migrations.py
mbetterclient/database/migrations.py
+50
-0
models.py
mbetterclient/database/models.py
+1
-0
routes.py
mbetterclient/web_dashboard/routes.py
+70
-38
bet_details.html
...client/web_dashboard/templates/dashboard/bet_details.html
+3
-3
bets.html
mbetterclient/web_dashboard/templates/dashboard/bets.html
+2
-2
fixtures.html
...terclient/web_dashboard/templates/dashboard/fixtures.html
+2
-1
index.html
mbetterclient/web_dashboard/templates/dashboard/index.html
+2
-0
No files found.
mbetterclient/api_client/client.py
View file @
c00f9cdb
...
@@ -248,6 +248,15 @@ class UpdatesResponseHandler(ResponseHandler):
...
@@ -248,6 +248,15 @@ class UpdatesResponseHandler(ResponseHandler):
matches
=
fixture_data
.
get
(
'matches'
,
[])
matches
=
fixture_data
.
get
(
'matches'
,
[])
logger
.
debug
(
f
"Synchronizing fixture {fixture_id} with {len(matches)} matches"
)
logger
.
debug
(
f
"Synchronizing fixture {fixture_id} with {len(matches)} matches"
)
# Handle both list and dict formats for matches
if
isinstance
(
matches
,
dict
):
# Convert dict to list of match data
matches
=
[
match_data
for
match_data
in
matches
.
values
()]
logger
.
debug
(
f
"Converted matches dict to list with {len(matches)} items"
)
elif
not
isinstance
(
matches
,
list
):
logger
.
warning
(
f
"Unexpected matches format: {type(matches)}, skipping fixture {fixture_id}"
)
continue
for
match_data
in
matches
:
for
match_data
in
matches
:
match_number
=
match_data
.
get
(
'match_number'
,
'unknown'
)
match_number
=
match_data
.
get
(
'match_number'
,
'unknown'
)
try
:
try
:
...
@@ -282,6 +291,9 @@ class UpdatesResponseHandler(ResponseHandler):
...
@@ -282,6 +291,9 @@ class UpdatesResponseHandler(ResponseHandler):
logger
.
debug
(
f
"Committing {processed_data['synchronized_matches']} synchronized matches to database"
)
logger
.
debug
(
f
"Committing {processed_data['synchronized_matches']} synchronized matches to database"
)
session
.
commit
()
session
.
commit
()
logger
.
debug
(
"Database commit completed successfully"
)
logger
.
debug
(
"Database commit completed successfully"
)
# Clean up old fixture templates and keep only the new ones
self
.
_cleanup_old_fixture_templates
(
session
,
fixtures
)
finally
:
finally
:
session
.
close
()
session
.
close
()
...
@@ -314,22 +326,45 @@ class UpdatesResponseHandler(ResponseHandler):
...
@@ -314,22 +326,45 @@ class UpdatesResponseHandler(ResponseHandler):
match_data
[
'zip_url'
]
=
match_data
[
'zip_download_url'
]
match_data
[
'zip_url'
]
=
match_data
[
'zip_download_url'
]
logger
.
debug
(
f
"Found ZIP file to download: {zip_filename} for match {match_number} in fixture {fixture_id}"
)
logger
.
debug
(
f
"Found ZIP file to download: {zip_filename} for match {match_number} in fixture {fixture_id}"
)
# Send progress update - starting individual download
# Attempt download with retries
progress_percent
=
int
((
processed_data
[
'downloaded_zips'
]
/
max
(
1
,
processed_data
[
'expected_zips'
]))
*
100
)
download_success
=
False
self
.
_send_download_progress
(
processed_data
[
'downloaded_zips'
],
processed_data
[
'expected_zips'
],
max_retries
=
3
f
"Downloading {zip_filename}..."
)
for
attempt
in
range
(
max_retries
):
try
:
download_success
=
self
.
_download_zip_file
(
match_data
)
# Send progress update - starting/retrying download
if
download_success
:
attempt_desc
=
f
" (attempt {attempt + 1}/{max_retries})"
if
attempt
>
0
else
""
processed_data
[
'downloaded_zips'
]
+=
1
progress_percent
=
int
((
processed_data
[
'downloaded_zips'
]
/
max
(
1
,
processed_data
[
'expected_zips'
]))
*
100
)
logger
.
debug
(
f
"Successfully downloaded ZIP file: {zip_filename} for match {match_number}"
)
self
.
_send_download_progress
(
processed_data
[
'downloaded_zips'
],
processed_data
[
'expected_zips'
],
f
"Downloading {zip_filename}{attempt_desc}..."
)
# Send progress update - download completed
progress_percent
=
int
((
processed_data
[
'downloaded_zips'
]
/
max
(
1
,
processed_data
[
'expected_zips'
]))
*
100
)
download_success
=
self
.
_download_zip_file
(
match_data
)
self
.
_send_download_progress
(
processed_data
[
'downloaded_zips'
],
processed_data
[
'expected_zips'
],
if
download_success
:
f
"Downloaded {zip_filename}"
)
processed_data
[
'downloaded_zips'
]
+=
1
elif
'zip_download_url'
in
match_data
:
logger
.
debug
(
f
"Successfully downloaded ZIP file: {zip_filename} for match {match_number}"
)
logger
.
debug
(
f
"ZIP file download skipped or failed: {zip_filename} for match {match_number}"
)
# Send progress update - download completed
progress_percent
=
int
((
processed_data
[
'downloaded_zips'
]
/
max
(
1
,
processed_data
[
'expected_zips'
]))
*
100
)
self
.
_send_download_progress
(
processed_data
[
'downloaded_zips'
],
processed_data
[
'expected_zips'
],
f
"Downloaded {zip_filename}"
)
break
else
:
if
attempt
<
max_retries
-
1
:
logger
.
warning
(
f
"ZIP download attempt {attempt + 1} failed for {zip_filename}, retrying..."
)
# Small delay before retry
import
time
time
.
sleep
(
1
)
else
:
logger
.
error
(
f
"All {max_retries} download attempts failed for {zip_filename}"
)
except
Exception
as
retry_e
:
if
attempt
<
max_retries
-
1
:
logger
.
warning
(
f
"ZIP download attempt {attempt + 1} failed for {zip_filename}: {retry_e}, retrying..."
)
import
time
time
.
sleep
(
1
)
else
:
logger
.
error
(
f
"All {max_retries} download attempts failed for {zip_filename}: {retry_e}"
)
if
not
download_success
:
logger
.
debug
(
f
"ZIP file download failed after retries: {zip_filename} for match {match_number}"
)
except
requests
.
exceptions
.
HTTPError
as
http_err
:
except
requests
.
exceptions
.
HTTPError
as
http_err
:
# Check if this is a "fixture no longer available" error (404)
# Check if this is a "fixture no longer available" error (404)
...
@@ -423,14 +458,45 @@ class UpdatesResponseHandler(ResponseHandler):
...
@@ -423,14 +458,45 @@ class UpdatesResponseHandler(ResponseHandler):
# Remove existing outcomes
# Remove existing outcomes
session
.
query
(
MatchOutcomeTemplateModel
)
.
filter_by
(
match_id
=
match
.
id
)
.
delete
()
session
.
query
(
MatchOutcomeTemplateModel
)
.
filter_by
(
match_id
=
match
.
id
)
.
delete
()
# Normalize outcomes_data to dict format
normalized_outcomes
=
{}
try
:
if
isinstance
(
outcomes_data
,
dict
):
# Check if values are dicts with 'value' key or direct float values
for
key
,
value
in
outcomes_data
.
items
():
if
isinstance
(
value
,
dict
)
and
'value'
in
value
:
normalized_outcomes
[
key
]
=
value
[
'value'
]
elif
isinstance
(
value
,
(
int
,
float
)):
normalized_outcomes
[
key
]
=
value
else
:
logger
.
warning
(
f
"Unexpected outcome value type for {key}: {type(value)}, skipping"
)
continue
elif
isinstance
(
outcomes_data
,
list
):
# Handle list format [{"name": "WIN1", "value": 2.5}, ...]
for
item
in
outcomes_data
:
if
isinstance
(
item
,
dict
)
and
'name'
in
item
and
'value'
in
item
:
normalized_outcomes
[
item
[
'name'
]]
=
item
[
'value'
]
else
:
logger
.
warning
(
f
"Unexpected list item format: {item}, skipping"
)
continue
else
:
logger
.
warning
(
f
"Unexpected outcomes_data type: {type(outcomes_data)}, expected dict or list"
)
except
Exception
as
e
:
logger
.
error
(
f
"Failed to normalize outcomes_data: {e}"
)
# Continue without outcomes if normalization fails
# Add new outcomes
# Add new outcomes
for
column_name
,
float_value
in
outcomes_data
.
items
():
for
column_name
,
float_value
in
normalized_outcomes
.
items
():
outcome
=
MatchOutcomeTemplateModel
(
try
:
match_id
=
match
.
id
,
outcome
=
MatchOutcomeTemplateModel
(
column_name
=
column_name
,
match_id
=
match
.
id
,
float_value
=
float
(
float_value
)
column_name
=
str
(
column_name
),
)
float_value
=
float
(
float_value
)
session
.
add
(
outcome
)
)
session
.
add
(
outcome
)
except
(
ValueError
,
TypeError
)
as
e
:
logger
.
warning
(
f
"Failed to process outcome {column_name}: {float_value} - {e}"
)
continue
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"Failed to synchronize match template: {e}"
)
logger
.
error
(
f
"Failed to synchronize match template: {e}"
)
...
@@ -500,6 +566,23 @@ class UpdatesResponseHandler(ResponseHandler):
...
@@ -500,6 +566,23 @@ class UpdatesResponseHandler(ResponseHandler):
logger
.
debug
(
f
"Starting validation for downloaded ZIP file: {zip_filename}"
)
logger
.
debug
(
f
"Starting validation for downloaded ZIP file: {zip_filename}"
)
if
not
self
.
_validate_downloaded_zip
(
zip_path
,
match_data
):
if
not
self
.
_validate_downloaded_zip
(
zip_path
,
match_data
):
logger
.
error
(
f
"ZIP file validation failed: {zip_filename}"
)
logger
.
error
(
f
"ZIP file validation failed: {zip_filename}"
)
# Update database validation status to 'invalid'
try
:
session
=
self
.
db_manager
.
get_session
()
try
:
# Find the match template and update validation status
match_template
=
session
.
query
(
MatchTemplateModel
)
.
filter_by
(
zip_filename
=
zip_filename
)
.
first
()
if
match_template
:
match_template
.
zip_validation_status
=
'invalid'
session
.
commit
()
logger
.
debug
(
f
"Updated database validation status to 'invalid' for {zip_filename}"
)
finally
:
session
.
close
()
except
Exception
as
db_e
:
logger
.
warning
(
f
"Failed to update database validation status for {zip_filename}: {db_e}"
)
# Remove corrupted file
# Remove corrupted file
try
:
try
:
zip_path
.
unlink
()
zip_path
.
unlink
()
...
@@ -508,6 +591,43 @@ class UpdatesResponseHandler(ResponseHandler):
...
@@ -508,6 +591,43 @@ class UpdatesResponseHandler(ResponseHandler):
logger
.
warning
(
f
"Failed to remove corrupted ZIP file {zip_filename}: {cleanup_e}"
)
logger
.
warning
(
f
"Failed to remove corrupted ZIP file {zip_filename}: {cleanup_e}"
)
return
False
return
False
# Update database validation status to 'valid' after successful validation
try
:
session
=
self
.
db_manager
.
get_session
()
try
:
# Find the match template and update validation status
match_template
=
session
.
query
(
MatchTemplateModel
)
.
filter_by
(
zip_filename
=
zip_filename
)
.
first
()
if
match_template
:
match_template
.
zip_validation_status
=
'valid'
session
.
commit
()
logger
.
debug
(
f
"Updated database validation status to 'valid' for {zip_filename}"
)
finally
:
session
.
close
()
except
Exception
as
db_e
:
logger
.
warning
(
f
"Failed to update database validation status for {zip_filename}: {db_e}"
)
# Start detailed ZIP validation (checking for video files) asynchronously
logger
.
debug
(
f
"Starting detailed ZIP validation for {zip_filename}"
)
try
:
# Find the match template to get its ID for validation
session
=
self
.
db_manager
.
get_session
()
try
:
match_template
=
session
.
query
(
MatchTemplateModel
)
.
filter_by
(
zip_filename
=
zip_filename
)
.
first
()
if
match_template
:
# Start detailed validation asynchronously
self
.
_validate_single_zip_async
(
match_template
.
id
,
session
,
MatchTemplateModel
)
logger
.
debug
(
f
"Started detailed validation for match template {match_template.id}"
)
else
:
logger
.
warning
(
f
"Could not find match template for ZIP file {zip_filename} to start detailed validation"
)
finally
:
session
.
close
()
except
Exception
as
val_e
:
logger
.
warning
(
f
"Failed to start detailed ZIP validation for {zip_filename}: {val_e}"
)
logger
.
info
(
f
"Successfully downloaded and validated ZIP file: {zip_filename}"
)
logger
.
info
(
f
"Successfully downloaded and validated ZIP file: {zip_filename}"
)
return
True
return
True
...
@@ -1259,9 +1379,6 @@ class APIClient(ThreadedComponent):
...
@@ -1259,9 +1379,6 @@ class APIClient(ThreadedComponent):
logger
.
info
(
f
"Fixture update completed successfully - {synchronized_matches} matches synchronized, {downloaded_zips}/{expected_zips} ZIPs downloaded"
)
logger
.
info
(
f
"Fixture update completed successfully - {synchronized_matches} matches synchronized, {downloaded_zips}/{expected_zips} ZIPs downloaded"
)
logger
.
debug
(
"All expected ZIP files downloaded - fixtures are ready for games"
)
logger
.
debug
(
"All expected ZIP files downloaded - fixtures are ready for games"
)
# Clean up old fixture templates and keep only the new ones
self
.
_cleanup_old_fixture_templates
(
session
,
fixtures
)
# Send a message to trigger game start check
# Send a message to trigger game start check
game_start_check_message
=
Message
(
game_start_check_message
=
Message
(
type
=
MessageType
.
SYSTEM_STATUS
,
type
=
MessageType
.
SYSTEM_STATUS
,
...
...
mbetterclient/core/games_thread.py
View file @
c00f9cdb
...
@@ -6,7 +6,7 @@ import time
...
@@ -6,7 +6,7 @@ import time
import
json
import
json
import
logging
import
logging
import
threading
import
threading
from
datetime
import
datetime
,
timedelta
from
datetime
import
datetime
,
timedelta
,
date
from
typing
import
Optional
,
Dict
,
Any
,
List
from
typing
import
Optional
,
Dict
,
Any
,
List
from
.thread_manager
import
ThreadedComponent
from
.thread_manager
import
ThreadedComponent
...
@@ -35,26 +35,27 @@ class GamesThread(ThreadedComponent):
...
@@ -35,26 +35,27 @@ class GamesThread(ThreadedComponent):
return
get_today_venue_date
(
self
.
db_manager
)
return
get_today_venue_date
(
self
.
db_manager
)
def
_check_and_handle_day_change
(
self
)
->
bool
:
def
_check_and_handle_day_change
(
self
)
->
bool
:
"""Check if day has changed and handle cleanup/reset accordingly.
"""Check if day has changed and handle continuous flow.
Returns True if day change was detected and handled, False otherwise."""
Returns True if day change was detected, False otherwise.
Note: No database changes or game state reset - allows continuous flow."""
try
:
try
:
session
=
self
.
db_manager
.
get_session
()
session
=
self
.
db_manager
.
get_session
()
try
:
try
:
today
=
self
.
_get_today_venue_date
()
today
=
self
.
_get_today_venue_date
()
# Get the current fixture if any
# Get the current fixture if any
if
not
self
.
current_fixture_id
:
if
not
self
.
current_fixture_id
:
return
False
return
False
# Get matches for current fixture
# Get matches for current fixture
current_matches
=
session
.
query
(
MatchModel
)
.
filter
(
current_matches
=
session
.
query
(
MatchModel
)
.
filter
(
MatchModel
.
fixture_id
==
self
.
current_fixture_id
,
MatchModel
.
fixture_id
==
self
.
current_fixture_id
,
MatchModel
.
active_status
==
True
MatchModel
.
active_status
==
True
)
.
all
()
)
.
all
()
if
not
current_matches
:
if
not
current_matches
:
return
False
return
False
# Check if all matches in current fixture are from a previous day
# Check if all matches in current fixture are from a previous day
all_matches_old_day
=
True
all_matches_old_day
=
True
for
match
in
current_matches
:
for
match
in
current_matches
:
...
@@ -66,38 +67,18 @@ class GamesThread(ThreadedComponent):
...
@@ -66,38 +67,18 @@ class GamesThread(ThreadedComponent):
if
match_date
==
today
:
if
match_date
==
today
:
all_matches_old_day
=
False
all_matches_old_day
=
False
break
break
if
all_matches_old_day
:
if
all_matches_old_day
:
logger
.
info
(
f
"Day change detected! Current fixture {self.current_fixture_id} is from previous day"
)
logger
.
info
(
f
"Day change detected! Current fixture {self.current_fixture_id} is from previous day - allowing continuous flow"
)
# Note: No database changes or game state reset - system will continue with existing matches
# Cancel all pending/bet/scheduled matches in the old fixture
# and create new fixtures with new fixture_id when current fixture is exhausted
old_matches
=
session
.
query
(
MatchModel
)
.
filter
(
MatchModel
.
fixture_id
==
self
.
current_fixture_id
,
MatchModel
.
active_status
==
True
,
MatchModel
.
status
.
in_
([
'pending'
,
'scheduled'
,
'bet'
,
'ingame'
])
)
.
all
()
for
match
in
old_matches
:
logger
.
info
(
f
"Cancelling old match {match.match_number} due to day change: {match.fighter1_township} vs {match.fighter2_township}"
)
match
.
status
=
'cancelled'
# Cancel/refund associated bets
self
.
_cancel_match_bets
(
match
.
id
,
session
)
session
.
commit
()
logger
.
info
(
f
"Cancelled {len(old_matches)} old matches due to day change"
)
# Reset game state
self
.
game_active
=
False
self
.
current_fixture_id
=
None
logger
.
info
(
"Game state reset due to day change - will initialize new fixture"
)
return
True
return
True
return
False
return
False
finally
:
finally
:
session
.
close
()
session
.
close
()
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"Failed to check/handle day change: {e}"
)
logger
.
error
(
f
"Failed to check/handle day change: {e}"
)
return
False
return
False
...
@@ -416,8 +397,8 @@ class GamesThread(ThreadedComponent):
...
@@ -416,8 +397,8 @@ class GamesThread(ThreadedComponent):
self
.
_activate_fixture
(
new_fixture_id
,
message
)
self
.
_activate_fixture
(
new_fixture_id
,
message
)
return
return
else
:
else
:
logger
.
warning
(
"Could not create new fixture from templates -
discarding START_GAME message to allow retry when fixtures
become available"
)
logger
.
warning
(
"Could not create new fixture from templates -
waiting for templates to
become available"
)
# Do not send response - discard message so it will be retried when fixtures become available
self
.
_send_response
(
message
,
"waiting_for_downloads"
,
"Waiting for match templates to be downloaded and validated"
)
return
return
finally
:
finally
:
...
@@ -744,7 +725,23 @@ class GamesThread(ThreadedComponent):
...
@@ -744,7 +725,23 @@ class GamesThread(ThreadedComponent):
)
.
count
()
)
.
count
()
if
active_count
==
0
:
if
active_count
==
0
:
logger
.
info
(
f
"All matches completed for fixture {self.current_fixture_id} - creating new matches"
)
logger
.
info
(
f
"All matches completed for fixture {self.current_fixture_id} - checking day change for new fixture creation"
)
# Check if day has changed - if so, create new fixture with new fixture_id
day_changed
=
self
.
_check_and_handle_day_change
()
if
day_changed
:
logger
.
info
(
"Day change detected - creating new fixture with new fixture_id"
)
new_fixture_id
=
self
.
_initialize_new_fixture
()
if
new_fixture_id
:
logger
.
info
(
f
"Created new fixture {new_fixture_id} for new day - switching to it"
)
self
.
current_fixture_id
=
new_fixture_id
return
else
:
logger
.
warning
(
"Could not create new fixture for new day"
)
# Fall through to create matches in current fixture
# No day change or failed to create new fixture - create matches in current fixture
logger
.
info
(
f
"Creating new matches in current fixture {self.current_fixture_id}"
)
# First try: Create 5 new matches from match templates
# First try: Create 5 new matches from match templates
template_matches
=
self
.
_select_random_match_templates
(
5
,
session
)
template_matches
=
self
.
_select_random_match_templates
(
5
,
session
)
...
@@ -1348,6 +1345,25 @@ class GamesThread(ThreadedComponent):
...
@@ -1348,6 +1345,25 @@ class GamesThread(ThreadedComponent):
if
match
:
if
match
:
match
.
zip_validation_status
=
'invalid'
match
.
zip_validation_status
=
'invalid'
session
.
commit
()
session
.
commit
()
# If this is a MatchTemplateModel and validation failed, trigger re-download
if
model_class
==
MatchTemplateModel
and
hasattr
(
match
,
'zip_filename'
)
and
match
.
zip_filename
:
logger
.
info
(
f
"Detailed validation failed for {match.zip_filename}, triggering re-download"
)
# Reset status to allow re-download on next sync
match
.
zip_validation_status
=
'pending'
session
.
commit
()
# Remove the corrupted file to force re-download
try
:
from
..config.settings
import
get_user_data_dir
user_data_dir
=
get_user_data_dir
()
zip_path
=
user_data_dir
/
"zip_files"
/
match
.
zip_filename
if
zip_path
.
exists
():
zip_path
.
unlink
()
logger
.
debug
(
f
"Removed corrupted ZIP file {match.zip_filename} to allow re-download"
)
except
Exception
as
cleanup_e
:
logger
.
warning
(
f
"Failed to remove corrupted ZIP file {match.zip_filename}: {cleanup_e}"
)
session
.
close
()
session
.
close
()
except
Exception
as
update_e
:
except
Exception
as
update_e
:
logger
.
error
(
f
"Failed to update validation status after error: {update_e}"
)
logger
.
error
(
f
"Failed to update validation status after error: {update_e}"
)
...
@@ -2046,27 +2062,28 @@ class GamesThread(ThreadedComponent):
...
@@ -2046,27 +2062,28 @@ class GamesThread(ThreadedComponent):
logger
.
error
(
f
"Failed to get redistribution CAP: {e}"
)
logger
.
error
(
f
"Failed to get redistribution CAP: {e}"
)
return
70.0
return
70.0
def
_get_
daily_redistribution_adjustment
(
self
,
date
,
session
)
->
float
:
def
_get_
global_redistribution_adjustment
(
self
,
session
)
->
float
:
"""Get accumulated
redistribution adjustment (persistent across application restarts)
"""
"""Get accumulated
global redistribution adjustment
"""
try
:
try
:
# Get the most recent accumulated_shortfall value from any record
# Get the global redistribution adjustment record (fixed date 1970-01-01)
latest_record
=
session
.
query
(
PersistentRedistributionAdjustmentModel
)
.
order_by
(
global_date
=
date
(
1970
,
1
,
1
)
PersistentRedistributionAdjustmentModel
.
updated_at
.
desc
()
global_record
=
session
.
query
(
PersistentRedistributionAdjustmentModel
)
\
)
.
first
()
.
filter_by
(
date
=
global_date
)
\
.
first
()
if
latest
_record
:
if
global
_record
:
logger
.
debug
(
f
"Found
persistent redistribution adjustment: {latest
_record.accumulated_shortfall}"
)
logger
.
debug
(
f
"Found
global redistribution adjustment: {global
_record.accumulated_shortfall}"
)
return
latest
_record
.
accumulated_shortfall
return
global
_record
.
accumulated_shortfall
else
:
else
:
logger
.
debug
(
"No
redistribution adjustment records
found, returning 0.0"
)
logger
.
debug
(
"No
global redistribution adjustment record
found, returning 0.0"
)
return
0.0
return
0.0
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"Failed to get
persistent
redistribution adjustment: {e}"
)
logger
.
error
(
f
"Failed to get
global
redistribution adjustment: {e}"
)
return
0.0
return
0.0
def
_update_
daily_redistribution_adjustment
(
self
,
date
,
payin_amount
,
redistributed_amount
,
cap_percentage
,
session
):
def
_update_
global_redistribution_adjustment
(
self
,
payin_amount
,
redistributed_amount
,
cap_percentage
,
session
):
"""Update persistent redistribution adjustment tracking after extraction"""
"""Update persistent
global
redistribution adjustment tracking after extraction"""
try
:
try
:
# Calculate the redistribution adjustment for this extraction
# Calculate the redistribution adjustment for this extraction
# Positive: under-redistribution (shortfall), Negative: over-redistribution (surplus)
# Positive: under-redistribution (shortfall), Negative: over-redistribution (surplus)
...
@@ -2076,8 +2093,8 @@ class GamesThread(ThreadedComponent):
...
@@ -2076,8 +2093,8 @@ class GamesThread(ThreadedComponent):
logger
.
info
(
f
"💰 [ADJUSTMENT DEBUG] Payin: {payin_amount:.2f}, Expected: {expected_redistribution:.2f}, Redistributed: {redistributed_amount:.2f}, Adjustment: {adjustment:.2f}"
)
logger
.
info
(
f
"💰 [ADJUSTMENT DEBUG] Payin: {payin_amount:.2f}, Expected: {expected_redistribution:.2f}, Redistributed: {redistributed_amount:.2f}, Adjustment: {adjustment:.2f}"
)
# Use a fixed date for the global persistent record
# Use a fixed date for the global persistent record
global_date
=
date
time
.
date
(
1970
,
1
,
1
)
# Fixed date for global record
global_date
=
date
(
1970
,
1
,
1
)
# Fixed date for global record
# Get or create the global record
# Get or create the global record
adjustment_record
=
session
.
query
(
PersistentRedistributionAdjustmentModel
)
.
filter_by
(
adjustment_record
=
session
.
query
(
PersistentRedistributionAdjustmentModel
)
.
filter_by
(
date
=
global_date
date
=
global_date
...
@@ -2585,9 +2602,8 @@ class GamesThread(ThreadedComponent):
...
@@ -2585,9 +2602,8 @@ class GamesThread(ThreadedComponent):
# Get redistribution CAP
# Get redistribution CAP
cap_percentage
=
self
.
_get_redistribution_cap
()
cap_percentage
=
self
.
_get_redistribution_cap
()
# Get accumulated redistribution adjustment from previous extractions for today
# Get accumulated redistribution adjustment from previous extractions
today
=
self
.
_get_today_venue_date
()
accumulated_shortfall
=
self
.
_get_global_redistribution_adjustment
(
session
)
accumulated_shortfall
=
self
.
_get_daily_redistribution_adjustment
(
today
,
session
)
logger
.
info
(
f
"🎯 [EXTRACTION DEBUG] Accumulated redistribution adjustment: {accumulated_shortfall:.2f}"
)
logger
.
info
(
f
"🎯 [EXTRACTION DEBUG] Accumulated redistribution adjustment: {accumulated_shortfall:.2f}"
)
# Calculate base CAP threshold using ALL bets (UNDER/OVER + other bets)
# Calculate base CAP threshold using ALL bets (UNDER/OVER + other bets)
...
@@ -2665,10 +2681,9 @@ class GamesThread(ThreadedComponent):
...
@@ -2665,10 +2681,9 @@ class GamesThread(ThreadedComponent):
logger
.
info
(
f
"📈 [EXTRACTION DEBUG] Step 9: Collecting match statistics"
)
logger
.
info
(
f
"📈 [EXTRACTION DEBUG] Step 9: Collecting match statistics"
)
self
.
_collect_match_statistics
(
match_id
,
fixture_id
,
selected_result
,
session
)
self
.
_collect_match_statistics
(
match_id
,
fixture_id
,
selected_result
,
session
)
# Step 10: Update daily redistribution adjustment tracking
# Step 10: Update global redistribution adjustment tracking
logger
.
info
(
f
"💰 [EXTRACTION DEBUG] Step 10: Updating daily redistribution adjustment tracking"
)
logger
.
info
(
f
"💰 [EXTRACTION DEBUG] Step 10: Updating global redistribution adjustment tracking"
)
today
=
self
.
_get_today_venue_date
()
self
.
_update_global_redistribution_adjustment
(
total_payin_all_bets
,
payouts
[
selected_result
],
cap_percentage
,
session
)
self
.
_update_daily_redistribution_adjustment
(
today
,
total_payin_all_bets
,
payouts
[
selected_result
],
cap_percentage
,
session
)
logger
.
info
(
f
"✅ [EXTRACTION DEBUG] Result extraction completed successfully: selected {selected_result}"
)
logger
.
info
(
f
"✅ [EXTRACTION DEBUG] Result extraction completed successfully: selected {selected_result}"
)
...
@@ -3796,23 +3811,25 @@ class GamesThread(ThreadedComponent):
...
@@ -3796,23 +3811,25 @@ class GamesThread(ThreadedComponent):
return
[]
return
[]
def
_select_random_match_templates
(
self
,
count
:
int
,
session
)
->
List
[
MatchTemplateModel
]:
def
_select_random_match_templates
(
self
,
count
:
int
,
session
)
->
List
[
MatchTemplateModel
]:
"""Select random match templates from the database"""
"""Select random match templates from the database
that have validated ZIP files
"""
try
:
try
:
from
sqlalchemy.orm
import
joinedload
from
sqlalchemy.orm
import
joinedload
# Get all active match templates
# Get all active match templates
with validated ZIP files
match_templates
=
session
.
query
(
MatchTemplateModel
)
.
options
(
joinedload
(
MatchTemplateModel
.
outcomes
))
.
filter
(
match_templates
=
session
.
query
(
MatchTemplateModel
)
.
options
(
joinedload
(
MatchTemplateModel
.
outcomes
))
.
filter
(
MatchTemplateModel
.
active_status
==
True
MatchTemplateModel
.
active_status
==
True
,
MatchTemplateModel
.
zip_upload_status
==
'completed'
,
MatchTemplateModel
.
zip_validation_status
==
'valid'
)
.
all
()
)
.
all
()
if
len
(
match_templates
)
<
count
:
if
len
(
match_templates
)
<
count
:
logger
.
warning
(
f
"Only {len(match_templates)} match templates found, requested {count}"
)
logger
.
warning
(
f
"Only {len(match_templates)}
validated
match templates found, requested {count}"
)
return
match_templates
return
match_templates
# Select random templates
# Select random templates
import
random
import
random
selected_templates
=
random
.
sample
(
match_templates
,
count
)
selected_templates
=
random
.
sample
(
match_templates
,
count
)
logger
.
info
(
f
"Selected {len(selected_templates)} random match templates"
)
logger
.
info
(
f
"Selected {len(selected_templates)} random
validated
match templates"
)
return
selected_templates
return
selected_templates
except
Exception
as
e
:
except
Exception
as
e
:
...
...
mbetterclient/core/match_timer.py
View file @
c00f9cdb
...
@@ -745,23 +745,25 @@ class MatchTimerComponent(ThreadedComponent):
...
@@ -745,23 +745,25 @@ class MatchTimerComponent(ThreadedComponent):
return
'all_bets_on_start'
return
'all_bets_on_start'
def
_select_random_match_templates
(
self
,
count
:
int
,
session
)
->
List
[
Any
]:
def
_select_random_match_templates
(
self
,
count
:
int
,
session
)
->
List
[
Any
]:
"""Select random match templates from the database"""
"""Select random match templates from the database
that have validated ZIP files
"""
try
:
try
:
from
..database.models
import
MatchTemplateModel
from
..database.models
import
MatchTemplateModel
from
sqlalchemy.orm
import
joinedload
from
sqlalchemy.orm
import
joinedload
# Get all active match templates
# Get all active match templates
with validated ZIP files
match_templates
=
session
.
query
(
MatchTemplateModel
)
.
options
(
joinedload
(
MatchTemplateModel
.
outcomes
))
.
filter
(
match_templates
=
session
.
query
(
MatchTemplateModel
)
.
options
(
joinedload
(
MatchTemplateModel
.
outcomes
))
.
filter
(
MatchTemplateModel
.
active_status
==
True
MatchTemplateModel
.
active_status
==
True
,
MatchTemplateModel
.
zip_upload_status
==
'completed'
,
MatchTemplateModel
.
zip_validation_status
==
'valid'
)
.
all
()
)
.
all
()
if
len
(
match_templates
)
<
count
:
if
len
(
match_templates
)
<
count
:
logger
.
warning
(
f
"Only {len(match_templates)} match templates found, requested {count}"
)
logger
.
warning
(
f
"Only {len(match_templates)}
validated
match templates found, requested {count}"
)
return
match_templates
return
match_templates
# Select random templates
# Select random templates
selected_templates
=
random
.
sample
(
match_templates
,
count
)
selected_templates
=
random
.
sample
(
match_templates
,
count
)
logger
.
info
(
f
"Selected {len(selected_templates)} random match templates"
)
logger
.
info
(
f
"Selected {len(selected_templates)} random
validated
match templates"
)
return
selected_templates
return
selected_templates
except
Exception
as
e
:
except
Exception
as
e
:
...
...
mbetterclient/database/migrations.py
View file @
c00f9cdb
...
@@ -2890,6 +2890,55 @@ class Migration_038_AddWin1Win2Associations(DatabaseMigration):
...
@@ -2890,6 +2890,55 @@ class Migration_038_AddWin1Win2Associations(DatabaseMigration):
return
False
return
False
class
Migration_039_AddMatchNumberToBetDetails
(
DatabaseMigration
):
"""Add match_number column to bets_details table for storing match numbers directly"""
def
__init__
(
self
):
super
()
.
__init__
(
"039"
,
"Add match_number column to bets_details table"
)
def
up
(
self
,
db_manager
)
->
bool
:
"""Add match_number column to bets_details table"""
try
:
with
db_manager
.
engine
.
connect
()
as
conn
:
# Check if match_number column already exists
result
=
conn
.
execute
(
text
(
"PRAGMA table_info(bets_details)"
))
columns
=
[
row
[
1
]
for
row
in
result
.
fetchall
()]
if
'match_number'
not
in
columns
:
# Add match_number column
conn
.
execute
(
text
(
"""
ALTER TABLE bets_details
ADD COLUMN match_number INTEGER
"""
))
# Populate existing records with match numbers from matches table
conn
.
execute
(
text
(
"""
UPDATE bets_details
SET match_number = (
SELECT m.match_number
FROM matches m
WHERE m.id = bets_details.match_id
)
WHERE match_number IS NULL
"""
))
conn
.
commit
()
logger
.
info
(
"match_number column added to bets_details table"
)
else
:
logger
.
info
(
"match_number column already exists in bets_details table"
)
return
True
except
Exception
as
e
:
logger
.
error
(
f
"Failed to add match_number column to bets_details: {e}"
)
return
False
def
down
(
self
,
db_manager
)
->
bool
:
"""Remove match_number column - SQLite doesn't support DROP COLUMN easily"""
logger
.
warning
(
"SQLite doesn't support DROP COLUMN - match_number column will remain"
)
return
True
class
Migration_036_AddMatchTemplatesTables
(
DatabaseMigration
):
class
Migration_036_AddMatchTemplatesTables
(
DatabaseMigration
):
"""Add matches_templates and match_outcomes_templates tables for storing match templates"""
"""Add matches_templates and match_outcomes_templates tables for storing match templates"""
...
@@ -3047,6 +3096,7 @@ MIGRATIONS: List[DatabaseMigration] = [
...
@@ -3047,6 +3096,7 @@ MIGRATIONS: List[DatabaseMigration] = [
Migration_036_AddMatchTemplatesTables
(),
Migration_036_AddMatchTemplatesTables
(),
Migration_037_RenameDailyRedistributionShortfallTable
(),
Migration_037_RenameDailyRedistributionShortfallTable
(),
Migration_038_AddWin1Win2Associations
(),
Migration_038_AddWin1Win2Associations
(),
Migration_039_AddMatchNumberToBetDetails
(),
]
]
...
...
mbetterclient/database/models.py
View file @
c00f9cdb
...
@@ -772,6 +772,7 @@ class BetDetailModel(BaseModel):
...
@@ -772,6 +772,7 @@ class BetDetailModel(BaseModel):
bet_id
=
Column
(
String
(
1024
),
ForeignKey
(
'bets.uuid'
),
nullable
=
False
,
comment
=
'Foreign key to bets table uuid field'
)
bet_id
=
Column
(
String
(
1024
),
ForeignKey
(
'bets.uuid'
),
nullable
=
False
,
comment
=
'Foreign key to bets table uuid field'
)
match_id
=
Column
(
Integer
,
ForeignKey
(
'matches.id'
),
nullable
=
False
,
comment
=
'Foreign key to matches table'
)
match_id
=
Column
(
Integer
,
ForeignKey
(
'matches.id'
),
nullable
=
False
,
comment
=
'Foreign key to matches table'
)
match_number
=
Column
(
Integer
,
comment
=
'Match number for display purposes'
)
outcome
=
Column
(
String
(
255
),
nullable
=
False
,
comment
=
'Bet outcome/prediction'
)
outcome
=
Column
(
String
(
255
),
nullable
=
False
,
comment
=
'Bet outcome/prediction'
)
amount
=
Column
(
Float
(
precision
=
2
),
nullable
=
False
,
comment
=
'Bet amount with 2 decimal precision'
)
amount
=
Column
(
Float
(
precision
=
2
),
nullable
=
False
,
comment
=
'Bet amount with 2 decimal precision'
)
win_amount
=
Column
(
Float
(
precision
=
2
),
default
=
0.0
,
nullable
=
False
,
comment
=
'Winning amount (calculated when result is win)'
)
win_amount
=
Column
(
Float
(
precision
=
2
),
default
=
0.0
,
nullable
=
False
,
comment
=
'Winning amount (calculated when result is win)'
)
...
...
mbetterclient/web_dashboard/routes.py
View file @
c00f9cdb
...
@@ -2592,59 +2592,78 @@ def get_fixture_details(fixture_id):
...
@@ -2592,59 +2592,78 @@ def get_fixture_details(fixture_id):
@
get_api_auth_decorator
()
@
get_api_auth_decorator
()
@
get_api_auth_decorator
(
require_admin
=
True
)
@
get_api_auth_decorator
(
require_admin
=
True
)
def
reset_fixtures
():
def
reset_fixtures
():
"""Reset all fixtures data (admin only) - clear matches, match_outcomes, and ZIP files"""
"""Reset all fixtures data (admin only) - clear matches, match_outcomes,
matches_templates, match_outcomes_templates, bets, extraction_stats,
and ZIP files"""
try
:
try
:
from
..database.models
import
MatchModel
,
MatchOutcomeModel
from
..database.models
import
MatchModel
,
MatchOutcomeModel
,
MatchTemplateModel
,
MatchOutcomeTemplateModel
,
BetModel
,
BetDetailModel
,
ExtractionStatsModel
from
..config.settings
import
get_user_data_dir
from
..config.settings
import
get_user_data_dir
from
pathlib
import
Path
from
pathlib
import
Path
import
shutil
import
shutil
session
=
api_bp
.
db_manager
.
get_session
()
session
=
api_bp
.
db_manager
.
get_session
()
try
:
try
:
# Count existing data before reset
# Count existing data before reset
matches_count
=
session
.
query
(
MatchModel
)
.
count
()
matches_count
=
session
.
query
(
MatchModel
)
.
count
()
outcomes_count
=
session
.
query
(
MatchOutcomeModel
)
.
count
()
outcomes_count
=
session
.
query
(
MatchOutcomeModel
)
.
count
()
templates_count
=
session
.
query
(
MatchTemplateModel
)
.
count
()
# Clear all match outcomes first (due to foreign key constraints)
template_outcomes_count
=
session
.
query
(
MatchOutcomeTemplateModel
)
.
count
()
session
.
query
(
MatchOutcomeModel
)
.
delete
()
bets_count
=
session
.
query
(
BetModel
)
.
count
()
bet_details_count
=
session
.
query
(
BetDetailModel
)
.
count
()
extraction_stats_count
=
session
.
query
(
ExtractionStatsModel
)
.
count
()
# Delete in correct order to handle foreign key constraints
# 1. Delete extraction_stats first (references matches)
deleted_extraction_stats
=
session
.
query
(
ExtractionStatsModel
)
.
delete
()
session
.
commit
()
session
.
commit
()
#
Clear all matches
#
2. Delete bets (will cascade to bet_details due to CASCADE constraint)
session
.
query
(
Match
Model
)
.
delete
()
deleted_bets
=
session
.
query
(
Bet
Model
)
.
delete
()
session
.
commit
()
session
.
commit
()
# 3. Delete matches (will cascade to match_outcomes due to CASCADE constraint)
deleted_matches
=
session
.
query
(
MatchModel
)
.
delete
()
session
.
commit
()
# 4. Delete match templates (will cascade to match_outcomes_templates due to CASCADE constraint)
deleted_templates
=
session
.
query
(
MatchTemplateModel
)
.
delete
()
session
.
commit
()
# Clear ZIP files from persistent storage
# Clear ZIP files from persistent storage
zip_storage_dir
=
Path
(
get_user_data_dir
())
/
"zip_files"
zip_storage_dir
=
Path
(
get_user_data_dir
())
/
"zip_files"
zip_files_removed
=
0
zip_files_removed
=
0
if
zip_storage_dir
.
exists
():
if
zip_storage_dir
.
exists
():
zip_files
=
list
(
zip_storage_dir
.
glob
(
"*.zip"
))
zip_files
=
list
(
zip_storage_dir
.
glob
(
"*.zip"
))
zip_files_removed
=
len
(
zip_files
)
zip_files_removed
=
len
(
zip_files
)
# Remove all ZIP files
# Remove all ZIP files
for
zip_file
in
zip_files
:
for
zip_file
in
zip_files
:
try
:
try
:
zip_file
.
unlink
()
zip_file
.
unlink
()
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
warning
(
f
"Failed to remove ZIP file {zip_file}: {e}"
)
logger
.
warning
(
f
"Failed to remove ZIP file {zip_file}: {e}"
)
logger
.
info
(
f
"Removed {zip_files_removed} ZIP files from {zip_storage_dir}"
)
logger
.
info
(
f
"Removed {zip_files_removed} ZIP files from {zip_storage_dir}"
)
logger
.
info
(
f
"Fixtures reset completed - Removed {matches_count} matches, {outcomes_count} outcomes, {zip_files_removed} ZIP files"
)
logger
.
info
(
f
"Fixtures reset completed - Removed {matches_count} matches, {outcomes_count} outcomes, {
templates_count} templates, {template_outcomes_count} template outcomes, {bets_count} bets, {bet_details_count} bet details, {extraction_stats_count} extraction stats, {
zip_files_removed} ZIP files"
)
return
jsonify
({
return
jsonify
({
"success"
:
True
,
"success"
:
True
,
"message"
:
"Fixtures data reset successfully"
,
"message"
:
"Fixtures data reset successfully"
,
"removed"
:
{
"removed"
:
{
"matches"
:
matches_count
,
"matches"
:
matches_count
,
"outcomes"
:
outcomes_count
,
"outcomes"
:
outcomes_count
,
"templates"
:
templates_count
,
"template_outcomes"
:
template_outcomes_count
,
"bets"
:
bets_count
,
"bet_details"
:
bet_details_count
,
"extraction_stats"
:
extraction_stats_count
,
"zip_files"
:
zip_files_removed
"zip_files"
:
zip_files_removed
}
}
})
})
finally
:
finally
:
session
.
close
()
session
.
close
()
except
Exception
as
e
:
except
Exception
as
e
:
logger
.
error
(
f
"API fixtures reset error: {e}"
)
logger
.
error
(
f
"API fixtures reset error: {e}"
)
return
jsonify
({
"error"
:
str
(
e
)}),
500
return
jsonify
({
"error"
:
str
(
e
)}),
500
...
@@ -4120,19 +4139,20 @@ def get_redistribution_balance():
...
@@ -4120,19 +4139,20 @@ def get_redistribution_balance():
session
=
api_bp
.
db_manager
.
get_session
()
session
=
api_bp
.
db_manager
.
get_session
()
try
:
try
:
# Get the latest redistribution adjustment record
# Get the global redistribution adjustment record (fixed date 1970-01-01)
latest_record
=
session
.
query
(
PersistentRedistributionAdjustmentModel
)
\
global_date
=
date
(
1970
,
1
,
1
)
.
order_by
(
PersistentRedistributionAdjustmentModel
.
date
.
desc
())
\
global_record
=
session
.
query
(
PersistentRedistributionAdjustmentModel
)
\
.
filter_by
(
date
=
global_date
)
\
.
first
()
.
first
()
current_balance
=
0.0
current_balance
=
0.0
if
latest
_record
:
if
global
_record
:
current_balance
=
float
(
latest
_record
.
accumulated_shortfall
)
current_balance
=
float
(
global
_record
.
accumulated_shortfall
)
return
jsonify
({
return
jsonify
({
"success"
:
True
,
"success"
:
True
,
"redistribution_balance"
:
current_balance
,
"redistribution_balance"
:
current_balance
,
"last_updated"
:
latest_record
.
date
.
isoformat
()
if
latest
_record
else
None
"last_updated"
:
global_record
.
updated_at
.
isoformat
()
if
global
_record
else
None
})
})
finally
:
finally
:
...
@@ -4154,19 +4174,20 @@ def reset_redistribution_balance():
...
@@ -4154,19 +4174,20 @@ def reset_redistribution_balance():
session
=
api_bp
.
db_manager
.
get_session
()
session
=
api_bp
.
db_manager
.
get_session
()
try
:
try
:
# Get the latest redistribution adjustment record
# Get the global redistribution adjustment record (fixed date 1970-01-01)
latest_record
=
session
.
query
(
PersistentRedistributionAdjustmentModel
)
\
global_date
=
date
(
1970
,
1
,
1
)
.
order_by
(
PersistentRedistributionAdjustmentModel
.
date
.
desc
())
\
global_record
=
session
.
query
(
PersistentRedistributionAdjustmentModel
)
\
.
filter_by
(
date
=
global_date
)
\
.
first
()
.
first
()
if
latest
_record
:
if
global
_record
:
# Reset the accumulated shortfall to zero
# Reset the accumulated shortfall to zero
old_balance
=
float
(
latest
_record
.
accumulated_shortfall
)
old_balance
=
float
(
global
_record
.
accumulated_shortfall
)
latest
_record
.
accumulated_shortfall
=
0.0
global
_record
.
accumulated_shortfall
=
0.0
latest
_record
.
updated_at
=
datetime
.
utcnow
()
global
_record
.
updated_at
=
datetime
.
utcnow
()
session
.
commit
()
session
.
commit
()
logger
.
info
(
f
"
R
edistribution balance reset from {old_balance} to 0.0"
)
logger
.
info
(
f
"
Global r
edistribution balance reset from {old_balance} to 0.0"
)
return
jsonify
({
return
jsonify
({
"success"
:
True
,
"success"
:
True
,
...
@@ -4175,21 +4196,20 @@ def reset_redistribution_balance():
...
@@ -4175,21 +4196,20 @@ def reset_redistribution_balance():
"new_balance"
:
0.0
"new_balance"
:
0.0
})
})
else
:
else
:
# No record exists, create one with zero balance
# No global record exists, create one with zero balance
today
=
date
.
today
()
new_record
=
PersistentRedistributionAdjustmentModel
(
new_record
=
PersistentRedistributionAdjustmentModel
(
date
=
today
,
date
=
global_date
,
accumulated_shortfall
=
0.0
,
accumulated_shortfall
=
0.0
,
cap_percentage
=
70.0
# Default cap
cap_percentage
=
70.0
# Default cap
)
)
session
.
add
(
new_record
)
session
.
add
(
new_record
)
session
.
commit
()
session
.
commit
()
logger
.
info
(
"Created
new
redistribution adjustment record with zero balance"
)
logger
.
info
(
"Created
global
redistribution adjustment record with zero balance"
)
return
jsonify
({
return
jsonify
({
"success"
:
True
,
"success"
:
True
,
"message"
:
"Redistribution balance set to 0.00 (
new
record created)"
,
"message"
:
"Redistribution balance set to 0.00 (
global
record created)"
,
"old_balance"
:
None
,
"old_balance"
:
None
,
"new_balance"
:
0.0
"new_balance"
:
0.0
})
})
...
@@ -4626,6 +4646,8 @@ def create_cashier_bet():
...
@@ -4626,6 +4646,8 @@ def create_cashier_bet():
existing_match
=
session
.
query
(
MatchModel
)
.
filter_by
(
id
=
match_id
)
.
first
()
existing_match
=
session
.
query
(
MatchModel
)
.
filter_by
(
id
=
match_id
)
.
first
()
if
not
existing_match
:
if
not
existing_match
:
return
jsonify
({
"error"
:
f
"Match {match_id} not found"
}),
404
return
jsonify
({
"error"
:
f
"Match {match_id} not found"
}),
404
# Store match_number for later use
detail
[
'_match_number'
]
=
existing_match
.
match_number
# Generate UUID for the bet
# Generate UUID for the bet
bet_uuid
=
str
(
uuid_lib
.
uuid4
())
bet_uuid
=
str
(
uuid_lib
.
uuid4
())
...
@@ -4702,6 +4724,7 @@ def create_cashier_bet():
...
@@ -4702,6 +4724,7 @@ def create_cashier_bet():
bet_detail
=
BetDetailModel
(
bet_detail
=
BetDetailModel
(
bet_id
=
bet_uuid
,
bet_id
=
bet_uuid
,
match_id
=
detail_data
[
'match_id'
],
match_id
=
detail_data
[
'match_id'
],
match_number
=
detail_data
[
'_match_number'
],
outcome
=
detail_data
[
'outcome'
],
outcome
=
detail_data
[
'outcome'
],
amount
=
float
(
detail_data
[
'amount'
]),
amount
=
float
(
detail_data
[
'amount'
]),
result
=
'pending'
result
=
'pending'
...
@@ -4773,7 +4796,10 @@ def get_cashier_bet_details(bet_id):
...
@@ -4773,7 +4796,10 @@ def get_cashier_bet_details(bet_id):
for
detail
in
bet_details
:
for
detail
in
bet_details
:
detail_data
=
detail
.
to_dict
()
detail_data
=
detail
.
to_dict
()
# Include stored match_number
detail_data
[
'match_number'
]
=
detail
.
match_number
# Get match information
# Get match information
match
=
session
.
query
(
MatchModel
)
.
filter_by
(
id
=
detail
.
match_id
)
.
first
()
match
=
session
.
query
(
MatchModel
)
.
filter_by
(
id
=
detail
.
match_id
)
.
first
()
if
match
:
if
match
:
...
@@ -5179,6 +5205,9 @@ def verify_bet_details(bet_id):
...
@@ -5179,6 +5205,9 @@ def verify_bet_details(bet_id):
for
detail
in
bet_details
:
for
detail
in
bet_details
:
detail_data
=
detail
.
to_dict
()
detail_data
=
detail
.
to_dict
()
# Include stored match_number
detail_data
[
'match_number'
]
=
detail
.
match_number
# Get match information
# Get match information
match
=
session
.
query
(
MatchModel
)
.
filter_by
(
id
=
detail
.
match_id
)
.
first
()
match
=
session
.
query
(
MatchModel
)
.
filter_by
(
id
=
detail
.
match_id
)
.
first
()
if
match
:
if
match
:
...
@@ -5255,6 +5284,9 @@ def verify_barcode():
...
@@ -5255,6 +5284,9 @@ def verify_barcode():
detail_data
=
detail
.
to_dict
()
detail_data
=
detail
.
to_dict
()
total_amount
+=
float
(
detail
.
amount
)
total_amount
+=
float
(
detail
.
amount
)
# Include stored match_number
detail_data
[
'match_number'
]
=
detail
.
match_number
# Get match information
# Get match information
match
=
session
.
query
(
MatchModel
)
.
filter_by
(
id
=
detail
.
match_id
)
.
first
()
match
=
session
.
query
(
MatchModel
)
.
filter_by
(
id
=
detail
.
match_id
)
.
first
()
if
match
:
if
match
:
...
...
mbetterclient/web_dashboard/templates/dashboard/bet_details.html
View file @
c00f9cdb
...
@@ -89,9 +89,9 @@
...
@@ -89,9 +89,9 @@
{% for detail in bet.bet_details %}
{% for detail in bet.bet_details %}
<tr>
<tr>
<td>
<td>
<strong>
Match #{{ detail.match
.match
_number }}
</strong><br>
<strong>
Match #{{ detail.match_number }}
</strong><br>
<small
class=
"text-muted"
>
<small
class=
"text-muted"
>
{{ detail.match.fighter1_township
}} vs {{ detail.match.fighter2_township
}}
{{ detail.match.fighter1_township
if detail.match else 'Unknown' }} vs {{ detail.match.fighter2_township if detail.match else 'Unknown'
}}
</small>
</small>
</td>
</td>
<td>
<td>
...
@@ -450,7 +450,7 @@
...
@@ -450,7 +450,7 @@
"bet_details"
:
[
"bet_details"
:
[
{
%
for
detail
in
bet
.
bet_details
%
}
{
%
for
detail
in
bet
.
bet_details
%
}
{
{
"match_number"
:
"{{ detail.match
.match_number if detail.match else 'Unknown'
}}"
,
"match_number"
:
"{{ detail.match
_number
}}"
,
"fighter1"
:
"{{ detail.match.fighter1_township if detail.match else 'Unknown' }}"
,
"fighter1"
:
"{{ detail.match.fighter1_township if detail.match else 'Unknown' }}"
,
"fighter2"
:
"{{ detail.match.fighter2_township if detail.match else 'Unknown' }}"
,
"fighter2"
:
"{{ detail.match.fighter2_township if detail.match else 'Unknown' }}"
,
"venue"
:
"{{ detail.match.venue_kampala_township if detail.match else 'Unknown' }}"
,
"venue"
:
"{{ detail.match.venue_kampala_township if detail.match else 'Unknown' }}"
,
...
...
mbetterclient/web_dashboard/templates/dashboard/bets.html
View file @
c00f9cdb
...
@@ -533,7 +533,7 @@ function updateBetsTable(data, container) {
...
@@ -533,7 +533,7 @@ function updateBetsTable(data, container) {
const
totalAmount
=
parseFloat
(
bet
.
total_amount
).
toFixed
(
2
);
const
totalAmount
=
parseFloat
(
bet
.
total_amount
).
toFixed
(
2
);
// Collect unique match numbers
// Collect unique match numbers
const
matchNumbers
=
[...
new
Set
(
bet
.
details
?
bet
.
details
.
map
(
detail
=>
detail
.
match
?
detail
.
match
.
match_number
:
'Unknown'
).
filter
(
n
=>
n
!==
'Unknown'
)
:
[])];
const
matchNumbers
=
[...
new
Set
(
bet
.
details
?
bet
.
details
.
map
(
detail
=>
detail
.
match
_number
||
'Unknown'
).
filter
(
n
=>
n
!==
'Unknown'
)
:
[])];
// Determine overall bet status based on details
// Determine overall bet status based on details
let
overallStatus
=
'pending'
;
let
overallStatus
=
'pending'
;
...
@@ -719,7 +719,7 @@ function transformBetDataForReceipt(betData) {
...
@@ -719,7 +719,7 @@ function transformBetDataForReceipt(betData) {
total_amount
:
betData
.
total_amount
,
total_amount
:
betData
.
total_amount
,
bet_count
:
betData
.
details_count
||
betData
.
details
.
length
,
bet_count
:
betData
.
details_count
||
betData
.
details
.
length
,
bet_details
:
betData
.
details
.
map
(
detail
=>
({
bet_details
:
betData
.
details
.
map
(
detail
=>
({
match_number
:
detail
.
match
?
detail
.
match
.
match_number
:
'Unknown'
,
match_number
:
detail
.
match
_number
||
'Unknown'
,
fighter1
:
detail
.
match
?
detail
.
match
.
fighter1_township
:
'Unknown'
,
fighter1
:
detail
.
match
?
detail
.
match
.
fighter1_township
:
'Unknown'
,
fighter2
:
detail
.
match
?
detail
.
match
.
fighter2_township
:
'Unknown'
,
fighter2
:
detail
.
match
?
detail
.
match
.
fighter2_township
:
'Unknown'
,
venue
:
detail
.
match
?
detail
.
match
.
venue_kampala_township
:
'Unknown'
,
venue
:
detail
.
match
?
detail
.
match
.
venue_kampala_township
:
'Unknown'
,
...
...
mbetterclient/web_dashboard/templates/dashboard/fixtures.html
View file @
c00f9cdb
...
@@ -477,6 +477,7 @@ function getUploadStatusBadge(fixture) {
...
@@ -477,6 +477,7 @@ function getUploadStatusBadge(fixture) {
function
resetFixtures
()
{
function
resetFixtures
()
{
const
confirmMessage
=
'WARNING: This will permanently delete ALL fixture data including:
\
n
\
n'
+
const
confirmMessage
=
'WARNING: This will permanently delete ALL fixture data including:
\
n
\
n'
+
'• All synchronized matches and outcomes
\
n'
+
'• All synchronized matches and outcomes
\
n'
+
'• All match templates and template outcomes
\
n'
+
'• All downloaded ZIP files
\
n'
+
'• All downloaded ZIP files
\
n'
+
'• This action cannot be undone!
\
n
\
n'
+
'• This action cannot be undone!
\
n
\
n'
+
'Are you sure you want to reset all fixtures data?'
;
'Are you sure you want to reset all fixtures data?'
;
...
@@ -500,7 +501,7 @@ function resetFixtures() {
...
@@ -500,7 +501,7 @@ function resetFixtures() {
.
then
(
response
=>
response
.
json
())
.
then
(
response
=>
response
.
json
())
.
then
(
data
=>
{
.
then
(
data
=>
{
if
(
data
.
success
)
{
if
(
data
.
success
)
{
alert
(
`Fixtures reset successfully!\n\nRemoved:\n•
${
data
.
removed
.
matches
}
matches\n•
${
data
.
removed
.
outcomes
}
outcomes\n•
${
data
.
removed
.
zip_files
}
ZIP files`
);
alert
(
`Fixtures reset successfully!\n\nRemoved:\n•
${
data
.
removed
.
matches
}
matches\n•
${
data
.
removed
.
outcomes
}
outcomes\n•
${
data
.
removed
.
templates
}
match templates\n•
${
data
.
removed
.
template_outcomes
}
template outcomes\n•
${
data
.
removed
.
zip_files
}
ZIP files`
);
// Reload fixtures to show empty state
// Reload fixtures to show empty state
loadFixtures
();
loadFixtures
();
}
else
{
}
else
{
...
...
mbetterclient/web_dashboard/templates/dashboard/index.html
View file @
c00f9cdb
...
@@ -499,6 +499,8 @@ document.addEventListener('DOMContentLoaded', function() {
...
@@ -499,6 +499,8 @@ document.addEventListener('DOMContentLoaded', function() {
// Load redistribution balance (admin only)
// Load redistribution balance (admin only)
if
(
document
.
getElementById
(
'redistribution-balance'
))
{
if
(
document
.
getElementById
(
'redistribution-balance'
))
{
loadRedistributionBalance
();
loadRedistributionBalance
();
// Periodic update of redistribution balance
setInterval
(
loadRedistributionBalance
,
5000
);
// Update every 5 seconds
}
}
// Quick action buttons
// Quick action buttons
...
...
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