Files
vontor-cz/backend/thirdparty/gopay/views.py
2025-11-05 18:13:01 +01:00

243 lines
7.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from __future__ import annotations
# Tato aplikace poskytuje samostatné HTTP API pro GoPay, připravené k připojení do eshopu.
# Pohledy volají GoPay SDK a ukládají minimální data o životním cyklu (platby/refundy).
# Koncové body jsou dokumentovány pro Swagger/Redoc pomocí drf-spectacular.
from rest_framework import permissions, status
from rest_framework.response import Response
from rest_framework.views import APIView
from drf_spectacular.utils import (
extend_schema,
OpenApiParameter,
OpenApiResponse,
OpenApiExample,
OpenApiTypes,
)
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
import gopay
from gopay.enums import TokenScope, Language
def payments_client():
"""Create a fresh GoPay payments client from settings.
Keeps it simple and explicit; no shared global state.
"""
required = [
("GOPAY_GOID", settings.GOPAY_GOID),
("GOPAY_CLIENT_ID", settings.GOPAY_CLIENT_ID),
("GOPAY_CLIENT_SECRET", settings.GOPAY_CLIENT_SECRET),
("GOPAY_GATEWAY_URL", settings.GOPAY_GATEWAY_URL),
]
missing = [name for name, val in required if not val]
if missing:
raise ImproperlyConfigured(f"Missing GoPay settings: {', '.join(missing)}")
cfg = {
"goid": settings.GOPAY_GOID,
"client_id": settings.GOPAY_CLIENT_ID,
"client_secret": settings.GOPAY_CLIENT_SECRET,
"gateway_url": settings.GOPAY_GATEWAY_URL,
# reasonable defaults; can be changed later via settings if desired
"scope": TokenScope.ALL,
"language": Language.CZECH,
}
return gopay.payments(cfg)
from .models import GoPayPayment, GoPayRefund
from .serializers import (
PaymentCreateSerializer,
RefundSerializer,
)
class CreatePaymentView(APIView):
"""Vytvoří novou platbu v GoPay a uloží odpověď lokálně.
Typický eshop flow:
- Frontend zavolá tento endpoint s GoPay payloadem.
- Backend předá payload GoPay SDK a vrátí upstream odpověď.
- Vznikne lokální záznam GoPayPayment pro pozdější sledování stavu.
"""
permission_classes = [permissions.IsAuthenticated]
@extend_schema(
tags=["GoPay"],
operation_id="gopay_create_payment",
summary="Vytvořit platbu (minimální vstup)",
description=(
"Vytvoří platbu v GoPay s minimálními povinnými poli. Citlivé údaje o kartě se neposílají,"
" platbu obslouží GoPay stránka (gw_url). Pokud není zadán notification_url, použije se"
" hodnota ze settings.GOPAY_NOTIFICATION_URL."
),
request=PaymentCreateSerializer,
responses={
201: OpenApiResponse(
response=OpenApiTypes.OBJECT,
description="Platba vytvořena. Vrací gw_url pro přesměrování na platební bránu."
),
502: OpenApiResponse(description="Chyba upstream GoPay"),
},
examples=[
OpenApiExample(
"Minimální platba",
value={
"payment": {
"amount": 10000,
"currency": "CZK",
"order_number": "123456",
"payer": {"contact": {"email": "john.doe@example.com"}},
"callback": {
"return_url": "https://example.com/your-return-url",
"notification_url": "https://example.com/your-notify-url"
}
}
},
)
],
)
def post(self, request):
ser = PaymentCreateSerializer(data=request.data)
ser.is_valid(raise_exception=True)
payload = ser.validated_data["payment"]
try:
res = payments_client().create_payment(payload) # Expecting dict-like/dict response
except Exception as e:
return Response({"detail": str(e)}, status=status.HTTP_502_BAD_GATEWAY)
# Map fields defensively
as_dict = res if isinstance(res, dict) else getattr(res, "__dict__", {}) or {}
gopay_id = str(as_dict.get("id", ""))
status_text = str(as_dict.get("state", ""))
amount = int(as_dict.get("amount", payload.get("amount", 0)) or 0)
currency = as_dict.get("currency", payload.get("currency", ""))
gw_url = as_dict.get("gw_url") or as_dict.get("gwUrl") or as_dict.get("gw-url")
payment = GoPayPayment.objects.create(
user=request.user if request.user and request.user.is_authenticated else None,
gopay_id=gopay_id or payload.get("id", ""),
order_number=str(payload.get("order_number", "")),
amount=amount,
currency=currency or "",
status=status_text,
preauthorized=bool(payload.get("preauthorize", False)),
request_payload=payload,
response_payload=as_dict or {"raw": str(res)},
)
return Response(
{
"payment_id": payment.gopay_id,
"state": payment.status,
"gw_url": gw_url,
"raw": payment.response_payload,
},
status=status.HTTP_201_CREATED,
)
class PaymentStatusView(APIView):
"""Načte aktuální stav platby GoPay a případně synchronizuje lokální záznam."""
permission_classes = [permissions.IsAuthenticated]
@extend_schema(
tags=["GoPay"],
operation_id="gopay_get_status",
summary="Získat stav platby",
parameters=[
OpenApiParameter(
name="payment_id",
type=OpenApiTypes.STR,
location=OpenApiParameter.PATH,
description="ID platby GoPay",
)
],
responses={
200: OpenApiResponse(OpenApiTypes.OBJECT, description="Aktuální stav platby"),
502: OpenApiResponse(description="Chyba upstream GoPay"),
},
)
def get(self, request, payment_id: str | int):
try:
res = payments_client().get_status(payment_id)
except Exception as e:
return Response({"detail": str(e)}, status=status.HTTP_502_BAD_GATEWAY)
# Normalize GoPay SDK response into a dict body
body = None
if hasattr(res, "success") and hasattr(res, "json"):
# Official SDK-style: check success and extract JSON body
ok = bool(getattr(res, "success", False))
if not ok:
return Response({"detail": "GoPay upstream error", "success": False}, status=status.HTTP_502_BAD_GATEWAY)
json_attr = getattr(res, "json")
body = json_attr() if callable(json_attr) else json_attr
elif isinstance(res, dict):
body = res
else:
body = getattr(res, "__dict__", {}) or {"raw": str(res)}
state_val = body.get("state") if isinstance(body, dict) else None
# Update local status if we have it
try:
local = GoPayPayment.objects.get(gopay_id=str(payment_id))
if state_val:
local.status = state_val
local.response_payload = body if isinstance(body, dict) else {"raw": str(body)}
local.save(update_fields=["status", "response_payload", "updated_at"])
except GoPayPayment.DoesNotExist:
pass
return Response(body, status=status.HTTP_200_OK)
class RefundPaymentView(APIView):
"""Provede refundaci platby v GoPay a uloží záznam refundace."""
permission_classes = [permissions.IsAuthenticated]
@extend_schema(
tags=["GoPay"],
operation_id="gopay_refund_payment",
summary="Refundovat platbu",
parameters=[
OpenApiParameter(
name="payment_id",
type=OpenApiTypes.STR,
location=OpenApiParameter.PATH,
description="ID platby GoPay k refundaci",
)
],
request=RefundSerializer,
responses={
200: OpenApiResponse(OpenApiTypes.OBJECT, description="Výsledek refundace"),
502: OpenApiResponse(description="Chyba upstream GoPay"),
},
)
def post(self, request, payment_id: str | int):
ser = RefundSerializer(data=request.data)
ser.is_valid(raise_exception=True)
amount = ser.validated_data["amount"]
try:
res = payments_client().refund_payment(payment_id, amount)
except Exception as e:
return Response({"detail": str(e)}, status=status.HTTP_502_BAD_GATEWAY)
payment = GoPayPayment.objects.filter(gopay_id=str(payment_id)).first()
ref = GoPayRefund.objects.create(
payment=payment,
gopay_refund_id=str((res.get("id") if isinstance(res, dict) else getattr(res, "id", "")) or ""),
amount=amount,
status=(res.get("state") if isinstance(res, dict) else getattr(res, "state", "")) or "",
payload=res if isinstance(res, dict) else {"raw": str(res)},
)
return Response({"refund": ref.payload}, status=status.HTTP_200_OK)