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= - 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.