188 lines
6.2 KiB
Python
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.
|