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:
# Configure SQLite for better performance and reliability
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 synchronous=NORMAL"))
conn.execute(text("PRAGMA synchronous=FULL")) # Ensure data is written synchronously
conn.execute(text("PRAGMA cache_size=10000"))
conn.execute(text("PRAGMA temp_store=MEMORY"))
conn.execute(text("PRAGMA mmap_size=268435456")) # 256MB
conn.execute(text("PRAGMA foreign_keys=ON")) # Enable foreign key constraints
conn.commit()
# Create session factory
......@@ -128,11 +130,13 @@ class DatabaseManager:
# Configure SQLite for better performance and reliability
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 synchronous=NORMAL"))
conn.execute(text("PRAGMA synchronous=FULL")) # Ensure data is written synchronously
conn.execute(text("PRAGMA cache_size=10000"))
conn.execute(text("PRAGMA temp_store=MEMORY"))
conn.execute(text("PRAGMA mmap_size=268435456")) # 256MB
conn.execute(text("PRAGMA foreign_keys=ON")) # Enable foreign key constraints
conn.commit()
# Create session factory
......@@ -176,7 +180,9 @@ class DatabaseManager:
"""Get database session"""
if not self._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):
"""Close database connections"""
......
......@@ -4,7 +4,7 @@ SQLAlchemy database models for MbetterClient
import json
import hashlib
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
from typing import Dict, Any, Optional, List
from sqlalchemy import (
Column, Integer, String, Text, DateTime, Boolean, Float,
......
......@@ -217,8 +217,8 @@ def format_bet_id_for_barcode(bet_uuid: str, standard: str) -> str:
clean_uuid = bet_uuid.replace('-', '').upper()
if standard in ['code128', 'code39']:
# These support alphanumeric, use first 16 characters
return clean_uuid[:16]
# These support alphanumeric, use full UUID for maximum uniqueness
return clean_uuid
elif standard in ['ean13', 'ean8', 'upca', 'upce', 'itf', 'codabar']:
# These require numeric data
......
......@@ -7,7 +7,7 @@ import secrets
import logging
from datetime import datetime, timedelta
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_jwt_extended import create_access_token, decode_token
import jwt
......@@ -478,24 +478,113 @@ class AuthManager:
def decorator(func):
@wraps(func)
def decorated_function(*args, **kwargs):
print(f"AUTH_DECORATOR: Called for {request.path}")
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 '):
token = auth_header.split(' ', 1)[1]
print(f"AUTH_DECORATOR: Token received: {token[:20]}...")
# Try JWT token first
payload = self.verify_jwt_token(token)
if payload:
print(f"AUTH_DECORATOR: JWT token verified for user: {payload.get('username')}")
request.current_user = payload
return func(*args, **kwargs)
else:
print("AUTH_DECORATOR: JWT token verification failed")
# Try API token
api_data = self.verify_api_token(token)
if api_data:
print(f"AUTH_DECORATOR: API token verified for user: {api_data.get('username')}")
request.current_user = api_data
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
......@@ -514,13 +603,13 @@ class AuthManager:
@wraps(func)
def decorated_function(*args, **kwargs):
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')
is_admin = request.current_user.get('is_admin', False)
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)
......@@ -541,12 +630,12 @@ class AuthManager:
@wraps(f)
def decorated_function(*args, **kwargs):
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')
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)
......
......@@ -5,12 +5,13 @@ Flask routes for web dashboard
import logging
import time
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_login import login_required, current_user, login_user, logout_user
from flask_socketio import emit, join_room, leave_room
from werkzeug.security import check_password_hash
from werkzeug.utils import secure_filename
from sqlalchemy import text
from .auth import AuthenticatedUser
from ..core.message_bus import Message, MessageType
......@@ -25,6 +26,41 @@ def conditional_auth_decorator(condition, auth_decorator, fallback_decorator=log
return fallback_decorator(func)
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__)
# Blueprint definitions
......@@ -41,6 +77,7 @@ auth_bp.auth_manager = None
auth_bp.db_manager = None
api_bp.api = None
api_bp.auth_manager = None
api_bp.db_manager = None
api_bp.config_manager = None
api_bp.message_bus = None
......@@ -224,6 +261,8 @@ def bet_details(bet_id):
'total_amount': total_amount,
'bet_count': len(bet_details_data),
'has_pending': has_pending,
'barcode_standard': bet.barcode_standard,
'barcode_data': bet.barcode_data,
'bet_details': bet_details_data
}
......@@ -681,6 +720,8 @@ def cashier_bet_details(bet_id):
'total_amount': total_amount,
'bet_count': len(bet_details_data),
'has_pending': has_pending,
'barcode_standard': bet.barcode_standard,
'barcode_data': bet.barcode_data,
'bet_details': bet_details_data
}
......@@ -869,7 +910,7 @@ def debug_match_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():
"""Get video player status"""
try:
......@@ -881,7 +922,7 @@ def video_status():
@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():
"""Control video player with enhanced looping support"""
try:
......@@ -990,7 +1031,7 @@ def video_control():
@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():
"""Update video overlay"""
try:
......@@ -1010,7 +1051,7 @@ def update_overlay():
@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():
"""Get available templates"""
try:
......@@ -1022,7 +1063,7 @@ def get_templates():
@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():
"""Get configuration"""
try:
......@@ -1035,7 +1076,7 @@ def get_configuration():
@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):
"""Get configuration section"""
try:
......@@ -1047,8 +1088,8 @@ def get_config_section(section):
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def update_config_section(section):
"""Update configuration section"""
try:
......@@ -1077,8 +1118,8 @@ def update_config_section(section):
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def update_configuration():
"""Update configuration"""
try:
......@@ -1098,7 +1139,7 @@ def update_configuration():
@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():
"""Get match interval configuration"""
try:
......@@ -1118,7 +1159,7 @@ def get_match_interval():
@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():
"""Set match interval configuration"""
try:
......@@ -1170,7 +1211,7 @@ def set_match_interval():
@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():
"""Get license text configuration"""
try:
......@@ -1201,7 +1242,7 @@ def get_license_text():
@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():
"""Set license text configuration"""
try:
......@@ -1259,8 +1300,8 @@ def set_license_text():
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def test_api_connection():
"""Test connection to FastAPI server using request data or configured values"""
try:
......@@ -1339,8 +1380,8 @@ def test_api_connection():
@api_bp.route('/users')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def get_users():
"""Get all users"""
try:
......@@ -1352,8 +1393,8 @@ def get_users():
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def create_user():
"""Create new user"""
try:
......@@ -1380,8 +1421,8 @@ def create_user():
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def update_user(user_id):
"""Update user"""
try:
......@@ -1405,8 +1446,8 @@ def update_user(user_id):
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def delete_user(user_id):
"""Delete user"""
try:
......@@ -1418,7 +1459,7 @@ def delete_user(user_id):
@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():
"""Get API tokens for current user"""
try:
......@@ -1436,7 +1477,7 @@ def get_api_tokens():
@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():
"""Create API token"""
try:
......@@ -1461,7 +1502,7 @@ def create_api_token():
@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):
"""Revoke API token"""
try:
......@@ -1479,8 +1520,8 @@ def revoke_api_token(token_id):
@api_bp.route('/logs')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def get_logs():
"""Get application logs"""
try:
......@@ -1496,8 +1537,8 @@ def get_logs():
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def send_test_message():
"""Send test message to component"""
try:
......@@ -1519,7 +1560,7 @@ def send_test_message():
# Video upload and delete routes
@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():
"""Upload video file"""
try:
......@@ -1557,7 +1598,7 @@ def upload_video():
@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():
"""Delete uploaded video"""
try:
......@@ -1609,8 +1650,8 @@ def create_auth_token():
logger.error(f"Token creation error: {e}")
return jsonify({"error": str(e)}), 500
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def shutdown_application():
"""Shutdown the application (admin only)"""
try:
......@@ -1639,8 +1680,8 @@ def shutdown_application():
return jsonify({"error": str(e)}), 500
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def upload_template():
"""Upload template file"""
try:
......@@ -1663,7 +1704,7 @@ def upload_template():
@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):
"""Get template content for preview"""
try:
......@@ -1699,8 +1740,8 @@ def get_template_content(template_name):
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def delete_template(template_name):
"""Delete uploaded template"""
try:
......@@ -1713,7 +1754,7 @@ def delete_template(template_name):
@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():
"""Get outcome template assignments"""
try:
......@@ -1748,8 +1789,8 @@ def get_outcome_assignments():
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def save_outcome_assignments():
"""Save outcome template assignments (admin only)"""
try:
......@@ -1799,7 +1840,7 @@ def save_outcome_assignments():
@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():
"""Send custom message to overlay with template selection and display time support"""
try:
......@@ -1911,7 +1952,7 @@ def send_custom_message():
@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():
"""Get intro templates configuration"""
try:
......@@ -1956,7 +1997,7 @@ def get_intro_templates():
@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():
"""Save intro templates configuration"""
try:
......@@ -2295,7 +2336,7 @@ def auto_fail_old_fixtures(session, cutoff_date):
@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():
"""Start games for the first fixture - send START_GAME message to message bus"""
try:
......@@ -2422,8 +2463,8 @@ def get_fixture_details(fixture_id):
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def reset_fixtures():
"""Reset all fixtures data (admin only) - clear matches, match_outcomes, and ZIP files"""
try:
......@@ -2484,7 +2525,7 @@ def reset_fixtures():
@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():
"""Get API client status and endpoint information"""
try:
......@@ -2539,8 +2580,8 @@ def get_api_client_status():
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def trigger_api_request():
"""Manually trigger an API request for testing"""
try:
......@@ -2575,7 +2616,7 @@ def trigger_api_request():
@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():
"""Get match timer configuration"""
try:
......@@ -2595,7 +2636,7 @@ def get_match_timer_config():
@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():
"""Start the next match by sending MATCH_START message"""
try:
......@@ -2754,7 +2795,7 @@ def get_server_time():
@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():
"""Long polling endpoint for real-time notifications"""
try:
......@@ -2855,7 +2896,7 @@ def notifications():
# Extraction API routes
@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():
"""Get all available outcomes for extraction"""
try:
......@@ -2883,7 +2924,13 @@ def get_extraction_outcomes():
})
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()
logger.debug("DEBUG: Session closed")
except Exception as e:
logger.error(f"API get extraction outcomes error: {e}")
......@@ -2891,7 +2938,7 @@ def get_extraction_outcomes():
@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():
"""Get current extraction associations with defaults"""
try:
......@@ -2917,7 +2964,7 @@ def get_extraction_associations():
@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():
"""Save extraction associations - unlimited associations per outcome"""
try:
......@@ -2984,7 +3031,7 @@ def save_extraction_associations():
@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():
"""Add a single extraction association"""
try:
......@@ -3075,7 +3122,7 @@ def add_extraction_association():
@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():
"""Remove a specific extraction association"""
try:
......@@ -3135,7 +3182,7 @@ def remove_extraction_association():
@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():
"""Get extraction configuration"""
try:
......@@ -3165,7 +3212,7 @@ def get_extraction_config():
@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():
"""Update extraction configuration"""
try:
......@@ -3221,7 +3268,7 @@ def update_extraction_config():
# Betting Mode API routes
@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():
"""Get global betting mode configuration"""
try:
......@@ -3258,8 +3305,8 @@ def get_betting_mode():
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def set_betting_mode():
"""Set global betting mode configuration (admin only)"""
try:
......@@ -3318,7 +3365,7 @@ def set_betting_mode():
# Extraction Configuration Persistence API routes
@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():
"""Get UNDER/OVER zone configurations with defaults"""
try:
......@@ -3410,7 +3457,7 @@ def get_under_over_config():
@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():
"""Save UNDER/OVER zone configurations"""
try:
......@@ -3477,7 +3524,7 @@ def save_under_over_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():
"""Get Results area configurations with defaults"""
try:
......@@ -3541,7 +3588,7 @@ def get_results_config():
@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():
"""Save Results area configurations"""
try:
......@@ -3590,7 +3637,7 @@ def save_results_config():
# Available Bets API routes
@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():
"""Get all available bets for extraction"""
try:
......@@ -3617,7 +3664,7 @@ def get_available_bets():
@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():
"""Add a new available bet"""
try:
......@@ -3665,7 +3712,7 @@ def add_available_bet():
@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():
"""Delete an available bet and all associated associations"""
try:
......@@ -3711,7 +3758,7 @@ def delete_available_bet():
# Result Options API routes
@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():
"""Get all result options for extraction"""
try:
......@@ -3738,7 +3785,7 @@ def get_result_options():
@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():
"""Add a new result option"""
try:
......@@ -3791,7 +3838,7 @@ def add_result_option():
@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():
"""Delete a result option and all associated associations"""
try:
......@@ -3839,8 +3886,8 @@ def delete_result_option():
# Redistribution CAP API routes (admin-only)
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def get_redistribution_cap():
"""Get redistribution CAP configuration (admin only)"""
try:
......@@ -3875,8 +3922,8 @@ def get_redistribution_cap():
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def save_redistribution_cap():
"""Save redistribution CAP configuration (admin only)"""
try:
......@@ -3936,7 +3983,7 @@ def save_redistribution_cap():
# Currency Settings API routes
@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():
"""Get currency symbol configuration"""
try:
......@@ -3961,8 +4008,8 @@ def get_currency_settings():
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def set_currency_settings():
"""Set currency symbol configuration (admin only)"""
try:
......@@ -4006,7 +4053,7 @@ def set_currency_settings():
# Server-side Match Timer API endpoints
@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():
"""Get current match timer state from server-side component"""
try:
......@@ -4036,8 +4083,8 @@ def get_match_timer_state():
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def control_match_timer():
"""Control the match timer (admin only)"""
try:
......@@ -4101,12 +4148,12 @@ def control_match_timer():
# Cashier Betting API endpoints
@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():
"""Get bets for a specific date (cashier)"""
try:
from ..database.models import BetModel, BetDetailModel
from datetime import datetime, date
from datetime import datetime, date, timezone, timedelta
# Get date parameter (default to today)
date_param = request.args.get('date')
......@@ -4121,12 +4168,48 @@ def get_cashier_bets():
session = api_bp.db_manager.get_session()
try:
# 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(
BetModel.bet_datetime >= datetime.combine(target_date, datetime.min.time()),
BetModel.bet_datetime < datetime.combine(target_date, datetime.max.time())
BetModel.bet_datetime >= start_datetime,
BetModel.bet_datetime <= end_datetime
).order_by(BetModel.bet_datetime.desc())
# DEBUG: Log the raw query
logger.info(f"DEBUG: Executing query: {bets_query}")
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 = []
# Statistics counters
......@@ -4187,14 +4270,74 @@ def get_cashier_bets():
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.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():
"""Create a new bet (cashier)"""
"""Create a new bet (cashier) - thread-safe implementation"""
try:
from ..database.models import BetModel, BetDetailModel
from datetime import datetime
import uuid as uuid_lib
import threading
data = request.get_json() or {}
bet_details = data.get('bet_details', [])
......@@ -4212,27 +4355,67 @@ def create_cashier_bet():
except (ValueError, TypeError):
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()
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
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
from ..database.models import MatchModel
first_match_id = bet_details[0]['match_id']
first_match = session.query(MatchModel).filter_by(id=first_match_id).first()
if not first_match:
return jsonify({"error": f"Match {first_match_id} not found"}), 404
fixture_id = first_match.fixture_id
# 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(
uuid=bet_uuid,
bet_datetime=datetime.now(),
fixture_id=first_match.fixture_id # Set from first bet detail's match
bet_datetime=bet_datetime,
fixture_id=fixture_id,
barcode_standard=barcode_standard,
barcode_data=barcode_data
)
session.add(new_bet)
session.flush() # Get the bet ID
# Create bet details
for detail_data in bet_details:
......@@ -4245,45 +4428,41 @@ def create_cashier_bet():
)
session.add(bet_detail)
# Generate and store barcode data if enabled
# Commit the transaction with proper error handling
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()
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}")
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")
logger.info(f"Created bet {bet_uuid} with {len(bet_details)} details on thread {current_thread.name}")
return jsonify({
"success": True,
"message": "Bet created successfully",
"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:
# Ensure session is properly closed in this thread context
try:
if session:
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:
logger.error(f"API create cashier bet error: {e}")
......@@ -4291,7 +4470,7 @@ def create_cashier_bet():
@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):
"""Get detailed information for a specific bet (cashier)"""
try:
......@@ -4352,7 +4531,7 @@ def get_cashier_bet_details(bet_id):
@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):
"""Cancel a bet and all its details (cashier)"""
try:
......@@ -4393,7 +4572,7 @@ def cancel_cashier_bet(bet_id):
@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():
"""Get matches that are available for betting (status = 'bet') with actual match outcomes"""
try:
......@@ -4468,7 +4647,7 @@ def get_available_matches_for_betting():
@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):
"""Delete a specific bet detail (cashier)"""
try:
......@@ -4506,6 +4685,50 @@ def delete_bet_detail(detail_id):
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
@main_bp.route('/verify-bet')
@login_required
......@@ -4743,7 +4966,7 @@ def verify_barcode():
# Mark Bet as Paid API endpoints
@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):
"""Mark bet as paid (cashier)"""
try:
......@@ -4784,7 +5007,7 @@ def mark_cashier_bet_paid(bet_id):
@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):
"""Mark bet as paid (admin/user)"""
try:
......@@ -4825,7 +5048,7 @@ def mark_admin_bet_paid(bet_id):
# Barcode Settings API routes
@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():
"""Get barcode configuration"""
try:
......@@ -4859,8 +5082,8 @@ def get_barcode_settings():
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def set_barcode_settings():
"""Set barcode configuration (admin only)"""
try:
......@@ -4919,7 +5142,7 @@ def set_barcode_settings():
# QR Code Settings API routes
@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():
"""Get QR code configuration"""
try:
......@@ -4951,8 +5174,8 @@ def get_qrcode_settings():
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def set_qrcode_settings():
"""Set QR code configuration (admin only)"""
try:
......@@ -5002,7 +5225,7 @@ def set_qrcode_settings():
# Statistics API endpoints
@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():
"""Get extraction statistics with filtering"""
try:
......@@ -5094,7 +5317,7 @@ def get_statistics():
@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):
"""Get detailed information for a specific statistics record"""
try:
......@@ -5271,8 +5494,8 @@ def get_template_preview(template_name):
@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
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@get_api_auth_decorator()
@get_api_auth_decorator(require_admin=True)
def upload_intro_video():
"""Upload default intro video file (admin only)"""
try:
......
......@@ -167,6 +167,15 @@
<dt class="text-muted">Bet UUID</dt>
<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>
<dd>{{ bet.bet_datetime.strftime('%Y-%m-%d %H:%M') }}</dd>
......
......@@ -473,6 +473,7 @@ function updateBetsTable(data, container) {
<thead class="table-dark">
<tr>
<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-list-ol me-1"></i>Details</th>
<th><i class="fas fa-hashtag me-1"></i>Match</th>
......@@ -522,6 +523,7 @@ function updateBetsTable(data, container) {
tableHTML += `
<tr>
<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>${bet.details ? bet.details.length : 0} selections</td>
<td>${matchNumbers.length > 0 ? matchNumbers.join(', ') : 'N/A'}</td>
......@@ -544,13 +546,11 @@ function updateBetsTable(data, container) {
title="Print Bet Receipt Directly">
<i class="fas fa-print"></i>
</button>
${overallStatus === 'pending' ? `
<button class="btn btn-sm btn-outline-danger ms-1 btn-cancel-bet"
<button class="btn btn-sm btn-outline-danger ms-1 btn-delete-bet"
data-bet-id="${bet.uuid}"
title="Cancel Bet">
<i class="fas fa-ban"></i>
title="Delete Bet">
<i class="fas fa-trash"></i>
</button>
` : ''}
</td>
</tr>
`;
......@@ -564,12 +564,12 @@ function updateBetsTable(data, container) {
container.innerHTML = tableHTML;
// Add event listeners for cancel buttons
container.querySelectorAll('.btn-cancel-bet').forEach(button => {
// Add event listeners for delete buttons
container.querySelectorAll('.btn-delete-bet').forEach(button => {
button.addEventListener('click', function() {
const betId = this.getAttribute('data-bet-id');
if (confirm('Are you sure you want to cancel this bet? This action cannot be undone.')) {
cancelBet(betId);
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.')) {
deleteBet(betId);
}
});
});
......@@ -603,8 +603,8 @@ function updateBettingStats(stats) {
document.getElementById('pending-bets').textContent = stats.pending_bets || 0;
}
function cancelBet(betId) {
fetch(`/api/cashier/bets/${betId}`, {
function deleteBet(betId) {
fetch(`/api/bets/${betId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
......@@ -615,13 +615,13 @@ function cancelBet(betId) {
if (data.success) {
// Refresh the bets table
loadBets();
showNotification('Bet cancelled successfully!', 'success');
showNotification('Bet deleted successfully!', 'success');
} else {
showNotification('Failed to cancel bet: ' + (data.error || 'Unknown error'), 'error');
showNotification('Failed to delete bet: ' + (data.error || 'Unknown error'), 'error');
}
})
.catch(error => {
showNotification('Error cancelling bet: ' + error.message, 'error');
showNotification('Error deleting bet: ' + error.message, 'error');
});
}
......
......@@ -167,6 +167,15 @@
<dt class="text-muted">Bet UUID</dt>
<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>
<dd>{{ bet.bet_datetime.strftime('%Y-%m-%d %H:%M') }}</dd>
......
......@@ -473,6 +473,7 @@ function updateBetsTable(data, container) {
<thead class="table-dark">
<tr>
<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-list-ol me-1"></i>Details</th>
<th><i class="fas fa-hashtag me-1"></i>Match</th>
......@@ -522,6 +523,7 @@ function updateBetsTable(data, container) {
tableHTML += `
<tr>
<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>${bet.details ? bet.details.length : 0} selections</td>
<td>${matchNumbers.length > 0 ? matchNumbers.join(', ') : 'N/A'}</td>
......
......@@ -560,14 +560,17 @@ function loadAvailableMatches() {
})
.then(data => {
console.log('📦 API response data:', data);
console.log('📦 Number of matches returned:', data.matches ? data.matches.length : 0);
if (data.success) {
// Update count badge
countBadge.textContent = data.total;
countBadge.className = data.total > 0 ? 'badge bg-success ms-2' : 'badge bg-warning ms-2';
console.log('✅ Updating available matches display');
updateAvailableMatchesDisplay(data, container);
} else {
console.error('❌ API returned success=false:', data.error);
container.innerHTML = `
<div class="text-center text-danger">
<i class="fas fa-exclamation-triangle me-2"></i>Error loading matches: ${data.error || 'Unknown error'}
......@@ -730,6 +733,8 @@ function updateAvailableMatchesDisplay(data, container) {
}
function updateBetSummary() {
console.log('🔄 updateBetSummary() called');
const summaryContent = document.getElementById('bet-summary-content');
const totalSection = document.getElementById('bet-total-section');
const totalAmountElement = document.getElementById('bet-total-amount');
......@@ -737,25 +742,33 @@ function updateBetSummary() {
// Clear previous selections
selectedOutcomes.clear();
console.log('🧹 Cleared selectedOutcomes');
let totalAmount = 0;
let hasSelections = false;
let summaryHTML = '';
// 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;
console.log(`💰 Input ${index}: value="${input.value}", parsed amount=${amount}`);
if (amount > 0) {
const matchId = input.getAttribute('data-match-id');
const outcome = input.getAttribute('data-outcome');
console.log(`✅ Adding selection: matchId=${matchId}, outcome=${outcome}, amount=${amount}`);
hasSelections = true;
totalAmount += amount;
// Store selection
if (!selectedOutcomes.has(matchId)) {
selectedOutcomes.set(matchId, { outcomes: [], amounts: [] });
console.log(`📝 Created new entry for match ${matchId}`);
}
const matchSelections = selectedOutcomes.get(matchId);
matchSelections.outcomes.push(outcome);
......@@ -777,12 +790,17 @@ function updateBetSummary() {
}
});
console.log('📋 Final selectedOutcomes:', selectedOutcomes);
console.log('💵 Total amount:', totalAmount, 'hasSelections:', hasSelections);
if (hasSelections) {
console.log('✅ Enabling submit button and showing summary');
summaryContent.innerHTML = summaryHTML;
totalSection.style.display = 'block';
totalAmountElement.textContent = formatCurrency(totalAmount);
submitButton.disabled = false;
} else {
console.log('❌ No selections, disabling submit button');
summaryContent.innerHTML = `
<div class="text-center text-muted">
<i class="fas fa-info-circle me-2"></i>
......@@ -795,7 +813,12 @@ function updateBetSummary() {
}
function submitBet() {
console.log('🎯 submitBet() called');
console.log('🎯 selectedOutcomes.size:', selectedOutcomes.size);
console.log('🎯 selectedOutcomes:', selectedOutcomes);
if (selectedOutcomes.size === 0) {
console.log('❌ No outcomes selected, showing error notification');
showNotification('Please select at least one outcome with an amount', 'error');
return;
}
......@@ -806,6 +829,7 @@ function submitBet() {
};
selectedOutcomes.forEach((selections, matchId) => {
console.log('📋 Processing match', matchId, 'with selections:', selections);
selections.outcomes.forEach((outcome, index) => {
betData.bet_details.push({
match_id: parseInt(matchId),
......@@ -816,8 +840,10 @@ function submitBet() {
});
console.log('📤 Submitting bet data:', betData);
console.log('📤 Bet data JSON:', JSON.stringify(betData));
// Submit to API
console.log('🌐 Making fetch request to /api/cashier/bets');
fetch('/api/cashier/bets', {
method: 'POST',
headers: {
......@@ -825,8 +851,13 @@ function submitBet() {
},
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 => {
console.log('📦 API response data:', data);
if (data.success) {
showNotification('Bet submitted successfully!', 'success');
setTimeout(() => {
......@@ -834,9 +865,11 @@ function submitBet() {
}, 1500);
} else {
showNotification('Failed to submit bet: ' + (data.error || 'Unknown error'), 'error');
console.error('❌ Bet submission failed:', data);
}
})
.catch(error => {
console.error('❌ Error submitting bet:', 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