Add accumulated_shortfall and cap_percentage to reports sync

- Add accumulated_shortfall field to ExtractionStats and MatchReport models
- Add cap_percentage field to ExtractionStats and MatchReport models
- Update API sync endpoint to accept and store accumulated_shortfall and cap_percentage for each match
- Update reports page to show totals at the last match of selected period instead of sync timestamp
- Update client report detail to display accumulated_shortfall and cap_percentage
- Add comprehensive test script for accumulated_shortfall and cap_percentage implementation
- Update templates to display cap_percentage in match lists

This allows tracking the progression of the cap redistribution balance for every client
over time, showing the actual values at the moment of each match extraction.
parent 12219916
......@@ -1228,6 +1228,7 @@ def api_reports_sync():
existing_stats.extraction_result = stats_data['extraction_result']
existing_stats.cap_applied = stats_data.get('cap_applied', False)
existing_stats.cap_percentage = stats_data.get('cap_percentage')
existing_stats.accumulated_shortfall = stats_data.get('accumulated_shortfall', 0.00)
existing_stats.under_bets = stats_data.get('under_bets', 0)
existing_stats.under_amount = stats_data.get('under_amount', 0.00)
existing_stats.over_bets = stats_data.get('over_bets', 0)
......@@ -1286,6 +1287,7 @@ def api_reports_sync():
extraction_result=stats_data['extraction_result'],
cap_applied=stats_data.get('cap_applied', False),
cap_percentage=stats_data.get('cap_percentage'),
accumulated_shortfall=stats_data.get('accumulated_shortfall', 0.00),
under_bets=stats_data.get('under_bets', 0),
under_amount=stats_data.get('under_amount', 0.00),
over_bets=stats_data.get('over_bets', 0),
......@@ -1358,6 +1360,7 @@ def api_reports_sync():
cap_applied=stats_data.get('cap_applied', False),
cap_percentage=stats_data.get('cap_percentage'),
cap_compensation_balance=cap_compensation_balance,
accumulated_shortfall=stats_data.get('accumulated_shortfall', 0.00),
under_bets=stats_data.get('under_bets', 0),
under_amount=stats_data.get('under_amount', 0.00),
over_bets=stats_data.get('over_bets', 0),
......
......@@ -1704,6 +1704,7 @@ def reports():
'losing_bets': 0,
'pending_bets': 0,
'cap_balance': 0.0,
'accumulated_shortfall': 0.0,
'last_match_timestamp': report.match_datetime
}
......@@ -1715,9 +1716,10 @@ 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 for this client
# Use the 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
client_aggregates[client_id]['last_match_timestamp'] = report.match_datetime
# Calculate balance for each client
......@@ -1746,6 +1748,8 @@ def reports():
clients_list.sort(key=lambda x: x['token_name'].lower(), reverse=(sort_order == 'desc'))
elif sort_by == 'cap_balance':
clients_list.sort(key=lambda x: x['cap_balance'], reverse=(sort_order == 'desc'))
elif sort_by == 'accumulated_shortfall':
clients_list.sort(key=lambda x: x['accumulated_shortfall'], reverse=(sort_order == 'desc'))
else:
clients_list.sort(key=lambda x: x['last_match_timestamp'], reverse=(sort_order == 'desc'))
......@@ -1754,6 +1758,8 @@ def reports():
total_payout = sum(c['total_payout'] for c in clients_list)
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)
......@@ -1817,7 +1823,8 @@ def reports():
'total_payin': total_payin,
'total_payout': total_payout,
'total_balance': total_balance,
'cap_balance': cap_balance
'cap_balance': cap_balance,
'accumulated_shortfall': accumulated_shortfall
},
filters={
'client_id': client_id_filter,
......@@ -2530,6 +2537,7 @@ def client_report_detail(client_id):
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
# Get client token name
client_activity = ClientActivity.query.filter_by(rustdesk_id=client_id).first()
......@@ -2548,7 +2556,8 @@ def client_report_detail(client_id):
'winning_bets': winning_bets,
'losing_bets': losing_bets,
'pending_bets': pending_bets,
'cap_balance': cap_balance
'cap_balance': cap_balance,
'accumulated_shortfall': accumulated_shortfall
},
filters={
'date_range': date_range_filter,
......
......@@ -984,6 +984,7 @@ class ExtractionStats(db.Model):
extraction_result = db.Column(db.String(50), nullable=False)
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)
under_bets = db.Column(db.Integer, default=0)
under_amount = db.Column(db.Numeric(15, 2), default=0.00)
over_bets = db.Column(db.Integer, default=0)
......@@ -1010,6 +1011,7 @@ class ExtractionStats(db.Model):
'extraction_result': self.extraction_result,
'cap_applied': self.cap_applied,
'cap_percentage': float(self.cap_percentage) if self.cap_percentage else None,
'accumulated_shortfall': float(self.accumulated_shortfall) if self.accumulated_shortfall else 0.0,
'under_bets': self.under_bets,
'under_amount': float(self.under_amount) if self.under_amount else 0.0,
'over_bets': self.over_bets,
......@@ -1131,6 +1133,7 @@ class MatchReport(db.Model):
cap_applied = db.Column(db.Boolean, default=False)
cap_percentage = db.Column(db.Numeric(5, 2))
cap_compensation_balance = db.Column(db.Numeric(15, 2), default=0.00)
accumulated_shortfall = db.Column(db.Numeric(15, 2), default=0.00)
# Detailed breakdown
under_bets = db.Column(db.Integer, default=0)
......@@ -1169,6 +1172,7 @@ class MatchReport(db.Model):
'cap_applied': self.cap_applied,
'cap_percentage': float(self.cap_percentage) if self.cap_percentage else None,
'cap_compensation_balance': float(self.cap_compensation_balance) if self.cap_compensation_balance else 0.0,
'accumulated_shortfall': float(self.accumulated_shortfall) if self.accumulated_shortfall else 0.0,
'under_bets': self.under_bets,
'under_amount': float(self.under_amount) if self.under_amount else 0.0,
'over_bets': self.over_bets,
......
......@@ -78,6 +78,12 @@
<h3 class="mb-0">{{ "{:,.2f}".format(totals.cap_balance) }}</h3>
</div>
</div>
<div class="col-md-3">
<div class="p-3 bg-info text-white rounded">
<h6 class="mb-1">Accumulated Shortfall</h6>
<h3 class="mb-0">{{ "{:,.2f}".format(totals.accumulated_shortfall) }}</h3>
</div>
</div>
</div>
<div class="row mt-3">
<div class="col-md-4">
......@@ -172,6 +178,7 @@
<th>Extraction Result</th>
<th>CAP Applied</th>
<th>CAP Balance</th>
<th>Accumulated Shortfall</th>
</tr>
</thead>
<tbody>
......@@ -200,6 +207,7 @@
{% endif %}
</td>
<td class="text-end">{{ "{:,.2f}".format(report.cap_compensation_balance) if report.cap_compensation_balance else '0.00' }}</td>
<td class="text-end">{{ "{:,.2f}".format(report.accumulated_shortfall) if report.accumulated_shortfall else '0.00' }}</td>
</tr>
{% endfor %}
</tbody>
......
......@@ -90,6 +90,23 @@
</div>
</div>
</div>
<div class="col-12 col-md-6 col-lg-3">
<div class="card bg-gradient-info text-white mb-3 shadow-sm border-0 h-100">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<div class="icon-box bg-white bg-opacity-25 rounded-circle p-3">
<i class="fas fa-chart-line fa-2x"></i>
</div>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="card-title mb-1 text-white-50">Accumulated Shortfall</h6>
<h3 class="mb-0 fw-bold">{{ "{:,.2f}".format(totals.accumulated_shortfall) }}</h3>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Filters -->
......@@ -150,6 +167,7 @@
<option value="total_bets" {% if filters.sort_by == 'total_bets' %}selected{% endif %}>Total Bets</option>
<option value="total_matches" {% if filters.sort_by == 'total_matches' %}selected{% endif %}>Total Matches</option>
<option value="cap_balance" {% if filters.sort_by == 'cap_balance' %}selected{% endif %}>CAP Balance</option>
<option value="accumulated_shortfall" {% if filters.sort_by == 'accumulated_shortfall' %}selected{% endif %}>Accumulated Shortfall</option>
</select>
</div>
</div>
......@@ -218,6 +236,7 @@
<th>Payout</th>
<th>Balance</th>
<th>CAP Redistribution Balance</th>
<th>Accumulated Shortfall</th>
<th>Actions</th>
</tr>
</thead>
......@@ -234,6 +253,7 @@
{{ "{:,.2f}".format(client.balance) }}
</td>
<td class="text-end">{{ "{:,.2f}".format(client.cap_balance) }}</td>
<td class="text-end">{{ "{:,.2f}".format(client.accumulated_shortfall) }}</td>
<td>
<a href="{{ url_for('main.client_report_detail', client_id=client.client_id, **filters) }}" class="btn btn-sm btn-info">
<i class="fas fa-eye"></i> View Details
......
#!/usr/bin/env python3
"""
Test script to verify accumulated_shortfall and cap_percentage implementation
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from app import create_app, db
from app.models import ExtractionStats, MatchReport, ReportSync
from datetime import datetime
def test_accumulated_shortfall():
"""Test accumulated_shortfall field in models"""
print("Testing accumulated_shortfall implementation...")
app = create_app()
with app.app_context():
# Test ExtractionStats model
print("\n1. Testing ExtractionStats model...")
try:
# Check if accumulated_shortfall column exists
columns = [col.name for col in ExtractionStats.__table__.columns]
if 'accumulated_shortfall' in columns:
print(" ✓ accumulated_shortfall column exists in ExtractionStats")
else:
print(" ✗ accumulated_shortfall column NOT found in ExtractionStats")
return False
except Exception as e:
print(f" ✗ Error checking ExtractionStats: {e}")
return False
# Test MatchReport model
print("\n2. Testing MatchReport model...")
try:
columns = [col.name for col in MatchReport.__table__.columns]
if 'accumulated_shortfall' in columns:
print(" ✓ accumulated_shortfall column exists in MatchReport")
else:
print(" ✗ accumulated_shortfall column NOT found in MatchReport")
return False
except Exception as e:
print(f" ✗ Error checking MatchReport: {e}")
return False
# Test to_dict methods include accumulated_shortfall
print("\n3. Testing to_dict methods...")
try:
# Create a dummy ExtractionStats object
dummy_stats = ExtractionStats(
sync_id=1,
client_id='test_client',
match_id=1,
match_number=1,
fixture_id='test_fixture',
match_datetime=datetime.utcnow(),
total_bets=10,
total_amount_collected=100.00,
total_redistributed=90.00,
actual_result='under',
extraction_result='under',
accumulated_shortfall=50.00
)
stats_dict = dummy_stats.to_dict()
if 'accumulated_shortfall' in stats_dict:
print(" ✓ ExtractionStats.to_dict() includes accumulated_shortfall")
else:
print(" ✗ ExtractionStats.to_dict() missing accumulated_shortfall")
return False
# Create a dummy MatchReport object
dummy_report = MatchReport(
sync_id=1,
client_id='test_client',
client_token_name='Test Token',
match_id=1,
match_number=1,
fixture_id='test_fixture',
match_datetime=datetime.utcnow(),
total_bets=10,
total_payin=100.00,
total_payout=90.00,
balance=10.00,
actual_result='under',
extraction_result='under',
accumulated_shortfall=50.00
)
report_dict = dummy_report.to_dict()
if 'accumulated_shortfall' in report_dict:
print(" ✓ MatchReport.to_dict() includes accumulated_shortfall")
else:
print(" ✗ MatchReport.to_dict() missing accumulated_shortfall")
return False
except Exception as e:
print(f" ✗ Error testing to_dict: {e}")
return False
print("\n✓ All tests passed!")
return True
def test_cap_percentage():
"""Test cap_percentage field in models"""
print("\nTesting cap_percentage implementation...")
app = create_app()
with app.app_context():
# Test ExtractionStats model
print("\n1. Testing ExtractionStats model...")
try:
# Check if cap_percentage column exists
columns = [column.name for column in ExtractionStats.__table__.columns]
if 'cap_percentage' in columns:
print(" ✓ cap_percentage column exists in ExtractionStats")
else:
print(" ✗ cap_percentage column missing in ExtractionStats")
return False
# Test to_dict method includes cap_percentage
dummy_stat = ExtractionStats(
sync_id=1,
client_id='test_client',
match_id=1,
match_number=1,
fixture_id='test-fixture',
match_datetime=datetime.utcnow(),
total_bets=10,
total_amount_collected=100.00,
total_redistributed=90.00,
actual_result='under',
extraction_result='under',
cap_applied=True,
cap_percentage=15.50,
accumulated_shortfall=50.00
)
stat_dict = dummy_stat.to_dict()
if 'cap_percentage' in stat_dict:
print(" ✓ ExtractionStats.to_dict() includes cap_percentage")
if stat_dict['cap_percentage'] == 15.50:
print(" ✓ cap_percentage value is correct")
else:
print(f" ✗ cap_percentage value is incorrect: {stat_dict['cap_percentage']}")
return False
else:
print(" ✗ ExtractionStats.to_dict() missing cap_percentage")
return False
except Exception as e:
print(f" ✗ Error testing to_dict: {e}")
return False
# Test MatchReport model
print("\n2. Testing MatchReport model...")
try:
# Check if cap_percentage column exists
columns = [column.name for column in MatchReport.__table__.columns]
if 'cap_percentage' in columns:
print(" ✓ cap_percentage column exists in MatchReport")
else:
print(" ✗ cap_percentage column missing in MatchReport")
return False
# Test to_dict method includes cap_percentage
dummy_report = MatchReport(
sync_id=1,
client_id='test_client',
client_token_name='Test Token',
match_id=1,
match_number=1,
fixture_id='test-fixture',
match_datetime=datetime.utcnow(),
total_bets=10,
winning_bets=5,
losing_bets=5,
pending_bets=0,
total_payin=100.00,
total_payout=90.00,
balance=10.00,
actual_result='under',
extraction_result='under',
cap_applied=True,
cap_percentage=15.50,
cap_compensation_balance=100.00,
accumulated_shortfall=50.00
)
report_dict = dummy_report.to_dict()
if 'cap_percentage' in report_dict:
print(" ✓ MatchReport.to_dict() includes cap_percentage")
if report_dict['cap_percentage'] == 15.50:
print(" ✓ cap_percentage value is correct")
else:
print(f" ✗ cap_percentage value is incorrect: {report_dict['cap_percentage']}")
return False
else:
print(" ✗ MatchReport.to_dict() missing cap_percentage")
return False
except Exception as e:
print(f" ✗ Error testing to_dict: {e}")
return False
print("\n✓ All cap_percentage tests passed!")
return True
if __name__ == '__main__':
success1 = test_accumulated_shortfall()
success2 = test_cap_percentage()
if success1 and success2:
print("\n" + "="*50)
print("✓ ALL TESTS PASSED!")
print("="*50)
sys.exit(0)
else:
print("\n" + "="*50)
print("✗ SOME TESTS FAILED!")
print("="*50)
sys.exit(1)
\ 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