Files
NightSpire/home/views/payments.py
T
Inna b3184fbdfe Divide home/views.py en el paquete home/views/ por secciones
El módulo monolítico (~3200 líneas) se convierte en un paquete cuyo
__init__.py re-exporta todas las vistas, de modo que `views.X`,
`from home.views import X` y la cadena 'home.views.not_found' (handler404)
siguen funcionando sin tocar urls.py.

Módulos por sección: _base (imports comunes + require_paid_stripe), auth,
pages, account, recruit, characters, store, points, guild, history,
players y payments. Cada submódulo hereda los imports con
`from ._base import *`; las dependencias entre secciones se resuelven con
imports explícitos (guild->auth, recruit/history->account).

Se eliminan dos definiciones duplicadas que quedaban muertas al cargar:
- get_character_image (primera copia, idéntica a la efectiva)
- store_novawow_success_view (stub sin SOAP, pisado por la versión real)

Validado con `manage.py check` (0 issues), resolución de las 100 rutas y
un chequeo estático de nombres no definidos por módulo.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 19:09:40 +00:00

133 lines
5.5 KiB
Python

from ._base import *
@csrf_exempt
def stripe_webhook(request):
payload = request.body
sig_header = request.META.get("HTTP_STRIPE_SIGNATURE")
try:
event = stripe.Webhook.construct_event(payload, sig_header, settings.STRIPE_WEBHOOK_SECRET)
if event["type"] == "checkout.session.completed":
session = event["data"]["object"]
session_id = session["id"]
stripe_ip = request.META.get("REMOTE_ADDR") # IP detectada
# 🔹 Actualizar la IP de Stripe en el log
StripeLog.objects.filter(session_id=session_id).update(stripe_ip=stripe_ip, timestamp=now())
return JsonResponse({"success": True})
except stripe.error.SignatureVerificationError:
return JsonResponse({"success": False, "message": "Firma de webhook no válida."}, status=400)
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 = 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', {
'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):
"""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:
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)
pagado = status in ('PAID', 'SUCCESSFUL') or event_type == 'transaction.successful'
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({"message": "Evento ignorado"}, status=200)