Integra Battlepay con SumUp (órdenes battlepay_orders)

- pagos_sumup.py: crear_checkout_battlepay(reference,...) y obtener_checkout()
- battlepay_view: lista las órdenes PENDING de la cuenta de juego
- battlepay_pay_view: crea checkout SumUp para una orden concreta (checkout_reference=reference)
- sumup_webhook: marca la orden PAID (valida estado contra la API de SumUp si falta)
- plantillas account/battlepay.html y partials/pago_sumup.html
- sql/battlepay_sumup.sql (tablas battlepay_orders/battlepay_price) y docs actualizados

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-12 16:48:16 +00:00
parent 215061776b
commit 0fef9ac35b
7 changed files with 317 additions and 60 deletions
+120 -32
View File
@@ -5,7 +5,7 @@ from django.http import HttpResponse, JsonResponse
from django.db import connections
from django.contrib import messages
from django.contrib.auth import logout
from .pagos_sumup import crear_checkout
from .pagos_sumup import crear_checkout, crear_checkout_battlepay, obtener_checkout
from django import forms
import stripe
from .pagos_stripe import create_checkout_session
@@ -2979,44 +2979,132 @@ def novawow_players_view(request):
})
def pagar_sumup(request):
"""Genera un pago de prueba y lo muestra en el widget"""
# ---------------------------------------------------------------------------
# Battlepay (tienda nativa del cliente) pagado vía SumUp
#
# Flujo: el worldserver crea una orden PENDING en `battlepay_orders` cuando el
# jugador pulsa "Comprar" en la tienda del cliente y el producto tiene precio en
# `battlepay_price`. El portal muestra esas órdenes, cobra por SumUp y las marca
# PAID; el worldserver las entrega y las marca DELIVERED.
# ---------------------------------------------------------------------------
def get_pending_battlepay_orders(account_id):
"""Órdenes Battlepay PENDING de una cuenta de juego."""
with connections['acore_auth'].cursor() as cursor:
cursor.execute(
"SELECT id, reference, product_id, product_name, price_eur, status, created_at "
"FROM battlepay_orders WHERE account_id = %s AND status = 'PENDING' "
"ORDER BY created_at DESC",
[account_id],
)
cols = [c[0] for c in cursor.description]
return [dict(zip(cols, row)) for row in cursor.fetchall()]
def _mark_order_paid_by_reference(reference):
"""Marca una orden como PAID solo si estaba PENDING. Devuelve filas afectadas."""
with connections['acore_auth'].cursor() as cursor:
cursor.execute(
"UPDATE battlepay_orders SET status = 'PAID', paid_at = UNIX_TIMESTAMP() "
"WHERE reference = %s AND status = 'PENDING'",
[reference],
)
return cursor.rowcount
def battlepay_view(request):
"""Lista las órdenes Battlepay pendientes de la cuenta de juego seleccionada."""
account_id = request.session.get('account_id')
if not account_id:
if request.session.get('bnet_id'):
return redirect('select-account')
return redirect('login')
orders = get_pending_battlepay_orders(account_id)
return render(request, 'account/battlepay.html', {
'orders': orders,
'username': request.session.get('username'),
})
def battlepay_pay_view(request):
"""Crea un checkout de SumUp para pagar una orden Battlepay pendiente."""
account_id = request.session.get('account_id')
if not account_id:
return redirect('login')
if request.method != 'POST':
return redirect('battlepay')
reference = request.POST.get('reference', '').strip()
# Validar que la orden pertenece a la cuenta y sigue pendiente
with connections['acore_auth'].cursor() as cursor:
cursor.execute(
"SELECT product_name, price_eur FROM battlepay_orders "
"WHERE reference = %s AND account_id = %s AND status = 'PENDING'",
[reference, account_id],
)
row = cursor.fetchone()
if not row:
return render(request, 'account/pago_sumup.html', {'error': 'Orden no encontrada o ya procesada.'})
product_name, price_eur = row
try:
checkout_id = crear_checkout(10.00) # Pago de 10 EUR
pedido = Pedido.objects.create(email_cliente="cliente@example.com", monto=10.00)
return render(request, "account/pago_sumup.html", {"checkout_id": checkout_id, "pedido": pedido})
checkout = crear_checkout_battlepay(reference, price_eur, product_name)
except Exception as e:
return render(request, "account/pago_sumup.html", {"error": str(e)})
return render(request, 'account/pago_sumup.html', {'error': str(e)})
return render(request, 'account/pago_sumup.html', {
'checkout_id': checkout.get('id'),
'reference': reference,
'product_name': product_name,
'price_eur': price_eur,
})
def pagar_sumup(request):
"""Alias de compatibilidad: envía al listado de órdenes Battlepay."""
return redirect('battlepay')
@csrf_exempt
def sumup_webhook(request):
"""Recibe el webhook de SumUp y actualiza la base de datos"""
if request.method == "POST":
"""Webhook de SumUp: marca la orden Battlepay como PAID al completarse el pago."""
if request.method != "POST":
return JsonResponse({"error": "Método no permitido"}, status=405)
try:
data = json.loads(request.body or b'{}')
except json.JSONDecodeError:
return JsonResponse({"error": "JSON inválido"}, status=400)
payload = data.get('payload') or data.get('data') or {}
reference = data.get('checkout_reference') or payload.get('checkout_reference')
status = (data.get('status') or payload.get('status') or '').upper()
checkout_id = data.get('id') or payload.get('checkout_id') or payload.get('id')
event_type = (data.get('event_type') or '').lower()
# Si falta la referencia o el estado, consultarlo a la API de SumUp
if (not reference or status not in ('PAID', 'SUCCESSFUL')) and checkout_id:
try:
data = json.loads(request.body)
checkout = obtener_checkout(checkout_id)
reference = reference or checkout.get('checkout_reference')
status = (checkout.get('status') or status).upper()
except Exception as e:
logger.error("SumUp webhook: no se pudo consultar el checkout %s: %s", checkout_id, e)
# Verificar si el evento es un pago exitoso
if data.get("event_type") == "transaction.successful":
transaction_id = data.get("data", {}).get("transaction_id")
amount = data.get("data", {}).get("amount")
email_cliente = data.get("data", {}).get("customer_email")
pagado = status in ('PAID', 'SUCCESSFUL') or event_type == 'transaction.successful'
# Buscar el pedido y actualizarlo como pagado
pedido = Pedido.objects.filter(email_cliente=email_cliente, monto=amount).first()
if pedido:
pedido.transaction_id = transaction_id
pedido.estado = "Pagado"
pedido.save()
return JsonResponse({"message": "Pago actualizado"}, status=200)
else:
return JsonResponse({"error": "Pedido no encontrado"}, status=404)
if reference and pagado:
try:
afectadas = _mark_order_paid_by_reference(reference)
except Exception as e:
logger.error("SumUp webhook: error actualizando la orden %s: %s", reference, e)
return JsonResponse({"error": "Error interno"}, status=500)
if afectadas:
return JsonResponse({"message": "Orden marcada como pagada"}, status=200)
return JsonResponse({"message": "Orden no pendiente o inexistente"}, status=200)
return JsonResponse({"error": "Evento no reconocido"}, status=400)
except json.JSONDecodeError:
return JsonResponse({"error": "JSON inválido"}, status=400)
return JsonResponse({"error": "Método no permitido"}, status=405)
return JsonResponse({"message": "Evento ignorado"}, status=200)