Balabce globalized

parent a1afa146
This diff is collapsed.
This diff is collapsed.
......@@ -745,23 +745,25 @@ class MatchTimerComponent(ThreadedComponent):
return 'all_bets_on_start'
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:
from ..database.models import MatchTemplateModel
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(
MatchTemplateModel.active_status == True
MatchTemplateModel.active_status == True,
MatchTemplateModel.zip_upload_status == 'completed',
MatchTemplateModel.zip_validation_status == 'valid'
).all()
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
# Select random templates
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
except Exception as e:
......
......@@ -2890,6 +2890,55 @@ class Migration_038_AddWin1Win2Associations(DatabaseMigration):
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):
"""Add matches_templates and match_outcomes_templates tables for storing match templates"""
......@@ -3047,6 +3096,7 @@ MIGRATIONS: List[DatabaseMigration] = [
Migration_036_AddMatchTemplatesTables(),
Migration_037_RenameDailyRedistributionShortfallTable(),
Migration_038_AddWin1Win2Associations(),
Migration_039_AddMatchNumberToBetDetails(),
]
......
......@@ -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')
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')
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)')
......
......@@ -2592,9 +2592,9 @@ def get_fixture_details(fixture_id):
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
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:
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 pathlib import Path
import shutil
......@@ -2604,13 +2604,27 @@ def reset_fixtures():
# Count existing data before reset
matches_count = session.query(MatchModel).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)
session.query(MatchOutcomeModel).delete()
# 3. Delete matches (will cascade to match_outcomes due to CASCADE constraint)
deleted_matches = session.query(MatchModel).delete()
session.commit()
# Clear all matches
session.query(MatchModel).delete()
# 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
......@@ -2630,7 +2644,7 @@ def reset_fixtures():
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({
"success": True,
......@@ -2638,6 +2652,11 @@ def reset_fixtures():
"removed": {
"matches": matches_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
}
})
......@@ -4120,19 +4139,20 @@ def get_redistribution_balance():
session = api_bp.db_manager.get_session()
try:
# Get the latest redistribution adjustment record
latest_record = session.query(PersistentRedistributionAdjustmentModel)\
.order_by(PersistentRedistributionAdjustmentModel.date.desc())\
# Get the global redistribution adjustment record (fixed date 1970-01-01)
global_date = date(1970, 1, 1)
global_record = session.query(PersistentRedistributionAdjustmentModel)\
.filter_by(date=global_date)\
.first()
current_balance = 0.0
if latest_record:
current_balance = float(latest_record.accumulated_shortfall)
if global_record:
current_balance = float(global_record.accumulated_shortfall)
return jsonify({
"success": True,
"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:
......@@ -4154,19 +4174,20 @@ def reset_redistribution_balance():
session = api_bp.db_manager.get_session()
try:
# Get the latest redistribution adjustment record
latest_record = session.query(PersistentRedistributionAdjustmentModel)\
.order_by(PersistentRedistributionAdjustmentModel.date.desc())\
# Get the global redistribution adjustment record (fixed date 1970-01-01)
global_date = date(1970, 1, 1)
global_record = session.query(PersistentRedistributionAdjustmentModel)\
.filter_by(date=global_date)\
.first()
if latest_record:
if global_record:
# Reset the accumulated shortfall to zero
old_balance = float(latest_record.accumulated_shortfall)
latest_record.accumulated_shortfall = 0.0
latest_record.updated_at = datetime.utcnow()
old_balance = float(global_record.accumulated_shortfall)
global_record.accumulated_shortfall = 0.0
global_record.updated_at = datetime.utcnow()
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({
"success": True,
......@@ -4175,21 +4196,20 @@ def reset_redistribution_balance():
"new_balance": 0.0
})
else:
# No record exists, create one with zero balance
today = date.today()
# No global record exists, create one with zero balance
new_record = PersistentRedistributionAdjustmentModel(
date=today,
date=global_date,
accumulated_shortfall=0.0,
cap_percentage=70.0 # Default cap
)
session.add(new_record)
session.commit()
logger.info("Created new redistribution adjustment record with zero balance")
logger.info("Created global redistribution adjustment record with zero balance")
return jsonify({
"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,
"new_balance": 0.0
})
......@@ -4626,6 +4646,8 @@ def create_cashier_bet():
existing_match = session.query(MatchModel).filter_by(id=match_id).first()
if not existing_match:
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
bet_uuid = str(uuid_lib.uuid4())
......@@ -4702,6 +4724,7 @@ def create_cashier_bet():
bet_detail = BetDetailModel(
bet_id=bet_uuid,
match_id=detail_data['match_id'],
match_number=detail_data['_match_number'],
outcome=detail_data['outcome'],
amount=float(detail_data['amount']),
result='pending'
......@@ -4774,6 +4797,9 @@ def get_cashier_bet_details(bet_id):
for detail in bet_details:
detail_data = detail.to_dict()
# Include stored match_number
detail_data['match_number'] = detail.match_number
# Get match information
match = session.query(MatchModel).filter_by(id=detail.match_id).first()
if match:
......@@ -5179,6 +5205,9 @@ def verify_bet_details(bet_id):
for detail in bet_details:
detail_data = detail.to_dict()
# Include stored match_number
detail_data['match_number'] = detail.match_number
# Get match information
match = session.query(MatchModel).filter_by(id=detail.match_id).first()
if match:
......@@ -5255,6 +5284,9 @@ def verify_barcode():
detail_data = detail.to_dict()
total_amount += float(detail.amount)
# Include stored match_number
detail_data['match_number'] = detail.match_number
# Get match information
match = session.query(MatchModel).filter_by(id=detail.match_id).first()
if match:
......
......@@ -89,9 +89,9 @@
{% for detail in bet.bet_details %}
<tr>
<td>
<strong>Match #{{ detail.match.match_number }}</strong><br>
<strong>Match #{{ detail.match_number }}</strong><br>
<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>
</td>
<td>
......@@ -450,7 +450,7 @@
"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' }}",
"fighter2": "{{ detail.match.fighter2_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) {
const totalAmount = parseFloat(bet.total_amount).toFixed(2);
// 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
let overallStatus = 'pending';
......@@ -719,7 +719,7 @@ function transformBetDataForReceipt(betData) {
total_amount: betData.total_amount,
bet_count: betData.details_count || betData.details.length,
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',
fighter2: detail.match ? detail.match.fighter2_township : 'Unknown',
venue: detail.match ? detail.match.venue_kampala_township : 'Unknown',
......
......@@ -477,6 +477,7 @@ function getUploadStatusBadge(fixture) {
function resetFixtures() {
const confirmMessage = 'WARNING: This will permanently delete ALL fixture data including:\n\n' +
'• All synchronized matches and outcomes\n' +
'• All match templates and template outcomes\n' +
'• All downloaded ZIP files\n' +
'• This action cannot be undone!\n\n' +
'Are you sure you want to reset all fixtures data?';
......@@ -500,7 +501,7 @@ function resetFixtures() {
.then(response => response.json())
.then(data => {
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
loadFixtures();
} else {
......
......@@ -499,6 +499,8 @@ document.addEventListener('DOMContentLoaded', function() {
// Load redistribution balance (admin only)
if (document.getElementById('redistribution-balance')) {
loadRedistributionBalance();
// Periodic update of redistribution balance
setInterval(loadRedistributionBalance, 5000); // Update every 5 seconds
}
// 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