Balabce globalized

parent a1afa146
This diff is collapsed.
This diff is collapsed.
...@@ -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:
......
...@@ -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(),
] ]
......
...@@ -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)')
......
...@@ -2592,9 +2592,9 @@ def get_fixture_details(fixture_id): ...@@ -2592,9 +2592,9 @@ 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
...@@ -2604,13 +2604,27 @@ def reset_fixtures(): ...@@ -2604,13 +2604,27 @@ def reset_fixtures():
# 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()
template_outcomes_count = session.query(MatchOutcomeTemplateModel).count()
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()
# 2. Delete bets (will cascade to bet_details due to CASCADE constraint)
deleted_bets = session.query(BetModel).delete()
session.commit()
# Clear all match outcomes first (due to foreign key constraints) # 3. Delete matches (will cascade to match_outcomes due to CASCADE constraint)
session.query(MatchOutcomeModel).delete() deleted_matches = session.query(MatchModel).delete()
session.commit() session.commit()
# Clear all matches # 4. Delete match templates (will cascade to match_outcomes_templates due to CASCADE constraint)
session.query(MatchModel).delete() deleted_templates = session.query(MatchTemplateModel).delete()
session.commit() session.commit()
# Clear ZIP files from persistent storage # Clear ZIP files from persistent storage
...@@ -2630,7 +2644,7 @@ def reset_fixtures(): ...@@ -2630,7 +2644,7 @@ def reset_fixtures():
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,
...@@ -2638,6 +2652,11 @@ def reset_fixtures(): ...@@ -2638,6 +2652,11 @@ def reset_fixtures():
"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
} }
}) })
...@@ -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"Redistribution balance reset from {old_balance} to 0.0") logger.info(f"Global redistribution 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'
...@@ -4774,6 +4797,9 @@ def get_cashier_bet_details(bet_id): ...@@ -4774,6 +4797,9 @@ 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:
......
...@@ -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' }}",
......
...@@ -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',
......
...@@ -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 {
......
...@@ -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
......
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