Commit 32d6926c authored by Your Name's avatar Your Name

Align report views with settled bets

parent 03d2e1c8
......@@ -1120,8 +1120,7 @@ class ReportsSyncResponseHandler(ResponseHandler):
stats_to_sync.append(stat)
logger.debug(f"Extraction stat {stat.match_id} is new, including in sync")
# Get cap compensation balance (accumulated shortfall)
# Use the global record (date 1970-01-01) to match dashboard display
# Get global redistribution carryover separately from day totals.
cap_compensation_balance = 0.0
try:
from datetime import date
......@@ -1155,10 +1154,12 @@ class ReportsSyncResponseHandler(ResponseHandler):
'bets': [],
'extraction_stats': [],
'cap_compensation_balance': cap_compensation_balance,
'global_carryover_balance': cap_compensation_balance,
'summary': self._calculate_summary(bets_to_sync, stats), # Use bets_to_sync which have bet_details loaded
'is_incremental': True, # Flag to indicate this is an incremental sync
'sync_type': 'incremental' if last_sync_time else 'full'
}
report_data['summary']['global_carryover_balance'] = float(cap_compensation_balance)
# Add bets data (only new/updated bets)
for bet in bets_to_sync:
......
......@@ -88,6 +88,17 @@ def is_bet_detail_winning(detail, match, session):
return False
def is_settled_bet_detail_winning(detail, match, session):
"""Prefer settled row status for completed matches, fallback to inferred logic otherwise."""
if detail.result == 'win':
return True
if detail.result in ['lost', 'cancelled']:
return False
if match and getattr(match, 'status', None) == 'done':
return False
return is_bet_detail_winning(detail, match, session)
def get_api_auth_decorator(require_admin=False):
"""Get API auth decorator that works with lazy initialization"""
def decorator(func):
......@@ -448,7 +459,7 @@ def bet_details(bet_id):
if detail.result == 'pending':
results['pending'] += 1
has_pending = True
elif is_bet_detail_winning(detail, match, session):
elif is_settled_bet_detail_winning(detail, match, session):
results['won'] += 1
results['winnings'] += float(detail.amount) * float(odds) # Use actual odds
elif detail.result == 'lost':
......@@ -1112,7 +1123,7 @@ def cashier_bet_details(bet_id):
if detail.result == 'pending':
results['pending'] += 1
has_pending = True
elif is_bet_detail_winning(detail, match, session):
elif is_settled_bet_detail_winning(detail, match, session):
results['won'] += 1
results['winnings'] += float(detail.win_amount or 0) # Use actual win amount
elif detail.result == 'lost':
......@@ -6579,7 +6590,7 @@ def get_cashier_bets():
# Get match information for winning check
match = session.query(MatchModel).filter_by(id=detail.match_id).first()
if is_bet_detail_winning(detail, match, session):
if is_settled_bet_detail_winning(detail, match, session):
won_count += 1
elif detail.result == 'lost':
lost_count += 1
......@@ -6933,7 +6944,7 @@ def get_cashier_bet_details(bet_id):
for detail in bet_details:
if detail.result == 'pending':
results['pending'] += 1
elif is_bet_detail_winning(detail, session.query(MatchModel).filter_by(id=detail.match_id).first(), session):
elif is_settled_bet_detail_winning(detail, session.query(MatchModel).filter_by(id=detail.match_id).first(), session):
results['won'] += 1
# Get odds for this outcome
odds = 0.0
......@@ -7467,7 +7478,7 @@ def verify_barcode():
for detail in bet_details:
if detail.result == 'pending':
results['pending'] += 1
elif is_bet_detail_winning(detail, session.query(MatchModel).filter_by(id=detail.match_id).first(), session):
elif is_settled_bet_detail_winning(detail, session.query(MatchModel).filter_by(id=detail.match_id).first(), session):
results['won'] += 1
# Get odds for this outcome
odds = 0.0
......@@ -7850,7 +7861,7 @@ def get_daily_reports_summary():
total_payout = 0.0
for detail in bet_details:
match = session.query(MatchModel).filter_by(id=detail.match_id).first()
if is_bet_detail_winning(detail, match, session):
if is_settled_bet_detail_winning(detail, match, session):
total_payout += float(detail.win_amount or 0)
# Get unique matches count
......@@ -7951,7 +7962,7 @@ def get_match_reports():
# Calculate payout using actual win_amount (stored when bet was settled)
# This ensures consistency with bet list and avoids recalculation issues
if is_bet_detail_winning(detail, match, session):
if is_settled_bet_detail_winning(detail, match, session):
# Use the actual win_amount stored when the bet was settled
match_stats[match_id]['payout'] += float(detail.win_amount or 0)
......@@ -8029,7 +8040,9 @@ def get_match_details():
logger.info(f"Querying match details for match {match_id}, local date {date_param}: UTC range {start_datetime} to {end_datetime}")
# Get bet details for this match on this date
bet_details_query = session.query(BetDetailModel).join(BetModel).filter(
bet_details_query = session.query(BetDetailModel).join(
BetModel, BetDetailModel.bet_id == BetModel.uuid
).filter(
BetDetailModel.match_id == match_id,
BetModel.bet_datetime >= start_datetime,
BetModel.bet_datetime <= end_datetime
......@@ -8057,7 +8070,7 @@ def get_match_details():
# Determine actual result status using is_bet_detail_winning function
# This checks if bet detail is winning based on match's winning outcomes
actual_result = detail.result
if is_bet_detail_winning(detail, match, session):
if is_settled_bet_detail_winning(detail, match, session):
actual_result = 'won'
elif detail.result == 'lost':
actual_result = 'lost'
......@@ -8190,7 +8203,7 @@ def download_excel_report():
total_payout = 0.0
for detail in bet_details:
match = session.query(MatchModel).filter_by(id=detail.match_id).first()
if is_bet_detail_winning(detail, match, session):
if is_settled_bet_detail_winning(detail, match, session):
total_payout += float(detail.win_amount or 0)
unique_match_ids = {detail.match_id for detail in bet_details}
......@@ -8237,7 +8250,9 @@ def download_excel_report():
cell.alignment = Alignment(horizontal='center')
# Get match data (excluding cancelled bets)
bet_details_query = session.query(BetDetailModel).join(BetModel).filter(
bet_details_query = session.query(BetDetailModel).join(
BetModel, BetDetailModel.bet_id == BetModel.uuid
).filter(
BetModel.bet_datetime >= start_datetime,
BetModel.bet_datetime <= end_datetime,
BetDetailModel.result != 'cancelled'
......@@ -8261,7 +8276,7 @@ def download_excel_report():
# Add actual payout for winning bets (use same logic as bet list)
match = session.query(MatchModel).filter_by(id=match_id).first()
if is_bet_detail_winning(detail, match, session):
if is_settled_bet_detail_winning(detail, match, session):
match_stats[match_id]['payout'] += float(detail.win_amount or 0)
# Write match data
......
......@@ -11,7 +11,7 @@ from openpyxl import load_workbook
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from mbetterclient.web_dashboard import routes as routes_module
from mbetterclient.web_dashboard.routes import api_bp, is_bet_detail_winning
from mbetterclient.web_dashboard.routes import api_bp, is_bet_detail_winning, is_settled_bet_detail_winning
class DummyDetail:
......@@ -167,6 +167,9 @@ class DummyModelColumn:
def __ne__(self, other):
return ('!=', self.name, other)
def desc(self):
return self
def _dummy_model(name, **columns):
attrs = {'__name__': name}
......@@ -237,6 +240,7 @@ def build_sample_excel_bytes(monkeypatch, blueprint):
cancelled_bet = DummyBet(uuid='bet-cancelled', bet_datetime=datetime(2026, 5, 1, 13, 0, 0))
out_of_window_bet = DummyBet(uuid='bet-outside', bet_datetime=datetime(2026, 5, 2, 12, 0, 0))
match = DummyMatch(match_id=10)
match.status = 'ingame'
outside_match = DummyMatch(match_id=99, match_number=2)
extraction_stat = type('ExtractionStat', (), {'match_datetime': datetime(2026, 5, 1, 15, 0, 0), 'total_redistributed': 999.0})()
session = DummySession(
......@@ -254,14 +258,27 @@ def build_sample_excel_bytes(monkeypatch, blueprint):
def test_match_report_payout_uses_loaded_match():
detail = DummyDetail(match_id=10, amount=100.0, win_amount=250.0, result='lost')
match = DummyMatch(match_id=10)
match.status = 'ingame'
session = Mock()
assert is_bet_detail_winning(detail, match, session) is True
def test_settled_winning_check_prefers_explicit_lost_for_completed_match():
detail = DummyDetail(match_id=10, amount=100.0, win_amount=250.0, result='lost')
match = DummyMatch(match_id=10)
match.status = 'ingame'
match.status = 'done'
session = Mock()
assert is_bet_detail_winning(detail, match, session) is True
assert is_settled_bet_detail_winning(detail, match, session) is False
def test_match_reports_route_counts_winning_payout_after_loading_match(monkeypatch, isolated_api_blueprint):
install_route_test_doubles(monkeypatch)
match = DummyMatch(match_id=10)
match.status = 'ingame'
detail = DummyDetail(match_id=10, amount=100.0, win_amount=250.0, result='lost')
cancelled_detail = DummyDetail(match_id=10, amount=15.0, win_amount=0.0, result='cancelled', bet_id='bet-cancelled')
out_of_window_detail = DummyDetail(match_id=99, amount=50.0, win_amount=500.0, result='lost', bet_id='bet-outside')
......@@ -284,8 +301,8 @@ def test_match_reports_route_counts_winning_payout_after_loading_match(monkeypat
payload = response.get_json()
assert payload['total'] == 1
assert payload['matches'][0]['payin'] == 100.0
assert payload['matches'][0]['payout'] == 250.0
assert payload['matches'][0]['net'] == -150.0
assert payload['matches'][0]['payout'] == 0.0
assert payload['matches'][0]['net'] == 100.0
def test_excel_summary_uses_same_payout_basis_as_daily_summary(monkeypatch, isolated_api_blueprint):
......@@ -293,8 +310,8 @@ def test_excel_summary_uses_same_payout_basis_as_daily_summary(monkeypatch, isol
sheet = workbook['Daily Summary']
assert sheet['B4'].value == '100.00'
assert sheet['B5'].value == '250.00'
assert sheet['B6'].value == '-150.00'
assert sheet['B5'].value == '0.00'
assert sheet['B6'].value == '100.00'
assert sheet['B7'].value == '1'
assert sheet['B8'].value == '1'
......@@ -316,3 +333,20 @@ def test_excel_download_returns_xlsx_response(monkeypatch, isolated_api_blueprin
assert response.status_code == 200
assert response.mimetype == 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
def test_match_details_route_uses_explicit_join_for_bet_lookup(monkeypatch, isolated_api_blueprint):
install_route_test_doubles(monkeypatch)
detail = DummyDetail(match_id=10, amount=100.0, win_amount=250.0, result='lost')
bet = DummyBet(uuid='bet1', bet_datetime=datetime(2026, 5, 1, 12, 0, 0))
match = DummyMatch(match_id=10)
client = make_test_client(
DummySession([detail], [bet], [match]),
isolated_api_blueprint,
)
response = client.get('/api/reports/match-details?date=2026-05-01&match_id=10')
assert response.status_code == 200
payload = response.get_json()
assert payload['total'] == 0
......@@ -46,3 +46,14 @@ def test_calculate_summary_uses_settled_rows_and_unique_matches(tmp_path):
finally:
session.close()
db_manager.close()
def test_calculate_summary_payload_can_carry_global_balance_separately(tmp_path):
handler, db_manager = _build_handler(tmp_path)
try:
summary = handler._calculate_summary([], stats=[])
assert summary['total_payin'] == 0.0
assert summary['total_payout'] == 0.0
assert summary['total_matches'] == 0
finally:
db_manager.close()
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment