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:
+35
-7
@@ -81,14 +81,42 @@ guardan `bnet_id` y `bnet_email` para cambios de contraseña/email.
|
|||||||
3. Registrar una cuenta desde la web y comprobar el email de activación.
|
3. Registrar una cuenta desde la web y comprobar el email de activación.
|
||||||
4. Activar y **probar login en el cliente 3.4.3** (ver nota de verificación).
|
4. Activar y **probar login en el cliente 3.4.3** (ver nota de verificación).
|
||||||
|
|
||||||
|
## Battlepay / SumUp (tienda del cliente)
|
||||||
|
|
||||||
|
Integración de la tienda nativa del cliente (Battlepay) con la pasarela SumUp,
|
||||||
|
usando las tablas `battlepay_orders` / `battlepay_price` del source
|
||||||
|
(`sql/battlepay_sumup.sql`, aplicar sobre la BD `auth`).
|
||||||
|
|
||||||
|
**Flujo:**
|
||||||
|
1. En el juego, el jugador pulsa *Comprar*; si el producto tiene precio en
|
||||||
|
`battlepay_price`, el worldserver crea una orden `PENDING` en
|
||||||
|
`battlepay_orders` (con `account_id` = id de cuenta de juego y una
|
||||||
|
`reference` única).
|
||||||
|
2. En la web, `/es/battlepay/` lista las órdenes `PENDING` de la cuenta de juego
|
||||||
|
seleccionada y permite pagarlas.
|
||||||
|
3. `/es/battlepay/pay/` crea un checkout de SumUp usando la `reference` de la
|
||||||
|
orden como `checkout_reference`.
|
||||||
|
4. El webhook `/es/sumup-webhook/` marca la orden como `PAID`
|
||||||
|
(`paid_at = UNIX_TIMESTAMP()`), validando el estado contra la API de SumUp si
|
||||||
|
hace falta.
|
||||||
|
5. El worldserver entrega el producto y marca la orden `DELIVERED`.
|
||||||
|
|
||||||
|
**Código:** `home/pagos_sumup.py` (`crear_checkout_battlepay`, `obtener_checkout`),
|
||||||
|
vistas `battlepay_view` / `battlepay_pay_view` / `sumup_webhook` en `home/views.py`,
|
||||||
|
plantillas `account/battlepay.html` y `partials/pago_sumup.html`.
|
||||||
|
|
||||||
|
> Nota: `battlepay_orders.account_id` es el **id de cuenta de juego**
|
||||||
|
> (`account.id`), mientras que `battlePayCredits` (créditos del sistema clásico
|
||||||
|
> de puntos) vive en la **cuenta bnet** (`battlenet_accounts.id`); se unen por
|
||||||
|
> `account.battlenet_account`. Esta pasarela usa las órdenes en €, no los créditos.
|
||||||
|
|
||||||
|
**Configuración necesaria:**
|
||||||
|
- Aplicar `sql/battlepay_sumup.sql` en la BD `auth` y rellenar `battlepay_price`
|
||||||
|
con tus `product_id` reales.
|
||||||
|
- Configurar el webhook de SumUp apuntando a `https://<tu-dominio>/es/sumup-webhook/`.
|
||||||
|
- Enlazar `/es/battlepay/` desde el menú de «Mi cuenta» (opcional).
|
||||||
|
|
||||||
## Pendiente / opcional
|
## Pendiente / opcional
|
||||||
|
|
||||||
- **Battlepay/SumUp**: el source trae `battlePayCredits` (en `battlenet_accounts`)
|
|
||||||
y las tablas `battlepay_orders` / `battlepay_price` (`sql/custom/battlepay_sumup.sql`).
|
|
||||||
Ojo: `battlepay_orders.account_id` es el **id de cuenta de juego**
|
|
||||||
(`account.id`), mientras que `battlePayCredits` está en la **cuenta bnet**
|
|
||||||
(`battlenet_accounts.id`); se unen por `account.battlenet_account`. La
|
|
||||||
integración de la pasarela con estas tablas se puede abordar como paso
|
|
||||||
siguiente.
|
|
||||||
- Mostrar el email (en lugar de `<bnetId>#1`) como nombre visible en
|
- Mostrar el email (en lugar de `<bnetId>#1`) como nombre visible en
|
||||||
«Mi cuenta» es un retoque cosmético opcional.
|
«Mi cuenta» es un retoque cosmético opcional.
|
||||||
|
|||||||
+46
-19
@@ -6,40 +6,67 @@ SUMUP_CLIENT_ID = settings.SUMUP_CLIENT_ID
|
|||||||
SUMUP_CLIENT_SECRET = settings.SUMUP_CLIENT_SECRET
|
SUMUP_CLIENT_SECRET = settings.SUMUP_CLIENT_SECRET
|
||||||
SUMUP_EMAIL = settings.SUMUP_MERCHANT_EMAIL
|
SUMUP_EMAIL = settings.SUMUP_MERCHANT_EMAIL
|
||||||
|
|
||||||
|
API_BASE = "https://api.sumup.com"
|
||||||
|
|
||||||
|
|
||||||
def obtener_token():
|
def obtener_token():
|
||||||
"""Obtiene un access token de SumUp"""
|
"""Obtiene un access token de SumUp (client_credentials)."""
|
||||||
url = "https://api.sumup.com/token"
|
url = f"{API_BASE}/token"
|
||||||
data = {
|
data = {
|
||||||
"grant_type": "client_credentials",
|
"grant_type": "client_credentials",
|
||||||
"client_id": SUMUP_CLIENT_ID,
|
"client_id": SUMUP_CLIENT_ID,
|
||||||
"client_secret": SUMUP_CLIENT_SECRET
|
"client_secret": SUMUP_CLIENT_SECRET,
|
||||||
}
|
}
|
||||||
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
headers = {"Content-Type": "application/x-www-form-urlencoded"}
|
||||||
|
|
||||||
response = requests.post(url, headers=headers, data=data)
|
response = requests.post(url, headers=headers, data=data, timeout=15)
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return response.json().get("access_token")
|
return response.json().get("access_token")
|
||||||
else:
|
raise Exception(f"Error al obtener token: {response.text}")
|
||||||
raise Exception(f"Error al obtener token: {response.json()}")
|
|
||||||
|
|
||||||
def crear_checkout(monto, moneda="EUR"):
|
|
||||||
"""Crea un pago en SumUp y devuelve el checkout_id"""
|
def crear_checkout_battlepay(reference, monto, descripcion, moneda="EUR", return_url=None):
|
||||||
|
"""
|
||||||
|
Crea un checkout de SumUp para una orden Battlepay concreta.
|
||||||
|
|
||||||
|
``reference`` es la referencia única de la orden (battlepay_orders.reference);
|
||||||
|
se usa como ``checkout_reference`` para poder mapear el webhook a la orden.
|
||||||
|
Devuelve el JSON del checkout (incluye ``id``).
|
||||||
|
"""
|
||||||
access_token = obtener_token()
|
access_token = obtener_token()
|
||||||
url = "https://api.sumup.com/v0.1/checkouts"
|
url = f"{API_BASE}/v0.1/checkouts"
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {access_token}",
|
"Authorization": f"Bearer {access_token}",
|
||||||
"Content-Type": "application/json"
|
"Content-Type": "application/json",
|
||||||
}
|
}
|
||||||
data = {
|
data = {
|
||||||
"checkout_reference": "pedido-12345",
|
"checkout_reference": reference,
|
||||||
"amount": monto,
|
"amount": float(monto),
|
||||||
"currency": moneda,
|
"currency": moneda,
|
||||||
"pay_to_email": SUMUP_EMAIL,
|
"pay_to_email": SUMUP_EMAIL,
|
||||||
"description": "Pago de prueba"
|
"description": descripcion,
|
||||||
}
|
}
|
||||||
|
if return_url:
|
||||||
response = requests.post(url, headers=headers, json=data)
|
data["return_url"] = return_url
|
||||||
|
|
||||||
|
response = requests.post(url, headers=headers, json=data, timeout=15)
|
||||||
if response.status_code == 201:
|
if response.status_code == 201:
|
||||||
return response.json().get("id") # checkout_id
|
return response.json()
|
||||||
else:
|
raise Exception(f"Error al crear checkout: {response.text}")
|
||||||
raise Exception(f"Error al crear checkout: {response.json()}")
|
|
||||||
|
|
||||||
|
def obtener_checkout(checkout_id):
|
||||||
|
"""Consulta un checkout por id. Devuelve su JSON (status, checkout_reference...)."""
|
||||||
|
access_token = obtener_token()
|
||||||
|
url = f"{API_BASE}/v0.1/checkouts/{checkout_id}"
|
||||||
|
headers = {"Authorization": f"Bearer {access_token}"}
|
||||||
|
response = requests.get(url, headers=headers, timeout=15)
|
||||||
|
if response.status_code == 200:
|
||||||
|
return response.json()
|
||||||
|
raise Exception(f"Error al consultar checkout: {response.text}")
|
||||||
|
|
||||||
|
|
||||||
|
def crear_checkout(monto, moneda="EUR"):
|
||||||
|
"""Compatibilidad: checkout genérico de prueba. Devuelve el checkout_id."""
|
||||||
|
checkout = crear_checkout_battlepay("pedido-prueba", monto, "Pago de prueba", moneda)
|
||||||
|
return checkout.get("id")
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
|
||||||
|
{% include 'partials/head.html' %}
|
||||||
|
{% include 'partials/header.html' %}
|
||||||
|
{% include 'partials/video.html' %}
|
||||||
|
|
||||||
|
<div class="main-page">
|
||||||
|
<div class="middle-content">
|
||||||
|
<div class="body-content">
|
||||||
|
<div class="title-content">
|
||||||
|
<h1>Tienda del cliente (Battlepay)</h1>
|
||||||
|
</div>
|
||||||
|
<div class="box-content">
|
||||||
|
<div class="body-box-content centered">
|
||||||
|
<p>Compras pendientes de pago de la cuenta <strong>{{ username }}</strong>.</p>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
{% if orders %}
|
||||||
|
<table class="middle-center-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Producto</th>
|
||||||
|
<th>Precio</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for order in orders %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ order.product_name }}</td>
|
||||||
|
<td>{{ order.price_eur }} €</td>
|
||||||
|
<td>
|
||||||
|
<form action="{% url 'battlepay_pay' %}" method="POST" accept-charset="utf-8">
|
||||||
|
{% csrf_token %}
|
||||||
|
<input type="hidden" name="reference" value="{{ order.reference }}">
|
||||||
|
<button type="submit" class="login-button">Pagar con SumUp</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<p>No tienes compras pendientes.</p>
|
||||||
|
<br>
|
||||||
|
<p>Para comprar, pulsa <strong>Comprar</strong> en la tienda del cliente dentro del juego;
|
||||||
|
la compra aparecerá aquí para completar el pago.</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
<p><a href="my-account">Volver a mi cuenta</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% include 'partials/social.html' %}
|
||||||
|
{% include 'partials/footer.html' %}
|
||||||
|
{% include 'partials/final.html' %}
|
||||||
@@ -10,7 +10,12 @@
|
|||||||
|
|
||||||
{% if error %}
|
{% if error %}
|
||||||
<p style="color: red;">Error: {{ error }}</p>
|
<p style="color: red;">Error: {{ error }}</p>
|
||||||
|
<p><a href="battlepay">Volver a mis compras</a></p>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
{% if product_name %}
|
||||||
|
<p>Producto: <strong>{{ product_name }}</strong></p>
|
||||||
|
<p>Importe: <strong>{{ price_eur }} €</strong></p>
|
||||||
|
{% endif %}
|
||||||
<div id="sumup-card"></div>
|
<div id="sumup-card"></div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -18,8 +23,8 @@
|
|||||||
checkoutId: "{{ checkout_id }}",
|
checkoutId: "{{ checkout_id }}",
|
||||||
onResponse: function (type, body) {
|
onResponse: function (type, body) {
|
||||||
if (type === 'success') {
|
if (type === 'success') {
|
||||||
alert("Pago exitoso. ID de transacción: " + body.transaction_code);
|
alert("Pago exitoso. El producto se entregará en el juego en breve.");
|
||||||
console.log("Pago exitoso:", body);
|
window.location = "battlepay";
|
||||||
} else if (type === 'error') {
|
} else if (type === 'error') {
|
||||||
alert("Error en el pago: " + body.message);
|
alert("Error en el pago: " + body.message);
|
||||||
console.error("Error en el pago:", body);
|
console.error("Error en el pago:", body);
|
||||||
@@ -27,6 +32,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<p><a href="battlepay">Cancelar y volver</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -77,6 +77,8 @@ urlpatterns = [
|
|||||||
path('guild-success/', views.guild_rename_success_view, name='guild-success'),
|
path('guild-success/', views.guild_rename_success_view, name='guild-success'),
|
||||||
path('guild-cancel/', views.guild_rename_cancel_view, name='guild-cancel'),
|
path('guild-cancel/', views.guild_rename_cancel_view, name='guild-cancel'),
|
||||||
path("pagar-sumup/", views.pagar_sumup, name="pagar_sumup"),
|
path("pagar-sumup/", views.pagar_sumup, name="pagar_sumup"),
|
||||||
|
path("battlepay/", views.battlepay_view, name="battlepay"),
|
||||||
|
path("battlepay/pay/", views.battlepay_pay_view, name="battlepay_pay"),
|
||||||
path("sumup-webhook/", views.sumup_webhook, name="sumup_webhook"),
|
path("sumup-webhook/", views.sumup_webhook, name="sumup_webhook"),
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|||||||
+120
-32
@@ -5,7 +5,7 @@ from django.http import HttpResponse, JsonResponse
|
|||||||
from django.db import connections
|
from django.db import connections
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth import logout
|
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
|
from django import forms
|
||||||
import stripe
|
import stripe
|
||||||
from .pagos_stripe import create_checkout_session
|
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:
|
try:
|
||||||
checkout_id = crear_checkout(10.00) # Pago de 10 EUR
|
checkout = crear_checkout_battlepay(reference, price_eur, product_name)
|
||||||
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})
|
|
||||||
except Exception as e:
|
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
|
@csrf_exempt
|
||||||
def sumup_webhook(request):
|
def sumup_webhook(request):
|
||||||
"""Recibe el webhook de SumUp y actualiza la base de datos"""
|
"""Webhook de SumUp: marca la orden Battlepay como PAID al completarse el pago."""
|
||||||
if request.method == "POST":
|
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:
|
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
|
pagado = status in ('PAID', 'SUCCESSFUL') or event_type == 'transaction.successful'
|
||||||
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")
|
|
||||||
|
|
||||||
# Buscar el pedido y actualizarlo como pagado
|
if reference and pagado:
|
||||||
pedido = Pedido.objects.filter(email_cliente=email_cliente, monto=amount).first()
|
try:
|
||||||
if pedido:
|
afectadas = _mark_order_paid_by_reference(reference)
|
||||||
pedido.transaction_id = transaction_id
|
except Exception as e:
|
||||||
pedido.estado = "Pagado"
|
logger.error("SumUp webhook: error actualizando la orden %s: %s", reference, e)
|
||||||
pedido.save()
|
return JsonResponse({"error": "Error interno"}, status=500)
|
||||||
return JsonResponse({"message": "Pago actualizado"}, status=200)
|
if afectadas:
|
||||||
else:
|
return JsonResponse({"message": "Orden marcada como pagada"}, status=200)
|
||||||
return JsonResponse({"error": "Pedido no encontrado"}, status=404)
|
return JsonResponse({"message": "Orden no pendiente o inexistente"}, status=200)
|
||||||
|
|
||||||
return JsonResponse({"error": "Evento no reconocido"}, status=400)
|
return JsonResponse({"message": "Evento ignorado"}, status=200)
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
return JsonResponse({"error": "JSON inválido"}, status=400)
|
|
||||||
|
|
||||||
return JsonResponse({"error": "Método no permitido"}, status=405)
|
|
||||||
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
-- ───────────────────────────────────────────────────────────────────────────
|
||||||
|
-- Battlepay nativo → pasarela SumUp (NovaWeb)
|
||||||
|
-- Se aplica sobre la base de datos `auth` (LoginDatabase).
|
||||||
|
--
|
||||||
|
-- Flujo:
|
||||||
|
-- 1) El jugador pulsa "Comprar" en la tienda nativa del cliente.
|
||||||
|
-- 2) El worldserver (SendMakePurchase) busca el producto en `battlepay_price`.
|
||||||
|
-- Si tiene precio en €, crea una orden PENDING aquí y NO exige puntos.
|
||||||
|
-- 3) El cliente abre /login/sso → /es/checkout (NovaWeb) y paga con SumUp.
|
||||||
|
-- 4) NovaWeb marca la orden PAID (creditPaymentIfPaid).
|
||||||
|
-- 5) El worldserver (BattlepayManager::Update) ve la orden PAID del jugador
|
||||||
|
-- conectado y la entrega con ProcessDelivery → la marca DELIVERED.
|
||||||
|
-- ───────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
-- Órdenes de compra (las escribe el worldserver, las lee/actualiza NovaWeb).
|
||||||
|
CREATE TABLE IF NOT EXISTS `battlepay_orders` (
|
||||||
|
`id` INT NOT NULL AUTO_INCREMENT,
|
||||||
|
`reference` VARCHAR(64) NOT NULL,
|
||||||
|
`account_id` INT NOT NULL, -- auth.account.id (cuenta de juego)
|
||||||
|
`product_id` INT NOT NULL, -- Battlepay ProductID nativo
|
||||||
|
`product_name` VARCHAR(120) NOT NULL DEFAULT '',
|
||||||
|
`price_eur` DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||||
|
`status` VARCHAR(16) NOT NULL DEFAULT 'PENDING', -- PENDING | PAID | DELIVERED
|
||||||
|
`created_at` INT NOT NULL,
|
||||||
|
`paid_at` INT NULL,
|
||||||
|
`delivered_at` INT NULL,
|
||||||
|
PRIMARY KEY (`id`),
|
||||||
|
UNIQUE KEY `uk_reference` (`reference`),
|
||||||
|
KEY `idx_account_status` (`account_id`, `status`),
|
||||||
|
KEY `idx_status` (`status`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- Mapa producto nativo → precio real en €. Si un ProductID NO está aquí (o
|
||||||
|
-- enabled=0), el worldserver usa el flujo clásico de puntos para ese producto.
|
||||||
|
CREATE TABLE IF NOT EXISTS `battlepay_price` (
|
||||||
|
`product_id` INT NOT NULL,
|
||||||
|
`product_name` VARCHAR(120) NOT NULL DEFAULT '',
|
||||||
|
`price_eur` DECIMAL(10,2) NOT NULL DEFAULT 0,
|
||||||
|
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
|
||||||
|
PRIMARY KEY (`product_id`)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||||
|
|
||||||
|
-- Ejemplo (ajusta product_id al de tu tienda nativa):
|
||||||
|
INSERT INTO `battlepay_price` (`product_id`, `product_name`, `price_eur`, `enabled`)
|
||||||
|
VALUES (54811, 'Corcel Celestial', 5.00, 1)
|
||||||
|
ON DUPLICATE KEY UPDATE `product_name`=VALUES(`product_name`), `price_eur`=VALUES(`price_eur`);
|
||||||
Reference in New Issue
Block a user