Files
telerifas/app/database.py
2025-10-29 11:14:34 +01:00

661 lines
26 KiB
Python

# database.py
import sqlite3
import logging
import time # Import time for timestamps
from config import DATABASE_PATH
logger = logging.getLogger(__name__)
# --- Database Initialization ---
def init_db():
conn = sqlite3.connect(DATABASE_PATH)
cur = conn.cursor()
# ... (raffles table definition remains the same) ...
cur.execute("""
CREATE TABLE IF NOT EXISTS raffles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
description TEXT,
image_file_id TEXT,
price INTEGER NOT NULL,
channel_id TEXT NOT NULL,
main_message_id INTEGER,
update_message_id INTEGER,
international_shipping INTEGER DEFAULT 0,
active INTEGER DEFAULT 1
)
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS participants (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
user_name TEXT,
raffle_id INTEGER NOT NULL,
numbers TEXT,
step TEXT NOT NULL,
invoice_id TEXT,
reservation_timestamp INTEGER,
completion_timestamp INTEGER,
UNIQUE(user_id, raffle_id, invoice_id),
FOREIGN KEY (raffle_id) REFERENCES raffles(id) ON DELETE CASCADE
)
""")
cur.execute("""
CREATE TABLE IF NOT EXISTS paypal_token (
id INTEGER PRIMARY KEY,
access_token TEXT,
expires_at INTEGER
)
""")
# Indexes
cur.execute("CREATE INDEX IF NOT EXISTS idx_participants_step_timestamp ON participants (step, reservation_timestamp)")
cur.execute("CREATE INDEX IF NOT EXISTS idx_participants_user_raffle_step ON participants (user_id, raffle_id, step)")
cur.execute("CREATE INDEX IF NOT EXISTS idx_raffles_active ON raffles (active)")
cur.execute("CREATE INDEX IF NOT EXISTS idx_participants_completion_ts ON participants (raffle_id, completion_timestamp DESC, step)")
conn.commit()
conn.close()
logger.info("Database initialized (ensuring participants.reservation_timestamp exists).")
# --- Database Connection --- (remains the same)
def connect_db():
conn = sqlite3.connect(DATABASE_PATH)
conn.row_factory = sqlite3.Row
return conn
# --- Raffle Management --- (remains the same)
# ... create_raffle, end_raffle, get_raffle, etc. ...
def create_raffle(name, description, price, image_file_id, channel_id, international_shipping=0):
"""Creates a new raffle in the database."""
conn = connect_db()
cur = conn.cursor()
try:
cur.execute(
"INSERT INTO raffles (name, description, price, image_file_id, channel_id, international_shipping) VALUES (?, ?, ?, ?, ?, ?)",
(name, description, price, image_file_id, channel_id, international_shipping)
)
raffle_id = cur.lastrowid
conn.commit()
logging.info(f"Created raffle '{name}' (ID: {raffle_id}) for channel: {channel_id}")
return raffle_id
except sqlite3.IntegrityError as e:
logging.error(f"Error creating raffle: Name '{name}' likely already exists. {e}")
return None
except Exception as e:
logging.error(f"Error creating raffle '{name}': {e}")
conn.rollback() # Rollback on error
return None
finally:
conn.close()
def end_raffle(raffle_id):
"""Marks a raffle as inactive."""
conn = connect_db()
cur = conn.cursor()
try:
cur.execute("UPDATE raffles SET active=0 WHERE id=? AND active=1", (raffle_id,))
updated = conn.total_changes > 0
conn.commit()
if updated:
logging.info(f"Ended raffle ID: {raffle_id}")
else:
logging.warning(f"Attempted to end raffle ID {raffle_id}, but it was not found or already inactive.")
return updated
except Exception as e:
logging.error(f"Error ending raffle ID {raffle_id}: {e}")
conn.rollback()
return False
finally:
conn.close()
def get_raffle(raffle_id):
"""Gets basic raffle details by ID."""
conn = connect_db()
cur = conn.cursor()
try:
# Does not include price directly, price is per channel
cur.execute("SELECT * FROM raffles WHERE id=?", (raffle_id,))
raffle = cur.fetchone()
return raffle
except Exception as e:
logger.error(f"Error getting raffle ID {raffle_id}: {e}")
return None
finally:
conn.close()
def get_active_raffles():
"""Gets all active raffles (basic info)."""
conn = connect_db()
cur = conn.cursor()
try:
cur.execute("SELECT * FROM raffles WHERE active=1")
raffles = cur.fetchall()
return raffles
except Exception as e:
logger.error(f"Error getting active raffles: {e}")
return []
finally:
conn.close()
def get_raffle_name(raffle_id):
"""Gets raffle name by ID."""
conn = connect_db()
cur = conn.cursor()
try:
cur.execute("SELECT name FROM raffles WHERE id=?", (raffle_id,))
name = cur.fetchone()
return name[0] if name else None
except Exception as e:
logging.error(f"Error getting raffle name for ID {raffle_id}: {e}")
return None
finally:
conn.close()
# --- Participant Management ---
def reserve_number(user_id, user_name, raffle_id, number):
"""Adds or updates a participant's reserved numbers."""
conn = connect_db()
cur = conn.cursor()
try:
cur.execute("SELECT id, numbers FROM participants WHERE raffle_id=? AND user_id=? AND step='waiting_for_payment'", (raffle_id, user_id))
existing_reservation = cur.fetchone()
if existing_reservation:
participant_id = existing_reservation['id']
numbers = existing_reservation['numbers']
numbers_list = numbers.split(',') if numbers else []
if str(number) not in numbers_list:
numbers_list.append(str(number))
numbers_str = ','.join(sorted(numbers_list))
cur.execute("UPDATE participants SET numbers=?, reservation_timestamp=? WHERE id=?",
(numbers_str, int(time.time()), participant_id))
else:
cur.execute(
"INSERT INTO participants (user_id, user_name, raffle_id, numbers, step, reservation_timestamp) VALUES (?, ?, ?, ?, ?, ?)",
(user_id, user_name, raffle_id, str(number), "waiting_for_payment", int(time.time()))
)
conn.commit()
except Exception as e:
logger.error(f"Error reserving number {number} for user {user_id} in raffle {raffle_id}: {e}")
conn.rollback()
finally:
conn.close()
def get_total_participations(raffle_id):
"""Gets the total number of participations (both reserved and confirmed) for a raffle."""
conn = connect_db()
cur = conn.cursor()
try:
cur.execute("SELECT COUNT(*) FROM participants WHERE raffle_id=?", (raffle_id,))
count = cur.fetchone()
return count[0] if count else 0
except Exception as e:
logger.error(f"Error getting total participations for raffle {raffle_id}: {e}")
return 0
finally:
conn.close()
def remove_reserved_number(participant_id, number):
"""Removes a specific number from a 'waiting_for_payment' reservation."""
conn = connect_db()
cur = conn.cursor()
try:
cur.execute("SELECT numbers FROM participants WHERE id=? AND step='waiting_for_payment'", (participant_id,))
result = cur.fetchone()
if result and result['numbers']:
numbers = result['numbers'].split(',')
if str(number) in numbers:
numbers.remove(str(number))
if numbers: # If list is not empty after removal
numbers_str = ','.join(sorted(numbers))
cur.execute("UPDATE participants SET numbers=? WHERE id=?", (numbers_str, participant_id))
else:
# If no numbers left, delete the entire reservation row
logger.info(f"No numbers left for participant {participant_id}, deleting reservation.")
cur.execute("DELETE FROM participants WHERE id=?", (participant_id,))
conn.commit()
return True
else:
logger.warning(f"Attempted to remove number {number} from non-existent or empty reservation {participant_id}")
return False # Indicate nothing changed
except Exception as e:
logger.error(f"Error removing reserved number {number} for participant {participant_id}: {e}")
conn.rollback()
return False
finally:
conn.close()
def mark_reservation_pending(participant_id, invoice_id):
"""Sets the invoice ID and reservation timestamp for a participant moving to pending payment."""
conn = connect_db()
cur = conn.cursor()
try:
cur.execute(
"UPDATE participants SET invoice_id=?, reservation_timestamp=? WHERE id=? AND step='waiting_for_payment'",
(invoice_id, int(time.time()), participant_id)
)
conn.commit()
logger.info(f"Marked reservation pending for participant {participant_id} with invoice {invoice_id} at {time.time()}")
except Exception as e:
logger.error(f"Error marking reservation pending for participant {participant_id}: {e}")
conn.rollback()
finally:
conn.close()
def get_expired_reservations(expiry_threshold_timestamp):
"""Finds participants whose reservations have expired."""
conn = connect_db()
cur = conn.cursor()
try:
cur.execute(
"""SELECT p.id, p.user_id, p.user_name, p.raffle_id, p.numbers, p.invoice_id, r.name as raffle_name
FROM participants p
JOIN raffles r ON p.raffle_id = r.id
WHERE p.step = 'waiting_for_payment'
AND p.reservation_timestamp IS NOT NULL
AND p.reservation_timestamp < ?""",
(int(expiry_threshold_timestamp),)
)
expired = cur.fetchall()
return expired # Returns list of Row objects
except Exception as e:
logger.error(f"Error fetching expired reservations: {e}")
return []
finally:
conn.close()
def cancel_reserved_numbers(user_id, raffle_id):
"""Deletes a 'waiting_for_payment' reservation for a user/raffle."""
conn = connect_db()
cur = conn.cursor()
deleted_count = 0
try:
cur.execute("DELETE FROM participants WHERE user_id=? AND raffle_id=? AND step='waiting_for_payment'", (user_id, raffle_id))
deleted_count = conn.total_changes
conn.commit()
if deleted_count > 0:
logger.info(f"Cancelled reservation for user {user_id}, raffle {raffle_id}.")
else:
logger.debug(f"No reservation found to cancel for user {user_id}, raffle {raffle_id}.")
except Exception as e:
logger.error(f"Error cancelling reservation for user {user_id}, raffle {raffle_id}: {e}")
conn.rollback()
finally:
conn.close()
return deleted_count > 0 # Return True if something was deleted
def cancel_reservation_by_id(participant_id):
"""Deletes a reservation using its specific participant ID."""
conn = connect_db()
cur = conn.cursor()
deleted_count = 0
try:
# Ensure we only delete if it's still in the correct state
cur.execute("DELETE FROM participants WHERE id=? AND step='waiting_for_payment'", (participant_id,))
deleted_count = conn.total_changes
conn.commit()
if deleted_count > 0:
logger.info(f"Cancelled reservation by participant ID: {participant_id}.")
else:
logger.warning(f"Attempted to cancel reservation by ID {participant_id}, but it was not found or not in 'waiting_for_payment' state.")
except Exception as e:
logger.error(f"Error cancelling reservation by ID {participant_id}: {e}")
conn.rollback()
finally:
conn.close()
return deleted_count > 0
def confirm_reserved_numbers(user_id, raffle_id, invoice_id):
"""Confirms payment, sets step to completed, removes reservation timestamp, and sets completion_timestamp."""
conn = connect_db()
cur = conn.cursor()
try:
current_completion_timestamp = int(time.time()) # Timestamp for completion
cur.execute(
"""UPDATE participants SET
step='completed',
invoice_id=?,
reservation_timestamp=NULL,
completion_timestamp=?
WHERE user_id=? AND raffle_id=? AND step='waiting_for_payment'""",
(invoice_id, current_completion_timestamp, user_id, raffle_id)
)
updated_count = conn.total_changes
conn.commit()
return updated_count > 0
except Exception as e:
logger.error(f"Error confirming reserved numbers for user {user_id}, raffle {raffle_id}: {e}")
conn.rollback()
return False
finally:
conn.close()
def get_last_n_other_participants(raffle_id, n=4):
"""
Gets the last N "completed" participants for a raffle, excluding the current one.
Returns a list of dictionaries [{'user_name', 'numbers'}].
"""
conn = connect_db()
cur = conn.cursor()
participants_info = []
try:
# Fetch N+1 recent completed participants, then filter out the current one in code
# if they are among them, ensuring we get N *other* participants.
# Order by completion_timestamp DESC.
cur.execute(
"""SELECT user_name, numbers
FROM participants
WHERE raffle_id = ? AND step = 'completed'
ORDER BY completion_timestamp DESC
LIMIT ?""",
(raffle_id, n)
)
rows = cur.fetchall()
skip_first = True
for row in rows:
if skip_first:
skip_first = False
continue
participants_info.append({'user_name': row['user_name'], 'numbers': row['numbers']})
# The list is already ordered newest first by the SQL query.
return participants_info # Newest other participants first
except Exception as e:
logger.error(f"Error getting last N other participants for raffle {raffle_id}: {e}")
return []
finally:
conn.close()
# --- Other participant functions (get_participant, get_participants, etc.) remain largely the same ---
# Ensure they exist as in your previous version. Add them back here if needed.
def get_participant_by_user_id_and_step(user_id, step):
conn = connect_db()
cur = conn.cursor()
try:
cur.execute("SELECT id, raffle_id, numbers, user_name FROM participants WHERE user_id=? AND step=?", (user_id, step))
participant = cur.fetchone()
return participant
except Exception as e:
logger.error(f"Error getting participant by user ID {user_id} and step {step}: {e}")
return None
finally:
conn.close()
def get_reserved_numbers(user_id, raffle_id):
conn = connect_db()
cur = conn.cursor()
try:
cur.execute("SELECT numbers FROM participants WHERE user_id=? AND raffle_id=? AND step='waiting_for_payment'", (user_id, raffle_id))
numbers = cur.fetchone()
return numbers['numbers'].split(',') if numbers and numbers['numbers'] else []
except Exception as e:
logger.error(f"Error getting reserved numbers for user {user_id}, raffle {raffle_id}: {e}")
return []
finally:
conn.close()
def get_participants(raffle_id):
conn = connect_db()
cur = conn.cursor()
try:
cur.execute("SELECT id, user_id, user_name, numbers, step FROM participants WHERE raffle_id=?", (raffle_id,))
participants = cur.fetchall()
return participants
except Exception as e:
logging.error(f"Error getting participants for raffle {raffle_id}: {e}")
return []
finally:
conn.close()
# Add any other necessary participant functions from your original code here
def get_participant_by_number(raffle_id, number):
conn = connect_db()
cur = conn.cursor()
try:
# Check both waiting and completed steps
cur.execute(
"SELECT id, user_id, user_name, numbers, step FROM participants WHERE raffle_id=? AND numbers LIKE ? AND (step='waiting_for_payment' OR step='completed')",
(raffle_id, f"%{number}%")
)
# This LIKE might match partial numbers if not careful (e.g., 1 matches 10).
# A better approach is to fetch all and check in Python, or use JSON functions if SQLite version supports it.
# For simplicity, let's refine the query slightly assuming numbers are comma-separated:
cur.execute(
"""SELECT id, user_id, user_name, numbers, step FROM participants
WHERE raffle_id=? AND (step='waiting_for_payment' OR step='completed')
AND (numbers = ? OR numbers LIKE ? OR numbers LIKE ? OR numbers LIKE ?)""",
(raffle_id, number, f"{number},%", f"%,{number},%", f"%,{number}")
)
participant = cur.fetchone() # Get the first match
# Verify the number actually exists in the list
if participant and participant['numbers']:
if str(number) in participant['numbers'].split(','):
return participant
else: # False positive from LIKE, try fetching next
# This gets complex quickly. Fetching all and filtering might be easier.
# Let's assume for now the refined LIKE works reasonably well for 00-99.
return participant # Return the first match found by SQL for now
return None # No match found
except Exception as e:
logger.error(f"Error getting participant by number {number} for raffle {raffle_id}: {e}")
return None
finally:
conn.close()
def get_raffle_by_user_id_waiting_payment(user_id):
conn = connect_db()
cur = conn.cursor()
try:
cur.execute("SELECT raffle_id FROM participants WHERE user_id=? AND step='waiting_for_payment'", (user_id,))
raffle_id = cur.fetchone()
return raffle_id[0] if raffle_id else None
except Exception as e:
logger.error(f"Error checking waiting payment raffle for user {user_id}: {e}")
return None
finally:
conn.close()
def get_user_by_invoice_id(invoice_id):
conn = connect_db()
cur = conn.cursor()
try:
# No need to join with raffles for price here, get price from raffle_channel_prices
cur.execute("SELECT * FROM participants WHERE invoice_id=? AND step='waiting_for_payment'", (invoice_id,))
data = cur.fetchone()
return data
except Exception as e:
logger.error(f"Error getting user/raffle by invoice ID {invoice_id}: {e}")
return None
finally:
conn.close()
def get_remaining_numbers_amount(raffle_id):
conn = connect_db()
cur = conn.cursor()
try:
# Count numbers from both 'waiting_for_payment' and 'completed' steps
cur.execute("SELECT numbers FROM participants WHERE raffle_id=? AND (step='waiting_for_payment' OR step='completed')", (raffle_id,))
all_numbers_rows = cur.fetchall()
taken_count = 0
for row in all_numbers_rows:
if row['numbers']:
try:
taken_count += len(row['numbers'].split(','))
except:
logger.warning(f"Invalid numbers format '{row['numbers']}' while counting remaining for raffle {raffle_id}")
return 100 - taken_count
except Exception as e:
logger.error(f"Error calculating remaining numbers for raffle {raffle_id}: {e}")
return -1 # Indicate error
finally:
conn.close()
def get_confirmed_numbers(user_id, raffle_id):
conn = connect_db()
cur = conn.cursor()
try:
cur.execute("SELECT numbers FROM participants WHERE user_id=? AND raffle_id=? AND step='completed'", (user_id, raffle_id))
numbers = cur.fetchone()
return numbers['numbers'].split(',') if numbers and numbers['numbers'] else []
except Exception as e:
logger.error(f"Error getting confirmed numbers for user {user_id}, raffle {raffle_id}: {e}")
return []
finally:
conn.close()
def get_remaining_numbers(raffle_id):
"""Gets a list of all remaining numbers for a raffle, formatted as two digits."""
conn = connect_db()
cur = conn.cursor()
try:
cur.execute(
"SELECT numbers FROM participants WHERE raffle_id=? AND (step='waiting_for_payment' OR step='completed')",
(raffle_id,)
)
all_numbers_rows = cur.fetchall()
taken_numbers_str_set = set() # Store taken numbers as strings (potentially "01", "10", etc.)
for row in all_numbers_rows:
if row['numbers']: # Ensure 'numbers' column is not None
# Split the comma-separated string and add individual numbers to the set
# Assumes numbers in DB are stored like "01,05,10"
taken_numbers_str_set.update(num.strip() for num in row['numbers'].split(',') if num.strip())
logger.debug(f"Raffle {raffle_id} - Taken numbers (string set): {taken_numbers_str_set}")
remaining_numbers_formatted = []
for num_int in range(100): # Iterate 0 through 99 as integers
# Format the integer as a two-digit string (e.g., 0 -> "00", 5 -> "05", 12 -> "12")
num_str_formatted = f"{num_int:02}"
# Check if this formatted string representation is in the set of taken numbers
if num_str_formatted not in taken_numbers_str_set:
remaining_numbers_formatted.append(num_str_formatted)
logger.debug(f"Raffle {raffle_id} - Remaining numbers formatted: {remaining_numbers_formatted}")
return remaining_numbers_formatted
except Exception as e:
logger.error(f"Error getting remaining numbers for raffle {raffle_id}: {e}")
return []
finally:
conn.close()
def store_main_message_id(raffle_id, message_id):
"""Updates the main_message_id for a given raffle."""
conn = connect_db()
cur = conn.cursor()
cur.execute("UPDATE raffles SET main_message_id=? WHERE id=?", (message_id, raffle_id))
conn.commit()
conn.close()
logger.info(f"Updated main_message_id for raffle {raffle_id} to {message_id}")
def get_main_message_id(raffle_id):
conn = connect_db()
cur = conn.cursor()
try:
cur.execute(
"SELECT main_message_id FROM raffles WHERE id=?",
(raffle_id,)
)
result = cur.fetchone()
return result['main_message_id'] if result else None
except Exception as e:
logger.error(f"Error getting main_message_id for raffle {raffle_id}: {e}")
return None
finally:
conn.close()
def store_update_message_id(raffle_id, message_id):
"""Updates the update_message_id for a given raffle."""
conn = connect_db()
cur = conn.cursor()
cur.execute("UPDATE raffles SET update_message_id=? WHERE id=?", (message_id, raffle_id))
conn.commit()
conn.close()
logger.info(f"Updated update_message_id for raffle {raffle_id} to {message_id}")
def get_update_message_id(raffle_id):
conn = connect_db()
cur = conn.cursor()
try:
cur.execute(
"SELECT update_message_id FROM raffles WHERE id=?",
(raffle_id,)
)
result = cur.fetchone()
return result['update_message_id'] if result else None
except Exception as e:
logger.error(f"Error getting update_message_id for raffle {raffle_id}: {e}")
return None
finally:
conn.close()
def get_paypal_access_token_db():
conn = connect_db()
cur = conn.cursor()
try:
cur.execute(
"SELECT * FROM paypal_token ORDER BY id DESC LIMIT 1"
)
result = cur.fetchone()
if result:
# Check if the token is still valid
if int(result['expires_at']) > int(time.time()):
return result['access_token']
except Exception as e:
logger.error(f"Error getting PayPal access token from DB: {e}")
return None
finally:
conn.close()
def store_paypal_access_token(access_token, expires_in):
conn = connect_db()
cur = conn.cursor()
try:
cur.execute(
"INSERT INTO paypal_token (access_token, expires_at) VALUES (?, ?)",
(access_token, int(time.time()) + expires_in)
)
conn.commit()
except Exception as e:
logger.error(f"Error storing PayPal access token in DB: {e}")
finally:
conn.close()
def get_all_invoice_ids(raffle_id):
conn = connect_db()
cur = conn.cursor()
try:
cur.execute(
"SELECT invoice_id FROM participants WHERE raffle_id=? AND step='completed' AND invoice_id IS NOT NULL",
(raffle_id,)
)
rows = cur.fetchall()
return [row['invoice_id'] for row in rows if row['invoice_id']]
except Exception as e:
logger.error(f"Error getting invoice IDs for raffle {raffle_id}: {e}")
return []
finally:
conn.close()
def delete_reservation_timestamp(invoice_id):
"""Clears the reservation_timestamp for a participant by invoice_id."""
conn = connect_db()
cur = conn.cursor()
try:
cur.execute(
"UPDATE participants SET reservation_timestamp=NULL WHERE invoice_id=? AND step='waiting_for_payment'",
(invoice_id,)
)
conn.commit()
logger.info(f"Cleared reservation_timestamp for invoice {invoice_id}")
except Exception as e:
logger.error(f"Error clearing reservation_timestamp for invoice {invoice_id}: {e}")
conn.rollback()
finally:
conn.close()