Files
vontor-cz/backend/thirdparty/gopay/views.py
2025-11-04 02:16:17 +01:00

188 lines
6.2 KiB
Python

from typing import Optional
from django.shortcuts import get_object_or_404
from django.conf import settings
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status, permissions, serializers
import gopay
from .models import GoPayPayment
def _gopay_api():
# SDK handles token internally; credentials from settings/env
return gopay.payments({
"goid": settings.GOPAY_GOID,
"client_id": settings.GOPAY_CLIENT_ID,
"client_secret": settings.GOPAY_CLIENT_SECRET,
"gateway_url": getattr(settings, 'GOPAY_GATEWAY_URL', 'https://gw.sandbox.gopay.com/api'),
})
def _as_dict(resp):
if resp is None:
return None
if hasattr(resp, "json") and not callable(getattr(resp, "json")):
return resp.json
if hasattr(resp, "json") and callable(getattr(resp, "json")):
try:
return resp.json()
except Exception:
pass
if isinstance(resp, dict):
return resp
try:
return dict(resp)
except Exception:
return {"raw": str(resp)}
def _map_status(provider_state: Optional[str]) -> str:
if not provider_state:
return 'UNKNOWN'
state = provider_state.upper()
if state == 'PAID':
return 'PAID'
if state in ('AUTHORIZED',):
return 'AUTHORIZED'
if state in ('PAYMENT_METHOD_CHOSEN',):
return 'PAYMENT_METHOD_CHOSEN'
if state in ('CREATED', 'CREATED_WITH_PAYMENT', 'PENDING'):
return 'CREATED'
if state in ('CANCELED', 'CANCELLED'):
return 'CANCELED'
if state in ('TIMEOUTED',):
return 'TIMEOUTED'
if state in ('REFUNDED',):
return 'REFUNDED'
if state in ('PARTIALLY_REFUNDED',):
return 'PARTIALLY_REFUNDED'
if state in ('FAILED', 'DECLINED'):
return 'FAILED'
return state
# --- Serializers kept here (small and read-only) ---
class GoPayRefundROSerializer(serializers.Serializer):
id = serializers.IntegerField()
amount_cents = serializers.IntegerField(allow_null=True)
reason = serializers.CharField(allow_null=True, allow_blank=True)
provider_refund_id = serializers.CharField(allow_null=True)
created_at = serializers.DateTimeField()
class GoPayPaymentROSerializer(serializers.Serializer):
id = serializers.IntegerField()
order = serializers.IntegerField(source='order_id', allow_null=True)
amount_cents = serializers.IntegerField()
currency = serializers.CharField()
status = serializers.CharField()
refunded_amount_cents = serializers.IntegerField()
gw_url = serializers.URLField(allow_null=True)
provider_payment_id = serializers.IntegerField(allow_null=True)
created_at = serializers.DateTimeField()
updated_at = serializers.DateTimeField()
class PaymentStatusView(APIView):
"""
GET /api/payments/payment/{id}
- Refresh status from GoPay (if provider_payment_id present).
- Return current local payment record (read-only).
"""
permission_classes = [permissions.IsAuthenticated]
def get(self, request, pk: int):
payment = get_object_or_404(GoPayPayment, pk=pk)
if payment.provider_payment_id:
api = _gopay_api()
resp = api.get_status(payment.provider_payment_id)
if getattr(resp, "success", False):
data = getattr(resp, "json", None)
payment.status = _map_status(data.get('state'))
payment.raw_last_status = data
payment.save(update_fields=['status', 'raw_last_status', 'updated_at'])
else:
err = getattr(resp, "json", None) or {"status_code": getattr(resp, "status_code", None), "raw": getattr(resp, "raw_body", None)}
return Response({'detail': 'Failed to fetch status', 'error': err}, status=status.HTTP_502_BAD_GATEWAY)
serialized = GoPayPaymentROSerializer(payment)
return Response(serialized.data)
class PaymentRefundListView(APIView):
"""
GET /api/payments/payment/{id}/refunds
- List local refund records for a payment (read-only).
"""
permission_classes = [permissions.IsAuthenticated]
def get(self, request, pk: int):
payment = get_object_or_404(GoPayPayment, pk=pk)
refunds = payment.refunds.all().values(
'id', 'amount_cents', 'reason', 'provider_refund_id', 'created_at'
)
ser = GoPayRefundROSerializer(refunds, many=True)
return Response(ser.data)
@method_decorator(csrf_exempt, name='dispatch')
class GoPayWebhookView(APIView):
"""
GET /api/payments/gopay/webhook?id=<provider_payment_id>
- Called by GoPay (HTTP GET with query params) on payment state change.
- We verify by fetching status via GoPay SDK and persist it locally.
"""
permission_classes = [permissions.AllowAny]
def get(self, request):
provider_id = request.GET.get("id") or request.GET.get("payment_id")
if not provider_id:
return Response({"detail": "Missing payment id"}, status=status.HTTP_400_BAD_REQUEST)
try:
provider_id_int = int(provider_id)
except Exception:
provider_id_int = provider_id # fallback
api = _gopay_api()
resp = api.get_status(provider_id_int)
if not getattr(resp, "success", False):
err = getattr(resp, "json", None) or {"status_code": getattr(resp, "status_code", None), "raw": getattr(resp, "raw_body", None)}
return Response({"detail": "Failed to verify status with GoPay", "error": err}, status=status.HTTP_502_BAD_GATEWAY)
data = getattr(resp, "json", None)
state = data.get("state")
# Find local payment by provider id, fallback to order_number from response
payment = None
try:
payment = GoPayPayment.objects.get(provider_payment_id=provider_id_int)
except GoPayPayment.DoesNotExist:
order_number = data.get("order_number")
if order_number:
try:
payment = GoPayPayment.objects.get(pk=int(order_number))
except Exception:
payment = None
if not payment:
return Response({"detail": "Payment not found locally", "provider_id": provider_id_int}, status=status.HTTP_202_ACCEPTED)
payment.status = _map_status(state)
payment.raw_last_status = data
payment.save(update_fields=["status", "raw_last_status", "updated_at"])
return Response({"ok": True})
# Implementation notes:
# - GoPay notification is an HTTP GET to notification_url with ?id=...
# - Always verify the state by calling get_status(id); never trust inbound payload alone.
# - Amounts are kept in minor units (cents). States covered: CREATED, PAYMENT_METHOD_CHOSEN, AUTHORIZED, PAID, CANCELED, TIMEOUTED, PARTIALLY_REFUNDED, REFUNDED.