Bet insert and verification fixed

parent adf01f7a
#!/usr/bin/env python3
"""
Script to check database schema and table structure
"""
import sqlite3
from pathlib import Path
# Database configuration
DATABASE_PATH = "data/mbetterclient.db"
def check_database_schema():
"""Check database schema and table structure"""
try:
print("Checking database schema...")
# Connect to the database directly
conn = sqlite3.connect(DATABASE_PATH)
cursor = conn.cursor()
# Get all tables in the database
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;")
tables = cursor.fetchall()
print(f"Found {len(tables)} tables:")
for table in tables:
print(f" - {table[0]}")
# Check if bet_details table exists
bet_details_exists = any(table[0] == 'bet_details' for table in tables)
print(f"\nbet_details table exists: {bet_details_exists}")
# Get schema for bets table
cursor.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='bets';")
bets_schema = cursor.fetchone()
if bets_schema:
print(f"\nBets table schema:")
print(bets_schema[0])
# Get schema for bet_details table if it exists
if bet_details_exists:
cursor.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='bet_details';")
bet_details_schema = cursor.fetchone()
if bet_details_schema:
print(f"\nBet details table schema:")
print(bet_details_schema[0])
# Check if there are any bets in the database
cursor.execute("SELECT COUNT(*) FROM bets;")
bet_count = cursor.fetchone()[0]
print(f"\nTotal bets in database: {bet_count}")
# Check if there are any bet_details in the database
if bet_details_exists:
cursor.execute("SELECT COUNT(*) FROM bet_details;")
details_count = cursor.fetchone()[0]
print(f"Total bet details in database: {details_count}")
conn.close()
print("\nDatabase schema check completed successfully")
except Exception as e:
print(f"Database schema check failed: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
check_database_schema()
\ No newline at end of file
...@@ -54,11 +54,13 @@ class DatabaseManager: ...@@ -54,11 +54,13 @@ class DatabaseManager:
# Configure SQLite for better performance and reliability # Configure SQLite for better performance and reliability
with self.engine.connect() as conn: with self.engine.connect() as conn:
# Use WAL mode with proper checkpoint configuration to fix bet insertion issues
conn.execute(text("PRAGMA journal_mode=WAL")) conn.execute(text("PRAGMA journal_mode=WAL"))
conn.execute(text("PRAGMA synchronous=NORMAL")) conn.execute(text("PRAGMA synchronous=FULL")) # Ensure data is written synchronously
conn.execute(text("PRAGMA cache_size=10000")) conn.execute(text("PRAGMA cache_size=10000"))
conn.execute(text("PRAGMA temp_store=MEMORY")) conn.execute(text("PRAGMA temp_store=MEMORY"))
conn.execute(text("PRAGMA mmap_size=268435456")) # 256MB conn.execute(text("PRAGMA mmap_size=268435456")) # 256MB
conn.execute(text("PRAGMA foreign_keys=ON")) # Enable foreign key constraints
conn.commit() conn.commit()
# Create session factory # Create session factory
...@@ -128,11 +130,13 @@ class DatabaseManager: ...@@ -128,11 +130,13 @@ class DatabaseManager:
# Configure SQLite for better performance and reliability # Configure SQLite for better performance and reliability
with self.engine.connect() as conn: with self.engine.connect() as conn:
# Use WAL mode with proper checkpoint configuration to fix bet insertion issues
conn.execute(text("PRAGMA journal_mode=WAL")) conn.execute(text("PRAGMA journal_mode=WAL"))
conn.execute(text("PRAGMA synchronous=NORMAL")) conn.execute(text("PRAGMA synchronous=FULL")) # Ensure data is written synchronously
conn.execute(text("PRAGMA cache_size=10000")) conn.execute(text("PRAGMA cache_size=10000"))
conn.execute(text("PRAGMA temp_store=MEMORY")) conn.execute(text("PRAGMA temp_store=MEMORY"))
conn.execute(text("PRAGMA mmap_size=268435456")) # 256MB conn.execute(text("PRAGMA mmap_size=268435456")) # 256MB
conn.execute(text("PRAGMA foreign_keys=ON")) # Enable foreign key constraints
conn.commit() conn.commit()
# Create session factory # Create session factory
...@@ -176,7 +180,9 @@ class DatabaseManager: ...@@ -176,7 +180,9 @@ class DatabaseManager:
"""Get database session""" """Get database session"""
if not self._initialized: if not self._initialized:
raise RuntimeError("Database manager not initialized") raise RuntimeError("Database manager not initialized")
return self.Session() session = self.Session()
logger.debug(f"DEBUG: Database manager returning session for database: {self.db_path}")
return session
def close(self): def close(self):
"""Close database connections""" """Close database connections"""
......
...@@ -4,7 +4,7 @@ SQLAlchemy database models for MbetterClient ...@@ -4,7 +4,7 @@ SQLAlchemy database models for MbetterClient
import json import json
import hashlib import hashlib
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from typing import Dict, Any, Optional, List from typing import Dict, Any, Optional, List
from sqlalchemy import ( from sqlalchemy import (
Column, Integer, String, Text, DateTime, Boolean, Float, Column, Integer, String, Text, DateTime, Boolean, Float,
......
...@@ -217,8 +217,8 @@ def format_bet_id_for_barcode(bet_uuid: str, standard: str) -> str: ...@@ -217,8 +217,8 @@ def format_bet_id_for_barcode(bet_uuid: str, standard: str) -> str:
clean_uuid = bet_uuid.replace('-', '').upper() clean_uuid = bet_uuid.replace('-', '').upper()
if standard in ['code128', 'code39']: if standard in ['code128', 'code39']:
# These support alphanumeric, use first 16 characters # These support alphanumeric, use full UUID for maximum uniqueness
return clean_uuid[:16] return clean_uuid
elif standard in ['ean13', 'ean8', 'upca', 'upce', 'itf', 'codabar']: elif standard in ['ean13', 'ean8', 'upca', 'upce', 'itf', 'codabar']:
# These require numeric data # These require numeric data
......
...@@ -7,7 +7,7 @@ import secrets ...@@ -7,7 +7,7 @@ import secrets
import logging import logging
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional, Dict, Any, Tuple, List from typing import Optional, Dict, Any, Tuple, List
from flask import Flask, request, session from flask import Flask, request, session, jsonify
from flask_login import UserMixin from flask_login import UserMixin
from flask_jwt_extended import create_access_token, decode_token from flask_jwt_extended import create_access_token, decode_token
import jwt import jwt
...@@ -478,24 +478,113 @@ class AuthManager: ...@@ -478,24 +478,113 @@ class AuthManager:
def decorator(func): def decorator(func):
@wraps(func) @wraps(func)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
print(f"AUTH_DECORATOR: Called for {request.path}")
auth_header = request.headers.get('Authorization') auth_header = request.headers.get('Authorization')
print(f"AUTH_DECORATOR: Auth header: {auth_header}")
# Check if request is from localhost or 127.0.0.1 - auto-authenticate as admin
if request.remote_addr in ['127.0.0.1', 'localhost']:
print("AUTH_DECORATOR: Localhost request detected - auto-authenticating as admin")
request.current_user = {
'user_id': 0,
'username': 'localhost_admin',
'is_admin': True,
'role': 'admin'
}
return func(*args, **kwargs)
if auth_header and auth_header.startswith('Bearer '): if auth_header and auth_header.startswith('Bearer '):
token = auth_header.split(' ', 1)[1] token = auth_header.split(' ', 1)[1]
print(f"AUTH_DECORATOR: Token received: {token[:20]}...")
# Try JWT token first # Try JWT token first
payload = self.verify_jwt_token(token) payload = self.verify_jwt_token(token)
if payload: if payload:
print(f"AUTH_DECORATOR: JWT token verified for user: {payload.get('username')}")
request.current_user = payload request.current_user = payload
return func(*args, **kwargs) return func(*args, **kwargs)
else:
print("AUTH_DECORATOR: JWT token verification failed")
# Try API token # Try API token
api_data = self.verify_api_token(token) api_data = self.verify_api_token(token)
if api_data: if api_data:
print(f"AUTH_DECORATOR: API token verified for user: {api_data.get('username')}")
request.current_user = api_data request.current_user = api_data
return func(*args, **kwargs) return func(*args, **kwargs)
else:
print("AUTH_DECORATOR: API token verification failed")
else:
print("AUTH_DECORATOR: No Bearer token in Authorization header")
print("AUTH_DECORATOR: Authentication failed, returning 401")
return jsonify({'error': 'Authentication required'}), 401
return decorated_function
# If called without arguments, return the decorator
if f is None:
return decorator
# If called with a function, apply the decorator immediately
else:
return decorator(f)
def require_api_auth(f=None):
"""Standalone API auth decorator that uses g.auth_manager"""
from functools import wraps
from flask import g
def decorator(func):
@wraps(func)
def decorated_function(*args, **kwargs):
print(f"API_AUTH_DECORATOR: Called for {request.path}")
auth_header = request.headers.get('Authorization')
print(f"API_AUTH_DECORATOR: Auth header: {auth_header}")
# Get auth_manager from Flask g context
auth_manager = g.get('auth_manager')
if not auth_manager:
print("API_AUTH_DECORATOR: No auth_manager in g context, falling back to 401")
return jsonify({'error': 'Authentication system not available'}), 401
# Check if request is from localhost or 127.0.0.1 - auto-authenticate as admin
if request.remote_addr in ['127.0.0.1', 'localhost']:
print("API_AUTH_DECORATOR: Localhost request detected - auto-authenticating as admin")
request.current_user = {
'user_id': 0,
'username': 'localhost_admin',
'is_admin': True,
'role': 'admin'
}
return func(*args, **kwargs)
if auth_header and auth_header.startswith('Bearer '):
token = auth_header.split(' ', 1)[1]
print(f"API_AUTH_DECORATOR: Token received: {token[:20]}...")
# Try JWT token first
payload = auth_manager.verify_jwt_token(token)
if payload:
print(f"API_AUTH_DECORATOR: JWT token verified for user: {payload.get('username')}")
request.current_user = payload
return func(*args, **kwargs)
else:
print("API_AUTH_DECORATOR: JWT token verification failed")
# Try API token
api_data = auth_manager.verify_api_token(token)
if api_data:
print(f"API_AUTH_DECORATOR: API token verified for user: {api_data.get('username')}")
request.current_user = api_data
return func(*args, **kwargs)
else:
print("API_AUTH_DECORATOR: API token verification failed")
else:
print("API_AUTH_DECORATOR: No Bearer token in Authorization header")
return {'error': 'Authentication required'}, 401 print("API_AUTH_DECORATOR: Authentication failed, returning 401")
return jsonify({'error': 'Authentication required'}), 401
return decorated_function return decorated_function
...@@ -514,13 +603,13 @@ class AuthManager: ...@@ -514,13 +603,13 @@ class AuthManager:
@wraps(func) @wraps(func)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if not hasattr(request, 'current_user'): if not hasattr(request, 'current_user'):
return {'error': 'Authentication required'}, 401 return jsonify({'error': 'Authentication required'}), 401
user_role = request.current_user.get('role', 'normal') user_role = request.current_user.get('role', 'normal')
is_admin = request.current_user.get('is_admin', False) is_admin = request.current_user.get('is_admin', False)
if user_role != 'admin' and not is_admin: if user_role != 'admin' and not is_admin:
return {'error': 'Admin access required'}, 403 return jsonify({'error': 'Admin access required'}), 403
return func(*args, **kwargs) return func(*args, **kwargs)
...@@ -541,12 +630,12 @@ class AuthManager: ...@@ -541,12 +630,12 @@ class AuthManager:
@wraps(f) @wraps(f)
def decorated_function(*args, **kwargs): def decorated_function(*args, **kwargs):
if not hasattr(request, 'current_user'): if not hasattr(request, 'current_user'):
return {'error': 'Authentication required'}, 401 return jsonify({'error': 'Authentication required'}), 401
user_role = request.current_user.get('role', 'normal') user_role = request.current_user.get('role', 'normal')
if user_role not in allowed_roles: if user_role not in allowed_roles:
return {'error': f'Access denied. Required roles: {", ".join(allowed_roles)}'}, 403 return jsonify({'error': f'Access denied. Required roles: {", ".join(allowed_roles)}'}), 403
return f(*args, **kwargs) return f(*args, **kwargs)
......
...@@ -5,12 +5,13 @@ Flask routes for web dashboard ...@@ -5,12 +5,13 @@ Flask routes for web dashboard
import logging import logging
import time import time
import json import json
from datetime import datetime, date from datetime import datetime, date, timezone
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash, session, g from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash, session, g
from flask_login import login_required, current_user, login_user, logout_user from flask_login import login_required, current_user, login_user, logout_user
from flask_socketio import emit, join_room, leave_room from flask_socketio import emit, join_room, leave_room
from werkzeug.security import check_password_hash from werkzeug.security import check_password_hash
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
from sqlalchemy import text
from .auth import AuthenticatedUser from .auth import AuthenticatedUser
from ..core.message_bus import Message, MessageType from ..core.message_bus import Message, MessageType
...@@ -25,6 +26,41 @@ def conditional_auth_decorator(condition, auth_decorator, fallback_decorator=log ...@@ -25,6 +26,41 @@ def conditional_auth_decorator(condition, auth_decorator, fallback_decorator=log
return fallback_decorator(func) return fallback_decorator(func)
return decorator return decorator
def get_api_auth_decorator(require_admin=False):
"""Get API auth decorator that works with lazy initialization"""
def decorator(func):
from functools import wraps
@wraps(func)
def decorated_function(*args, **kwargs):
# Get auth_manager from blueprint context (set during app initialization)
auth_manager = getattr(api_bp, 'auth_manager', None)
if auth_manager:
# Use the auth manager's require_auth method
if require_admin:
# Check if user is admin after authentication
@auth_manager.require_auth
def admin_check(*args, **kwargs):
from flask import request
if not hasattr(request, 'current_user'):
return jsonify({'error': 'Authentication required'}), 401
user_role = request.current_user.get('role', 'normal')
is_admin = request.current_user.get('is_admin', False)
if user_role != 'admin' and not is_admin:
return jsonify({'error': 'Admin access required'}), 403
return func(*args, **kwargs)
return admin_check(*args, **kwargs)
else:
return auth_manager.require_auth(func)(*args, **kwargs)
else:
# Fallback to login_required if auth_manager not available
return login_required(func)(*args, **kwargs)
return decorated_function
return decorator
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Blueprint definitions # Blueprint definitions
...@@ -41,6 +77,7 @@ auth_bp.auth_manager = None ...@@ -41,6 +77,7 @@ auth_bp.auth_manager = None
auth_bp.db_manager = None auth_bp.db_manager = None
api_bp.api = None api_bp.api = None
api_bp.auth_manager = None
api_bp.db_manager = None api_bp.db_manager = None
api_bp.config_manager = None api_bp.config_manager = None
api_bp.message_bus = None api_bp.message_bus = None
...@@ -224,6 +261,8 @@ def bet_details(bet_id): ...@@ -224,6 +261,8 @@ def bet_details(bet_id):
'total_amount': total_amount, 'total_amount': total_amount,
'bet_count': len(bet_details_data), 'bet_count': len(bet_details_data),
'has_pending': has_pending, 'has_pending': has_pending,
'barcode_standard': bet.barcode_standard,
'barcode_data': bet.barcode_data,
'bet_details': bet_details_data 'bet_details': bet_details_data
} }
...@@ -681,6 +720,8 @@ def cashier_bet_details(bet_id): ...@@ -681,6 +720,8 @@ def cashier_bet_details(bet_id):
'total_amount': total_amount, 'total_amount': total_amount,
'bet_count': len(bet_details_data), 'bet_count': len(bet_details_data),
'has_pending': has_pending, 'has_pending': has_pending,
'barcode_standard': bet.barcode_standard,
'barcode_data': bet.barcode_data,
'bet_details': bet_details_data 'bet_details': bet_details_data
} }
...@@ -869,7 +910,7 @@ def debug_match_status(): ...@@ -869,7 +910,7 @@ def debug_match_status():
@api_bp.route('/video/status') @api_bp.route('/video/status')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def video_status(): def video_status():
"""Get video player status""" """Get video player status"""
try: try:
...@@ -881,7 +922,7 @@ def video_status(): ...@@ -881,7 +922,7 @@ def video_status():
@api_bp.route('/video/control', methods=['POST']) @api_bp.route('/video/control', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def video_control(): def video_control():
"""Control video player with enhanced looping support""" """Control video player with enhanced looping support"""
try: try:
...@@ -990,7 +1031,7 @@ def video_control(): ...@@ -990,7 +1031,7 @@ def video_control():
@api_bp.route('/overlay', methods=['POST']) @api_bp.route('/overlay', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def update_overlay(): def update_overlay():
"""Update video overlay""" """Update video overlay"""
try: try:
...@@ -1010,7 +1051,7 @@ def update_overlay(): ...@@ -1010,7 +1051,7 @@ def update_overlay():
@api_bp.route('/templates') @api_bp.route('/templates')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_templates(): def get_templates():
"""Get available templates""" """Get available templates"""
try: try:
...@@ -1022,7 +1063,7 @@ def get_templates(): ...@@ -1022,7 +1063,7 @@ def get_templates():
@api_bp.route('/config') @api_bp.route('/config')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_configuration(): def get_configuration():
"""Get configuration""" """Get configuration"""
try: try:
...@@ -1035,7 +1076,7 @@ def get_configuration(): ...@@ -1035,7 +1076,7 @@ def get_configuration():
@api_bp.route('/config/<section>') @api_bp.route('/config/<section>')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_config_section(section): def get_config_section(section):
"""Get configuration section""" """Get configuration section"""
try: try:
...@@ -1047,8 +1088,8 @@ def get_config_section(section): ...@@ -1047,8 +1088,8 @@ def get_config_section(section):
@api_bp.route('/config/<section>', methods=['POST']) @api_bp.route('/config/<section>', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def update_config_section(section): def update_config_section(section):
"""Update configuration section""" """Update configuration section"""
try: try:
...@@ -1077,8 +1118,8 @@ def update_config_section(section): ...@@ -1077,8 +1118,8 @@ def update_config_section(section):
@api_bp.route('/config', methods=['POST']) @api_bp.route('/config', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def update_configuration(): def update_configuration():
"""Update configuration""" """Update configuration"""
try: try:
...@@ -1098,7 +1139,7 @@ def update_configuration(): ...@@ -1098,7 +1139,7 @@ def update_configuration():
@api_bp.route('/config/match-interval') @api_bp.route('/config/match-interval')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_match_interval(): def get_match_interval():
"""Get match interval configuration""" """Get match interval configuration"""
try: try:
...@@ -1118,7 +1159,7 @@ def get_match_interval(): ...@@ -1118,7 +1159,7 @@ def get_match_interval():
@api_bp.route('/config/match-interval', methods=['POST']) @api_bp.route('/config/match-interval', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def set_match_interval(): def set_match_interval():
"""Set match interval configuration""" """Set match interval configuration"""
try: try:
...@@ -1170,7 +1211,7 @@ def set_match_interval(): ...@@ -1170,7 +1211,7 @@ def set_match_interval():
@api_bp.route('/config/license-text') @api_bp.route('/config/license-text')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_license_text(): def get_license_text():
"""Get license text configuration""" """Get license text configuration"""
try: try:
...@@ -1201,7 +1242,7 @@ def get_license_text(): ...@@ -1201,7 +1242,7 @@ def get_license_text():
@api_bp.route('/config/license-text', methods=['POST']) @api_bp.route('/config/license-text', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def set_license_text(): def set_license_text():
"""Set license text configuration""" """Set license text configuration"""
try: try:
...@@ -1259,8 +1300,8 @@ def set_license_text(): ...@@ -1259,8 +1300,8 @@ def set_license_text():
@api_bp.route('/config/test-connection', methods=['POST']) @api_bp.route('/config/test-connection', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def test_api_connection(): def test_api_connection():
"""Test connection to FastAPI server using request data or configured values""" """Test connection to FastAPI server using request data or configured values"""
try: try:
...@@ -1339,8 +1380,8 @@ def test_api_connection(): ...@@ -1339,8 +1380,8 @@ def test_api_connection():
@api_bp.route('/users') @api_bp.route('/users')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def get_users(): def get_users():
"""Get all users""" """Get all users"""
try: try:
...@@ -1352,8 +1393,8 @@ def get_users(): ...@@ -1352,8 +1393,8 @@ def get_users():
@api_bp.route('/users', methods=['POST']) @api_bp.route('/users', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def create_user(): def create_user():
"""Create new user""" """Create new user"""
try: try:
...@@ -1380,8 +1421,8 @@ def create_user(): ...@@ -1380,8 +1421,8 @@ def create_user():
@api_bp.route('/users/<int:user_id>', methods=['PUT']) @api_bp.route('/users/<int:user_id>', methods=['PUT'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def update_user(user_id): def update_user(user_id):
"""Update user""" """Update user"""
try: try:
...@@ -1405,8 +1446,8 @@ def update_user(user_id): ...@@ -1405,8 +1446,8 @@ def update_user(user_id):
@api_bp.route('/users/<int:user_id>', methods=['DELETE']) @api_bp.route('/users/<int:user_id>', methods=['DELETE'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def delete_user(user_id): def delete_user(user_id):
"""Delete user""" """Delete user"""
try: try:
...@@ -1418,7 +1459,7 @@ def delete_user(user_id): ...@@ -1418,7 +1459,7 @@ def delete_user(user_id):
@api_bp.route('/tokens') @api_bp.route('/tokens')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_api_tokens(): def get_api_tokens():
"""Get API tokens for current user""" """Get API tokens for current user"""
try: try:
...@@ -1436,7 +1477,7 @@ def get_api_tokens(): ...@@ -1436,7 +1477,7 @@ def get_api_tokens():
@api_bp.route('/tokens', methods=['POST']) @api_bp.route('/tokens', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def create_api_token(): def create_api_token():
"""Create API token""" """Create API token"""
try: try:
...@@ -1461,7 +1502,7 @@ def create_api_token(): ...@@ -1461,7 +1502,7 @@ def create_api_token():
@api_bp.route('/tokens/<int:token_id>', methods=['DELETE']) @api_bp.route('/tokens/<int:token_id>', methods=['DELETE'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def revoke_api_token(token_id): def revoke_api_token(token_id):
"""Revoke API token""" """Revoke API token"""
try: try:
...@@ -1479,8 +1520,8 @@ def revoke_api_token(token_id): ...@@ -1479,8 +1520,8 @@ def revoke_api_token(token_id):
@api_bp.route('/logs') @api_bp.route('/logs')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def get_logs(): def get_logs():
"""Get application logs""" """Get application logs"""
try: try:
...@@ -1496,8 +1537,8 @@ def get_logs(): ...@@ -1496,8 +1537,8 @@ def get_logs():
@api_bp.route('/test-message', methods=['POST']) @api_bp.route('/test-message', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def send_test_message(): def send_test_message():
"""Send test message to component""" """Send test message to component"""
try: try:
...@@ -1519,7 +1560,7 @@ def send_test_message(): ...@@ -1519,7 +1560,7 @@ def send_test_message():
# Video upload and delete routes # Video upload and delete routes
@api_bp.route('/video/upload', methods=['POST']) @api_bp.route('/video/upload', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def upload_video(): def upload_video():
"""Upload video file""" """Upload video file"""
try: try:
...@@ -1557,7 +1598,7 @@ def upload_video(): ...@@ -1557,7 +1598,7 @@ def upload_video():
@api_bp.route('/video/delete', methods=['POST']) @api_bp.route('/video/delete', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def delete_video(): def delete_video():
"""Delete uploaded video""" """Delete uploaded video"""
try: try:
...@@ -1609,8 +1650,8 @@ def create_auth_token(): ...@@ -1609,8 +1650,8 @@ def create_auth_token():
logger.error(f"Token creation error: {e}") logger.error(f"Token creation error: {e}")
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@api_bp.route('/system/shutdown', methods=['POST']) @api_bp.route('/system/shutdown', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def shutdown_application(): def shutdown_application():
"""Shutdown the application (admin only)""" """Shutdown the application (admin only)"""
try: try:
...@@ -1639,8 +1680,8 @@ def shutdown_application(): ...@@ -1639,8 +1680,8 @@ def shutdown_application():
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@api_bp.route('/templates/upload', methods=['POST']) @api_bp.route('/templates/upload', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def upload_template(): def upload_template():
"""Upload template file""" """Upload template file"""
try: try:
...@@ -1663,7 +1704,7 @@ def upload_template(): ...@@ -1663,7 +1704,7 @@ def upload_template():
@api_bp.route('/templates/<template_name>', methods=['GET']) @api_bp.route('/templates/<template_name>', methods=['GET'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_template_content(template_name): def get_template_content(template_name):
"""Get template content for preview""" """Get template content for preview"""
try: try:
...@@ -1699,8 +1740,8 @@ def get_template_content(template_name): ...@@ -1699,8 +1740,8 @@ def get_template_content(template_name):
@api_bp.route('/templates/<template_name>', methods=['DELETE']) @api_bp.route('/templates/<template_name>', methods=['DELETE'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def delete_template(template_name): def delete_template(template_name):
"""Delete uploaded template""" """Delete uploaded template"""
try: try:
...@@ -1713,7 +1754,7 @@ def delete_template(template_name): ...@@ -1713,7 +1754,7 @@ def delete_template(template_name):
@api_bp.route('/outcome-assignments') @api_bp.route('/outcome-assignments')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_outcome_assignments(): def get_outcome_assignments():
"""Get outcome template assignments""" """Get outcome template assignments"""
try: try:
...@@ -1748,8 +1789,8 @@ def get_outcome_assignments(): ...@@ -1748,8 +1789,8 @@ def get_outcome_assignments():
@api_bp.route('/outcome-assignments', methods=['POST']) @api_bp.route('/outcome-assignments', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def save_outcome_assignments(): def save_outcome_assignments():
"""Save outcome template assignments (admin only)""" """Save outcome template assignments (admin only)"""
try: try:
...@@ -1799,7 +1840,7 @@ def save_outcome_assignments(): ...@@ -1799,7 +1840,7 @@ def save_outcome_assignments():
@api_bp.route('/send-custom-message', methods=['POST']) @api_bp.route('/send-custom-message', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def send_custom_message(): def send_custom_message():
"""Send custom message to overlay with template selection and display time support""" """Send custom message to overlay with template selection and display time support"""
try: try:
...@@ -1911,7 +1952,7 @@ def send_custom_message(): ...@@ -1911,7 +1952,7 @@ def send_custom_message():
@api_bp.route('/intro-templates', methods=['GET']) @api_bp.route('/intro-templates', methods=['GET'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_intro_templates(): def get_intro_templates():
"""Get intro templates configuration""" """Get intro templates configuration"""
try: try:
...@@ -1956,7 +1997,7 @@ def get_intro_templates(): ...@@ -1956,7 +1997,7 @@ def get_intro_templates():
@api_bp.route('/intro-templates', methods=['POST']) @api_bp.route('/intro-templates', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def save_intro_templates(): def save_intro_templates():
"""Save intro templates configuration""" """Save intro templates configuration"""
try: try:
...@@ -2295,7 +2336,7 @@ def auto_fail_old_fixtures(session, cutoff_date): ...@@ -2295,7 +2336,7 @@ def auto_fail_old_fixtures(session, cutoff_date):
@api_bp.route('/cashier/start-games', methods=['POST']) @api_bp.route('/cashier/start-games', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def start_games(): def start_games():
"""Start games for the first fixture - send START_GAME message to message bus""" """Start games for the first fixture - send START_GAME message to message bus"""
try: try:
...@@ -2422,8 +2463,8 @@ def get_fixture_details(fixture_id): ...@@ -2422,8 +2463,8 @@ def get_fixture_details(fixture_id):
@api_bp.route('/fixtures/reset', methods=['POST']) @api_bp.route('/fixtures/reset', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @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, and ZIP files"""
try: try:
...@@ -2484,7 +2525,7 @@ def reset_fixtures(): ...@@ -2484,7 +2525,7 @@ def reset_fixtures():
@api_bp.route('/api-client/status') @api_bp.route('/api-client/status')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_api_client_status(): def get_api_client_status():
"""Get API client status and endpoint information""" """Get API client status and endpoint information"""
try: try:
...@@ -2539,8 +2580,8 @@ def get_api_client_status(): ...@@ -2539,8 +2580,8 @@ def get_api_client_status():
@api_bp.route('/api-client/trigger', methods=['POST']) @api_bp.route('/api-client/trigger', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def trigger_api_request(): def trigger_api_request():
"""Manually trigger an API request for testing""" """Manually trigger an API request for testing"""
try: try:
...@@ -2575,7 +2616,7 @@ def trigger_api_request(): ...@@ -2575,7 +2616,7 @@ def trigger_api_request():
@api_bp.route('/match-timer/config') @api_bp.route('/match-timer/config')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_match_timer_config(): def get_match_timer_config():
"""Get match timer configuration""" """Get match timer configuration"""
try: try:
...@@ -2595,7 +2636,7 @@ def get_match_timer_config(): ...@@ -2595,7 +2636,7 @@ def get_match_timer_config():
@api_bp.route('/match-timer/start-match', methods=['POST']) @api_bp.route('/match-timer/start-match', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def start_next_match(): def start_next_match():
"""Start the next match by sending MATCH_START message""" """Start the next match by sending MATCH_START message"""
try: try:
...@@ -2754,7 +2795,7 @@ def get_server_time(): ...@@ -2754,7 +2795,7 @@ def get_server_time():
@api_bp.route('/notifications') @api_bp.route('/notifications')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def notifications(): def notifications():
"""Long polling endpoint for real-time notifications""" """Long polling endpoint for real-time notifications"""
try: try:
...@@ -2855,7 +2896,7 @@ def notifications(): ...@@ -2855,7 +2896,7 @@ def notifications():
# Extraction API routes # Extraction API routes
@api_bp.route('/extraction/outcomes') @api_bp.route('/extraction/outcomes')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_extraction_outcomes(): def get_extraction_outcomes():
"""Get all available outcomes for extraction""" """Get all available outcomes for extraction"""
try: try:
...@@ -2883,7 +2924,13 @@ def get_extraction_outcomes(): ...@@ -2883,7 +2924,13 @@ def get_extraction_outcomes():
}) })
finally: finally:
logger.debug(f"DEBUG: Session being closed. Session is active: {session.is_active}")
# Check if session has pending changes when closing
if session.is_active and (session.new or session.dirty or session.deleted):
logger.warning(f"DEBUG: Session has pending changes when closing: new={len(session.new)}, dirty={len(session.dirty)}, deleted={len(session.deleted)}")
# Don't rollback here as it might interfere with successful commits
session.close() session.close()
logger.debug("DEBUG: Session closed")
except Exception as e: except Exception as e:
logger.error(f"API get extraction outcomes error: {e}") logger.error(f"API get extraction outcomes error: {e}")
...@@ -2891,7 +2938,7 @@ def get_extraction_outcomes(): ...@@ -2891,7 +2938,7 @@ def get_extraction_outcomes():
@api_bp.route('/extraction/associations') @api_bp.route('/extraction/associations')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_extraction_associations(): def get_extraction_associations():
"""Get current extraction associations with defaults""" """Get current extraction associations with defaults"""
try: try:
...@@ -2917,7 +2964,7 @@ def get_extraction_associations(): ...@@ -2917,7 +2964,7 @@ def get_extraction_associations():
@api_bp.route('/extraction/associations', methods=['POST']) @api_bp.route('/extraction/associations', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def save_extraction_associations(): def save_extraction_associations():
"""Save extraction associations - unlimited associations per outcome""" """Save extraction associations - unlimited associations per outcome"""
try: try:
...@@ -2984,7 +3031,7 @@ def save_extraction_associations(): ...@@ -2984,7 +3031,7 @@ def save_extraction_associations():
@api_bp.route('/extraction/associations/add', methods=['POST']) @api_bp.route('/extraction/associations/add', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def add_extraction_association(): def add_extraction_association():
"""Add a single extraction association""" """Add a single extraction association"""
try: try:
...@@ -3075,7 +3122,7 @@ def add_extraction_association(): ...@@ -3075,7 +3122,7 @@ def add_extraction_association():
@api_bp.route('/extraction/associations/remove', methods=['POST']) @api_bp.route('/extraction/associations/remove', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def remove_extraction_association(): def remove_extraction_association():
"""Remove a specific extraction association""" """Remove a specific extraction association"""
try: try:
...@@ -3135,7 +3182,7 @@ def remove_extraction_association(): ...@@ -3135,7 +3182,7 @@ def remove_extraction_association():
@api_bp.route('/extraction/config') @api_bp.route('/extraction/config')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_extraction_config(): def get_extraction_config():
"""Get extraction configuration""" """Get extraction configuration"""
try: try:
...@@ -3165,7 +3212,7 @@ def get_extraction_config(): ...@@ -3165,7 +3212,7 @@ def get_extraction_config():
@api_bp.route('/extraction/config', methods=['POST']) @api_bp.route('/extraction/config', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def update_extraction_config(): def update_extraction_config():
"""Update extraction configuration""" """Update extraction configuration"""
try: try:
...@@ -3221,7 +3268,7 @@ def update_extraction_config(): ...@@ -3221,7 +3268,7 @@ def update_extraction_config():
# Betting Mode API routes # Betting Mode API routes
@api_bp.route('/betting-mode') @api_bp.route('/betting-mode')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_betting_mode(): def get_betting_mode():
"""Get global betting mode configuration""" """Get global betting mode configuration"""
try: try:
...@@ -3258,8 +3305,8 @@ def get_betting_mode(): ...@@ -3258,8 +3305,8 @@ def get_betting_mode():
@api_bp.route('/betting-mode', methods=['POST']) @api_bp.route('/betting-mode', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def set_betting_mode(): def set_betting_mode():
"""Set global betting mode configuration (admin only)""" """Set global betting mode configuration (admin only)"""
try: try:
...@@ -3318,7 +3365,7 @@ def set_betting_mode(): ...@@ -3318,7 +3365,7 @@ def set_betting_mode():
# Extraction Configuration Persistence API routes # Extraction Configuration Persistence API routes
@api_bp.route('/extraction/under-over-config') @api_bp.route('/extraction/under-over-config')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_under_over_config(): def get_under_over_config():
"""Get UNDER/OVER zone configurations with defaults""" """Get UNDER/OVER zone configurations with defaults"""
try: try:
...@@ -3410,7 +3457,7 @@ def get_under_over_config(): ...@@ -3410,7 +3457,7 @@ def get_under_over_config():
@api_bp.route('/extraction/under-over-config', methods=['POST']) @api_bp.route('/extraction/under-over-config', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def save_under_over_config(): def save_under_over_config():
"""Save UNDER/OVER zone configurations""" """Save UNDER/OVER zone configurations"""
try: try:
...@@ -3477,7 +3524,7 @@ def save_under_over_config(): ...@@ -3477,7 +3524,7 @@ def save_under_over_config():
@api_bp.route('/extraction/results-config') @api_bp.route('/extraction/results-config')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_results_config(): def get_results_config():
"""Get Results area configurations with defaults""" """Get Results area configurations with defaults"""
try: try:
...@@ -3541,7 +3588,7 @@ def get_results_config(): ...@@ -3541,7 +3588,7 @@ def get_results_config():
@api_bp.route('/extraction/results-config', methods=['POST']) @api_bp.route('/extraction/results-config', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def save_results_config(): def save_results_config():
"""Save Results area configurations""" """Save Results area configurations"""
try: try:
...@@ -3590,7 +3637,7 @@ def save_results_config(): ...@@ -3590,7 +3637,7 @@ def save_results_config():
# Available Bets API routes # Available Bets API routes
@api_bp.route('/extraction/available-bets') @api_bp.route('/extraction/available-bets')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_available_bets(): def get_available_bets():
"""Get all available bets for extraction""" """Get all available bets for extraction"""
try: try:
...@@ -3617,7 +3664,7 @@ def get_available_bets(): ...@@ -3617,7 +3664,7 @@ def get_available_bets():
@api_bp.route('/extraction/available-bets/add', methods=['POST']) @api_bp.route('/extraction/available-bets/add', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def add_available_bet(): def add_available_bet():
"""Add a new available bet""" """Add a new available bet"""
try: try:
...@@ -3665,7 +3712,7 @@ def add_available_bet(): ...@@ -3665,7 +3712,7 @@ def add_available_bet():
@api_bp.route('/extraction/available-bets/delete', methods=['POST']) @api_bp.route('/extraction/available-bets/delete', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def delete_available_bet(): def delete_available_bet():
"""Delete an available bet and all associated associations""" """Delete an available bet and all associated associations"""
try: try:
...@@ -3711,7 +3758,7 @@ def delete_available_bet(): ...@@ -3711,7 +3758,7 @@ def delete_available_bet():
# Result Options API routes # Result Options API routes
@api_bp.route('/extraction/result-options') @api_bp.route('/extraction/result-options')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_result_options(): def get_result_options():
"""Get all result options for extraction""" """Get all result options for extraction"""
try: try:
...@@ -3738,7 +3785,7 @@ def get_result_options(): ...@@ -3738,7 +3785,7 @@ def get_result_options():
@api_bp.route('/extraction/result-options/add', methods=['POST']) @api_bp.route('/extraction/result-options/add', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def add_result_option(): def add_result_option():
"""Add a new result option""" """Add a new result option"""
try: try:
...@@ -3791,7 +3838,7 @@ def add_result_option(): ...@@ -3791,7 +3838,7 @@ def add_result_option():
@api_bp.route('/extraction/result-options/delete', methods=['POST']) @api_bp.route('/extraction/result-options/delete', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def delete_result_option(): def delete_result_option():
"""Delete a result option and all associated associations""" """Delete a result option and all associated associations"""
try: try:
...@@ -3839,8 +3886,8 @@ def delete_result_option(): ...@@ -3839,8 +3886,8 @@ def delete_result_option():
# Redistribution CAP API routes (admin-only) # Redistribution CAP API routes (admin-only)
@api_bp.route('/extraction/redistribution-cap') @api_bp.route('/extraction/redistribution-cap')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def get_redistribution_cap(): def get_redistribution_cap():
"""Get redistribution CAP configuration (admin only)""" """Get redistribution CAP configuration (admin only)"""
try: try:
...@@ -3875,8 +3922,8 @@ def get_redistribution_cap(): ...@@ -3875,8 +3922,8 @@ def get_redistribution_cap():
@api_bp.route('/extraction/redistribution-cap', methods=['POST']) @api_bp.route('/extraction/redistribution-cap', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def save_redistribution_cap(): def save_redistribution_cap():
"""Save redistribution CAP configuration (admin only)""" """Save redistribution CAP configuration (admin only)"""
try: try:
...@@ -3936,7 +3983,7 @@ def save_redistribution_cap(): ...@@ -3936,7 +3983,7 @@ def save_redistribution_cap():
# Currency Settings API routes # Currency Settings API routes
@api_bp.route('/currency-settings') @api_bp.route('/currency-settings')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_currency_settings(): def get_currency_settings():
"""Get currency symbol configuration""" """Get currency symbol configuration"""
try: try:
...@@ -3961,8 +4008,8 @@ def get_currency_settings(): ...@@ -3961,8 +4008,8 @@ def get_currency_settings():
@api_bp.route('/currency-settings', methods=['POST']) @api_bp.route('/currency-settings', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def set_currency_settings(): def set_currency_settings():
"""Set currency symbol configuration (admin only)""" """Set currency symbol configuration (admin only)"""
try: try:
...@@ -4006,7 +4053,7 @@ def set_currency_settings(): ...@@ -4006,7 +4053,7 @@ def set_currency_settings():
# Server-side Match Timer API endpoints # Server-side Match Timer API endpoints
@api_bp.route('/match-timer/state') @api_bp.route('/match-timer/state')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_match_timer_state(): def get_match_timer_state():
"""Get current match timer state from server-side component""" """Get current match timer state from server-side component"""
try: try:
...@@ -4036,8 +4083,8 @@ def get_match_timer_state(): ...@@ -4036,8 +4083,8 @@ def get_match_timer_state():
@api_bp.route('/match-timer/control', methods=['POST']) @api_bp.route('/match-timer/control', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def control_match_timer(): def control_match_timer():
"""Control the match timer (admin only)""" """Control the match timer (admin only)"""
try: try:
...@@ -4101,12 +4148,12 @@ def control_match_timer(): ...@@ -4101,12 +4148,12 @@ def control_match_timer():
# Cashier Betting API endpoints # Cashier Betting API endpoints
@api_bp.route('/cashier/bets') @api_bp.route('/cashier/bets')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_cashier_bets(): def get_cashier_bets():
"""Get bets for a specific date (cashier)""" """Get bets for a specific date (cashier)"""
try: try:
from ..database.models import BetModel, BetDetailModel from ..database.models import BetModel, BetDetailModel
from datetime import datetime, date from datetime import datetime, date, timezone, timedelta
# Get date parameter (default to today) # Get date parameter (default to today)
date_param = request.args.get('date') date_param = request.args.get('date')
...@@ -4121,12 +4168,48 @@ def get_cashier_bets(): ...@@ -4121,12 +4168,48 @@ def get_cashier_bets():
session = api_bp.db_manager.get_session() session = api_bp.db_manager.get_session()
try: try:
# Get all bets for the target date # Get all bets for the target date
# The date picker sends dates in YYYY-MM-DD format representing local dates
# Since the user is in UTC+2, we need to find bets from the previous day in UTC
# For example, if user selects "2025-12-17", they want bets from 2025-12-17 in local time
# Which is 2025-12-16 22:00 UTC to 2025-12-17 21:59 UTC
# Create the date range in UTC directly
# Since bets are stored in UTC, we need to query for the entire day in UTC
# The user selects a date in their local timezone (UTC+2), so we need to find
# bets from that date in local time, which means the previous day in UTC
# For a user in UTC+2 selecting "2025-12-17", they want bets from
# 2025-12-17 00:00 to 23:59 in local time, which is
# 2025-12-16 22:00 to 2025-12-17 21:59 in UTC
utc_offset = timedelta(hours=2)
local_start = datetime.combine(target_date, datetime.min.time())
local_end = datetime.combine(target_date, datetime.max.time())
# Convert local date range to naive UTC datetimes
start_datetime = (local_start - utc_offset)
end_datetime = (local_end - utc_offset)
logger.info(f"Querying bets for local date {date_param}: naive UTC range {start_datetime} to {end_datetime}")
# DEBUG: Log all bets in database for debugging
all_bets = session.query(BetModel).all()
logger.info(f"DEBUG: Total bets in database: {len(all_bets)}")
for bet in all_bets:
logger.info(f"DEBUG: Bet {bet.uuid} datetime: {bet.bet_datetime} (type: {type(bet.bet_datetime)}, tz: {bet.bet_datetime.tzinfo if hasattr(bet.bet_datetime, 'tzinfo') else 'naive'})")
bets_query = session.query(BetModel).filter( bets_query = session.query(BetModel).filter(
BetModel.bet_datetime >= datetime.combine(target_date, datetime.min.time()), BetModel.bet_datetime >= start_datetime,
BetModel.bet_datetime < datetime.combine(target_date, datetime.max.time()) BetModel.bet_datetime <= end_datetime
).order_by(BetModel.bet_datetime.desc()) ).order_by(BetModel.bet_datetime.desc())
# DEBUG: Log the raw query
logger.info(f"DEBUG: Executing query: {bets_query}")
bets = bets_query.all() bets = bets_query.all()
logger.info(f"DEBUG: Found {len(bets)} bets in database for date {date_param}")
for bet in bets:
logger.info(f"DEBUG: Bet {bet.uuid} has datetime: {bet.bet_datetime} (type: {type(bet.bet_datetime)}, tz: {bet.bet_datetime.tzinfo if hasattr(bet.bet_datetime, 'tzinfo') else 'naive'})")
bets_data = [] bets_data = []
# Statistics counters # Statistics counters
...@@ -4187,14 +4270,74 @@ def get_cashier_bets(): ...@@ -4187,14 +4270,74 @@ def get_cashier_bets():
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@api_bp.route('/cashier/bets/test-simple', methods=['POST'])
def create_test_bet_simple():
"""Test endpoint: Create a bet without bet_details to isolate the issue"""
try:
from ..database.models import BetModel
from datetime import datetime
import uuid as uuid_lib
# Get database session
session = api_bp.db_manager.get_session()
# Force enable foreign keys for this session
try:
session.execute(text("PRAGMA foreign_keys=ON"))
logger.debug("Foreign keys enabled for test session")
except Exception as fk_e:
logger.warning(f"Failed to enable foreign keys: {fk_e}")
try:
# Generate UUID for the bet
bet_uuid = str(uuid_lib.uuid4())
# Create a simple bet without bet_details
bet_datetime = datetime.utcnow()
logger.info(f"TEST: Creating simple bet with datetime: {bet_datetime}")
new_bet = BetModel(
uuid=bet_uuid,
bet_datetime=bet_datetime,
fixture_id="test-fixture-123" # Dummy fixture_id
)
session.add(new_bet)
session.flush()
# Test commit
session.commit()
# Verify bet was committed
committed_bet = session.query(BetModel).filter_by(uuid=bet_uuid).first()
if committed_bet:
logger.info(f"TEST: Simple bet {bet_uuid} created and committed successfully")
return jsonify({
"success": True,
"message": "Simple bet created successfully (no bet_details)",
"bet_id": bet_uuid
})
else:
logger.error(f"TEST: Simple bet {bet_uuid} commit appeared successful but bet not found")
return jsonify({"error": "Simple bet commit failed - bet not persisted"}), 500
finally:
session.close()
except Exception as e:
logger.error(f"TEST: Simple bet creation error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/cashier/bets', methods=['POST']) @api_bp.route('/cashier/bets', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def create_cashier_bet(): def create_cashier_bet():
"""Create a new bet (cashier)""" """Create a new bet (cashier) - thread-safe implementation"""
try: try:
from ..database.models import BetModel, BetDetailModel from ..database.models import BetModel, BetDetailModel
from datetime import datetime from datetime import datetime
import uuid as uuid_lib import uuid as uuid_lib
import threading
data = request.get_json() or {} data = request.get_json() or {}
bet_details = data.get('bet_details', []) bet_details = data.get('bet_details', [])
...@@ -4212,27 +4355,67 @@ def create_cashier_bet(): ...@@ -4212,27 +4355,67 @@ def create_cashier_bet():
except (ValueError, TypeError): except (ValueError, TypeError):
return jsonify({"error": "Amount must be a valid number"}), 400 return jsonify({"error": "Amount must be a valid number"}), 400
# Log threading context for debugging
current_thread = threading.current_thread()
logger.debug(f"Bet creation started on thread: {current_thread.name} (ID: {current_thread.ident})")
logger.debug(f"Is main thread: {current_thread is threading.main_thread()}")
# Get database session - scoped_session should handle thread-local sessions
session = api_bp.db_manager.get_session() session = api_bp.db_manager.get_session()
try: try:
# Validate match_id exists for all bet details
from ..database.models import MatchModel
for detail in bet_details:
match_id = detail['match_id']
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
# Generate UUID for the bet # Generate UUID for the bet
bet_uuid = str(uuid_lib.uuid4()) bet_uuid = str(uuid_lib.uuid4())
# Ensure UUID is unique
max_attempts = 10
attempt = 0
while attempt < max_attempts:
existing_bet = session.query(BetModel).filter_by(uuid=bet_uuid).first()
if existing_bet:
bet_uuid = str(uuid_lib.uuid4())
attempt += 1
else:
break
if attempt >= max_attempts:
return jsonify({"error": "Failed to generate unique bet ID"}), 500
# Get fixture_id from the first bet detail's match # Get fixture_id from the first bet detail's match
from ..database.models import MatchModel
first_match_id = bet_details[0]['match_id'] first_match_id = bet_details[0]['match_id']
first_match = session.query(MatchModel).filter_by(id=first_match_id).first() first_match = session.query(MatchModel).filter_by(id=first_match_id).first()
fixture_id = first_match.fixture_id
if not first_match:
return jsonify({"error": f"Match {first_match_id} not found"}), 404
# Create the bet record # Create the bet record
bet_datetime = datetime.utcnow()
# Get barcode configuration if available
barcode_standard = 'none'
barcode_data = None
if api_bp.db_manager:
barcode_standard = api_bp.db_manager.get_config_value('barcode.standard', 'none')
if barcode_standard and barcode_standard != 'none':
# Format bet UUID for barcode
from ..utils.barcode_utils import format_bet_id_for_barcode
barcode_data = format_bet_id_for_barcode(bet_uuid, barcode_standard)
new_bet = BetModel( new_bet = BetModel(
uuid=bet_uuid, uuid=bet_uuid,
bet_datetime=datetime.now(), bet_datetime=bet_datetime,
fixture_id=first_match.fixture_id # Set from first bet detail's match fixture_id=fixture_id,
barcode_standard=barcode_standard,
barcode_data=barcode_data
) )
session.add(new_bet) session.add(new_bet)
session.flush() # Get the bet ID
# Create bet details # Create bet details
for detail_data in bet_details: for detail_data in bet_details:
...@@ -4245,45 +4428,41 @@ def create_cashier_bet(): ...@@ -4245,45 +4428,41 @@ def create_cashier_bet():
) )
session.add(bet_detail) session.add(bet_detail)
# Generate and store barcode data if enabled # Commit the transaction with proper error handling
try: try:
from ..utils.barcode_utils import format_bet_id_for_barcode
# Get barcode configuration
if api_bp.db_manager:
barcode_enabled = api_bp.db_manager.get_config_value('barcode.enabled', False)
barcode_standard = api_bp.db_manager.get_config_value('barcode.standard', 'none')
if barcode_enabled and barcode_standard != 'none':
# Generate barcode data for the bet
barcode_data = format_bet_id_for_barcode(bet_uuid, barcode_standard)
# Update the bet with barcode information
new_bet.barcode_standard = barcode_standard
new_bet.barcode_data = barcode_data
session.commit() session.commit()
logger.info(f"Bet {bet_uuid} committed successfully on thread {current_thread.name}")
except Exception as commit_error:
logger.error(f"Commit failed on thread {current_thread.name}: {commit_error}")
session.rollback()
raise commit_error
logger.info(f"Generated barcode data for bet {bet_uuid}: {barcode_standard} -> {barcode_data}") logger.info(f"Created bet {bet_uuid} with {len(bet_details)} details on thread {current_thread.name}")
else:
logger.debug(f"Barcode generation disabled or not configured for bet {bet_uuid}")
else:
logger.warning("Database manager not available for barcode generation")
except Exception as barcode_e:
logger.error(f"Failed to generate barcode data for bet {bet_uuid}: {barcode_e}")
# Don't fail the bet creation if barcode generation fails
session.commit()
logger.info(f"Created bet {bet_uuid} with {len(bet_details)} details")
return jsonify({ return jsonify({
"success": True, "success": True,
"message": "Bet created successfully", "message": "Bet created successfully",
"bet_id": bet_uuid, "bet_id": bet_uuid,
"details_count": len(bet_details) "details_count": len(bet_details),
"thread_info": {
"thread_name": current_thread.name,
"thread_id": current_thread.ident,
"is_main_thread": current_thread is threading.main_thread()
}
}) })
except Exception as e:
logger.error(f"Failed to create bet on thread {current_thread.name}: {e}")
session.rollback()
return jsonify({"error": str(e)}), 500
finally: finally:
# Ensure session is properly closed in this thread context
try:
if session:
session.close() session.close()
logger.debug(f"Session closed properly on thread {current_thread.name}")
except Exception as close_error:
logger.error(f"Error closing session on thread {current_thread.name}: {close_error}")
except Exception as e: except Exception as e:
logger.error(f"API create cashier bet error: {e}") logger.error(f"API create cashier bet error: {e}")
...@@ -4291,7 +4470,7 @@ def create_cashier_bet(): ...@@ -4291,7 +4470,7 @@ def create_cashier_bet():
@api_bp.route('/cashier/bets/<uuid:bet_id>') @api_bp.route('/cashier/bets/<uuid:bet_id>')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_cashier_bet_details(bet_id): def get_cashier_bet_details(bet_id):
"""Get detailed information for a specific bet (cashier)""" """Get detailed information for a specific bet (cashier)"""
try: try:
...@@ -4352,7 +4531,7 @@ def get_cashier_bet_details(bet_id): ...@@ -4352,7 +4531,7 @@ def get_cashier_bet_details(bet_id):
@api_bp.route('/cashier/bets/<uuid:bet_id>', methods=['DELETE']) @api_bp.route('/cashier/bets/<uuid:bet_id>', methods=['DELETE'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def cancel_cashier_bet(bet_id): def cancel_cashier_bet(bet_id):
"""Cancel a bet and all its details (cashier)""" """Cancel a bet and all its details (cashier)"""
try: try:
...@@ -4393,7 +4572,7 @@ def cancel_cashier_bet(bet_id): ...@@ -4393,7 +4572,7 @@ def cancel_cashier_bet(bet_id):
@api_bp.route('/cashier/available-matches') @api_bp.route('/cashier/available-matches')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_available_matches_for_betting(): def get_available_matches_for_betting():
"""Get matches that are available for betting (status = 'bet') with actual match outcomes""" """Get matches that are available for betting (status = 'bet') with actual match outcomes"""
try: try:
...@@ -4468,7 +4647,7 @@ def get_available_matches_for_betting(): ...@@ -4468,7 +4647,7 @@ def get_available_matches_for_betting():
@api_bp.route('/cashier/bet-details/<int:detail_id>', methods=['DELETE']) @api_bp.route('/cashier/bet-details/<int:detail_id>', methods=['DELETE'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def delete_bet_detail(detail_id): def delete_bet_detail(detail_id):
"""Delete a specific bet detail (cashier)""" """Delete a specific bet detail (cashier)"""
try: try:
...@@ -4506,6 +4685,50 @@ def delete_bet_detail(detail_id): ...@@ -4506,6 +4685,50 @@ def delete_bet_detail(detail_id):
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
@api_bp.route('/bets/<uuid:bet_id>', methods=['DELETE'])
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def delete_admin_bet(bet_id):
"""Delete a complete bet and all its details (admin only)"""
try:
from ..database.models import BetModel, BetDetailModel
bet_uuid = str(bet_id)
session = api_bp.db_manager.get_session()
try:
# Get the bet
bet = session.query(BetModel).filter_by(uuid=bet_uuid).first()
if not bet:
return jsonify({"error": "Bet not found"}), 404
# Delete all bet details first (due to foreign key constraints)
bet_details = session.query(BetDetailModel).filter_by(bet_id=bet_uuid).all()
deleted_details_count = len(bet_details)
for detail in bet_details:
session.delete(detail)
# Delete the bet record
session.delete(bet)
session.commit()
logger.info(f"Admin deleted bet {bet_uuid} and {deleted_details_count} details")
return jsonify({
"success": True,
"message": f"Bet and {deleted_details_count} details deleted successfully",
"bet_id": bet_uuid,
"details_deleted": deleted_details_count
})
finally:
session.close()
except Exception as e:
logger.error(f"API delete admin bet error: {e}")
return jsonify({"error": str(e)}), 500
# Bet Verification Routes # Bet Verification Routes
@main_bp.route('/verify-bet') @main_bp.route('/verify-bet')
@login_required @login_required
...@@ -4743,7 +4966,7 @@ def verify_barcode(): ...@@ -4743,7 +4966,7 @@ def verify_barcode():
# Mark Bet as Paid API endpoints # Mark Bet as Paid API endpoints
@api_bp.route('/cashier/bets/<uuid:bet_id>/mark-paid', methods=['POST']) @api_bp.route('/cashier/bets/<uuid:bet_id>/mark-paid', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def mark_cashier_bet_paid(bet_id): def mark_cashier_bet_paid(bet_id):
"""Mark bet as paid (cashier)""" """Mark bet as paid (cashier)"""
try: try:
...@@ -4784,7 +5007,7 @@ def mark_cashier_bet_paid(bet_id): ...@@ -4784,7 +5007,7 @@ def mark_cashier_bet_paid(bet_id):
@api_bp.route('/bets/<uuid:bet_id>/mark-paid', methods=['POST']) @api_bp.route('/bets/<uuid:bet_id>/mark-paid', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def mark_admin_bet_paid(bet_id): def mark_admin_bet_paid(bet_id):
"""Mark bet as paid (admin/user)""" """Mark bet as paid (admin/user)"""
try: try:
...@@ -4825,7 +5048,7 @@ def mark_admin_bet_paid(bet_id): ...@@ -4825,7 +5048,7 @@ def mark_admin_bet_paid(bet_id):
# Barcode Settings API routes # Barcode Settings API routes
@api_bp.route('/barcode-settings') @api_bp.route('/barcode-settings')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_barcode_settings(): def get_barcode_settings():
"""Get barcode configuration""" """Get barcode configuration"""
try: try:
...@@ -4859,8 +5082,8 @@ def get_barcode_settings(): ...@@ -4859,8 +5082,8 @@ def get_barcode_settings():
@api_bp.route('/barcode-settings', methods=['POST']) @api_bp.route('/barcode-settings', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def set_barcode_settings(): def set_barcode_settings():
"""Set barcode configuration (admin only)""" """Set barcode configuration (admin only)"""
try: try:
...@@ -4919,7 +5142,7 @@ def set_barcode_settings(): ...@@ -4919,7 +5142,7 @@ def set_barcode_settings():
# QR Code Settings API routes # QR Code Settings API routes
@api_bp.route('/qrcode-settings') @api_bp.route('/qrcode-settings')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_qrcode_settings(): def get_qrcode_settings():
"""Get QR code configuration""" """Get QR code configuration"""
try: try:
...@@ -4951,8 +5174,8 @@ def get_qrcode_settings(): ...@@ -4951,8 +5174,8 @@ def get_qrcode_settings():
@api_bp.route('/qrcode-settings', methods=['POST']) @api_bp.route('/qrcode-settings', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def set_qrcode_settings(): def set_qrcode_settings():
"""Set QR code configuration (admin only)""" """Set QR code configuration (admin only)"""
try: try:
...@@ -5002,7 +5225,7 @@ def set_qrcode_settings(): ...@@ -5002,7 +5225,7 @@ def set_qrcode_settings():
# Statistics API endpoints # Statistics API endpoints
@api_bp.route('/statistics') @api_bp.route('/statistics')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_statistics(): def get_statistics():
"""Get extraction statistics with filtering""" """Get extraction statistics with filtering"""
try: try:
...@@ -5094,7 +5317,7 @@ def get_statistics(): ...@@ -5094,7 +5317,7 @@ def get_statistics():
@api_bp.route('/statistics/<int:stats_id>') @api_bp.route('/statistics/<int:stats_id>')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
def get_statistics_details(stats_id): def get_statistics_details(stats_id):
"""Get detailed information for a specific statistics record""" """Get detailed information for a specific statistics record"""
try: try:
...@@ -5271,8 +5494,8 @@ def get_template_preview(template_name): ...@@ -5271,8 +5494,8 @@ def get_template_preview(template_name):
@api_bp.route('/upload-intro-video', methods=['POST']) @api_bp.route('/upload-intro-video', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator()
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required @get_api_auth_decorator(require_admin=True)
def upload_intro_video(): def upload_intro_video():
"""Upload default intro video file (admin only)""" """Upload default intro video file (admin only)"""
try: try:
......
...@@ -167,6 +167,15 @@ ...@@ -167,6 +167,15 @@
<dt class="text-muted">Bet UUID</dt> <dt class="text-muted">Bet UUID</dt>
<dd class="font-monospace">{{ bet.uuid }}</dd> <dd class="font-monospace">{{ bet.uuid }}</dd>
<dt class="text-muted">Barcode ID</dt>
<dd class="font-monospace">
{% if bet.barcode_data %}
{{ bet.barcode_data }}
{% else %}
<span class="text-muted">Not available</span>
{% endif %}
</dd>
<dt class="text-muted">Created</dt> <dt class="text-muted">Created</dt>
<dd>{{ bet.bet_datetime.strftime('%Y-%m-%d %H:%M') }}</dd> <dd>{{ bet.bet_datetime.strftime('%Y-%m-%d %H:%M') }}</dd>
......
...@@ -473,6 +473,7 @@ function updateBetsTable(data, container) { ...@@ -473,6 +473,7 @@ function updateBetsTable(data, container) {
<thead class="table-dark"> <thead class="table-dark">
<tr> <tr>
<th><i class="fas fa-hashtag me-1"></i>Bet ID</th> <th><i class="fas fa-hashtag me-1"></i>Bet ID</th>
<th><i class="fas fa-barcode me-1"></i>Barcode</th>
<th><i class="fas fa-clock me-1"></i>Date & Time</th> <th><i class="fas fa-clock me-1"></i>Date & Time</th>
<th><i class="fas fa-list-ol me-1"></i>Details</th> <th><i class="fas fa-list-ol me-1"></i>Details</th>
<th><i class="fas fa-hashtag me-1"></i>Match</th> <th><i class="fas fa-hashtag me-1"></i>Match</th>
...@@ -522,6 +523,7 @@ function updateBetsTable(data, container) { ...@@ -522,6 +523,7 @@ function updateBetsTable(data, container) {
tableHTML += ` tableHTML += `
<tr> <tr>
<td><strong>${bet.uuid.substring(0, 8)}...</strong></td> <td><strong>${bet.uuid.substring(0, 8)}...</strong></td>
<td>${bet.barcode_data ? bet.barcode_data.substring(0, 16) + '...' : 'N/A'}</td>
<td>${betDateTime}</td> <td>${betDateTime}</td>
<td>${bet.details ? bet.details.length : 0} selections</td> <td>${bet.details ? bet.details.length : 0} selections</td>
<td>${matchNumbers.length > 0 ? matchNumbers.join(', ') : 'N/A'}</td> <td>${matchNumbers.length > 0 ? matchNumbers.join(', ') : 'N/A'}</td>
...@@ -544,13 +546,11 @@ function updateBetsTable(data, container) { ...@@ -544,13 +546,11 @@ function updateBetsTable(data, container) {
title="Print Bet Receipt Directly"> title="Print Bet Receipt Directly">
<i class="fas fa-print"></i> <i class="fas fa-print"></i>
</button> </button>
${overallStatus === 'pending' ? ` <button class="btn btn-sm btn-outline-danger ms-1 btn-delete-bet"
<button class="btn btn-sm btn-outline-danger ms-1 btn-cancel-bet"
data-bet-id="${bet.uuid}" data-bet-id="${bet.uuid}"
title="Cancel Bet"> title="Delete Bet">
<i class="fas fa-ban"></i> <i class="fas fa-trash"></i>
</button> </button>
` : ''}
</td> </td>
</tr> </tr>
`; `;
...@@ -564,12 +564,12 @@ function updateBetsTable(data, container) { ...@@ -564,12 +564,12 @@ function updateBetsTable(data, container) {
container.innerHTML = tableHTML; container.innerHTML = tableHTML;
// Add event listeners for cancel buttons // Add event listeners for delete buttons
container.querySelectorAll('.btn-cancel-bet').forEach(button => { container.querySelectorAll('.btn-delete-bet').forEach(button => {
button.addEventListener('click', function() { button.addEventListener('click', function() {
const betId = this.getAttribute('data-bet-id'); const betId = this.getAttribute('data-bet-id');
if (confirm('Are you sure you want to cancel this bet? This action cannot be undone.')) { if (confirm('Are you sure you want to permanently delete this bet? This action cannot be undone and will remove all bet data from the database.')) {
cancelBet(betId); deleteBet(betId);
} }
}); });
}); });
...@@ -603,8 +603,8 @@ function updateBettingStats(stats) { ...@@ -603,8 +603,8 @@ function updateBettingStats(stats) {
document.getElementById('pending-bets').textContent = stats.pending_bets || 0; document.getElementById('pending-bets').textContent = stats.pending_bets || 0;
} }
function cancelBet(betId) { function deleteBet(betId) {
fetch(`/api/cashier/bets/${betId}`, { fetch(`/api/bets/${betId}`, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...@@ -615,13 +615,13 @@ function cancelBet(betId) { ...@@ -615,13 +615,13 @@ function cancelBet(betId) {
if (data.success) { if (data.success) {
// Refresh the bets table // Refresh the bets table
loadBets(); loadBets();
showNotification('Bet cancelled successfully!', 'success'); showNotification('Bet deleted successfully!', 'success');
} else { } else {
showNotification('Failed to cancel bet: ' + (data.error || 'Unknown error'), 'error'); showNotification('Failed to delete bet: ' + (data.error || 'Unknown error'), 'error');
} }
}) })
.catch(error => { .catch(error => {
showNotification('Error cancelling bet: ' + error.message, 'error'); showNotification('Error deleting bet: ' + error.message, 'error');
}); });
} }
......
...@@ -167,6 +167,15 @@ ...@@ -167,6 +167,15 @@
<dt class="text-muted">Bet UUID</dt> <dt class="text-muted">Bet UUID</dt>
<dd class="font-monospace">{{ bet.uuid }}</dd> <dd class="font-monospace">{{ bet.uuid }}</dd>
<dt class="text-muted">Barcode ID</dt>
<dd class="font-monospace">
{% if bet.barcode_data %}
{{ bet.barcode_data }}
{% else %}
<span class="text-muted">Not available</span>
{% endif %}
</dd>
<dt class="text-muted">Created</dt> <dt class="text-muted">Created</dt>
<dd>{{ bet.bet_datetime.strftime('%Y-%m-%d %H:%M') }}</dd> <dd>{{ bet.bet_datetime.strftime('%Y-%m-%d %H:%M') }}</dd>
......
...@@ -473,6 +473,7 @@ function updateBetsTable(data, container) { ...@@ -473,6 +473,7 @@ function updateBetsTable(data, container) {
<thead class="table-dark"> <thead class="table-dark">
<tr> <tr>
<th><i class="fas fa-hashtag me-1"></i>Bet ID</th> <th><i class="fas fa-hashtag me-1"></i>Bet ID</th>
<th><i class="fas fa-barcode me-1"></i>Barcode</th>
<th><i class="fas fa-clock me-1"></i>Date & Time</th> <th><i class="fas fa-clock me-1"></i>Date & Time</th>
<th><i class="fas fa-list-ol me-1"></i>Details</th> <th><i class="fas fa-list-ol me-1"></i>Details</th>
<th><i class="fas fa-hashtag me-1"></i>Match</th> <th><i class="fas fa-hashtag me-1"></i>Match</th>
...@@ -522,6 +523,7 @@ function updateBetsTable(data, container) { ...@@ -522,6 +523,7 @@ function updateBetsTable(data, container) {
tableHTML += ` tableHTML += `
<tr> <tr>
<td><strong>${bet.uuid.substring(0, 8)}...</strong></td> <td><strong>${bet.uuid.substring(0, 8)}...</strong></td>
<td>${bet.barcode_data ? bet.barcode_data.substring(0, 16) + '...' : 'N/A'}</td>
<td>${betDateTime}</td> <td>${betDateTime}</td>
<td>${bet.details ? bet.details.length : 0} selections</td> <td>${bet.details ? bet.details.length : 0} selections</td>
<td>${matchNumbers.length > 0 ? matchNumbers.join(', ') : 'N/A'}</td> <td>${matchNumbers.length > 0 ? matchNumbers.join(', ') : 'N/A'}</td>
......
...@@ -560,14 +560,17 @@ function loadAvailableMatches() { ...@@ -560,14 +560,17 @@ function loadAvailableMatches() {
}) })
.then(data => { .then(data => {
console.log('📦 API response data:', data); console.log('📦 API response data:', data);
console.log('📦 Number of matches returned:', data.matches ? data.matches.length : 0);
if (data.success) { if (data.success) {
// Update count badge // Update count badge
countBadge.textContent = data.total; countBadge.textContent = data.total;
countBadge.className = data.total > 0 ? 'badge bg-success ms-2' : 'badge bg-warning ms-2'; countBadge.className = data.total > 0 ? 'badge bg-success ms-2' : 'badge bg-warning ms-2';
console.log('✅ Updating available matches display');
updateAvailableMatchesDisplay(data, container); updateAvailableMatchesDisplay(data, container);
} else { } else {
console.error('❌ API returned success=false:', data.error);
container.innerHTML = ` container.innerHTML = `
<div class="text-center text-danger"> <div class="text-center text-danger">
<i class="fas fa-exclamation-triangle me-2"></i>Error loading matches: ${data.error || 'Unknown error'} <i class="fas fa-exclamation-triangle me-2"></i>Error loading matches: ${data.error || 'Unknown error'}
...@@ -730,6 +733,8 @@ function updateAvailableMatchesDisplay(data, container) { ...@@ -730,6 +733,8 @@ function updateAvailableMatchesDisplay(data, container) {
} }
function updateBetSummary() { function updateBetSummary() {
console.log('🔄 updateBetSummary() called');
const summaryContent = document.getElementById('bet-summary-content'); const summaryContent = document.getElementById('bet-summary-content');
const totalSection = document.getElementById('bet-total-section'); const totalSection = document.getElementById('bet-total-section');
const totalAmountElement = document.getElementById('bet-total-amount'); const totalAmountElement = document.getElementById('bet-total-amount');
...@@ -737,25 +742,33 @@ function updateBetSummary() { ...@@ -737,25 +742,33 @@ function updateBetSummary() {
// Clear previous selections // Clear previous selections
selectedOutcomes.clear(); selectedOutcomes.clear();
console.log('🧹 Cleared selectedOutcomes');
let totalAmount = 0; let totalAmount = 0;
let hasSelections = false; let hasSelections = false;
let summaryHTML = ''; let summaryHTML = '';
// Collect all amount inputs with values > 0 // Collect all amount inputs with values > 0
document.querySelectorAll('.amount-input').forEach(input => { const amountInputs = document.querySelectorAll('.amount-input');
console.log('📊 Found', amountInputs.length, 'amount inputs');
amountInputs.forEach((input, index) => {
const amount = parseFloat(input.value) || 0; const amount = parseFloat(input.value) || 0;
console.log(`💰 Input ${index}: value="${input.value}", parsed amount=${amount}`);
if (amount > 0) { if (amount > 0) {
const matchId = input.getAttribute('data-match-id'); const matchId = input.getAttribute('data-match-id');
const outcome = input.getAttribute('data-outcome'); const outcome = input.getAttribute('data-outcome');
console.log(`✅ Adding selection: matchId=${matchId}, outcome=${outcome}, amount=${amount}`);
hasSelections = true; hasSelections = true;
totalAmount += amount; totalAmount += amount;
// Store selection // Store selection
if (!selectedOutcomes.has(matchId)) { if (!selectedOutcomes.has(matchId)) {
selectedOutcomes.set(matchId, { outcomes: [], amounts: [] }); selectedOutcomes.set(matchId, { outcomes: [], amounts: [] });
console.log(`📝 Created new entry for match ${matchId}`);
} }
const matchSelections = selectedOutcomes.get(matchId); const matchSelections = selectedOutcomes.get(matchId);
matchSelections.outcomes.push(outcome); matchSelections.outcomes.push(outcome);
...@@ -777,12 +790,17 @@ function updateBetSummary() { ...@@ -777,12 +790,17 @@ function updateBetSummary() {
} }
}); });
console.log('📋 Final selectedOutcomes:', selectedOutcomes);
console.log('💵 Total amount:', totalAmount, 'hasSelections:', hasSelections);
if (hasSelections) { if (hasSelections) {
console.log('✅ Enabling submit button and showing summary');
summaryContent.innerHTML = summaryHTML; summaryContent.innerHTML = summaryHTML;
totalSection.style.display = 'block'; totalSection.style.display = 'block';
totalAmountElement.textContent = formatCurrency(totalAmount); totalAmountElement.textContent = formatCurrency(totalAmount);
submitButton.disabled = false; submitButton.disabled = false;
} else { } else {
console.log('❌ No selections, disabling submit button');
summaryContent.innerHTML = ` summaryContent.innerHTML = `
<div class="text-center text-muted"> <div class="text-center text-muted">
<i class="fas fa-info-circle me-2"></i> <i class="fas fa-info-circle me-2"></i>
...@@ -795,7 +813,12 @@ function updateBetSummary() { ...@@ -795,7 +813,12 @@ function updateBetSummary() {
} }
function submitBet() { function submitBet() {
console.log('🎯 submitBet() called');
console.log('🎯 selectedOutcomes.size:', selectedOutcomes.size);
console.log('🎯 selectedOutcomes:', selectedOutcomes);
if (selectedOutcomes.size === 0) { if (selectedOutcomes.size === 0) {
console.log('❌ No outcomes selected, showing error notification');
showNotification('Please select at least one outcome with an amount', 'error'); showNotification('Please select at least one outcome with an amount', 'error');
return; return;
} }
...@@ -806,6 +829,7 @@ function submitBet() { ...@@ -806,6 +829,7 @@ function submitBet() {
}; };
selectedOutcomes.forEach((selections, matchId) => { selectedOutcomes.forEach((selections, matchId) => {
console.log('📋 Processing match', matchId, 'with selections:', selections);
selections.outcomes.forEach((outcome, index) => { selections.outcomes.forEach((outcome, index) => {
betData.bet_details.push({ betData.bet_details.push({
match_id: parseInt(matchId), match_id: parseInt(matchId),
...@@ -816,8 +840,10 @@ function submitBet() { ...@@ -816,8 +840,10 @@ function submitBet() {
}); });
console.log('📤 Submitting bet data:', betData); console.log('📤 Submitting bet data:', betData);
console.log('📤 Bet data JSON:', JSON.stringify(betData));
// Submit to API // Submit to API
console.log('🌐 Making fetch request to /api/cashier/bets');
fetch('/api/cashier/bets', { fetch('/api/cashier/bets', {
method: 'POST', method: 'POST',
headers: { headers: {
...@@ -825,8 +851,13 @@ function submitBet() { ...@@ -825,8 +851,13 @@ function submitBet() {
}, },
body: JSON.stringify(betData) body: JSON.stringify(betData)
}) })
.then(response => response.json()) .then(response => {
console.log('📡 API response status:', response.status);
console.log('📡 API response headers:', response.headers);
return response.json();
})
.then(data => { .then(data => {
console.log('📦 API response data:', data);
if (data.success) { if (data.success) {
showNotification('Bet submitted successfully!', 'success'); showNotification('Bet submitted successfully!', 'success');
setTimeout(() => { setTimeout(() => {
...@@ -834,9 +865,11 @@ function submitBet() { ...@@ -834,9 +865,11 @@ function submitBet() {
}, 1500); }, 1500);
} else { } else {
showNotification('Failed to submit bet: ' + (data.error || 'Unknown error'), 'error'); showNotification('Failed to submit bet: ' + (data.error || 'Unknown error'), 'error');
console.error('❌ Bet submission failed:', data);
} }
}) })
.catch(error => { .catch(error => {
console.error('❌ Error submitting bet:', error);
showNotification('Error submitting bet: ' + error.message, 'error'); showNotification('Error submitting bet: ' + error.message, 'error');
}); });
} }
......
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