Did some things...
This commit is contained in:
38
app/app.py
38
app/app.py
@@ -16,7 +16,7 @@ from telegram.ext import (
|
||||
ContextTypes,
|
||||
)
|
||||
from telegram.error import Forbidden, BadRequest
|
||||
# REMOVE: from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from newrelic_telemetry_sdk import Log, LogClient
|
||||
|
||||
# Import handlers and db/config
|
||||
from handlers import *
|
||||
@@ -27,8 +27,35 @@ from config import *
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
|
||||
)
|
||||
|
||||
# Suppress overly verbose logs from libraries
|
||||
logging.getLogger("httpx").setLevel(logging.WARNING)
|
||||
# REMOVE: logging.getLogger("apscheduler").setLevel(logging.WARNING) # No longer needed
|
||||
logging.getLogger("apscheduler").setLevel(logging.WARNING)
|
||||
|
||||
client = LogClient(license_key=NEWRELIC_API_KEY, host="log-api.eu.newrelic.com")
|
||||
|
||||
class NewRelicHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
try:
|
||||
log = Log(
|
||||
message=self.format(record),
|
||||
level=record.levelname,
|
||||
timestamp_ms=int(record.created * 1000),
|
||||
attributes={
|
||||
"logger": record.name,
|
||||
"app_name": BOT_NAME,
|
||||
"docker_container": "telerifas"
|
||||
}
|
||||
)
|
||||
client.send(log)
|
||||
except Exception:
|
||||
self.handleError(record)
|
||||
|
||||
nr_handler = NewRelicHandler()
|
||||
nr_handler.setFormatter(logging.Formatter("%(message)s"))
|
||||
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.addHandler(nr_handler)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -65,7 +92,7 @@ async def check_expired_reservations(context: ContextTypes.DEFAULT_TYPE):
|
||||
cancelled_count += 1
|
||||
# Try to notify the user using context.bot
|
||||
notification_text = (
|
||||
f"Las papeletas `{numbers}` que tenías reservadas para la rifa **{raffle_name}** han sido liberadas.\n\n"
|
||||
f"Las participaciones `{numbers}` que tenías reservadas para el sorteo **{raffle_name}** han sido liberadas.\n\n"
|
||||
f"Puedes volver a reservarlas, ¡pero tienes {RESERVATION_TIMEOUT_MINUTES} minutos para completar el pago!."
|
||||
)
|
||||
try:
|
||||
@@ -147,7 +174,7 @@ def main():
|
||||
# --- Handlers (Remain the same) ---
|
||||
# 1. Raffle Creation Conversation Handler
|
||||
raffle_creation_conv = ConversationHandler(
|
||||
entry_points=[CommandHandler("crear_rifa", new_raffle_start)],
|
||||
entry_points=[CommandHandler("crear_sorteo", new_raffle_start)],
|
||||
states={
|
||||
SELECTING_CHANNEL: [CallbackQueryHandler(select_channel, pattern=f"^{SELECT_CHANNEL_PREFIX}.*")],
|
||||
TYPING_TITLE: [MessageHandler(filters.TEXT & ~filters.COMMAND, receive_title)],
|
||||
@@ -156,6 +183,9 @@ def main():
|
||||
MessageHandler(filters.PHOTO, receive_image),
|
||||
MessageHandler(filters.TEXT & ~filters.COMMAND, incorrect_input_type)
|
||||
],
|
||||
INTERNATIONAL_SHIPPING: [
|
||||
MessageHandler(filters.TEXT & ~filters.COMMAND, receive_international_shipping)
|
||||
],
|
||||
TYPING_PRICE_FOR_CHANNEL: [
|
||||
MessageHandler(filters.TEXT & ~filters.COMMAND, receive_price_for_channel)
|
||||
],
|
||||
|
||||
@@ -21,9 +21,13 @@ WEBHOOK_URL = os.getenv("WEBHOOK_URL")
|
||||
WEBHOOK_ID = os.getenv("WEBHOOK_ID")
|
||||
RESERVATION_TIMEOUT_MINUTES = 15
|
||||
TYC_DOCUMENT_URL = os.getenv("TYC_DOCUMENT_URL")
|
||||
NEWRELIC_API_KEY = os.getenv("NEWRELIC_API_KEY")
|
||||
PAYPAL_PERCENTAGE_FEE = float(os.getenv("PAYPAL_PERCENTAGE_FEE", "1.90")) # Default 1.90%
|
||||
PAYPAL_FIXED_FEE = float(os.getenv("PAYPAL_FIXED_FEE", "0.35")) # Default 0.35 EUR
|
||||
PAYPAL_URL = os.getenv("PAYPAL_URL", "https://api-m.paypal.com") # Default to live URL
|
||||
|
||||
# Conversation States for Raffle Creation
|
||||
(SELECTING_CHANNEL, TYPING_TITLE, TYPING_DESCRIPTION, TYPING_PRICE_FOR_CHANNEL, SENDING_IMAGE, CONFIRMING_CREATION) = range(6)
|
||||
(SELECTING_CHANNEL, TYPING_TITLE, TYPING_DESCRIPTION, TYPING_PRICE_FOR_CHANNEL, INTERNATIONAL_SHIPPING, SENDING_IMAGE, CONFIRMING_CREATION) = range(7)
|
||||
|
||||
# Conversation States for Editing Raffles
|
||||
(EDIT_SELECT_RAFFLE, EDIT_SELECT_NEW_CHANNELS, EDIT_TYPING_PRICE_FOR_NEW_CHANNELS, EDIT_CONFIRM) = range(6, 10)
|
||||
|
||||
@@ -21,6 +21,7 @@ def init_db():
|
||||
channel_id TEXT NOT NULL,
|
||||
main_message_id INTEGER,
|
||||
update_message_id INTEGER,
|
||||
international_shipping INTEGER DEFAULT 0,
|
||||
active INTEGER DEFAULT 1
|
||||
)
|
||||
""")
|
||||
@@ -67,14 +68,14 @@ def connect_db():
|
||||
|
||||
# --- Raffle Management --- (remains the same)
|
||||
# ... create_raffle, end_raffle, get_raffle, etc. ...
|
||||
def create_raffle(name, description, price, image_file_id, channel_id):
|
||||
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) VALUES (?, ?, ?, ?, ?)",
|
||||
(name, description, price, image_file_id, channel_id)
|
||||
"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()
|
||||
@@ -171,12 +172,12 @@ def reserve_number(user_id, user_name, raffle_id, number):
|
||||
if str(number) not in numbers_list:
|
||||
numbers_list.append(str(number))
|
||||
numbers_str = ','.join(sorted(numbers_list))
|
||||
cur.execute("UPDATE participants SET numbers=? WHERE id=?",
|
||||
(numbers_str, participant_id))
|
||||
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) VALUES (?, ?, ?, ?, ?)",
|
||||
(user_id, user_name, raffle_id, str(number), "waiting_for_payment")
|
||||
"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:
|
||||
@@ -185,6 +186,20 @@ def reserve_number(user_id, user_name, raffle_id, number):
|
||||
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()
|
||||
@@ -215,17 +230,17 @@ def remove_reserved_number(participant_id, number):
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def mark_reservation_pending(participant_id, invoice_id, timestamp):
|
||||
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(timestamp), participant_id)
|
||||
(invoice_id, int(time.time()), participant_id)
|
||||
)
|
||||
conn.commit()
|
||||
logger.info(f"Marked reservation pending for participant {participant_id} with invoice {invoice_id} at {timestamp}")
|
||||
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()
|
||||
@@ -481,6 +496,19 @@ def get_remaining_numbers_amount(raffle_id):
|
||||
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()
|
||||
@@ -599,3 +627,19 @@ def store_paypal_access_token(access_token, expires_in):
|
||||
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()
|
||||
257
app/handlers.py
257
app/handlers.py
@@ -21,8 +21,6 @@ from datetime import time as dtime
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# --- Conversation Handler for Raffle Creation ---
|
||||
|
||||
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
user = update.message.from_user
|
||||
args = context.args
|
||||
@@ -36,33 +34,37 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
madrid_tz = pytz.timezone("Europe/Madrid")
|
||||
current_time = datetime.now(madrid_tz)
|
||||
if current_time.time() >= dtime(20, 55) and current_time.time() <= dtime(22, 0):
|
||||
await update.message.reply_text("No puedes unirte a la rifa en este momento.")
|
||||
await update.message.reply_text("No puedes unirte al sorteo en este momento.")
|
||||
return
|
||||
|
||||
if len(get_reserved_numbers(user.id, raffle_id)) > 0:
|
||||
await update.message.reply_text("Ya tienes participaciones reservadas para este sorteo. Por favor, completa la donación o espera a que caduquen antes de unirte de nuevo.")
|
||||
return
|
||||
|
||||
# The user wants to join this raffle.
|
||||
# Start the private conversation for number selection.
|
||||
logger.info(f"User {user.id} started bot with join link for raffle {raffle_id}")
|
||||
context.user_data['joining_raffle_id'] = raffle_id
|
||||
keyboard = generate_numbers_keyboard(raffle_id, user.id)
|
||||
await update.message.reply_text(
|
||||
f"¡Hola! Vamos a unirnos a la rifa '{raffle['name']}'.\n\n"
|
||||
f"El precio por papeleta es de {raffle['price']}€.\n\n"
|
||||
f"¡Hola! Vamos a unirnos al sorteo '{raffle['name']}'.\n\n"
|
||||
f"🌍 Envío internacional: {'Sí ✅' if raffle['international_shipping'] else 'No ❌'}\n"
|
||||
f"La donación mínima es de {raffle['price']}€.\n\n"
|
||||
"Por favor, selecciona tus números:",
|
||||
reply_markup=keyboard
|
||||
)
|
||||
else:
|
||||
await update.message.reply_text("Esta rifa ya no está activa o no tiene números disponibles.")
|
||||
await update.message.reply_text("Este sorteo ya no está activo o no tiene números disponibles.")
|
||||
except (ValueError, IndexError):
|
||||
await update.message.reply_text("Enlace de participación inválido.")
|
||||
else:
|
||||
# Generic start message
|
||||
await update.message.reply_text("Hola, soy el bot de rifas. Puedes participar desde los anuncios en los canales.")
|
||||
await update.message.reply_text("Hola, soy el bot de La Suerte es Tuya. Puedes participar desde los anuncios en los canales.")
|
||||
|
||||
async def new_raffle_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||
"""Starts the conversation to create a new raffle. Asks for channels."""
|
||||
user_id = update.message.from_user.id
|
||||
if user_id not in ADMIN_IDS:
|
||||
await update.message.reply_text("No tienes permiso para crear rifas.")
|
||||
await update.message.reply_text("No tienes permiso para crear sorteos.")
|
||||
return ConversationHandler.END
|
||||
|
||||
if not CHANNELS:
|
||||
@@ -72,8 +74,8 @@ async def new_raffle_start(update: Update, context: ContextTypes.DEFAULT_TYPE) -
|
||||
context.user_data['new_raffle'] = {'channel': ""} # Initialize data for this user
|
||||
keyboard = generate_channel_selection_keyboard()
|
||||
await update.message.reply_text(
|
||||
"Vamos a crear una nueva rifa.\n\n"
|
||||
"**Paso 1:** Selecciona el canal donde se publicará la rifa.",
|
||||
"Vamos a crear un nuevo sorteo.\n\n"
|
||||
"**Paso 1:** Selecciona el canal donde se publicará el sorteo.",
|
||||
reply_markup=keyboard,
|
||||
parse_mode=ParseMode.MARKDOWN
|
||||
)
|
||||
@@ -91,7 +93,7 @@ async def select_channel(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
|
||||
context.user_data['new_raffle']['channel'] = channel_id
|
||||
|
||||
await query.edit_message_text(
|
||||
"Canal seleccionad. Ahora, por favor, envía el **título** de la rifa.",
|
||||
"Canal seleccionad. Ahora, por favor, envía el **título** del sorteo.",
|
||||
parse_mode=ParseMode.MARKDOWN
|
||||
)
|
||||
return TYPING_TITLE
|
||||
@@ -108,7 +110,7 @@ async def receive_title(update: Update, context: ContextTypes.DEFAULT_TYPE) -> i
|
||||
return TYPING_TITLE
|
||||
|
||||
context.user_data['new_raffle']['title'] = title
|
||||
await update.message.reply_text("Título guardado. Ahora envía la **descripción** de la rifa.", parse_mode=ParseMode.MARKDOWN)
|
||||
await update.message.reply_text("Título guardado. Ahora envía la **descripción** del sorteo.", parse_mode=ParseMode.MARKDOWN)
|
||||
return TYPING_DESCRIPTION
|
||||
|
||||
async def receive_description(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||
@@ -119,7 +121,7 @@ async def receive_description(update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||
return TYPING_DESCRIPTION
|
||||
|
||||
context.user_data['new_raffle']['description'] = description
|
||||
await update.message.reply_text("Descripción guardada. Ahora envía la **imagen** para la rifa.", parse_mode=ParseMode.MARKDOWN)
|
||||
await update.message.reply_text("Descripción guardada. Ahora envía la **imagen** para el sorteo.", parse_mode=ParseMode.MARKDOWN)
|
||||
return SENDING_IMAGE
|
||||
|
||||
async def receive_image(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||
@@ -132,7 +134,24 @@ async def receive_image(update: Update, context: ContextTypes.DEFAULT_TYPE) -> i
|
||||
context.user_data['new_raffle']['image_file_id'] = photo_file_id
|
||||
|
||||
await update.message.reply_text(
|
||||
"Imagen guardada.\nAhora, introduce el precio por número para el canal seleccionado (solo el número, ej: 5).",
|
||||
"Imagen guardada. Ahora, ¿el sorteo permite envíos internacionales? Responde con 'Sí' o 'No'.",
|
||||
parse_mode=ParseMode.MARKDOWN
|
||||
)
|
||||
return INTERNATIONAL_SHIPPING
|
||||
|
||||
async def receive_international_shipping(update: Update, context: ContextTypes.DEFAULT_TYPE) -> int:
|
||||
"""Receives whether international shipping is available."""
|
||||
text = update.message.text.strip().lower()
|
||||
if text in ['sí', 'si', 's', 'yes', 'y']:
|
||||
context.user_data['new_raffle']['international_shipping'] = 1
|
||||
elif text in ['no', 'n']:
|
||||
context.user_data['new_raffle']['international_shipping'] = 0
|
||||
else:
|
||||
await update.message.reply_text("Por favor, responde con 'Sí' o 'No'.")
|
||||
return INTERNATIONAL_SHIPPING
|
||||
|
||||
await update.message.reply_text(
|
||||
"Perfecto. Ahora, introduce el precio por número para el canal seleccionado (solo el número, ej: 5).",
|
||||
parse_mode=ParseMode.MARKDOWN
|
||||
)
|
||||
return TYPING_PRICE_FOR_CHANNEL
|
||||
@@ -144,8 +163,8 @@ async def receive_price_for_channel(update: Update, context: ContextTypes.DEFAUL
|
||||
|
||||
try:
|
||||
price = int(price_text)
|
||||
if not (0 <= price <= 999): # Allow higher prices maybe
|
||||
raise ValueError("El precio debe ser un número entre 0 y 999.")
|
||||
if not (1 <= price <= 999): # Allow higher prices maybe
|
||||
raise ValueError("El precio debe ser un número entre 1 y 999.")
|
||||
except ValueError:
|
||||
channel_alias = REVERSE_CHANNELS.get(channel_id, f"ID:{channel_id}")
|
||||
await update.message.reply_text(f"Precio inválido para {channel_alias}. Debe ser un número (ej: 5). Inténtalo de nuevo.")
|
||||
@@ -164,13 +183,14 @@ async def _show_creation_confirmation(update: Update, context: ContextTypes.DEFA
|
||||
price = raffle_data.get('price', 0)
|
||||
|
||||
confirmation_text = (
|
||||
"¡Perfecto! Revisa los datos de la rifa:\n\n"
|
||||
"¡Perfecto! Revisa los datos del sortoe:\n\n"
|
||||
f"📌 **Título:** {raffle_data.get('title', 'N/A')}\n"
|
||||
f"📝 **Descripción:** {raffle_data.get('description', 'N/A')}\n"
|
||||
f"📺 **Canal:** {REVERSE_CHANNELS.get(channel_id, channel_id)}\n"
|
||||
f"💶 **Precio:** {price}€\n"
|
||||
f"🌍 **Envío internacional:** {'Sí ✅' if raffle_data.get('international_shipping', 0) else 'No ❌'}\n"
|
||||
f"💶 **Donación mínima:** {price}€\n"
|
||||
f"🖼️ **Imagen:** (Adjunta)\n\n"
|
||||
"¿Confirmas la creación de esta rifa?"
|
||||
"¿Confirmas la creación de este sorteo?"
|
||||
)
|
||||
keyboard = generate_confirmation_keyboard()
|
||||
# Message from which confirmation is triggered is the last price input message.
|
||||
@@ -209,10 +229,10 @@ async def confirm_creation(update: Update, context: ContextTypes.DEFAULT_TYPE) -
|
||||
raffle_id = create_raffle(name, description, price, image_file_id, channel_id)
|
||||
|
||||
if raffle_id:
|
||||
await context.bot.send_message(query.from_user.id, f"✅ ¡Rifa '{name}' creada con éxito!")
|
||||
await context.bot.send_message(query.from_user.id, f"✅ ¡Sorteo '{name}' creado con éxito!")
|
||||
await _announce_raffle_in_channels(context, raffle_id, query.from_user.id, initial_announcement=True)
|
||||
else:
|
||||
await context.bot.send_message(query.from_user.id, f"❌ Error al guardar la rifa. Nombre '{name}' podría existir.")
|
||||
await context.bot.send_message(query.from_user.id, f"❌ Error al guardar el sorteo. Nombre '{name}' podría existir.")
|
||||
|
||||
elif query.data == CANCEL_CREATION_CALLBACK:
|
||||
await query.edit_message_caption("Creación cancelada.", reply_markup=None)
|
||||
@@ -225,11 +245,11 @@ async def cancel_creation_command(update: Update, context: ContextTypes.DEFAULT_
|
||||
user_id = update.message.from_user.id
|
||||
if 'new_raffle' in context.user_data:
|
||||
context.user_data.pop('new_raffle', None)
|
||||
await update.message.reply_text("Creación de rifa cancelada.")
|
||||
await update.message.reply_text("Creación de sorteo cancelada.")
|
||||
logger.info(f"Admin {user_id} cancelled raffle creation via /cancel.")
|
||||
return ConversationHandler.END
|
||||
else:
|
||||
await update.message.reply_text("No hay ninguna creación de rifa en curso para cancelar.")
|
||||
await update.message.reply_text("No hay ninguna creación de sorteo en curso para cancelar.")
|
||||
return ConversationHandler.END # Or return current state if applicable
|
||||
|
||||
# ... (other handlers and functions) ...
|
||||
@@ -249,7 +269,7 @@ async def incorrect_input_type(update: Update, context: ContextTypes.DEFAULT_TYP
|
||||
current_conversation_data = context.user_data['new_raffle']
|
||||
current_state_key = '_new_raffle_conv_state'
|
||||
current_state = context.user_data[current_state_key]
|
||||
conversation_type = "creación de rifa"
|
||||
conversation_type = "creación de sorteo"
|
||||
else:
|
||||
# Not in a known conversation, or state tracking key is different.
|
||||
# This message might be outside any conversation this handler is for.
|
||||
@@ -273,24 +293,24 @@ async def incorrect_input_type(update: Update, context: ContextTypes.DEFAULT_TYP
|
||||
logger.warning(f"User {user_id} sent incorrect input type during {conversation_type} (State: {current_state}). Message: {update.message.text or '<Not Text>'}")
|
||||
|
||||
# --- Handle incorrect input for RAFFLE CREATION states ---
|
||||
if conversation_type == "creación de rifa":
|
||||
if conversation_type == "creación de sorteo":
|
||||
if current_state == SENDING_IMAGE:
|
||||
await update.message.reply_text(
|
||||
"Por favor, envía una IMAGEN para la rifa, no texto u otro tipo de archivo.\n"
|
||||
"Por favor, envía una IMAGEN para el sorteo, no texto u otro tipo de archivo.\n"
|
||||
"Si quieres cancelar, usa /cancelar."
|
||||
)
|
||||
return SENDING_IMAGE # Stay in the image sending state
|
||||
elif current_state == TYPING_TITLE:
|
||||
await update.message.reply_text("Por favor, envía TEXTO para el título de la rifa. Usa /cancelar para salir.")
|
||||
await update.message.reply_text("Por favor, envía TEXTO para el título del sorteo. Usa /cancelar para salir.")
|
||||
return TYPING_TITLE
|
||||
elif current_state == TYPING_DESCRIPTION:
|
||||
await update.message.reply_text("Por favor, envía TEXTO para la descripción de la rifa. Usa /cancelar para salir.")
|
||||
await update.message.reply_text("Por favor, envía TEXTO para la descripción del sorteo. Usa /cancelar para salir.")
|
||||
return TYPING_DESCRIPTION
|
||||
elif current_state == TYPING_PRICE_FOR_CHANNEL:
|
||||
channel_id_for_price = current_conversation_data.get('current_channel_for_price')
|
||||
channel_alias = REVERSE_CHANNELS.get(channel_id_for_price, f"ID:{channel_id_for_price}") if channel_id_for_price else "el canal actual"
|
||||
await update.message.reply_text(
|
||||
f"Por favor, envía un NÚMERO para el precio de la rifa en {channel_alias}.\n"
|
||||
f"Por favor, envía un NÚMERO para el precio del sorteo en {channel_alias}.\n"
|
||||
"Usa /cancelar para salir."
|
||||
)
|
||||
return TYPING_PRICE_FOR_CHANNEL
|
||||
@@ -299,8 +319,8 @@ async def incorrect_input_type(update: Update, context: ContextTypes.DEFAULT_TYP
|
||||
# Generic fallback message if specific state isn't handled above for some reason
|
||||
await update.message.reply_text(
|
||||
"Entrada no válida para este paso de la conversación.\n"
|
||||
"Si estás creando una rifa, usa /cancelar para salir.\n"
|
||||
"Si estás editando una rifa, usa /cancelar_edicion para salir."
|
||||
"Si estás creando un sorteo, usa /cancelar para salir.\n"
|
||||
"Si estás editando un sorteo, usa /cancelar_edicion para salir."
|
||||
)
|
||||
return current_state # Return to the state the conversation was in
|
||||
|
||||
@@ -384,14 +404,14 @@ async def number_callback(update: Update, context: CallbackContext):
|
||||
page = 0 if number < 50 else 1
|
||||
except ValueError:
|
||||
logger.warning(f"Invalid number value in callback: {value}")
|
||||
await query.answer("Papeleta no válida.")
|
||||
await query.answer("Participación no válida.")
|
||||
return
|
||||
elif action == "random_num":
|
||||
# Handle random number selection
|
||||
try:
|
||||
remaining_free_numbers = get_remaining_numbers(raffle_id)
|
||||
if not remaining_free_numbers:
|
||||
await query.answer("No hay papeletas disponibles para seleccionar aleatoriamente.")
|
||||
await query.answer("No hay participaciones disponibles para seleccionar aleatoriamente.")
|
||||
return
|
||||
else:
|
||||
# Select a random number from the available ones
|
||||
@@ -401,7 +421,7 @@ async def number_callback(update: Update, context: CallbackContext):
|
||||
page = 0 if int(number_string) < 50 else 1
|
||||
except ValueError:
|
||||
logger.warning(f"Invalid random number value in callback: {value}")
|
||||
await query.answer("Papeleta aleatoria no válido.")
|
||||
await query.answer("Participación aleatoria no válida.")
|
||||
return
|
||||
|
||||
logger.debug(f"User {user_id} interacted with number {number_string} for raffle {raffle_id}")
|
||||
@@ -419,30 +439,30 @@ async def number_callback(update: Update, context: CallbackContext):
|
||||
if participant_step == "waiting_for_payment":
|
||||
# User clicked a number they have reserved -> Deselect it
|
||||
remove_reserved_number(participant_db_id, number_string)
|
||||
await query.answer(f"Has quitado la reserva de la papeleta {number_string}.")
|
||||
await query.answer(f"Has quitado la reserva de la participación {number_string}.")
|
||||
logger.info(f"User {user_id} deselected reserved number {number_string} for raffle {raffle_id}")
|
||||
# Refresh keyboard
|
||||
keyboard = generate_numbers_keyboard(raffle_id, user_id, page=page)
|
||||
await query.edit_message_reply_markup(reply_markup=keyboard)
|
||||
elif participant_step == "completed":
|
||||
# User clicked a number they have paid for -> Inform them
|
||||
await query.answer(f"Ya tienes la papeleta {number_string} (pagado).")
|
||||
await query.answer(f"Ya tienes la participación {number_string} (donada).")
|
||||
logger.debug(f"User {user_id} clicked their own completed number {number_string}")
|
||||
else:
|
||||
# Should not happen with current steps, but catch just in case
|
||||
await query.answer(f"Estado desconocido para tu papeleta {number_string}.")
|
||||
await query.answer(f"Estado desconocido para tu participación {number_string}.")
|
||||
logger.warning(f"User {user_id} clicked number {number_string} with unexpected step {participant_step}")
|
||||
|
||||
else:
|
||||
# User clicked a number taken by someone else
|
||||
status_msg = "reservada" if participant_step == "waiting_for_payment" else "comprada"
|
||||
await query.answer(f"La papeleta {number_string} ya ha sido {status_msg} por otro usuario.")
|
||||
status_msg = "reservada" if participant_step == "waiting_for_payment" else "donada"
|
||||
await query.answer(f"La participación {number_string} ya ha sido {status_msg} por otro usuario.")
|
||||
logger.debug(f"User {user_id} clicked number {number_string} taken by user {participant_user_id}")
|
||||
|
||||
else:
|
||||
# Number is free -> Reserve it for the user
|
||||
reserve_number(user_id, username, raffle_id, number_string)
|
||||
await query.answer(f"Papeleta {number_string} reservada para ti. Confirma tu selección cuando termines.")
|
||||
await query.answer(f"Participación {number_string} reservada para ti. Confirma tu selección cuando termines.")
|
||||
logger.info(f"User {user_id} reserved number {number_string} for raffle {raffle_id}")
|
||||
# Refresh keyboard to show the lock icon
|
||||
keyboard = generate_numbers_keyboard(raffle_id, user_id, page=page)
|
||||
@@ -465,19 +485,19 @@ async def confirm_callback(update: Update, context: CallbackContext):
|
||||
reserved_numbers = get_reserved_numbers(user_id, raffle_id) # Returns a list of strings
|
||||
|
||||
if not reserved_numbers:
|
||||
await query.answer("No has seleccionado ninguna papeleta nueva para confirmar.")
|
||||
await query.answer("No has seleccionado ninguna participación nueva para confirmar.")
|
||||
return
|
||||
|
||||
await query.answer("Generando enlace de pago...") # Give feedback
|
||||
await query.answer("Generando enlace de donación...") # Give feedback
|
||||
|
||||
raffle_info = get_raffle(raffle_id)
|
||||
if not raffle_info:
|
||||
logger.error(f"Cannot find raffle {raffle_id} during confirmation for user {user_id}")
|
||||
# Use query.edit_message_caption or text depending on what was sent before
|
||||
try:
|
||||
await query.edit_message_caption("Error: No se pudo encontrar la información de la rifa.", reply_markup=None)
|
||||
await query.edit_message_caption("Error: No se pudo encontrar la información del sorteo.", reply_markup=None)
|
||||
except BadRequest:
|
||||
await query.edit_message_text("Error: No se pudo encontrar la información de la rifa.", reply_markup=None)
|
||||
await query.edit_message_text("Error: No se pudo encontrar la información del sorteo.", reply_markup=None)
|
||||
return
|
||||
|
||||
# Get participant DB ID (needed for setting invoice ID)
|
||||
@@ -486,9 +506,9 @@ async def confirm_callback(update: Update, context: CallbackContext):
|
||||
if not participant:
|
||||
logger.error(f"Cannot find participant record for user {user_id}, raffle {raffle_id} in 'waiting_for_payment' step during confirmation.")
|
||||
try:
|
||||
await query.edit_message_caption("Error: No se encontró tu registro. Selecciona las papeletas de nuevo.", reply_markup=None)
|
||||
await query.edit_message_caption("Error: No se encontró tu registro. Selecciona las participaciones de nuevo.", reply_markup=None)
|
||||
except BadRequest:
|
||||
await query.edit_message_text("Error: No se encontró tu registro. Selecciona las papeletas de nuevo.", reply_markup=None)
|
||||
await query.edit_message_text("Error: No se encontró tu registro. Selecciona las participaciones de nuevo.", reply_markup=None)
|
||||
return
|
||||
participant_db_id = participant['id']
|
||||
|
||||
@@ -497,37 +517,16 @@ async def confirm_callback(update: Update, context: CallbackContext):
|
||||
logger.error(f"Price not found for raffle {raffle_id} during confirmation for user {user_id}")
|
||||
await query.answer("Error: Ha habido un problema desconocido, contacta con el administrador.", show_alert=True)
|
||||
return
|
||||
|
||||
|
||||
total_price = len(reserved_numbers) * price_per_number
|
||||
|
||||
# Generate a unique invoice ID for PayPal
|
||||
#invoice_id = str(uuid.uuid4())
|
||||
|
||||
current_timestamp = time.time()
|
||||
paypal_link, invoice_id = create_paypal_order(get_paypal_access_token(), total_price)
|
||||
mark_reservation_pending(participant_db_id, invoice_id, current_timestamp)
|
||||
|
||||
# Construct PayPal link
|
||||
# Using _xclick for simple payments. Consider PayPal REST API for more robust integration if needed.
|
||||
# paypal_link = (
|
||||
# f"https://sandbox.paypal.com/cgi-bin/webscr?"
|
||||
# f"cmd=_xclick&business={PAYPAL_EMAIL}"
|
||||
# f"&item_name=Numeros Rifa con ID: {raffle_info['id']} ({', '.join(reserved_numbers)})" # Item name for clarity
|
||||
# f"&amount={total_price:.2f}" # Format price to 2 decimal places
|
||||
# f"¤cy_code=EUR"
|
||||
# f"&invoice={invoice_id}" # CRITICAL: This links the payment back
|
||||
# f"&verify_url={WEBHOOK_URL}" # IMPORTANT: Set your actual webhook URL here!
|
||||
# # custom field can be used for extra data if needed, e.g., participant_db_id again
|
||||
# f"&custom={participant_db_id}"
|
||||
# f"&return=https://t.me/{BOT_NAME}" # Optional: URL after successful payment
|
||||
# f"&cancel_return=https://t.me/{BOT_NAME}" # Optional: URL if user cancels on PayPal
|
||||
# )
|
||||
|
||||
paypal_link, invoice_id = create_paypal_order(get_paypal_access_token(), total_price, raffle_info['id'], reserved_numbers)
|
||||
mark_reservation_pending(participant_db_id, invoice_id)
|
||||
|
||||
# Log the PayPal link for debugging
|
||||
logger.info(f"Generated PayPal link for user {user_id}: {paypal_link}")
|
||||
# Define the button text and create the URL button
|
||||
paypal_button_text = "💳 Pagar con PayPal 💳"
|
||||
paypal_button_text = "💳 Donar con PayPal 💳"
|
||||
paypal_button = InlineKeyboardButton(paypal_button_text, url=paypal_link)
|
||||
|
||||
# Create the InlineKeyboardMarkup containing the button
|
||||
@@ -536,13 +535,13 @@ async def confirm_callback(update: Update, context: CallbackContext):
|
||||
# Modify the message text - remove the link placeholder, adjust instruction
|
||||
payment_message = (
|
||||
f"👍 Selección Confirmada 👍\n\n"
|
||||
f"Papeletas reservadas: {', '.join(reserved_numbers)}\n"
|
||||
f"Precio total: {total_price:.2f}€\n\n"
|
||||
f"Participaciones reservadas: {', '.join(reserved_numbers)}\n"
|
||||
f"Donación mínima: {total_price:.2f}€\n\n"
|
||||
f"El ID de la factura es: {invoice_id}\n"
|
||||
f"Pulsa el botón para completar el pago en PayPal:\n" # Adjusted instruction
|
||||
f"Pulsa el botón para completar la donación en PayPal:\n" # Adjusted instruction
|
||||
# Link is now in the button below
|
||||
f"⚠️ Tienes {RESERVATION_TIMEOUT_MINUTES} minutos para pagar antes de que las papeletas se liberen.\n\n"
|
||||
f"Una vez hayas pagado, se te notificará aquí. El pago puede tardar hasta 5 minutos en procesarse, sé paciente."
|
||||
f"⚠️ Tienes {RESERVATION_TIMEOUT_MINUTES} minutos para pagar antes de que las participaciones se liberen.\n\n"
|
||||
f"Una vez hayas donado, se te notificará aquí. La donación puede tardar hasta 5 minutos en procesarse, sé paciente."
|
||||
)
|
||||
|
||||
try:
|
||||
@@ -574,26 +573,26 @@ async def cancel_callback(update: Update, context: CallbackContext):
|
||||
reserved_numbers = get_reserved_numbers(user_id, raffle_id)
|
||||
|
||||
if not reserved_numbers:
|
||||
await query.answer("No tienes ninguna selección de papeletas pendiente para cancelar.")
|
||||
await query.answer("No tienes ninguna selección de participaciones pendiente para cancelar.")
|
||||
# Optionally, revert the message back to the initial raffle selection prompt or just inform the user.
|
||||
# Let's just edit the message to say nothing is pending.
|
||||
try:
|
||||
await query.edit_message_caption("No hay papeletas reservadas para cancelar.", reply_markup=None)
|
||||
await query.edit_message_caption("No hay participaciones reservadas para cancelar.", reply_markup=None)
|
||||
except BadRequest: # If message was text, not photo caption
|
||||
await query.edit_message_text("No hay papeletas reservadas para cancelar.", reply_markup=None)
|
||||
await query.edit_message_text("No hay participaciones reservadas para cancelar.", reply_markup=None)
|
||||
return
|
||||
|
||||
# Cancel the reservation in the database
|
||||
cancelled = cancel_reserved_numbers(user_id, raffle_id) # This function deletes the 'waiting_for_payment' row
|
||||
|
||||
if cancelled: # Check if the function indicated success (e.g., return True or affected rows)
|
||||
await query.answer(f"Selección cancelada. Papeletas liberadas: {', '.join(reserved_numbers)}")
|
||||
await query.answer(f"Selección cancelada. Participaciones liberadas: {', '.join(reserved_numbers)}")
|
||||
logger.info(f"User {user_id} cancelled reservation for raffle {raffle_id}. Numbers: {reserved_numbers}")
|
||||
# Edit the message to confirm cancellation
|
||||
try:
|
||||
await query.edit_message_caption("Tu selección de papeletas ha sido cancelada y las papeletas han sido liberadas.", reply_markup=None)
|
||||
await query.edit_message_caption("Tu selección de participaciones ha sido cancelada y las participaciones han sido liberadas.", reply_markup=None)
|
||||
except BadRequest:
|
||||
await query.edit_message_text("Tu selección de papeletas ha sido cancelada y las papeletas han sido liberadas.", reply_markup=None)
|
||||
await query.edit_message_text("Tu selección de participaciones ha sido cancelada y las participaciones han sido liberadas.", reply_markup=None)
|
||||
# Optionally, you could send the user back to the raffle selection list or number grid.
|
||||
# For simplicity, we just end the interaction here.
|
||||
else:
|
||||
@@ -609,7 +608,7 @@ async def end_raffle_logic(context: ContextTypes.DEFAULT_TYPE, raffle_id: int, w
|
||||
if not raffle_details:
|
||||
logger.error(f"Attempted to end non-existent raffle ID {raffle_id}")
|
||||
try:
|
||||
await context.bot.send_message(admin_user_id, f"Error: No se encontró la rifa con ID {raffle_id}.")
|
||||
await context.bot.send_message(admin_user_id, f"Error: No se encontró el sorteo con ID {raffle_id}.")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send error message to admin {admin_user_id}: {e}")
|
||||
return False
|
||||
@@ -619,7 +618,7 @@ async def end_raffle_logic(context: ContextTypes.DEFAULT_TYPE, raffle_id: int, w
|
||||
if not raffle_details['active']:
|
||||
logger.warning(f"Admin {admin_user_id} tried to end already inactive raffle '{raffle_name}' (ID: {raffle_id})")
|
||||
try:
|
||||
await context.bot.send_message(admin_user_id, f"La rifa '{raffle_name}' ya estaba terminada.")
|
||||
await context.bot.send_message(admin_user_id, f"El sorteo '{raffle_name}' ya estaba terminado.")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send 'already inactive' message to admin {admin_user_id}: {e}")
|
||||
return False
|
||||
@@ -628,7 +627,7 @@ async def end_raffle_logic(context: ContextTypes.DEFAULT_TYPE, raffle_id: int, w
|
||||
if not end_raffle(raffle_id):
|
||||
logger.error(f"Failed to mark raffle ID {raffle_id} as inactive in the database.")
|
||||
try:
|
||||
await context.bot.send_message(admin_user_id, f"Error al marcar la rifa '{raffle_name}' como terminada en la base de datos.")
|
||||
await context.bot.send_message(admin_user_id, f"Error al marcar el sorteo '{raffle_name}' como terminado en la base de datos.")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send DB error message to admin {admin_user_id}: {e}")
|
||||
return False
|
||||
@@ -641,19 +640,20 @@ async def end_raffle_logic(context: ContextTypes.DEFAULT_TYPE, raffle_id: int, w
|
||||
|
||||
winners_str = get_winners(raffle_id, winner_numbers)
|
||||
formatted_winner_numbers = ", ".join(f"{n:02}" for n in sorted(winner_numbers))
|
||||
announcement = f"🎯🏆🎯 **¡Resultados de la Rifa '{raffle_name}'!** 🎯🏆🎯\n\n"
|
||||
announcement += f"Detalles de la rifa: https://t.me/{channel_alias}/{get_main_message_id(raffle_id)}\n"
|
||||
announcement += f"Papeletas ganadoras: **{formatted_winner_numbers}**\n\n" if len(winner_numbers) > 1 else f"Papeleta ganadora: **{formatted_winner_numbers}**\n\n"
|
||||
announcement = f"🎯🏆🎯 **¡Resultados del Sorteo '{raffle_name}'!** 🎯🏆🎯\n\n"
|
||||
announcement += f"Detalles del sorteo: https://t.me/{channel_alias}/{get_main_message_id(raffle_id)}\n"
|
||||
announcement += f"Participaciones ganadoras: **{formatted_winner_numbers}**\n\n" if len(winner_numbers) > 1 else f"Participación ganadora: **{formatted_winner_numbers}**\n\n"
|
||||
if winners_str: # Ensure winners_str is not empty or a "no winners" message itself
|
||||
announcement += f"Ganadores:\n{winners_str}\n\n¡Felicidades!" if len(winner_numbers) > 1 else f"Ganador:\n{winners_str}\n\n¡Felicidades!"
|
||||
else:
|
||||
announcement += "No hubo ganadores para estas papeletas." if len(winner_numbers) > 1 else "No hubo ganador para esta papeleta."
|
||||
announcement += "No hubo ganadores para estas participaciones." if len(winner_numbers) > 1 else "No hubo ganador para esta participación."
|
||||
announcement += f"\nPuedes comprobar los resultados en {JUEGOS_ONCE_URL}"
|
||||
announcement += "\n\nGracias a todos por participar. Mantente atento a futuras rifas."
|
||||
announcement += "\n\nGracias a todos por participar. Mantente atento a futuros sorteos."
|
||||
|
||||
main_announcement = f"🎯🏆🎯 **Rifa '{raffle_name}' Terminada** 🎯🏆🎯\n\n"
|
||||
main_announcement = f"🎯🏆🎯 **Sorteo '{raffle_name}' terminado** 🎯🏆🎯\n\n"
|
||||
main_announcement += f"{raffle_details['description']}\n\n"
|
||||
main_announcement += f"💵 **Precio por papeleta:** {raffle_details['price']}€\n"
|
||||
main_announcement += f"🌍 **Envío internacional:** {'Sí ✅' if raffle_details['international_shipping'] else 'No ❌'}\n"
|
||||
main_announcement += f"💵 **Donación mínima:** {raffle_details['price']}€\n"
|
||||
main_announcement += f"📜 Normas y condiciones: {TYC_DOCUMENT_URL}"
|
||||
|
||||
main_message_id = get_main_message_id(raffle_id)
|
||||
@@ -668,6 +668,31 @@ async def end_raffle_logic(context: ContextTypes.DEFAULT_TYPE, raffle_id: int, w
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to announce winners in channel {channel_alias} (ID: {channel_id_str}): {e}")
|
||||
|
||||
# Notify admin of success
|
||||
try:
|
||||
for admin_user_id in ADMIN_IDS:
|
||||
await context.bot.send_message(admin_user_id, f"Sorteo '{raffle_name}' terminado y ganadores anunciados.")
|
||||
number_of_participations = get_total_participations(raffle_id)
|
||||
price_per_number = raffle_details['price']
|
||||
invoice_ids = get_all_invoice_ids(raffle_id)
|
||||
for inv_id in invoice_ids:
|
||||
gross, net, fees = get_paypal_amounts_for_invoice(inv_id)
|
||||
total_gross += gross
|
||||
total_net += net
|
||||
total_fees += fees
|
||||
await context.bot.send_message(
|
||||
admin_user_id,
|
||||
f"Resumen del sorteo '{raffle_name}':\n"
|
||||
f"- Participaciones totales: {number_of_participations}\n"
|
||||
f"- Donación mínima por participación: {price_per_number}€\n"
|
||||
f"- Total recaudado: {total_gross}€\n"
|
||||
f"- Total en comisiones de PayPal: {total_fees:.2f}€\n"
|
||||
f"- Beneficio neto estimado: {total_net}€"
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to notify admin {admin_user_id} about ended raffle '{raffle_name}': {e}")
|
||||
|
||||
return True
|
||||
|
||||
# --- Admin Menu Handlers ---
|
||||
@@ -710,8 +735,8 @@ async def admin_menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||
if data == ADMIN_MENU_CREATE:
|
||||
# Guide admin to use the conversation command
|
||||
await query.edit_message_text(
|
||||
"Para crear una nueva rifa, por favor, inicia la conversación usando el comando:\n\n"
|
||||
"/crear_rifa\n\n"
|
||||
"Para crear un nuevo sorteo, inicia la conversación usando el comando:\n\n"
|
||||
"/crear_sorteo\n\n"
|
||||
"Pulsa el botón abajo para volver al menú principal.",
|
||||
reply_markup=InlineKeyboardMarkup([[InlineKeyboardButton("⬅️ Volver al Menú", callback_data=ADMIN_MENU_BACK_MAIN)]])
|
||||
)
|
||||
@@ -720,7 +745,7 @@ async def admin_menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||
logger.info(f"Admin {user_id} requested raffle list.")
|
||||
keyboard = generate_admin_list_raffles_keyboard()
|
||||
active_raffles = get_active_raffles()
|
||||
message_text = "**Rifas Activas**\n\nSelecciona una rifa para ver detalles, anunciar o terminar:" if active_raffles else "**Rifas Activas**\n\nNo hay rifas activas."
|
||||
message_text = "**Sorteos Activos**\n\nSelecciona un sorteo para ver detalles, anunciar o terminar:" if active_raffles else "**Sorteos Activas**\n\nNo hay sorteos activos."
|
||||
await query.edit_message_text(message_text, reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN)
|
||||
|
||||
elif data == ADMIN_MENU_BACK_MAIN:
|
||||
@@ -730,39 +755,40 @@ async def admin_menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||
# --- Raffle Specific Actions ---
|
||||
elif data.startswith(ADMIN_VIEW_RAFFLE_PREFIX):
|
||||
try:
|
||||
await query.answer("Cargando detalles del sorteo...")
|
||||
raffle_id = int(data[len(ADMIN_VIEW_RAFFLE_PREFIX):])
|
||||
logger.info(f"Admin {user_id} requested details for raffle {raffle_id}")
|
||||
details_text = format_raffle_details(raffle_id) # Use helper
|
||||
details_keyboard = generate_admin_raffle_details_keyboard(raffle_id)
|
||||
await query.edit_message_text(details_text, reply_markup=details_keyboard, parse_mode=ParseMode.MARKDOWN)
|
||||
await query.edit_message_text(details_text, reply_markup=details_keyboard, parse_mode=ParseMode.HTML)
|
||||
except (ValueError, IndexError):
|
||||
logger.error(f"Invalid callback data for view raffle: {data}")
|
||||
await query.edit_message_text("Error: ID de rifa inválido.", reply_markup=generate_admin_list_raffles_keyboard())
|
||||
await query.edit_message_text("Error: ID de sorteo inválido.", reply_markup=generate_admin_list_raffles_keyboard())
|
||||
|
||||
elif data.startswith(ADMIN_ANNOUNCE_RAFFLE_PREFIX):
|
||||
try:
|
||||
raffle_id = int(data[len(ADMIN_ANNOUNCE_RAFFLE_PREFIX):])
|
||||
logger.info(f"Admin {user_id} requested re-announce for raffle {raffle_id}")
|
||||
await query.edit_message_text(f"📢 Re-anunciando la rifa {get_raffle_name(raffle_id)}...", reply_markup=None) # Give feedback
|
||||
await query.edit_message_text(f"📢 Re-anunciando el sorteo {get_raffle_name(raffle_id)}...", reply_markup=None) # Give feedback
|
||||
await _announce_raffle_in_channels(context, raffle_id, user_id)
|
||||
# Optionally go back to the list or details view after announcing
|
||||
keyboard = generate_admin_list_raffles_keyboard() # Go back to list
|
||||
await context.bot.send_message(user_id, "Volviendo a la lista de rifas:", reply_markup=keyboard)
|
||||
await context.bot.send_message(user_id, "Volviendo a la lista de sorteos:", reply_markup=keyboard)
|
||||
except (ValueError, IndexError):
|
||||
logger.error(f"Invalid callback data for announce raffle: {data}")
|
||||
await query.edit_message_text("Error: ID de rifa inválido.", reply_markup=generate_admin_list_raffles_keyboard())
|
||||
await query.edit_message_text("Error: ID de sorteo inválido.", reply_markup=generate_admin_list_raffles_keyboard())
|
||||
|
||||
elif data.startswith(ADMIN_END_RAFFLE_PROMPT_PREFIX):
|
||||
try:
|
||||
raffle_id = int(data[len(ADMIN_END_RAFFLE_PROMPT_PREFIX):])
|
||||
except (ValueError, IndexError):
|
||||
logger.error(f"Invalid callback data for end raffle prompt: {data}")
|
||||
await query.edit_message_text("Error: ID de rifa inválido.", reply_markup=generate_admin_main_menu_keyboard())
|
||||
await query.edit_message_text("Error: ID de sorteo inválido.", reply_markup=generate_admin_main_menu_keyboard())
|
||||
return
|
||||
|
||||
raffle = get_raffle(raffle_id)
|
||||
if not raffle or not raffle['active']:
|
||||
await query.edit_message_text("Esta rifa no existe o ya ha terminado.", reply_markup=generate_admin_list_raffles_keyboard())
|
||||
await query.edit_message_text("Este sorteo no existe o ya ha terminado.", reply_markup=generate_admin_list_raffles_keyboard())
|
||||
return
|
||||
|
||||
# Store raffle ID and set flag to expect winner numbers
|
||||
@@ -772,8 +798,8 @@ async def admin_menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||
|
||||
keyboard = generate_admin_cancel_end_keyboard()
|
||||
await query.edit_message_text(
|
||||
f"Vas a terminar la rifa: **{raffle['name']}**\n\n"
|
||||
"Por favor, envía ahora las **papeletas ganadoras** separadas por espacios (ej: `7 23 81`).",
|
||||
f"Vas a terminar el sorteo: **{raffle['name']}**\n\n"
|
||||
"Por favor, envía ahora las **participaciones ganadoras** separadas por espacios (ej: `7 23 81`).",
|
||||
reply_markup=keyboard,
|
||||
parse_mode=ParseMode.MARKDOWN
|
||||
)
|
||||
@@ -784,10 +810,10 @@ async def admin_menu_callback(update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||
logger.info(f"Admin {user_id} cancelled the raffle end process.")
|
||||
# Go back to the raffle list
|
||||
keyboard = generate_admin_list_raffles_keyboard()
|
||||
await query.edit_message_text("**Rifas Activas**\n\nSelecciona una rifa para terminarla:", reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN)
|
||||
await query.edit_message_text("**Sorteos Activos**\n\nSelecciona un sorteo para terminarlo:", reply_markup=keyboard, parse_mode=ParseMode.MARKDOWN)
|
||||
|
||||
elif data == ADMIN_NO_OP:
|
||||
# Just ignore clicks on placeholder buttons like "No hay rifas activas"
|
||||
# Just ignore clicks on placeholder buttons like "No hay sorteos activos"
|
||||
pass
|
||||
|
||||
async def admin_receive_winner_numbers(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
@@ -825,8 +851,8 @@ async def admin_receive_winner_numbers(update: Update, context: ContextTypes.DEF
|
||||
logger.warning(f"Invalid winner numbers format from admin {user_id}: {e}")
|
||||
keyboard = generate_admin_cancel_end_keyboard()
|
||||
await update.message.reply_text(
|
||||
f"❌ Papeletas inválidas: {e}\n\n"
|
||||
"Por favor, envía las papeletas ganadoras (0-99) separadas por espacios (ej: `7 23 81`).",
|
||||
f"❌ Participaciones inválidas: {e}\n\n"
|
||||
"Por favor, envía las participaciones ganadoras (0-99) separadas por espacios (ej: `7 23 81`).",
|
||||
reply_markup=keyboard,
|
||||
parse_mode=ParseMode.MARKDOWN
|
||||
)
|
||||
@@ -839,7 +865,7 @@ async def admin_receive_winner_numbers(update: Update, context: ContextTypes.DEF
|
||||
context.user_data.pop('admin_ending_raffle_id', None)
|
||||
|
||||
# Call the refactored ending logic
|
||||
await update.message.reply_text(f"Procesando finalización con papeletas: {', '.join(f'{n:02}' for n in winner_numbers)}...")
|
||||
await update.message.reply_text(f"Procesando finalización con participaciones: {', '.join(f'{n:02}' for n in winner_numbers)}...")
|
||||
success = await end_raffle_logic(context, raffle_id_to_end, winner_numbers, user_id)
|
||||
|
||||
# Send admin back to the main menu after processing
|
||||
@@ -852,14 +878,14 @@ async def _announce_raffle_in_channels(context: ContextTypes.DEFAULT_TYPE, raffl
|
||||
if not raffle:
|
||||
logger.warning(f"Admin {admin_user_id} tried to announce non-existent raffle {raffle_id}.")
|
||||
try:
|
||||
await context.bot.send_message(admin_user_id, f"Error: El ID de la rifa {raffle_id} no existe.")
|
||||
await context.bot.send_message(admin_user_id, f"Error: El ID del sorteo {raffle_id} no existe.")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send 'raffle not found' error to admin {admin_user_id}: {e}")
|
||||
return False
|
||||
if not initial_announcement and not raffle['active']: # For re-announcements, it must be active
|
||||
logger.warning(f"Admin {admin_user_id} tried to re-announce inactive raffle {raffle_id} ('{raffle['name']}').")
|
||||
try:
|
||||
await context.bot.send_message(admin_user_id, f"Error: La rifa '{raffle['name']}' (ID {raffle_id}) no está activa y no se puede re-anunciar.")
|
||||
await context.bot.send_message(admin_user_id, f"Error: El sorteo '{raffle['name']}' (ID {raffle_id}) no está activo y no se puede re-anunciar.")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send 'raffle inactive' error to admin {admin_user_id}: {e}")
|
||||
return False
|
||||
@@ -878,11 +904,12 @@ async def _announce_raffle_in_channels(context: ContextTypes.DEFAULT_TYPE, raffl
|
||||
channel_alias = REVERSE_CHANNELS.get(channel_id_str, f"ID:{channel_id_str}")
|
||||
|
||||
announce_caption = (
|
||||
f"🏆 **¡{'Nueva ' if initial_announcement else ''}Rifa Disponible!** 🏆\n\n"
|
||||
f"🏆 **¡{'Nuevo ' if initial_announcement else ''}Sorteo Disponible!** 🏆\n\n"
|
||||
f"🌟 **{raffle_name}** 🌟\n\n"
|
||||
f"{raffle_description}\n\n"
|
||||
f"💵 **Precio por papeleta:** {price}€\n"
|
||||
f"🎟️ **Papeletas disponibles:** {remaining_count if remaining_count >= 0 else 'N/A'}\n\n"
|
||||
f"🌍 **Envío internacional:** {'Sí ✅' if raffle['international_shipping'] else 'No ❌'}\n"
|
||||
f"💵 **Donación mínima:** {price}€\n"
|
||||
f"🎟️ **Participaciones disponibles:** {remaining_count if remaining_count >= 0 else 'N/A'}\n\n"
|
||||
f"📜 Normas y condiciones: {TYC_DOCUMENT_URL}"
|
||||
)
|
||||
|
||||
|
||||
@@ -12,22 +12,23 @@ def format_raffle_details(raffle_id):
|
||||
"""Fetches and formats raffle details for display, including multi-channel prices."""
|
||||
raffle = get_raffle(raffle_id) # Fetches basic info from 'raffles' table
|
||||
if not raffle:
|
||||
return "Error: No se encontró la rifa."
|
||||
return "Error: No se encontró el sorteo."
|
||||
|
||||
details = (
|
||||
f"ℹ️ **Detalles de la rifa** ℹ️\n\n"
|
||||
f"**ID:** `{raffle['id']}`\n"
|
||||
f"**Nombre:** {raffle['name']}\n"
|
||||
f"**Descripción:**\n{raffle['description']}\n\n"
|
||||
f"**Activo:** {'Sí' if raffle['active'] else 'No (Terminado)'}\n"
|
||||
f"**Precio por número (canal principal):** {raffle['price']}€\n"
|
||||
f"ℹ️ <b>Detalles del sorteo</b> ℹ️\n\n"
|
||||
f"<b>ID:</b> <code>{raffle['id']}</code>\n"
|
||||
f"<b>Nombre:</b> {raffle['name']}\n"
|
||||
f"<b>Descripción:</b>\n{raffle['description'][:100]}...\n\n"
|
||||
f"<b>Envío internacional:</b> {'Sí' if raffle['international_shipping'] else 'No'}\n"
|
||||
f"<b>Activo:</b> {'Sí' if raffle['active'] else 'No (Terminado)'}\n"
|
||||
f"<b>Donación mínima (canal principal):</b> {raffle['price']}€\n"
|
||||
)
|
||||
|
||||
# Image ID (optional display)
|
||||
if raffle['image_file_id']:
|
||||
details += f"**ID Imagen:** (Presente)\n"
|
||||
details += f"<b>ID Imagen:</b> {raffle['image_file_id']} (Presente)\n"
|
||||
else:
|
||||
details += f"**ID Imagen:** (No asignada)\n"
|
||||
details += f"<b>ID Imagen:</b> (No asignada)\n"
|
||||
|
||||
# Add participant count and remaining numbers
|
||||
participants = get_participants(raffle_id) # Fetches list of Rows
|
||||
@@ -38,11 +39,25 @@ def format_raffle_details(raffle_id):
|
||||
# pending_participants_count = sum(1 for p in participants if p['step'] == 'waiting_for_payment')
|
||||
|
||||
|
||||
details += f"\n**Participantes Confirmados:** {completed_participants_count}\n"
|
||||
# details += f"**Reservas Pendientes:** {pending_participants_count}\n"
|
||||
details += f"\n<b>Participantes Confirmados:</b> {completed_participants_count}\n"
|
||||
# details += f"<b>Reservas Pendientes:</b> {pending_participants_count}\n"
|
||||
|
||||
remaining_count = get_remaining_numbers_amount(raffle_id)
|
||||
details += f"**Números Disponibles:** {remaining_count if remaining_count >= 0 else 'Error al calcular'}\n"
|
||||
details += f"<b>Números Disponibles:</b> {remaining_count if remaining_count >= 0 else 'Error al calcular'}\n"
|
||||
|
||||
# Gross and net amounts
|
||||
total_gross = 0.0
|
||||
total_fees = 0.0
|
||||
total_net = 0.0
|
||||
invoice_ids = get_all_invoice_ids(raffle_id)
|
||||
for inv_id in invoice_ids:
|
||||
gross, net, fees = get_paypal_amounts_for_invoice(inv_id)
|
||||
total_gross += gross
|
||||
total_fees += fees
|
||||
total_net += net
|
||||
details += f"\n<b>Total Recaudado (bruto):</b> {total_gross:.2f}€\n"
|
||||
details += f"<b>Total Gastos (comisiones):</b> {total_fees:.2f}€\n"
|
||||
details += f"<b>Total Beneficio (neto):</b> {total_net:.2f}€\n"
|
||||
|
||||
return details
|
||||
|
||||
@@ -131,12 +146,12 @@ def get_winners(raffle_id, winner_numbers_int):
|
||||
# if not raffle_details:
|
||||
# logger.error(f"Cannot generate image: Raffle {raffle_id} not found.")
|
||||
# # Draw error message on image
|
||||
# draw.text((10, 10), f"Error: Rifa {raffle_id} no encontrado", fill="red", font=title_font)
|
||||
# draw.text((10, 10), f"Error: Sorteo {raffle_id} no encontrado", fill="red", font=title_font)
|
||||
# img.save(f"/app/data/raffles/raffle_table_{raffle_id}_error.png")
|
||||
# return False # Indicate failure
|
||||
|
||||
# raffle_name = raffle_details['name']
|
||||
# title_text = f"Rifa: {raffle_name}"
|
||||
# title_text = f"Sorteo: {raffle_name}"
|
||||
# # Calculate text bounding box for centering
|
||||
# try:
|
||||
# # Use textbbox for more accurate centering
|
||||
@@ -262,12 +277,12 @@ def generate_table_image(raffle_id):
|
||||
# --- Title Bar ---
|
||||
raffle_details = get_raffle(raffle_id)
|
||||
if not raffle_details:
|
||||
draw.text((10, 10), f"Error: Rifa {raffle_id} no encontrada", fill="red", font=title_font)
|
||||
draw.text((10, 10), f"Error: Sorteo {raffle_id} no encontrado", fill="red", font=title_font)
|
||||
img.save(f"/app/data/raffles/raffle_table_{raffle_id}_error.png")
|
||||
return False
|
||||
|
||||
raffle_name = raffle_details['name']
|
||||
title_text = f"Rifa: {raffle_name}"
|
||||
title_text = f"Sorteo: {raffle_name}"
|
||||
|
||||
# Draw title bar (full width)
|
||||
title_bar_color = "#4a90e2"
|
||||
@@ -403,7 +418,7 @@ def get_paypal_access_token():
|
||||
logger.info(f"Using cached PayPal access token")
|
||||
return old_token
|
||||
logger.info("Fetching new PayPal access token")
|
||||
url = "https://api-m.sandbox.paypal.com/v1/oauth2/token"
|
||||
url = f"{PAYPAL_URL}/v1/oauth2/token"
|
||||
headers = {"Accept": "application/json", "Accept-Language": "en_US"}
|
||||
data = {"grant_type": "client_credentials"}
|
||||
|
||||
@@ -413,8 +428,8 @@ def get_paypal_access_token():
|
||||
store_paypal_access_token(response.json()["access_token"], response.json()["expires_in"])
|
||||
return response.json()["access_token"]
|
||||
|
||||
def create_paypal_order(access_token, value):
|
||||
url = "https://api-m.sandbox.paypal.com/v2/checkout/orders"
|
||||
def create_paypal_order(access_token, value, raffle_id, numbers):
|
||||
url = f"{PAYPAL_URL}/v2/checkout/orders"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {access_token}"
|
||||
@@ -423,10 +438,12 @@ def create_paypal_order(access_token, value):
|
||||
"intent": "CAPTURE",
|
||||
"purchase_units": [
|
||||
{
|
||||
"amount": {"currency_code": "EUR", "value": f"{value:.2f}"}
|
||||
"amount": {"currency_code": "EUR", "value": f"{value:.2f}"},
|
||||
"description": f"Donación para participar en el sorteo de HomeLabs Club (ID: {raffle_id}, Números: {numbers})",
|
||||
}
|
||||
],
|
||||
"application_context": {
|
||||
"locale": "es-ES",
|
||||
"return_url": f"https://t.me/{BOT_NAME}",
|
||||
"cancel_url": f"https://t.me/{BOT_NAME}"
|
||||
}
|
||||
@@ -439,3 +456,45 @@ def create_paypal_order(access_token, value):
|
||||
# Extract the approval link
|
||||
approval_url = next(link["href"] for link in order["links"] if link["rel"] == "approve")
|
||||
return approval_url, order["id"]
|
||||
|
||||
|
||||
def get_paypal_amounts_for_invoice(invoice_id):
|
||||
"""Fetches the gross, net, and fee amounts for a given PayPal invoice ID."""
|
||||
access_token = get_paypal_access_token()
|
||||
url = f"{PAYPAL_URL}/v2/checkout/orders/{invoice_id}"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {access_token}"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
except requests.RequestException as e:
|
||||
logger.error(f"Error fetching PayPal invoice {invoice_id}: {e}")
|
||||
return 0.0, 0.0, 0.0
|
||||
|
||||
order = response.json()
|
||||
|
||||
if order["status"] != "COMPLETED":
|
||||
logger.warning(f"Invoice {invoice_id} is not completed. Status: {order['status']}")
|
||||
return 0.0, 0.0, 0.0
|
||||
|
||||
gross_amount = float(order["purchase_units"][0]["amount"]["value"])
|
||||
fee_amount = 0.0
|
||||
net_amount = gross_amount
|
||||
|
||||
# Fetch capture details to get fee information
|
||||
capture_id = order["purchase_units"][0]["payments"]["captures"][0]["id"]
|
||||
capture_url = f"{PAYPAL_URL}/v2/payments/captures/{capture_id}"
|
||||
|
||||
capture_response = requests.get(capture_url, headers=headers)
|
||||
capture_response.raise_for_status()
|
||||
capture_details = capture_response.json()
|
||||
|
||||
if "seller_receivable_breakdown" in capture_details:
|
||||
breakdown = capture_details["seller_receivable_breakdown"]
|
||||
fee_amount = float(breakdown.get("paypal_fee", {}).get("value", 0.0))
|
||||
net_amount = float(breakdown.get("net_amount", {}).get("value", gross_amount))
|
||||
|
||||
return gross_amount, net_amount, fee_amount
|
||||
@@ -72,10 +72,10 @@ def generate_numbers_keyboard(raffle_id, user_id, page=0):
|
||||
if paging_buttons:
|
||||
keyboard.append(paging_buttons)
|
||||
|
||||
action_buttons_row = [
|
||||
InlineKeyboardButton("✨ Número Aleatorio ✨", callback_data=f"random_num:{raffle_id}")
|
||||
]
|
||||
keyboard.append(action_buttons_row)
|
||||
#action_buttons_row = [
|
||||
# InlineKeyboardButton("✨ Número Aleatorio ✨", callback_data=f"random_num:{raffle_id}")
|
||||
#]
|
||||
#keyboard.append(action_buttons_row)
|
||||
|
||||
# Add Confirm/Cancel Buttons
|
||||
confirm_cancel_row = [
|
||||
@@ -90,8 +90,8 @@ def generate_numbers_keyboard(raffle_id, user_id, page=0):
|
||||
|
||||
def generate_admin_main_menu_keyboard():
|
||||
keyboard = [
|
||||
[InlineKeyboardButton("➕ Crear Nueva Rifa", callback_data=ADMIN_MENU_CREATE)],
|
||||
[InlineKeyboardButton("📋 Listar/Gestionar Rifas", callback_data=ADMIN_MENU_LIST)],
|
||||
[InlineKeyboardButton("➕ Crear Nuevo Sorteo", callback_data=ADMIN_MENU_CREATE)],
|
||||
[InlineKeyboardButton("📋 Listar/Gestionar Sorteos", callback_data=ADMIN_MENU_LIST)],
|
||||
]
|
||||
return InlineKeyboardMarkup(keyboard)
|
||||
|
||||
@@ -102,7 +102,7 @@ def generate_admin_list_raffles_keyboard():
|
||||
keyboard = []
|
||||
|
||||
if not active_raffles:
|
||||
keyboard.append([InlineKeyboardButton("No hay rifas activas.", callback_data=ADMIN_NO_OP)])
|
||||
keyboard.append([InlineKeyboardButton("No hay sorteos activos.", callback_data=ADMIN_NO_OP)])
|
||||
else:
|
||||
for raffle in active_raffles:
|
||||
raffle_id = raffle['id']
|
||||
@@ -122,7 +122,7 @@ def generate_admin_raffle_details_keyboard(raffle_id):
|
||||
keyboard = [
|
||||
# Add relevant actions here if needed later, e.g., edit description?
|
||||
[InlineKeyboardButton("📢 Anunciar de Nuevo", callback_data=f"{ADMIN_ANNOUNCE_RAFFLE_PREFIX}{raffle_id}")],
|
||||
[InlineKeyboardButton("🏁 Terminar Rifa", callback_data=f"{ADMIN_END_RAFFLE_PROMPT_PREFIX}{raffle_id}")],
|
||||
[InlineKeyboardButton("🏁 Terminar Sorteo", callback_data=f"{ADMIN_END_RAFFLE_PROMPT_PREFIX}{raffle_id}")],
|
||||
[InlineKeyboardButton("⬅️ Volver a la Lista", callback_data=ADMIN_MENU_LIST)] # Back to list view
|
||||
]
|
||||
return InlineKeyboardMarkup(keyboard)
|
||||
@@ -150,7 +150,7 @@ def generate_confirmation_keyboard():
|
||||
"""Generates Yes/No keyboard for final confirmation."""
|
||||
keyboard = [
|
||||
[
|
||||
InlineKeyboardButton("✅ Sí, crear rifa", callback_data=CONFIRM_CREATION_CALLBACK),
|
||||
InlineKeyboardButton("✅ Sí, crear sorteo", callback_data=CONFIRM_CREATION_CALLBACK),
|
||||
InlineKeyboardButton("❌ No, cancelar", callback_data=CANCEL_CREATION_CALLBACK),
|
||||
]
|
||||
]
|
||||
|
||||
@@ -14,11 +14,12 @@ from database import (
|
||||
get_user_by_invoice_id, confirm_reserved_numbers,
|
||||
get_raffle_name, get_raffle,
|
||||
get_remaining_numbers_amount,
|
||||
store_main_message_id, get_main_message_id,
|
||||
get_main_message_id,
|
||||
store_update_message_id, get_update_message_id,
|
||||
get_last_n_other_participants, get_remaining_numbers
|
||||
)
|
||||
from config import BOT_TOKEN, BOT_NAME, WEBHOOK_ID, TYC_DOCUMENT_URL, REVERSE_CHANNELS
|
||||
from config import BOT_TOKEN, BOT_NAME, WEBHOOK_ID, TYC_DOCUMENT_URL, REVERSE_CHANNELS, NEWRELIC_API_KEY, PAYPAL_URL
|
||||
from newrelic_telemetry_sdk import Log, LogClient
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@@ -26,6 +27,33 @@ app = Flask(__name__)
|
||||
logging.basicConfig(
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
|
||||
)
|
||||
|
||||
client = LogClient(license_key=NEWRELIC_API_KEY, host="log-api.eu.newrelic.com")
|
||||
|
||||
# Define a handler that sends records to New Relic
|
||||
class NewRelicHandler(logging.Handler):
|
||||
def emit(self, record):
|
||||
try:
|
||||
log = Log(
|
||||
message=self.format(record),
|
||||
level=record.levelname,
|
||||
timestamp_ms=int(record.created * 1000),
|
||||
attributes={
|
||||
"logger": record.name,
|
||||
"app_name": BOT_NAME,
|
||||
"docker_container": "paypal_processor"
|
||||
}
|
||||
)
|
||||
client.send(log)
|
||||
except Exception:
|
||||
self.handleError(record)
|
||||
|
||||
nr_handler = NewRelicHandler()
|
||||
nr_handler.setFormatter(logging.Formatter("%(message)s"))
|
||||
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.addHandler(nr_handler)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Define the Telegram API URL base
|
||||
@@ -135,7 +163,7 @@ def receive_paypal_payment(invoice_id, payment_status, payment_amount_str):
|
||||
# send_telegram_message(
|
||||
# user_id,
|
||||
# f"El estado de tu pago para la factura {invoice_id} es '{payment_status}'. "
|
||||
# f"La rifa solo se confirma con pagos 'Completed'. Contacta con un administrador si crees que es un error."
|
||||
# f"El sorteo solo se confirma con pagos 'Completed'. Contacta con un administrador si crees que es un error."
|
||||
# )
|
||||
# return
|
||||
|
||||
@@ -157,7 +185,7 @@ def receive_paypal_payment(invoice_id, payment_status, payment_amount_str):
|
||||
logger.error(f"Payment amount mismatch for Invoice ID {invoice_id}. Expected: {expected_amount:.2f}, Received: {payment_amount:.2f}")
|
||||
send_telegram_message(
|
||||
user_id,
|
||||
f"⚠️ La cantidad pagada ({payment_amount:.2f}€) para la factura {invoice_id} no coincide con el total esperado ({expected_amount:.2f}€) para los números {', '.join(numbers)}. "
|
||||
f"⚠️ La cantidad donada ({payment_amount:.2f}€) para la factura {invoice_id} no coincide con el total esperado ({expected_amount:.2f}€) para los números {', '.join(numbers)}. "
|
||||
f"Por favor, contacta con un administrador."
|
||||
)
|
||||
# Do NOT confirm the numbers if amount is wrong
|
||||
@@ -172,8 +200,8 @@ def receive_paypal_payment(invoice_id, payment_status, payment_amount_str):
|
||||
# Send confirmation to user
|
||||
send_telegram_message(
|
||||
user_id,
|
||||
f"✅ ¡Pago confirmado para la factura {invoice_id}!\n\n"
|
||||
f"Te has apuntado con éxito a la rifa '{raffle_name}' con las papeletas: {', '.join(numbers)}."
|
||||
f"✅ ¡Donación confirmada para la factura {invoice_id}!\n\n"
|
||||
f"Te has apuntado con éxito al sorteo '{raffle_name}' con las participaciones: {', '.join(numbers)}."
|
||||
# Raffle name can be added here if desired, requires one more DB call or adding it to get_user_by_invoice_id
|
||||
)
|
||||
|
||||
@@ -193,9 +221,10 @@ def receive_paypal_payment(invoice_id, payment_status, payment_amount_str):
|
||||
# If it's the last number, update the main message and delete the participate button
|
||||
if remaining_numbers_amount == 0:
|
||||
keyboard = None
|
||||
main_announcement = f"🎯🏆🎯 **Rifa '{raffle_name}' Terminada** 🎯🏆🎯\n\n"
|
||||
main_announcement = f"🎯🏆🎯 **Sorteo '{raffle_name}' terminado** 🎯🏆🎯\n\n"
|
||||
main_announcement += f"{raffle_details['description']}\n\n"
|
||||
main_announcement += f"💵 **Precio por papeleta:** {raffle_details['price']}€\n"
|
||||
main_announcement += f"🌍 Envío internacional: {'Sí ✅' if raffle_details['international_shipping'] else 'No ❌'}\n"
|
||||
main_announcement += f"💵 **Donación mínima:** {raffle_details['price']}€\n"
|
||||
main_announcement += f"📜 Normas y condiciones: {TYC_DOCUMENT_URL}"
|
||||
main_message_id = get_main_message_id(raffle_id)
|
||||
requests.post(f"{TELEGRAM_API_URL}/editMessageCaption", json={
|
||||
@@ -214,10 +243,11 @@ def receive_paypal_payment(invoice_id, payment_status, payment_amount_str):
|
||||
]
|
||||
]
|
||||
}
|
||||
main_announcement = f"🏆 Rifa '{raffle_name}' en progreso 🏆\n\n"
|
||||
main_announcement = f"🏆 Sorteo '{raffle_name}' en progreso 🏆\n\n"
|
||||
main_announcement += f"{raffle_details['description']}\n\n"
|
||||
main_announcement += f"💵 **Precio por papeleta:** {raffle_details['price']}€\n"
|
||||
main_announcement += f"🗒️ Quedan {remaining_numbers_amount} papeletas disponibles. ¡Date prisa! 🗒️\n\n"
|
||||
main_announcement += f"🌍 Envío internacional: {'Sí ✅' if raffle_details['international_shipping'] else 'No ❌'}\n"
|
||||
main_announcement += f"💵 **Donación mínima:** {raffle_details['price']}€\n"
|
||||
main_announcement += f"🗒️ Quedan {remaining_numbers_amount} participaciones disponibles. ¡Date prisa! 🗒️\n\n"
|
||||
main_announcement += f"📜 Normas y condiciones: {TYC_DOCUMENT_URL}"
|
||||
main_message_id = get_main_message_id(raffle_id)
|
||||
requests.post(f"{TELEGRAM_API_URL}/editMessageCaption", json={
|
||||
@@ -235,26 +265,26 @@ def receive_paypal_payment(invoice_id, payment_status, payment_amount_str):
|
||||
|
||||
numbers_text = ""
|
||||
if len(numbers) > 1:
|
||||
numbers_text = f"con las papeletas: {', '.join(numbers)}"
|
||||
numbers_text = f"con las participaciones: {', '.join(numbers)}"
|
||||
else:
|
||||
numbers_text = f"con la papeleta: {', '.join(numbers)}"
|
||||
numbers_text = f"con la participación: {', '.join(numbers)}"
|
||||
|
||||
new_participant_line = f"🗳️ @{escaped_current_user_name} se ha unido a la rifa {numbers_text}. ¡Mucha suerte! 🗳️"
|
||||
new_participant_line = f"🗳️ @{escaped_current_user_name} se ha unido al sorteo {numbers_text}. ¡Mucha suerte! 🗳️"
|
||||
|
||||
remaining_numbers_text = ""
|
||||
if remaining_numbers_amount > 10:
|
||||
remaining_numbers_text = f"🗒️ Todavía hay {remaining_numbers_amount} papeletas. 🗒️"
|
||||
remaining_numbers_text = f"🗒️ Todavía hay {remaining_numbers_amount} participaciones disponibles. 🗒️"
|
||||
elif remaining_numbers_amount == 1:
|
||||
remaining_numbers = get_remaining_numbers(raffle_id)
|
||||
remaining_numbers_text = f"⏰⏰⏰ ¡Última papeleta! ⏰⏰⏰\n\n"
|
||||
remaining_numbers_text += f"Queda la papeleta: {remaining_numbers[0]}"
|
||||
remaining_numbers_text = f"⏰⏰⏰ ¡Última participación! ⏰⏰⏰\n\n"
|
||||
remaining_numbers_text += f"Queda la participación: {remaining_numbers[0]}"
|
||||
elif remaining_numbers_amount == 0:
|
||||
remaining_numbers_text = "⌛ ¡Ya no hay papeletas! ⌛\n\n"
|
||||
remaining_numbers_text += "¡El resultado de la rifa se dará a conocer a las 21:45h!"
|
||||
remaining_numbers_text = "⌛ ¡Ya no hay participaciones! ⌛\n\n"
|
||||
remaining_numbers_text += "¡El resultado del sorteo se dará a conocer a las 21:45h!"
|
||||
else:
|
||||
remaining_numbers = get_remaining_numbers(raffle_id)
|
||||
remaining_numbers_text = f"🔔🔔🔔 ¡Últimas {remaining_numbers_amount} papeletas disponibles! 🔔🔔🔔\n\n"
|
||||
remaining_numbers_text += f"Quedan las papeletas: {', '.join(remaining_numbers)}"
|
||||
remaining_numbers_text = f"🔔🔔🔔 ¡Últimas {remaining_numbers_amount} participaciones disponibles! 🔔🔔🔔\n\n"
|
||||
remaining_numbers_text += f"Quedan las participaciones: {', '.join(remaining_numbers)}"
|
||||
|
||||
caption = (
|
||||
f"{new_participant_line}\n\n"
|
||||
@@ -262,7 +292,8 @@ def receive_paypal_payment(invoice_id, payment_status, payment_amount_str):
|
||||
f"{remaining_numbers_text}\n\n"
|
||||
f"{raffle_description_for_announce}\n\n"
|
||||
f"🔎 Ver detalles: https://t.me/{REVERSE_CHANNELS.get(channel_id_to_announce)}/{get_main_message_id(raffle_id)}\n\n"
|
||||
f"💵 Precio por papeleta: {price_per_number}€\n\n" # Use the specific price
|
||||
f"🌍 Envío internacional: {'Sí ✅' if raffle_details['international_shipping'] else 'No ❌'}\n"
|
||||
f"💵 Donación mínima: {price_per_number}€\n\n"
|
||||
f"📜 Normas y condiciones: {TYC_DOCUMENT_URL} \n\n"
|
||||
)
|
||||
|
||||
@@ -307,7 +338,7 @@ def receive_paypal_payment(invoice_id, payment_status, payment_amount_str):
|
||||
store_update_message_id(raffle_id, sent_or_edited_message_id)
|
||||
|
||||
# Send image confirmation to user (price not needed in this caption)
|
||||
user_caption = f"¡Apuntado satisfactoriamente a la rifa '{raffle_name}'! Tus números son: {', '.join(numbers)}"
|
||||
user_caption = f"¡Apuntado satisfactoriamente al sorteo '{raffle_name}'! Tus participaciones son: {', '.join(numbers)}"
|
||||
send_telegram_photo(user_id, image_path, caption=user_caption)
|
||||
|
||||
else:
|
||||
@@ -326,6 +357,15 @@ def receive_paypal_payment(invoice_id, payment_status, payment_amount_str):
|
||||
# admin_chat_id = "YOUR_ADMIN_CHAT_ID"
|
||||
# send_telegram_message(admin_chat_id, f"CRITICAL DB Error: Failed to confirm numbers for invoice {invoice_id}, user {user_id}, raffle {raffle_id}. Payment was valid. Manual check needed.")
|
||||
|
||||
def capture_order(order_id, access_token):
|
||||
url = f"{PAYPAL_URL}/v2/checkout/orders/{order_id}/capture"
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {access_token}"
|
||||
}
|
||||
response = requests.post(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
@app.route("/paypal-webhook", methods=["POST"])
|
||||
def paypal_webhook():
|
||||
@@ -343,7 +383,7 @@ def paypal_webhook():
|
||||
|
||||
# 3. Verify signature
|
||||
access_token = get_paypal_access_token()
|
||||
verify_url = "https://api-m.sandbox.paypal.com/v1/notifications/verify-webhook-signature"
|
||||
verify_url = f"{PAYPAL_URL}/v1/notifications/verify-webhook-signature"
|
||||
|
||||
payload = {
|
||||
"auth_algo": headers["paypal-auth-algo"],
|
||||
@@ -367,11 +407,18 @@ def paypal_webhook():
|
||||
logger.info(f"EVENT DATA: {event}")
|
||||
if event_type == "CHECKOUT.ORDER.APPROVED":
|
||||
# process approved order
|
||||
logger.info(f"✅ Order approved: {event['resource']['id']}")
|
||||
logger.info(f"✅ Order approved: {event['resource']['id']}. Capturing payment...")
|
||||
# Capture payment
|
||||
resource = event["resource"]
|
||||
invoice_id = resource.get("id") # capture ID
|
||||
payment_status = resource.get("status") # e.g. COMPLETED
|
||||
payment_amount = resource["purchase_units"][0]["amount"]["value"]
|
||||
capture_order(invoice_id, access_token)
|
||||
elif event_type == "PAYMENT.CAPTURE.COMPLETED":
|
||||
logger.info(f"✅ Payment completed: {event['resource']['id']}")
|
||||
resource = event["resource"]
|
||||
invoice_id = resource["supplementary_data"]["related_ids"]["order_id"]
|
||||
|
||||
payment_status = resource.get("status")
|
||||
payment_amount = resource["amount"]["value"]
|
||||
|
||||
if not all([invoice_id, payment_status, payment_amount]):
|
||||
logger.warning(f"Missing one or more required fields in VERIFIED IPN data: {resource}")
|
||||
@@ -379,9 +426,6 @@ def paypal_webhook():
|
||||
|
||||
# Process the valid payment
|
||||
receive_paypal_payment(invoice_id, payment_status, payment_amount)
|
||||
elif event_type == "PAYMENT.CAPTURE.COMPLETED":
|
||||
logger.info(f"✅ Payment completed: {event['resource']['id']}")
|
||||
# Extract key fields (adjust keys based on your PayPal setup/IPN variables)
|
||||
else:
|
||||
logger.info(f"ℹ️ Received event: {event_type}")
|
||||
else:
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
python-telegram-bot[ext]==21.1.1 # Use specific version, ensure [ext] is included
|
||||
python-telegram-bot[ext]==21.1.1
|
||||
python-dotenv==1.0.1
|
||||
pillow==10.2.0 # Use specific version
|
||||
requests==2.31.0 # Use specific version
|
||||
Flask==3.0.0 # Use specific version
|
||||
APScheduler==3.10.4 # Add APScheduler
|
||||
pillow==10.2.0
|
||||
requests==2.31.0
|
||||
Flask==3.0.0
|
||||
APScheduler==3.10.4
|
||||
pytz==2025.2
|
||||
beautifulsoup4==4.13.5
|
||||
beautifulsoup4==4.13.5
|
||||
newrelic-telemetry-sdk==0.8.0
|
||||
@@ -23,6 +23,10 @@ services:
|
||||
- WEBHOOK_URL=${WEBHOOK_URL}
|
||||
- WEBHOOK_ID=${WEBHOOK_ID}
|
||||
- TYC_DOCUMENT_URL=${TYC_DOCUMENT_URL}
|
||||
- NEWRELIC_API_KEY=${NEWRELIC_API_KEY}
|
||||
- PAYPAL_PERCENTAGE_FEE=${PAYPAL_PERCENTAGE_FEE}
|
||||
- PAYPAL_FIXED_FEE=${PAYPAL_FIXED_FEE}
|
||||
- PAYPAL_URL=${PAYPAL_URL}
|
||||
telerifas_paypal_processor:
|
||||
build:
|
||||
context: app
|
||||
@@ -44,6 +48,8 @@ services:
|
||||
- WEBHOOK_URL=${WEBHOOK_URL}
|
||||
- WEBHOOK_ID=${WEBHOOK_ID}
|
||||
- TYC_DOCUMENT_URL=${TYC_DOCUMENT_URL}
|
||||
- NEWRELIC_API_KEY=${NEWRELIC_API_KEY}
|
||||
- PAYPAL_URL=${PAYPAL_URL}
|
||||
networks:
|
||||
- traefik
|
||||
labels:
|
||||
|
||||
Reference in New Issue
Block a user