Fix CAP calculation ad reporting, limit cashier cancel ability

parent b35eb9f0
......@@ -4,12 +4,22 @@
echo "🚀 MbetterClient Build Script"
echo "============================="
USE_BUILT_PYQT6=false
# Check if Python 3 is available
if ! command -v python3 &> /dev/null; then
echo "❌ Python 3 is required but not installed."
exit 1
fi
# Check if built PyQt6 directory exists
if [ -d "pyqt6_built" ]; then
echo "📦 Built PyQt6 found in pyqt6_built/. Using local build..."
USE_BUILT_PYQT6=true
else
USE_BUILT_PYQT6=false
fi
# Check if virtual environment exists
if [ ! -d "venv" ]; then
echo "⚠️ Virtual environment not found. Creating one..."
......@@ -20,12 +30,22 @@ fi
echo "🔧 Activating virtual environment..."
source venv/bin/activate
# Set paths for built PyQt6 if available
if $USE_BUILT_PYQT6; then
export PYTHONPATH="pyqt6_built/usr/lib/python3/dist-packages:$PYTHONPATH"
export LD_LIBRARY_PATH="pyqt6_built/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH"
fi
# Install/upgrade dependencies
echo "📦 Installing dependencies..."
if [ -n "$VIRTUAL_ENV" ]; then
echo " 📦 Using virtual environment: $VIRTUAL_ENV"
pip install --upgrade pip
if $USE_BUILT_PYQT6; then
pip install -r requirements.txt --ignore-installed PyQt6 PyQt6-WebEngine
else
pip install -r requirements.txt
fi
# Verify critical package installations
echo " 🔍 Verifying critical package installations..."
......
#!/bin/bash
# Build PyQt6 without SSSE4.2 and POPCNT requirements using QEMU with restricted CPU
echo "🚀 Building PyQt6 with restricted CPU using QEMU"
echo "================================================"
# Check and install required tools
echo "🔧 Checking for required tools..."
if ! command -v sbuild-qemu-create &> /dev/null || ! command -v sbuild &> /dev/null; then
echo " 📦 Installing sbuild and qemu..."
sudo apt-get update
sudo apt-get install -y sbuild sbuild-qemu qemu-system-x86 qemu-user qemu-user-binfmt qemu devuan-archive-keyring
else
echo " ✅ Required tools are available"
fi
# Create symlink for qemu command
#if [ -f /usr/bin/qemu-system-x86_64 ]; then
# sudo ln -sf /usr/bin/qemu-system-x86_64 /usr/bin/qemu
#el
if [ -f /usr/bin/qemu-amd64 ]; then
sudo ln -sf /usr/bin/qemu-amd64 /usr/bin/qemu
elif [ -f /usr/bin/qemu-x86_64 ]; then
sudo ln -sf /usr/bin/qemu-x86_64 /usr/bin/qemu
else
echo " ⚠️ No suitable QEMU binary found"
fi
IMG="/srv/sbuild/qemu/sid-amd64.img"
PACKAGE="python3-pyqt6"
# 1. Create QEMU image if it doesn't exist
if [ ! -f "$IMG" ]; then
echo "📦 Creating QEMU image..."
sudo mkdir -p /srv/sbuild/qemu/
sudo sbuild-qemu-create -o "$IMG" daedalus http://deb.devuan.org/
else
echo "📦 QEMU image already exists"
fi
# Update the QEMU image
echo "🔄 Updating QEMU image..."
sudo sbuild-qemu-update "$IMG"
# --- Configuration ---
IMG_PATH=$IMG
MOUNT_DIR="/mnt/sbuild_img"
CHROOT_NAME="temp-img-chroot"
CPU_FLAG="Penryn"
PACKAGE_build="pyqt6"
if [ -z "$PACKAGE" ]; then
echo "Usage: $0 <package_name>"
exit 1
fi
# 1. Clean up any previous failed mounts
sudo umount -R $MOUNT_DIR 2>/dev/null || true
sudo qemu-nbd --disconnect /dev/nbd0 2>/dev/null || true
# 2. Mount the image
echo "Connecting image..."
sudo modprobe nbd
sudo qemu-nbd --connect=/dev/nbd0 "$IMG_PATH"
sleep 1 # Wait for partitions to register
# Create mount point and mount the first partition (adjust p1 if necessary)
sudo mkdir -p $MOUNT_DIR
sudo mount /dev/nbd0p1 $MOUNT_DIR
# 3. Setup QEMU-User environment
echo "Setting up qemu-user-static..."
# Copy the static binary into the image so the chroot can execute it
sudo cp /usr/bin/qemu-x86_64 "$MOUNT_DIR/usr/bin/qemu-x86_64-static"
# Bind mount system paths
for i in /dev /dev/pts /proc /sys /run; do
sudo mount -B $i "$MOUNT_DIR$i"
done
# 4. Create temporary schroot config
echo "Configuring schroot..."
SCHROOT_CONF="/etc/schroot/chroot.d/$CHROOT_NAME"
sudo bash -c "cat > $SCHROOT_CONF" <<EOF
[$CHROOT_NAME]
description=Temporary chroot from $IMG_PATH
directory=$MOUNT_DIR
type=directory
users=$(whoami)
groups=sbuild
root-groups=sbuild
profile=sbuild
EOF
# 5. Run the build with the CPU flag
echo "Starting build with QEMU_CPU=$CPU_FLAG..."
# QEMU_CPU is the environment variable used by qemu-user to set the CPU model
export QEMU_CPU="$CPU_FLAG"
read aaa
# Run sbuild
# --chroot points to the [name] we defined in the .conf file above
sbuild -d unstable --chroot "$CHROOT_NAME" "$PACKAGE_build"
# 6. Cleanup
echo "Cleaning up..."
sudo rm "$SCHROOT_CONF"
sudo umount -R $MOUNT_DIR
sudo qemu-nbd --disconnect /dev/nbd0
sudo rmdir $MOUNT_DIR
echo "Build complete."
# Extract the built package to pyqt6_built directory
if ls ${PACKAGE}_*.deb 1> /dev/null 2>&1; then
echo "📦 Extracting built package to pyqt6_built/..."
mkdir -p pyqt6_built
dpkg -x ${PACKAGE}_*.deb pyqt6_built/
echo "✅ PyQt6 extracted to pyqt6_built/"
else
echo "❌ No .deb file found after build"
fi
echo "✅ PyQt6 build completed!"
# --- Configuration ---
IMG_PATH="/srv/sbuild/qemu/sid-amd64.img"
MOUNT_DIR="/mnt/sbuild_img"
CHROOT_NAME="temp-img-chroot"
CPU_FLAG="Penryn"
PACKAGE=$1
if [ -z "$PACKAGE" ]; then
echo "Usage: $0 <package_name>"
exit 1
fi
# 1. Clean up any previous failed mounts
sudo umount -R $MOUNT_DIR 2>/dev/null || true
sudo qemu-nbd --disconnect /dev/nbd0 2>/dev/null || true
# 2. Mount the image
echo "Connecting image..."
sudo modprobe nbd
sudo qemu-nbd --connect=/dev/nbd0 "$IMG_PATH"
sleep 1 # Wait for partitions to register
# Create mount point and mount the first partition (adjust p1 if necessary)
sudo mkdir -p $MOUNT_DIR
sudo mount /dev/nbd0p1 $MOUNT_DIR
# 3. Setup QEMU-User environment
echo "Setting up qemu-user-static..."
# Copy the static binary into the image so the chroot can execute it
sudo cp /usr/bin/qemu-x86_64 "$MOUNT_DIR/usr/bin/qemu-x86_64-static"
# Bind mount system paths
for i in /dev /dev/pts /proc /sys /run; do
sudo mount -B $i "$MOUNT_DIR$i"
done
# 4. Create temporary schroot config
echo "Configuring schroot..."
SCHROOT_CONF="/etc/schroot/chroot.d/$CHROOT_NAME"
sudo bash -c "cat > $SCHROOT_CONF" <<EOF
[$CHROOT_NAME]
description=Temporary chroot from $IMG_PATH
directory=$MOUNT_DIR
type=directory
users=$(whoami)
groups=sbuild
root-groups=sbuild
profile=sbuild
EOF
# 5. Run the build with the CPU flag
echo "Starting build with QEMU_CPU=$CPU_FLAG..."
# QEMU_CPU is the environment variable used by qemu-user to set the CPU model
export QEMU_CPU="$CPU_FLAG"
# Run sbuild
# --chroot points to the [name] we defined in the .conf file above
sbuild -d testing --chroot "$CHROOT_NAME" "$PACKAGE"
# 6. Cleanup
echo "Cleaning up..."
sudo rm "$SCHROOT_CONF"
sudo umount -R $MOUNT_DIR
sudo qemu-nbd --disconnect /dev/nbd0
sudo rmdir $MOUNT_DIR
echo "Build complete."
#!/bin/bash
# Clean up files created by build_pyqt6.sh
echo "🧹 Cleaning up PyQt6 build artifacts"
echo "===================================="
# Remove QEMU image
IMG="/srv/sbuild/qemu/sid-amd64.img"
if [ -f "$IMG" ]; then
echo "🗑️ Removing QEMU image: $IMG"
sudo rm -f "$IMG"
else
echo "ℹ️ QEMU image not found"
fi
# Remove QEMU directory if empty
QEMU_DIR="/srv/sbuild/qemu"
if [ -d "$QEMU_DIR" ] && [ -z "$(ls -A $QEMU_DIR)" ]; then
echo "🗑️ Removing empty QEMU directory: $QEMU_DIR"
sudo rmdir "$QEMU_DIR"
fi
# Remove extracted PyQt6 directory
if [ -d "pyqt6_built" ]; then
echo "🗑️ Removing extracted PyQt6 directory: pyqt6_built"
rm -rf pyqt6_built
else
echo "ℹ️ pyqt6_built directory not found"
fi
# Remove built .deb files
if ls python3-pyqt6_*.deb 1> /dev/null 2>&1; then
echo "🗑️ Removing built .deb files: python3-pyqt6_*.deb"
rm -f python3-pyqt6_*.deb
else
echo "ℹ️ No python3-pyqt6 .deb files found"
fi
echo "✅ Cleanup completed!"
\ No newline at end of file
......@@ -1792,7 +1792,8 @@ class GamesThread(ThreadedComponent):
).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'UNDER',
BetDetailModel.result == 'pending'
BetDetailModel.result == 'pending',
BetDetailModel.result != 'cancelled'
).all()
under_count = len(total_under) if total_under else 0
......@@ -1806,7 +1807,8 @@ class GamesThread(ThreadedComponent):
).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'OVER',
BetDetailModel.result == 'pending'
BetDetailModel.result == 'pending',
BetDetailModel.result != 'cancelled'
).all()
over_count = len(total_over) if total_over else 0
......@@ -2303,6 +2305,7 @@ class GamesThread(ThreadedComponent):
logger.info(f"🎯 [EXTRACTION DEBUG] Filtered to {len(result_options)} result options: {[opt.result_name for opt in result_options]}")
payouts = {}
bet_counts = {}
total_bet_amount = 0.0
# Step 3: Calculate payouts for each result option
......@@ -2318,6 +2321,7 @@ class GamesThread(ThreadedComponent):
logger.info(f"💰 [EXTRACTION DEBUG] Found {len(associations)} associations for {result_name}")
payout = 0.0
total_bet_count = 0
for association in associations:
outcome_name = association.outcome_name
......@@ -2340,26 +2344,30 @@ class GamesThread(ThreadedComponent):
BetDetailModel.result == 'pending'
).all()
bet_count_for_outcome = len(bet_amount) if bet_amount else 0
total_outcome_amount = sum(bet.amount for bet in bet_amount) if bet_amount else 0.0
logger.info(f"💰 [EXTRACTION DEBUG] Total bet amount for {outcome_name}: {total_outcome_amount:.2f}")
logger.info(f"💰 [EXTRACTION DEBUG] Total bet amount for {outcome_name}: {total_outcome_amount:.2f} ({bet_count_for_outcome} bets)")
payout += total_outcome_amount * coefficient
total_bet_count += bet_count_for_outcome
logger.info(f"💰 [EXTRACTION DEBUG] Added payout for {outcome_name}: {total_outcome_amount:.2f} × {coefficient} = {total_outcome_amount * coefficient:.2f}")
else:
logger.warning(f"💰 [EXTRACTION DEBUG] No match outcome found for {outcome_name}")
payouts[result_name] = payout
logger.info(f"💰 [EXTRACTION DEBUG] Total payout for {result_name}: {payout:.2f}")
bet_counts[result_name] = total_bet_count
logger.info(f"💰 [EXTRACTION DEBUG] Total payout for {result_name}: {payout:.2f} (from {total_bet_count} bets)")
# Step 4: Calculate total bet amount (excluding UNDER/OVER)
# Step 4: Calculate total bet amount (excluding UNDER/OVER and cancelled bets)
logger.info(f"💵 [EXTRACTION DEBUG] Step 4: Calculating total bet amount for match {match_id}")
all_bets = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.result == 'pending',
BetDetailModel.result != 'cancelled',
~BetDetailModel.outcome.in_(['UNDER', 'OVER'])
).all()
total_bet_amount = sum(bet.amount for bet in all_bets) if all_bets else 0.0
logger.info(f"💵 [EXTRACTION DEBUG] Total bet amount calculated: {total_bet_amount:.2f} from {len(all_bets)} bets")
logger.info(f"💵 [EXTRACTION DEBUG] Total bet amount calculated: {total_bet_amount:.2f} from {len(all_bets)} bets (excluding cancelled bets)")
# Step 5: Get UNDER/OVER result and calculate payouts for CAP adjustment
logger.info(f"🎯 [EXTRACTION DEBUG] Step 5: Getting UNDER/OVER result and calculating payouts for CAP adjustment")
......@@ -2405,7 +2413,13 @@ class GamesThread(ThreadedComponent):
logger.info(f"🎯 [EXTRACTION DEBUG] CAP percentage: {cap_percentage}%, base threshold: {base_cap_threshold:.2f}, final threshold: {cap_threshold:.2f}")
logger.info(f"📊 [EXTRACTION DEBUG] Extraction summary - {len(payouts)} results, total_bet_amount={total_bet_amount:.2f}, CAP={cap_percentage}%, threshold={cap_threshold:.2f}")
logger.info(f"📊 [EXTRACTION DEBUG] Payouts: {payouts}")
logger.info(f"📊 [EXTRACTION DEBUG] All possible results, their payouts, and bet counts:")
for result_name in payouts.keys():
payout = payouts[result_name]
bet_count = bet_counts[result_name]
logger.info(f"📊 [EXTRACTION DEBUG] Result '{result_name}': payout = {payout:.2f}, bet_count = {bet_count}")
logger.info(f"📊 [EXTRACTION DEBUG] Full payouts dict: {payouts}")
logger.info(f"📊 [EXTRACTION DEBUG] Full bet counts dict: {bet_counts}")
# Step 6: Filter payouts below adjusted CAP threshold
logger.info(f"🎯 [EXTRACTION DEBUG] Step 6: Filtering payouts below adjusted CAP threshold")
......@@ -2466,42 +2480,37 @@ class GamesThread(ThreadedComponent):
return fallback_result, []
def _weighted_result_selection(self, eligible_payouts: Dict[str, float], session, match_id: int) -> str:
"""Perform weighted random selection based on inverse coefficients"""
"""Select result that maximizes redistribution up to CAP without exceeding"""
try:
import random
weights = {}
total_weight = 0.0
for result_name in eligible_payouts.keys():
# Get coefficient for this result (inverse weighting - higher coefficient = lower probability)
# For simplicity, we'll use a default coefficient or calculate based on payout
# In a real implementation, you'd have result-specific coefficients
coefficient = 1.0 / (eligible_payouts[result_name] + 1.0) # Avoid division by zero
weights[result_name] = coefficient
total_weight += coefficient
if not eligible_payouts:
logger.error("No eligible payouts provided")
return None
if total_weight == 0:
# Fallback to equal weights
weight_value = 1.0 / len(eligible_payouts)
weights = {k: weight_value for k in eligible_payouts.keys()}
total_weight = sum(weights.values())
# Find the maximum payout among eligible results
max_payout = max(eligible_payouts.values())
logger.info(f"🎯 [EXTRACTION DEBUG] Maximum eligible payout: {max_payout:.2f}")
# Generate random selection
rand = random.uniform(0, total_weight)
cumulative = 0.0
# Get all results that have this maximum payout
candidates = [result_name for result_name, payout in eligible_payouts.items() if payout == max_payout]
logger.info(f"🎯 [EXTRACTION DEBUG] Candidates with max payout: {candidates}")
for result_name, weight in weights.items():
cumulative += weight
if rand <= cumulative:
return result_name
# If only one candidate, return it
if len(candidates) == 1:
selected_result = candidates[0]
logger.info(f"🎯 [EXTRACTION DEBUG] Single candidate selected: {selected_result}")
return selected_result
# Fallback
return list(eligible_payouts.keys())[0]
# If multiple candidates with same max payout, select randomly among them
selected_result = random.choice(candidates)
logger.info(f"🎯 [EXTRACTION DEBUG] Random selection among {len(candidates)} candidates: {selected_result}")
return selected_result
except Exception as e:
logger.error(f"Failed to perform weighted result selection: {e}")
return list(eligible_payouts.keys())[0]
logger.error(f"Failed to select max redistribution result: {e}")
# Fallback to first available
return list(eligible_payouts.keys())[0] if eligible_payouts else None
def _get_outcome_coefficient(self, match_id: int, outcome: str, session) -> float:
"""Get coefficient for a specific outcome from match outcomes"""
......@@ -2695,9 +2704,10 @@ class GamesThread(ThreadedComponent):
logger.warning(f"Match {match_id} not found for statistics collection")
return
# Calculate statistics
# Calculate statistics (excluding cancelled bets)
total_bets = session.query(BetDetailModel).join(MatchModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.result != 'cancelled',
MatchModel.active_status == True
).count()
......@@ -2705,6 +2715,7 @@ class GamesThread(ThreadedComponent):
BetDetailModel.amount
).join(MatchModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.result != 'cancelled',
MatchModel.active_status == True
).all()
total_amount_collected = sum(bet.amount for bet in total_amount_collected) if total_amount_collected else 0.0
......@@ -2719,10 +2730,11 @@ class GamesThread(ThreadedComponent):
).all()
total_redistributed = sum(bet.win_amount for bet in total_redistributed) if total_redistributed else 0.0
# Get UNDER/OVER specific statistics
# Get UNDER/OVER specific statistics (excluding cancelled bets)
under_bets = session.query(BetDetailModel).join(MatchModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'UNDER',
BetDetailModel.result != 'cancelled',
MatchModel.active_status == True
).count()
......@@ -2731,6 +2743,7 @@ class GamesThread(ThreadedComponent):
).join(MatchModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'UNDER',
BetDetailModel.result != 'cancelled',
MatchModel.active_status == True
).all()
under_amount = sum(bet.amount for bet in under_amount) if under_amount else 0.0
......@@ -2738,6 +2751,7 @@ class GamesThread(ThreadedComponent):
over_bets = session.query(BetDetailModel).join(MatchModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'OVER',
BetDetailModel.result != 'cancelled',
MatchModel.active_status == True
).count()
......@@ -2746,6 +2760,7 @@ class GamesThread(ThreadedComponent):
).join(MatchModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.outcome == 'OVER',
BetDetailModel.result != 'cancelled',
MatchModel.active_status == True
).all()
over_amount = sum(bet.amount for bet in over_amount) if over_amount else 0.0
......
......@@ -789,8 +789,10 @@ class DatabaseManager:
try:
session = self.get_session()
# Check if admin user already exists by username (more specific than is_admin check)
admin_user = session.query(UserModel).filter_by(username='admin').first()
# Check if admin user already exists by username or email
admin_user = session.query(UserModel).filter(
(UserModel.username == 'admin') | (UserModel.email == 'admin@mbetterclient.local')
).first()
if not admin_user:
# Only create if no admin user exists at all
......@@ -816,10 +818,12 @@ class DatabaseManager:
else:
logger.info("Admin users exist, skipping default admin creation")
else:
logger.info("Admin user 'admin' already exists, skipping creation")
logger.info("Admin user 'admin' or email 'admin@mbetterclient.local' already exists, skipping creation")
# Check if default cashier exists (this should be handled by Migration_007)
cashier_user = session.query(UserModel).filter_by(username='cashier').first()
cashier_user = session.query(UserModel).filter(
(UserModel.username == 'cashier') | (UserModel.email == 'cashier@mbetterclient.local')
).first()
if not cashier_user:
# Create default cashier - migrations should handle this, but fallback just in case
......@@ -839,7 +843,32 @@ class DatabaseManager:
session.add(cashier)
logger.info("Default cashier user created via fallback method (cashier/cashier123)")
else:
logger.info("Cashier user 'cashier' already exists, skipping creation")
logger.info("Cashier user 'cashier' or email 'cashier@mbetterclient.local' already exists, skipping creation")
# Check if default normal user exists
normal_user = session.query(UserModel).filter(
(UserModel.username == 'user') | (UserModel.email == 'user@mbetterclient.local')
).first()
if not normal_user:
# Create default normal user
user = UserModel(
username='user',
email='user@mbetterclient.local',
is_admin=False
)
user.set_password('user123')
# Set normal role (handle backward compatibility)
if hasattr(user, 'set_role'):
user.set_role('normal')
elif hasattr(user, 'role'):
user.role = 'normal'
session.add(user)
logger.info("Default normal user created via fallback method (user/user123)")
else:
logger.info("Normal user 'user' or email 'user@mbetterclient.local' already exists, skipping creation")
session.commit()
......
......@@ -355,8 +355,8 @@ class Migration_007_CreateDefaultCashierUser(DatabaseMigration):
"""Create default cashier user"""
try:
with db_manager.engine.connect() as conn:
# Check if cashier user already exists
result = conn.execute(text("SELECT COUNT(*) FROM users WHERE username = 'cashier'"))
# Check if cashier user already exists (by username or email)
result = conn.execute(text("SELECT COUNT(*) FROM users WHERE username = 'cashier' OR email = 'cashier@mbetterclient.local'"))
cashier_count = result.scalar()
if cashier_count == 0:
......
......@@ -4767,7 +4767,7 @@ def get_cashier_bet_details(bet_id):
def cancel_cashier_bet(bet_id):
"""Cancel a bet and all its details (cashier)"""
try:
from ..database.models import BetModel, BetDetailModel
from ..database.models import BetModel, BetDetailModel, MatchModel
bet_uuid = str(bet_id)
session = api_bp.db_manager.get_session()
......@@ -4777,8 +4777,16 @@ def cancel_cashier_bet(bet_id):
if not bet:
return jsonify({"error": "Bet not found"}), 404
# Check if bet can be cancelled (only pending bets)
# Get bet details
bet_details = session.query(BetDetailModel).filter_by(bet_id=bet_uuid).all()
# Check if any match has already started
match_ids = [detail.match_id for detail in bet_details]
matches = session.query(MatchModel).filter(MatchModel.id.in_(match_ids)).all()
if any(match.status in ['ingame', 'done'] for match in matches):
return jsonify({"error": "Cannot cancel bet because one or more matches have already started"}), 400
# Check if bet can be cancelled (only pending bets)
if any(detail.result != 'pending' for detail in bet_details):
return jsonify({"error": "Cannot cancel bet with non-pending results"}), 400
......@@ -5504,13 +5512,15 @@ def get_daily_reports_summary():
bets = bets_query.all()
logger.info(f"Found {len(bets)} bets for daily summary")
# Calculate totals
# Calculate totals (excluding cancelled bets)
total_payin = 0.0
total_bets = 0
for bet in bets:
# Get bet details for this bet
bet_details = session.query(BetDetailModel).filter_by(bet_id=bet.uuid).all()
# Get bet details for this bet (excluding cancelled bets)
bet_details = session.query(BetDetailModel).filter_by(
bet_id=bet.uuid
).filter(BetDetailModel.result != 'cancelled').all()
bet_total = sum(float(detail.amount) for detail in bet_details)
total_payin += bet_total
total_bets += len(bet_details)
......@@ -5576,10 +5586,11 @@ def get_match_reports():
logger.info(f"Querying match reports for local date {date_param}: UTC range {start_datetime} to {end_datetime}")
# Get all matches that had bets on this day
# Get all matches that had bets on this day (excluding cancelled bets)
bet_details_query = session.query(BetDetailModel).join(BetModel).filter(
BetModel.bet_datetime >= start_datetime,
BetModel.bet_datetime <= end_datetime
BetModel.bet_datetime <= end_datetime,
BetDetailModel.result != 'cancelled'
)
# Group by match_id and calculate statistics
......@@ -5789,7 +5800,7 @@ def download_excel_report():
ws_summary['A1'].font = Font(bold=True, size=16)
ws_summary.merge_cells('A1:E1')
# Get daily summary data
# Get daily summary data (excluding cancelled bets)
bets_query = session.query(BetModel).filter(
BetModel.bet_datetime >= start_datetime,
BetModel.bet_datetime <= end_datetime
......@@ -5799,7 +5810,9 @@ def download_excel_report():
total_payin = 0.0
total_bets = 0
for bet in bets:
bet_details = session.query(BetDetailModel).filter_by(bet_id=bet.uuid).all()
bet_details = session.query(BetDetailModel).filter_by(
bet_id=bet.uuid
).filter(BetDetailModel.result != 'cancelled').all()
bet_total = sum(float(detail.amount) for detail in bet_details)
total_payin += bet_total
total_bets += len(bet_details)
......@@ -5851,10 +5864,11 @@ def download_excel_report():
cell.border = border
cell.alignment = Alignment(horizontal='center')
# Get match data
# Get match data (excluding cancelled bets)
bet_details_query = session.query(BetDetailModel).join(BetModel).filter(
BetModel.bet_datetime >= start_datetime,
BetModel.bet_datetime <= end_datetime
BetModel.bet_datetime <= end_datetime,
BetDetailModel.result != 'cancelled'
)
match_stats = {}
......
......@@ -518,7 +518,8 @@ function updateBetsTable(data, container) {
<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>
<th><i class="fas fa-dollar-sign me-1"></i>Total Amount</th>
<th><i class="fas fa-dollar-sign me-1"></i>Payin</th>
<th><i class="fas fa-trophy me-1"></i>Payout</th>
<th><i class="fas fa-chart-line me-1"></i>Status</th>
<th><i class="fas fa-money-bill me-1"></i>Payment</th>
<th><i class="fas fa-cogs me-1"></i>Actions</th>
......@@ -561,6 +562,15 @@ function updateBetsTable(data, container) {
'<span class="badge bg-success"><i class="fas fa-check me-1"></i>Paid</span>' :
'<span class="badge bg-secondary"><i class="fas fa-clock me-1"></i>Unpaid</span>';
// Calculate payout for winning bets
let payoutAmount = 0.0;
if (overallStatus === 'won' && bet.details) {
payoutAmount = bet.details
.filter(detail => detail.result === 'win')
.reduce((sum, detail) => sum + parseFloat(detail.win_amount || 0), 0);
}
const payoutDisplay = payoutAmount > 0 ? formatCurrency(payoutAmount.toFixed(2)) : '-';
tableHTML += `
<tr>
<td><strong>${bet.uuid.substring(0, 8)}...</strong></td>
......@@ -569,6 +579,7 @@ function updateBetsTable(data, container) {
<td>${bet.details ? bet.details.length : 0} selections</td>
<td>${matchNumbers.length > 0 ? matchNumbers.join(', ') : 'N/A'}</td>
<td><strong class="currency-amount" data-amount="${totalAmount}">${formatCurrency(totalAmount)}</strong></td>
<td><strong class="currency-amount" data-amount="${payoutAmount}">${payoutDisplay}</strong></td>
<td>${statusBadge}</td>
<td>${paidBadge}</td>
<td>
......
......@@ -518,7 +518,8 @@ function updateBetsTable(data, container) {
<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>
<th><i class="fas fa-dollar-sign me-1"></i>Total Amount</th>
<th><i class="fas fa-dollar-sign me-1"></i>Payin</th>
<th><i class="fas fa-trophy me-1"></i>Payout</th>
<th><i class="fas fa-chart-line me-1"></i>Status</th>
<th><i class="fas fa-money-bill me-1"></i>Payment</th>
<th><i class="fas fa-cogs me-1"></i>Actions</th>
......@@ -561,6 +562,15 @@ function updateBetsTable(data, container) {
'<span class="badge bg-success"><i class="fas fa-check me-1"></i>Paid</span>' :
'<span class="badge bg-secondary"><i class="fas fa-clock me-1"></i>Unpaid</span>';
// Calculate payout for winning bets
let payoutAmount = 0.0;
if (overallStatus === 'won' && bet.details) {
payoutAmount = bet.details
.filter(detail => detail.result === 'win')
.reduce((sum, detail) => sum + parseFloat(detail.win_amount || 0), 0);
}
const payoutDisplay = payoutAmount > 0 ? formatCurrency(payoutAmount.toFixed(2)) : '-';
tableHTML += `
<tr>
<td><strong>${bet.uuid.substring(0, 8)}...</strong></td>
......@@ -569,6 +579,7 @@ function updateBetsTable(data, container) {
<td>${bet.details ? bet.details.length : 0} selections</td>
<td>${matchNumbers.length > 0 ? matchNumbers.join(', ') : 'N/A'}</td>
<td><strong class="currency-amount" data-amount="${totalAmount}">${formatCurrency(totalAmount)}</strong></td>
<td><strong class="currency-amount" data-amount="${payoutAmount}">${payoutDisplay}</strong></td>
<td>${statusBadge}</td>
<td>${paidBadge}</td>
<td>
......
This source diff could not be displayed because it is too large. You can view the blob instead.
pyqt6_amd64-2026-01-05T23:47:39Z.build
\ No newline at end of file
#!/usr/bin/env python3
"""
Test script to verify bet cancellation logic
"""
import sqlite3
import json
from datetime import datetime
def check_database():
"""Check what's in the database"""
from mbetterclient.config.settings import get_user_data_dir
db_path = get_user_data_dir() / "mbetterclient.db"
conn = sqlite3.connect(str(db_path))
cursor = conn.cursor()
# Check table names
cursor.execute("SELECT name FROM sqlite_master WHERE type='table'")
tables = cursor.fetchall()
print(f"Tables in database: {[t[0] for t in tables]}")
# Check bets
try:
cursor.execute("SELECT uuid, fixture_id, paid FROM bets LIMIT 5")
bets = cursor.fetchall()
print(f"\nFound {len(bets)} bets:")
for bet in bets:
print(f" UUID: {bet[0]}, Fixture: {bet[1]}, Paid: {bet[2]}")
# Check bet details for first bet
if bets:
bet_uuid = bets[0][0]
cursor.execute("SELECT id, bet_id, match_id, outcome, amount, result FROM bets_details WHERE bet_id = ?", (bet_uuid,))
details = cursor.fetchall()
print(f"\nBet details for {bet_uuid}:")
for detail in details:
print(f" ID: {detail[0]}, Match: {detail[2]}, Outcome: {detail[3]}, Amount: {detail[4]}, Result: {detail[5]}")
# Check match statuses for this bet
match_ids = [detail[2] for detail in details]
if match_ids:
placeholders = ','.join('?' * len(match_ids))
cursor.execute(f"SELECT id, status FROM matches WHERE id IN ({placeholders})", match_ids)
match_statuses = cursor.fetchall()
print(f"\nMatch statuses for bet {bet_uuid}:")
for match_id, status in match_statuses:
print(f" Match {match_id}: {status}")
except sqlite3.OperationalError as e:
print(f"\nError querying bets table: {e}")
# Check matches
try:
cursor.execute("SELECT id, match_number, status, fixture_id FROM matches ORDER BY id LIMIT 10")
matches = cursor.fetchall()
print(f"\nFound {len(matches)} matches (showing first 10):")
for match in matches:
print(f" ID: {match[0]}, Number: {match[1]}, Status: {match[2]}, Fixture: {match[3]}")
# Count matches by status
cursor.execute("SELECT status, COUNT(*) FROM matches GROUP BY status ORDER BY COUNT(*) DESC")
status_counts = cursor.fetchall()
print(f"\nMatch status counts:")
for status, count in status_counts:
print(f" {status}: {count}")
# Check total matches
cursor.execute("SELECT COUNT(*) FROM matches")
total_matches = cursor.fetchone()[0]
print(f"\nTotal matches in database: {total_matches}")
except sqlite3.OperationalError as e:
print(f"\nError querying matches table: {e}")
conn.close()
if __name__ == "__main__":
check_database()
\ No newline at end of file
#!/usr/bin/env python3
"""
Test script to verify the bet cancellation logic directly
"""
from mbetterclient.database.models import BetModel, BetDetailModel, MatchModel
from mbetterclient.database.manager import DatabaseManager
from mbetterclient.config.settings import get_user_data_dir
def test_cancel_logic():
"""Test the cancel logic directly"""
# Use the default database path
db_path = get_user_data_dir() / "mbetterclient.db"
db_manager = DatabaseManager(str(db_path))
if not db_manager.initialize():
print("Failed to initialize database")
return False
session = db_manager.get_session()
try:
# Get all bets and check their matches
bets = session.query(BetModel).all()
print(f"Found {len(bets)} bets total")
for bet in bets:
print(f"\nTesting cancellation for bet: {bet.uuid}")
# Get bet details
bet_details = session.query(BetDetailModel).filter_by(bet_id=bet.uuid).all()
print(f"Bet has {len(bet_details)} details")
# Check if any match has already started
match_ids = [detail.match_id for detail in bet_details]
matches = session.query(MatchModel).filter(MatchModel.id.in_(match_ids)).all()
print("Match statuses:")
for match in matches:
print(f" Match {match.id}: {match.status}")
# Check the logic
blocked = any(match.status in ['ingame', 'done'] for match in matches)
if blocked:
print("❌ Cancellation should be BLOCKED - match has already started")
else:
print("✅ Cancellation should be ALLOWED - no matches have started yet")
return True # Found one that should be allowed
print("\nAll existing bets are on matches that have already started.")
return True
# Get bet details
bet_details = session.query(BetDetailModel).filter_by(bet_id=bet.uuid).all()
print(f"Bet has {len(bet_details)} details")
# Check if any match has already started
match_ids = [detail.match_id for detail in bet_details]
matches = session.query(MatchModel).filter(MatchModel.id.in_(match_ids)).all()
print("Match statuses:")
for match in matches:
print(f" Match {match.id}: {match.status}")
# Check the logic
if any(match.status in ['ingame', 'done'] for match in matches):
print("❌ Cancellation should be BLOCKED - match has already started")
return True # Test passed - correctly blocked
else:
print("✅ Cancellation should be ALLOWED - no matches have started yet")
return True # Test passed - correctly allowed
except Exception as e:
print(f"Error: {e}")
return False
finally:
session.close()
db_manager.close()
if __name__ == "__main__":
print("Testing bet cancellation logic...")
success = test_cancel_logic()
if success:
print("\n✅ Logic test completed successfully!")
else:
print("\n❌ Logic test failed.")
\ No newline at end of file
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