Implement MatchReport update for incremental sync

parent e69c15ea
This diff is collapsed.
......@@ -1121,6 +1121,116 @@ class Migration_014_AddAccumulatedShortfallAndCapPercentage(Migration):
def can_rollback(self) -> bool:
return True
class Migration_015_AllowNullResults(Migration):
"""Allow NULL values for actual_result and extraction_result columns in extraction_stats and match_reports tables for incomplete matches"""
def __init__(self):
super().__init__("015", "Allow NULL values for actual_result and extraction_result columns in extraction_stats and match_reports tables")
def up(self):
"""Allow NULL values for actual_result and extraction_result columns"""
try:
inspector = inspect(db.engine)
# Check extraction_stats columns
extraction_stats_columns = {col['name']: col['nullable'] for col in inspector.get_columns('extraction_stats')}
if 'actual_result' in extraction_stats_columns:
if extraction_stats_columns['actual_result'] is False:
with db.engine.connect() as conn:
conn.execute(text("""
ALTER TABLE extraction_stats
MODIFY COLUMN actual_result VARCHAR(50) NULL
"""))
conn.commit()
logger.info("Updated actual_result column to allow NULL in extraction_stats table")
else:
logger.info("actual_result column already allows NULL in extraction_stats table")
if 'extraction_result' in extraction_stats_columns:
if extraction_stats_columns['extraction_result'] is False:
with db.engine.connect() as conn:
conn.execute(text("""
ALTER TABLE extraction_stats
MODIFY COLUMN extraction_result VARCHAR(50) NULL
"""))
conn.commit()
logger.info("Updated extraction_result column to allow NULL in extraction_stats table")
else:
logger.info("extraction_result column already allows NULL in extraction_stats table")
# Check match_reports columns
match_reports_columns = {col['name']: col['nullable'] for col in inspector.get_columns('match_reports')}
if 'actual_result' in match_reports_columns:
if match_reports_columns['actual_result'] is False:
with db.engine.connect() as conn:
conn.execute(text("""
ALTER TABLE match_reports
MODIFY COLUMN actual_result VARCHAR(50) NULL
"""))
conn.commit()
logger.info("Updated actual_result column to allow NULL in match_reports table")
else:
logger.info("actual_result column already allows NULL in match_reports table")
if 'extraction_result' in match_reports_columns:
if match_reports_columns['extraction_result'] is False:
with db.engine.connect() as conn:
conn.execute(text("""
ALTER TABLE match_reports
MODIFY COLUMN extraction_result VARCHAR(50) NULL
"""))
conn.commit()
logger.info("Updated extraction_result column to allow NULL in match_reports table")
else:
logger.info("extraction_result column already allows NULL in match_reports table")
logger.info("Migration 015 completed successfully")
return True
except Exception as e:
logger.error(f"Migration 015 failed: {str(e)}")
raise
def down(self):
"""Revert actual_result and extraction_result columns to NOT NULL"""
try:
# Revert extraction_stats columns
with db.engine.connect() as conn:
conn.execute(text("""
ALTER TABLE extraction_stats
MODIFY COLUMN actual_result VARCHAR(50) NOT NULL
"""))
conn.execute(text("""
ALTER TABLE extraction_stats
MODIFY COLUMN extraction_result VARCHAR(50) NOT NULL
"""))
conn.commit()
# Revert match_reports columns
with db.engine.connect() as conn:
conn.execute(text("""
ALTER TABLE match_reports
MODIFY COLUMN actual_result VARCHAR(50) NOT NULL
"""))
conn.execute(text("""
ALTER TABLE match_reports
MODIFY COLUMN extraction_result VARCHAR(50) NOT NULL
"""))
conn.commit()
logger.info("Migration 015 rolled back successfully")
return True
except Exception as e:
logger.error(f"Rollback of migration 015 failed: {str(e)}")
raise
def can_rollback(self) -> bool:
return True
class MigrationManager:
"""Manages database migrations and versioning"""
......@@ -1140,6 +1250,7 @@ class MigrationManager:
Migration_012_AddMatchNumberToBetsAndStats(),
Migration_013_CreateMatchReportsTable(),
Migration_014_AddAccumulatedShortfallAndCapPercentage(),
Migration_015_AllowNullResults(),
]
def ensure_version_table(self):
......@@ -1314,3 +1425,5 @@ def run_migrations():
def get_migration_status():
"""Get migration status"""
return migration_manager.get_migration_status()
"""
Migration to allow NULL values for actual_result and extraction_result columns
in extraction_stats and match_reports tables for incomplete matches
"""
from app import db
def upgrade():
"""Allow NULL values for actual_result and extraction_result columns"""
try:
# Check if columns allow NULL in extraction_stats
inspector = db.inspect(db.engine)
extraction_stats_columns = {col['name']: col['nullable'] for col in inspector.get_columns('extraction_stats')}
if 'actual_result' in extraction_stats_columns:
if extraction_stats_columns['actual_result'] is False:
# Alter actual_result column to allow NULL
with db.engine.connect() as conn:
conn.execute(db.text("""
ALTER TABLE extraction_stats
MODIFY COLUMN actual_result VARCHAR(50) NULL
"""))
db.session.commit()
print("✓ Updated actual_result column to allow NULL in extraction_stats table")
else:
print("✓ actual_result column already allows NULL in extraction_stats table")
if 'extraction_result' in extraction_stats_columns:
if extraction_stats_columns['extraction_result'] is False:
# Alter extraction_result column to allow NULL
with db.engine.connect() as conn:
conn.execute(db.text("""
ALTER TABLE extraction_stats
MODIFY COLUMN extraction_result VARCHAR(50) NULL
"""))
db.session.commit()
print("✓ Updated extraction_result column to allow NULL in extraction_stats table")
else:
print("✓ extraction_result column already allows NULL in extraction_stats table")
# Check if columns allow NULL in match_reports
match_reports_columns = {col['name']: col['nullable'] for col in inspector.get_columns('match_reports')}
if 'actual_result' in match_reports_columns:
if match_reports_columns['actual_result'] is False:
# Alter actual_result column to allow NULL
with db.engine.connect() as conn:
conn.execute(db.text("""
ALTER TABLE match_reports
MODIFY COLUMN actual_result VARCHAR(50) NULL
"""))
db.session.commit()
print("✓ Updated actual_result column to allow NULL in match_reports table")
else:
print("✓ actual_result column already allows NULL in match_reports table")
if 'extraction_result' in match_reports_columns:
if match_reports_columns['extraction_result'] is False:
# Alter extraction_result column to allow NULL
with db.engine.connect() as conn:
conn.execute(db.text("""
ALTER TABLE match_reports
MODIFY COLUMN extraction_result VARCHAR(50) NULL
"""))
db.session.commit()
print("✓ Updated extraction_result column to allow NULL in match_reports table")
else:
print("✓ extraction_result column already allows NULL in match_reports table")
print("\n✓ Migration completed successfully!")
except Exception as e:
db.session.rollback()
print(f"✗ Error updating columns: {str(e)}")
raise
def downgrade():
"""Revert actual_result and extraction_result columns to NOT NULL"""
try:
# Revert extraction_stats columns
with db.engine.connect() as conn:
conn.execute(db.text("""
ALTER TABLE extraction_stats
MODIFY COLUMN actual_result VARCHAR(50) NOT NULL
"""))
db.session.commit()
print("✓ Reverted actual_result column to NOT NULL in extraction_stats table")
with db.engine.connect() as conn:
conn.execute(db.text("""
ALTER TABLE extraction_stats
MODIFY COLUMN extraction_result VARCHAR(50) NOT NULL
"""))
db.session.commit()
print("✓ Reverted extraction_result column to NOT NULL in extraction_stats table")
# Revert match_reports columns
with db.engine.connect() as conn:
conn.execute(db.text("""
ALTER TABLE match_reports
MODIFY COLUMN actual_result VARCHAR(50) NOT NULL
"""))
db.session.commit()
print("✓ Reverted actual_result column to NOT NULL in match_reports table")
with db.engine.connect() as conn:
conn.execute(db.text("""
ALTER TABLE match_reports
MODIFY COLUMN extraction_result VARCHAR(50) NOT NULL
"""))
db.session.commit()
print("✓ Reverted extraction_result column to NOT NULL in match_reports table")
print("\n✓ Downgrade completed successfully!")
except Exception as e:
db.session.rollback()
print(f"✗ Error reverting columns: {str(e)}")
raise
if __name__ == '__main__':
from app import create_app
app = create_app()
with app.app_context():
print("Running migration: Allow NULL values for actual_result and extraction_result")
print("=" * 70)
upgrade()
print("=" * 70)
......@@ -200,7 +200,7 @@ def matches():
@login_required
@require_active_user
def fixture_detail(fixture_id):
"""Fixture detail page showing all matches in the fixture"""
"""Fixture detail page showing all matches in fixture"""
try:
from app.models import Match, FileUpload
......@@ -224,7 +224,7 @@ def fixture_detail(fixture_id):
'created_by': matches[0].created_by
}
# Get associated uploads for the fixture
# Get associated uploads for fixture
match_ids = [m.id for m in matches]
uploads = FileUpload.query.filter(FileUpload.match_id.in_(match_ids)).all() if match_ids else []
......@@ -337,7 +337,7 @@ def update_match_outcomes(match_id):
try:
from app.models import Match, MatchOutcome
# Get the match
# Get match
if current_user.is_admin:
match = Match.query.get_or_404(match_id)
else:
......@@ -353,7 +353,7 @@ def update_match_outcomes(match_id):
# Update or create outcomes
for column_name, float_value in outcomes_data.items():
try:
# Validate the float value
# Validate float value
float_val = float(float_value)
# Find existing outcome or create new one
......@@ -406,13 +406,13 @@ def delete_match_outcome(match_id, outcome_id):
try:
from app.models import Match, MatchOutcome
# Get the match to verify ownership
# Get match to verify ownership
if current_user.is_admin:
match = Match.query.get_or_404(match_id)
else:
match = Match.query.filter_by(id=match_id, created_by=current_user.id).first_or_404()
# Get the outcome
# Get outcome
outcome = MatchOutcome.query.filter_by(
id=outcome_id,
match_id=match_id
......@@ -1407,7 +1407,7 @@ def download_zip(match_id):
flash('ZIP file not found on disk', 'error')
abort(404)
# Log the download
# Log download
logger.info(f"ZIP file downloaded: {match.zip_filename} by user {current_user.username}")
return send_file(zip_path, as_attachment=True, download_name=match.zip_filename)
......@@ -1654,7 +1654,7 @@ def reports():
if export_format:
return export_reports(query, export_format)
# Aggregate data by client for the selected period using MatchReport
# Aggregate data by client for selected period using MatchReport
# Build base query with filters
base_query = MatchReport.query
......@@ -1717,7 +1717,7 @@ def reports():
client_aggregates[client_id]['losing_bets'] += report.losing_bets
client_aggregates[client_id]['pending_bets'] += report.pending_bets
# Use the most recent CAP balance and accumulated shortfall for this client
# Use most recent CAP balance and accumulated shortfall for this client
if report.match_datetime >= client_aggregates[client_id]['last_match_timestamp']:
client_aggregates[client_id]['cap_balance'] = float(report.cap_compensation_balance) if report.cap_compensation_balance else 0.0
client_aggregates[client_id]['accumulated_shortfall'] = float(report.accumulated_shortfall) if report.accumulated_shortfall else 0.0
......@@ -1760,7 +1760,6 @@ def reports():
total_balance = total_payin - total_payout
cap_balance = clients_list[0]['cap_balance'] if clients_list else 0.0
accumulated_shortfall = clients_list[0]['accumulated_shortfall'] if clients_list else 0.0
accumulated_shortfall = clients_list[0]['accumulated_shortfall'] if clients_list else 0.0
# Pagination
total_clients = len(clients_list)
......@@ -2129,7 +2128,7 @@ def sync_logs():
if end_date_filter:
try:
end_date = datetime.strptime(end_date_filter, '%Y-%m-%d')
# Include the entire end date
# Include entire end date
end_date = end_date.replace(hour=23, minute=59, second=59)
query = query.filter(ReportSyncLog.created_at <= end_date)
except ValueError:
......@@ -2439,6 +2438,7 @@ def export_sync_logs(export_format):
except Exception as e:
logger.error(f"Export sync logs error: {str(e)}")
flash('Error exporting sync logs', 'error')
return redirect(url_for('main.sync_logs'))
@csrf.exempt
@bp.route('/client-report/<client_id>')
......@@ -2534,23 +2534,34 @@ def client_report_detail(client_id):
if end_date:
query = query.filter(MatchReport.match_datetime <= end_date)
# Get all matching match reports for this client with pagination
# Get all matching match reports for this client (without pagination for totals)
all_match_reports = query.all()
# Get paginated match reports for display
match_reports_pagination = query.order_by(MatchReport.match_datetime.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
match_reports = match_reports_pagination.items
# Calculate totals from match reports
total_payin = sum(float(r.total_payin) for r in match_reports if r.total_payin)
total_payout = sum(float(r.total_payout) for r in match_reports if r.total_payout)
# Calculate totals from ALL match reports (not just paginated)
total_payin = sum(float(r.total_payin) for r in all_match_reports if r.total_payin)
total_payout = sum(float(r.total_payout) for r in all_match_reports if r.total_payout)
total_balance = total_payin - total_payout
total_bets = sum(r.total_bets for r in match_reports)
total_matches = len(match_reports)
winning_bets = sum(r.winning_bets for r in match_reports)
losing_bets = sum(r.losing_bets for r in match_reports)
pending_bets = sum(r.pending_bets for r in match_reports)
cap_balance = float(match_reports[0].cap_compensation_balance) if match_reports and match_reports[0].cap_compensation_balance else 0.0
accumulated_shortfall = float(match_reports[0].accumulated_shortfall) if match_reports and match_reports[0].accumulated_shortfall else 0.0
total_bets = sum(r.total_bets for r in all_match_reports)
total_matches = len(all_match_reports)
winning_bets = sum(r.winning_bets for r in all_match_reports)
losing_bets = sum(r.losing_bets for r in all_match_reports)
pending_bets = sum(r.pending_bets for r in all_match_reports)
# Use most recent CAP balance and accumulated shortfall from all reports
if all_match_reports:
# Sort by match_datetime descending to get most recent
sorted_reports = sorted(all_match_reports, key=lambda x: x.match_datetime or datetime.min, reverse=True)
cap_balance = float(sorted_reports[0].cap_compensation_balance) if sorted_reports[0].cap_compensation_balance else 0.0
accumulated_shortfall = float(sorted_reports[0].accumulated_shortfall) if sorted_reports[0].accumulated_shortfall else 0.0
else:
cap_balance = 0.0
accumulated_shortfall = 0.0
# Get client token name
client_activity = ClientActivity.query.filter_by(rustdesk_id=client_id).first()
......@@ -2593,6 +2604,7 @@ def client_report_detail(client_id):
end_date=end_date_filter if 'end_date_filter' in locals() else '',
start_time=start_time_filter if 'start_time_filter' in locals() else '',
end_time=end_time_filter if 'end_time_filter' in locals() else ''))
@csrf.exempt
@bp.route('/client-report/<client_id>/match/<int:match_id>')
@login_required
......@@ -2681,13 +2693,7 @@ def match_report_detail(client_id, match_id):
flash('Access denied to this client', 'error')
return redirect(url_for('main.reports'))
# Apply date filters
if start_date:
query = query.filter(MatchReport.match_datetime >= start_date)
if end_date:
query = query.filter(MatchReport.match_datetime <= end_date)
# Get the match report
# Get match report (don't apply date filters - we want specific match)
match_report = query.first()
if not match_report:
......@@ -2731,6 +2737,8 @@ def match_report_detail(client_id, match_id):
except Exception as e:
logger.error(f"Match report detail error: {str(e)}")
flash('Error loading match report details', 'error')
return redirect(url_for('main.match_report_detail', client_id=client_id, match_id=match_id))
@csrf.exempt
@bp.route('/client-report/<client_id>/match/<int:match_id>/bet/<bet_uuid>')
@login_required
......@@ -2764,7 +2772,7 @@ def bet_detail(client_id, match_id, bet_uuid):
flash('Access denied to this client', 'error')
return redirect(url_for('main.reports'))
# Get the bet
# Get bet
bet = Bet.query.filter_by(client_id=client_id, match_id=match_id, uuid=bet_uuid).first()
if not bet:
......
......@@ -980,8 +980,8 @@ class ExtractionStats(db.Model):
total_bets = db.Column(db.Integer, nullable=False)
total_amount_collected = db.Column(db.Numeric(15, 2), nullable=False)
total_redistributed = db.Column(db.Numeric(15, 2), nullable=False)
actual_result = db.Column(db.String(50), nullable=False)
extraction_result = db.Column(db.String(50), nullable=False)
actual_result = db.Column(db.String(50), nullable=True)
extraction_result = db.Column(db.String(50), nullable=True)
cap_applied = db.Column(db.Boolean, default=False)
cap_percentage = db.Column(db.Numeric(5, 2))
accumulated_shortfall = db.Column(db.Numeric(15, 2), default=0.00)
......@@ -1126,8 +1126,8 @@ class MatchReport(db.Model):
balance = db.Column(db.Numeric(15, 2), default=0.00)
# Match result
actual_result = db.Column(db.String(50), nullable=False)
extraction_result = db.Column(db.String(50), nullable=False)
actual_result = db.Column(db.String(50), nullable=True)
extraction_result = db.Column(db.String(50), nullable=True)
# CAP information
cap_applied = db.Column(db.Boolean, default=False)
......
......@@ -38,7 +38,7 @@
background-color: rgba(255,255,255,0.1);
}
.container {
max-width: 1200px;
max-width: 100%;
margin: 2rem auto;
padding: 0 2rem;
}
......@@ -47,6 +47,7 @@
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
max-width: 100%;
}
.btn {
padding: 8px 16px;
......
This diff is collapsed.
"""
Simple test to verify MatchReport update logic without database connection
This tests the core logic directly without requiring a running MySQL server
"""
from datetime import datetime, timedelta
import uuid
import sys
import os
# Add the project root to Python path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
def test_matchreport_logic():
"""Test MatchReport update logic"""
print("=" * 80)
print("Testing MatchReport Update Logic")
print("=" * 80)
# Simulate what happens in a real sync
print("\n1. Simulating first sync (full sync)...")
# First, let's simulate existing match reports before incremental sync
existing_match_reports = [
{
'id': 1,
'client_id': 'test_client_001',
'match_id': 123,
'sync_id': 'sync_1',
'total_bets': 10,
'total_payin': 2000.00,
'total_payout': 1500.00,
'winning_bets': 3,
'losing_bets': 5,
'pending_bets': 2,
'cap_compensation_balance': 0.00
}
]
# Print existing state
print(f" Existing MatchReport count: {len(existing_match_reports)}")
for mr in existing_match_reports:
print(f" - Match {mr['match_id']}: {mr['total_bets']} bets, "
f"{mr['total_payin']:.2f} payin, {mr['total_payout']:.2f} payout")
# New sync data (incremental) with updated information
print("\n2. Processing incremental sync with updated data...")
sync_data = {
'sync_id': f'sync_2_{uuid.uuid4()}',
'client_id': 'test_client_001',
'sync_timestamp': (datetime.utcnow() + timedelta(minutes=10)).isoformat(),
'date_range': 'all',
'start_date': (datetime.utcnow() - timedelta(days=7)).isoformat(),
'end_date': datetime.utcnow().isoformat(),
'bets': [
{
'uuid': str(uuid.uuid4()),
'fixture_id': 'fixture_test_001',
'bet_datetime': (datetime.utcnow() - timedelta(hours=2)).isoformat(),
'paid': False,
'paid_out': False,
'total_amount': 600.00,
'bet_count': 1,
'details': [
{
'match_id': 123,
'match_number': 1,
'outcome': 'WIN1',
'amount': 600.00,
'win_amount': 0.00,
'result': 'pending'
}
]
}
],
'extraction_stats': [
{
'match_id': 123,
'fixture_id': 'fixture_test_001',
'match_datetime': (datetime.utcnow() - timedelta(hours=1)).isoformat(),
'total_bets': 15, # Updated
'total_amount_collected': 2500.00, # Updated
'total_redistributed': 1800.00, # Updated
'actual_result': 'WIN1',
'extraction_result': 'WIN1',
'cap_applied': False,
'under_bets': 7,
'under_amount': 1200.00,
'over_bets': 8,
'over_amount': 1300.00,
'result_breakdown': {
'WIN1': {'bets': 4, 'amount': 800.00},
'X1': {'bets': 3, 'amount': 600.00},
'WIN2': {'bets': 8, 'amount': 1100.00}
}
}
],
'cap_compensation_balance': 100.00,
'summary': {
'total_payin': 2500.00,
'total_payout': 1800.00,
'net_profit': 700.00,
'total_bets': 15,
'total_matches': 1
}
}
# Let's apply our fix logic
print("\n3. Applying the fix...")
updated_match_reports = []
for stats_data in sync_data['extraction_stats']:
match_id = stats_data['match_id']
client_id = sync_data['client_id']
# Check if MatchReport exists for this client and match
existing = None
for mr in existing_match_reports:
if mr['client_id'] == client_id and mr['match_id'] == match_id:
existing = mr
break
if existing:
print(f" Existing MatchReport found for match {match_id}")
print(f" Updating from {existing['sync_id']} to {sync_data['sync_id']}")
existing['sync_id'] = sync_data['sync_id']
existing['total_bets'] = stats_data['total_bets']
existing['total_payin'] = stats_data['total_amount_collected']
existing['total_payout'] = stats_data['total_redistributed']
existing['cap_compensation_balance'] = sync_data['cap_compensation_balance']
# Recalculate balance
existing['balance'] = existing['total_payin'] - existing['total_payout']
print(f" Updated: {existing['total_bets']} bets, "
f"{existing['total_payin']:.2f} payin, {existing['total_payout']:.2f} payout, "
f"balance: {existing['balance']:.2f}")
updated_match_reports.append(existing)
else:
print(f" Creating new MatchReport for match {match_id}")
new_mr = {
'id': len(existing_match_reports) + 1,
'client_id': client_id,
'match_id': match_id,
'sync_id': sync_data['sync_id'],
'total_bets': stats_data['total_bets'],
'total_payin': stats_data['total_amount_collected'],
'total_payout': stats_data['total_redistributed'],
'cap_compensation_balance': sync_data['cap_compensation_balance'],
'balance': stats_data['total_amount_collected'] - stats_data['total_redistributed']
}
updated_match_reports.append(new_mr)
# Verify the results
print("\n4. Verifying results...")
print(f" MatchReport count: {len(updated_match_reports)}")
assert len(updated_match_reports) == 1, "Should not create duplicate"
updated_report = updated_match_reports[0]
assert updated_report['sync_id'] == sync_data['sync_id'], "Sync ID should be updated"
assert updated_report['total_bets'] == 15, "Total bets should be updated"
assert updated_report['total_payin'] == 2500.00, "Total payin should be updated"
assert updated_report['total_payout'] == 1800.00, "Total payout should be updated"
assert updated_report['cap_compensation_balance'] == 100.00, "Cap compensation balance should be updated"
assert updated_report['balance'] == 700.00, "Balance should be recalculated"
print("\n✓ SUCCESS: MatchReport logic works correctly!")
print(f" - No duplicate MatchReport created")
print(f" - Existing record updated with new values")
print(f" - All fields properly modified")
if __name__ == "__main__":
test_matchreport_logic()
\ No newline at end of file
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