Add comprehensive user workflow documentation and statistics page

- Create USER_WORKFLOW_DOCUMENTATION.md with extensive coverage of:
  * Complete cashier workflow from login to end-of-day procedures
  * Game flow and match progression with state machine details
  * Extraction algorithm and redistribution cap system
  * Major functionality and configuration options
- Add statistics.html template for admin statistics dashboard
- Update core components for improved functionality
parent 71d9820e
This diff is collapsed.
......@@ -1395,6 +1395,9 @@ class GamesThread(ThreadedComponent):
# Update bet results for UNDER/OVER and the selected result
self._update_bet_results(match_id, selected_result, session)
# Collect statistics for this match completion
self._collect_match_statistics(match_id, fixture_id, selected_result, session)
logger.info(f"Result extraction completed: selected {selected_result}")
return selected_result
......@@ -1530,6 +1533,119 @@ class GamesThread(ThreadedComponent):
logger.error(f"Failed to update bet results: {e}")
session.rollback()
def _collect_match_statistics(self, match_id: int, fixture_id: str, selected_result: str, session):
"""Collect and store statistics for match completion"""
try:
from ..database.models import ExtractionStatsModel, BetDetailModel, MatchModel
import json
# Get match information
match = session.query(MatchModel).filter_by(id=match_id).first()
if not match:
logger.warning(f"Match {match_id} not found for statistics collection")
return
# Calculate statistics
total_bets = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.active_status == True
).count()
total_amount_collected = session.query(
BetDetailModel.amount
).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.active_status == True
).all()
total_amount_collected = sum(bet.amount for bet in total_amount_collected) if total_amount_collected else 0.0
# Calculate redistribution amount (sum of all win_amounts)
total_redistributed = session.query(
BetDetailModel.win_amount
).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.result == 'win',
BetDetailModel.active_status == True
).all()
total_redistributed = sum(bet.win_amount for bet in total_redistributed) if total_redistributed else 0.0
# Get UNDER/OVER specific statistics
under_bets = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'UNDER',
BetDetailModel.active_status == True
).count()
under_amount = session.query(
BetDetailModel.amount
).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'UNDER',
BetDetailModel.active_status == True
).all()
under_amount = sum(bet.amount for bet in under_amount) if under_amount else 0.0
over_bets = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'OVER',
BetDetailModel.active_status == True
).count()
over_amount = session.query(
BetDetailModel.amount
).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'OVER',
BetDetailModel.active_status == True
).all()
over_amount = sum(bet.amount for bet in over_amount) if over_amount else 0.0
# Check if CAP was applied
cap_percentage = self._get_redistribution_cap()
cap_applied = False
cap_threshold = total_amount_collected * (cap_percentage / 100.0)
# Get extraction result (the actual result selected)
extraction_result = selected_result if selected_result not in ['UNDER', 'OVER'] else None
# Create result breakdown (simplified for now)
result_breakdown = {
'selected_result': selected_result,
'extraction_result': extraction_result,
'under_over_result': selected_result if selected_result in ['UNDER', 'OVER'] else None,
'total_payin': total_amount_collected,
'total_payout': total_redistributed,
'profit': total_amount_collected - total_redistributed
}
# Create or update extraction stats record
stats_record = ExtractionStatsModel(
match_id=match_id,
fixture_id=fixture_id,
match_datetime=match.start_time or datetime.utcnow(),
total_bets=total_bets,
total_amount_collected=total_amount_collected,
total_redistributed=total_redistributed,
actual_result=selected_result,
result_breakdown=json.dumps(result_breakdown),
under_bets=under_bets,
under_amount=under_amount,
over_bets=over_bets,
over_amount=over_amount,
extraction_result=extraction_result,
cap_applied=cap_applied,
cap_percentage=cap_percentage if cap_applied else None
)
session.add(stats_record)
session.commit()
logger.info(f"Collected statistics for match {match_id}: {total_bets} bets, collected={total_amount_collected:.2f}, redistributed={total_redistributed:.2f}")
except Exception as e:
logger.error(f"Failed to collect match statistics: {e}")
session.rollback()
def _fallback_result_selection(self) -> str:
"""Fallback result selection when extraction fails"""
try:
......
......@@ -1928,6 +1928,79 @@ class Migration_025_AddResultOptionModel(DatabaseMigration):
logger.info("ResultOptionModel migration rollback - no database changes")
return True
class Migration_026_AddExtractionStatsTable(DatabaseMigration):
"""Add extraction_stats table for collecting match betting statistics"""
def __init__(self):
super().__init__("026", "Add extraction_stats table for collecting match betting statistics")
def up(self, db_manager) -> bool:
"""Create extraction_stats table"""
try:
with db_manager.engine.connect() as conn:
# Create extraction_stats table
conn.execute(text("""
CREATE TABLE IF NOT EXISTS extraction_stats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
match_id INTEGER NOT NULL REFERENCES matches(id),
fixture_id VARCHAR(255) NOT NULL,
match_datetime DATETIME NOT NULL,
total_bets INTEGER DEFAULT 0 NOT NULL,
total_amount_collected REAL DEFAULT 0.0 NOT NULL,
total_redistributed REAL DEFAULT 0.0 NOT NULL,
actual_result VARCHAR(50) NOT NULL,
result_breakdown TEXT NOT NULL,
under_bets INTEGER DEFAULT 0 NOT NULL,
under_amount REAL DEFAULT 0.0 NOT NULL,
over_bets INTEGER DEFAULT 0 NOT NULL,
over_amount REAL DEFAULT 0.0 NOT NULL,
extraction_result VARCHAR(50),
cap_applied BOOLEAN DEFAULT FALSE NOT NULL,
cap_percentage REAL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL
)
"""))
# Create indexes for extraction_stats table
indexes = [
"CREATE INDEX IF NOT EXISTS ix_extraction_stats_match_id ON extraction_stats(match_id)",
"CREATE INDEX IF NOT EXISTS ix_extraction_stats_fixture_id ON extraction_stats(fixture_id)",
"CREATE INDEX IF NOT EXISTS ix_extraction_stats_match_datetime ON extraction_stats(match_datetime)",
"CREATE INDEX IF NOT EXISTS ix_extraction_stats_actual_result ON extraction_stats(actual_result)",
"CREATE INDEX IF NOT EXISTS ix_extraction_stats_composite ON extraction_stats(fixture_id, match_id)",
]
for index_sql in indexes:
conn.execute(text(index_sql))
conn.commit()
logger.info("Extraction stats table created successfully")
return True
except Exception as e:
logger.error(f"Failed to create extraction_stats table: {e}")
return False
def down(self, db_manager) -> bool:
"""Drop extraction_stats table"""
try:
with db_manager.engine.connect() as conn:
conn.execute(text("DROP TABLE IF EXISTS extraction_stats"))
conn.commit()
logger.info("Extraction stats table dropped")
return True
except Exception as e:
logger.error(f"Failed to drop extraction_stats table: {e}")
return False
# Registry of all migrations in order
MIGRATIONS: List[DatabaseMigration] = [
Migration_001_InitialSchema(),
......@@ -1955,6 +2028,7 @@ MIGRATIONS: List[DatabaseMigration] = [
Migration_023_AddAvailableBetsTable(),
Migration_024_AddResultOptionsTable(),
Migration_025_AddResultOptionModel(),
Migration_026_AddExtractionStatsTable(),
]
......
......@@ -837,3 +837,101 @@ class ResultOptionModel(BaseModel):
def __repr__(self):
return f'<ResultOption {self.result_name}: {self.description}>'
class ExtractionStatsModel(BaseModel):
"""Statistics for extraction results and betting patterns"""
__tablename__ = 'extraction_stats'
__table_args__ = (
Index('ix_extraction_stats_match_id', 'match_id'),
Index('ix_extraction_stats_fixture_id', 'fixture_id'),
Index('ix_extraction_stats_match_datetime', 'match_datetime'),
Index('ix_extraction_stats_actual_result', 'actual_result'),
Index('ix_extraction_stats_composite', 'fixture_id', 'match_id'),
)
# Match identification
match_id = Column(Integer, ForeignKey('matches.id'), nullable=False, comment='Foreign key to matches table')
fixture_id = Column(String(255), nullable=False, comment='Fixture identifier')
match_datetime = Column(DateTime, nullable=False, comment='When the match was completed')
# Overall betting statistics
total_bets = Column(Integer, default=0, nullable=False, comment='Total number of bets placed on this match')
total_amount_collected = Column(Float(precision=2), default=0.0, nullable=False, comment='Total amount collected from all bets')
total_redistributed = Column(Float(precision=2), default=0.0, nullable=False, comment='Total amount redistributed to winners')
# Result statistics (JSON structure for flexible result tracking)
actual_result = Column(String(50), nullable=False, comment='The actual result of the match (WIN1, DRAW, WIN2, etc.)')
result_breakdown = Column(JSON, nullable=False, comment='Detailed breakdown of bets and amounts by result option')
# UNDER/OVER specific statistics
under_bets = Column(Integer, default=0, nullable=False, comment='Number of UNDER bets')
under_amount = Column(Float(precision=2), default=0.0, nullable=False, comment='Total amount bet on UNDER')
over_bets = Column(Integer, default=0, nullable=False, comment='Number of OVER bets')
over_amount = Column(Float(precision=2), default=0.0, nullable=False, comment='Total amount bet on OVER')
# Extraction system statistics
extraction_result = Column(String(50), comment='Result from extraction system (if different from actual)')
cap_applied = Column(Boolean, default=False, nullable=False, comment='Whether redistribution CAP was applied')
cap_percentage = Column(Float(precision=2), comment='CAP percentage used (if applied)')
# Relationships
match = relationship('MatchModel')
def set_result_breakdown(self, breakdown: Dict[str, Dict[str, Any]]):
"""Set result breakdown as JSON
Expected format:
{
"WIN1": {"bets": 10, "amount": 500.00, "coefficient": 2.0},
"DRAW": {"bets": 5, "amount": 250.00, "coefficient": 3.0},
...
}
"""
self.result_breakdown = breakdown
def get_result_breakdown(self) -> Dict[str, Dict[str, Any]]:
"""Get result breakdown from JSON"""
if isinstance(self.result_breakdown, dict):
return self.result_breakdown
elif isinstance(self.result_breakdown, str):
try:
return json.loads(self.result_breakdown)
except json.JSONDecodeError:
return {}
else:
return {}
def add_result_stat(self, result_name: str, bet_count: int, amount: float, coefficient: float = None):
"""Add or update statistics for a specific result"""
breakdown = self.get_result_breakdown()
if result_name not in breakdown:
breakdown[result_name] = {"bets": 0, "amount": 0.0}
breakdown[result_name]["bets"] += bet_count
breakdown[result_name]["amount"] += amount
if coefficient is not None:
breakdown[result_name]["coefficient"] = coefficient
self.set_result_breakdown(breakdown)
def get_profit_loss(self) -> float:
"""Calculate profit/loss for this match (collected - redistributed)"""
return self.total_amount_collected - self.total_redistributed
def get_payout_ratio(self) -> float:
"""Calculate payout ratio (redistributed / collected)"""
if self.total_amount_collected > 0:
return self.total_redistributed / self.total_amount_collected
return 0.0
def to_dict(self, exclude_fields: Optional[List[str]] = None) -> Dict[str, Any]:
"""Convert to dictionary with calculated fields"""
result = super().to_dict(exclude_fields)
result['result_breakdown'] = self.get_result_breakdown()
result['profit_loss'] = self.get_profit_loss()
result['payout_ratio'] = self.get_payout_ratio()
return result
def __repr__(self):
return f'<ExtractionStats Match {self.match_id}: {self.total_bets} bets, {self.total_amount_collected:.2f} collected, {self.actual_result}>'
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