gopay done

This commit is contained in:
2025-11-05 18:13:01 +01:00
parent de5f54f4bc
commit 05055415de
3 changed files with 169 additions and 117 deletions

View File

@@ -1,74 +0,0 @@
from __future__ import annotations
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
try:
# Expecting official SDK providing `payments` with required methods
from gopay import payments as _payments # type: ignore
except Exception: # pragma: no cover
_payments = None
def _get_client():
if _payments is None:
raise ImproperlyConfigured(
"GoPay SDK not installed or not importable. Install and configure the GoPay SDK."
)
# If your SDK requires explicit initialization with credentials, do it here.
# Example (pseudo):
# return PaymentsClient(
# goid=settings.GOPAY_GOID,
# client_id=settings.GOPAY_CLIENT_ID,
# client_secret=settings.GOPAY_CLIENT_SECRET,
# gateway_url=settings.GOPAY_GATEWAY_URL,
# )
return _payments
def create_payment(payload: dict):
return _get_client().create_payment(payload)
def get_status(payment_id: str | int):
return _get_client().get_status(payment_id)
def refund_payment(payment_id: str | int, amount: int):
return _get_client().refund_payment(payment_id, amount)
def create_recurrence(payment_id: str | int, payload: dict):
return _get_client().create_recurrence(payment_id, payload)
def void_recurrence(payment_id: str | int):
return _get_client().void_recurrence(payment_id)
def capture_authorization(payment_id: str | int):
return _get_client().capture_authorization(payment_id)
def capture_authorization_partial(payment_id: str | int, payload: dict):
return _get_client().capture_authorization_partial(payment_id, payload)
def get_card_details(card_id: str | int):
return _get_client().get_card_details(card_id)
def delete_card(card_id: str | int):
return _get_client().delete_card(card_id)
def get_payment_instruments(goid: str | int, currency: str):
return _get_client().get_payment_instruments(goid, currency)
def get_payment_instruments_all(goid: str | int):
return _get_client().get_payment_instruments_all(goid)
def get_account_statement(statement_request: dict):
return _get_client().get_account_statement(statement_request)

View File

@@ -1,13 +1,86 @@
from __future__ import annotations from __future__ import annotations
from django.conf import settings
from rest_framework import serializers from rest_framework import serializers
class PaymentCreateSerializer(serializers.Serializer): class ContactSerializer(serializers.Serializer):
# Entire GoPay payment payload is passed through email = serializers.EmailField()
payment = serializers.DictField() first_name = serializers.CharField(required=False, allow_blank=True)
last_name = serializers.CharField(required=False, allow_blank=True)
phone_number = serializers.CharField(required=False, allow_blank=True)
city = serializers.CharField(required=False, allow_blank=True)
street = serializers.CharField(required=False, allow_blank=True)
postal_code = serializers.CharField(required=False, allow_blank=True)
country_code = serializers.CharField(required=False, allow_blank=True)
# Optional: help store local metadata
class PayerSerializer(serializers.Serializer):
contact = ContactSerializer()
# Optional controls keep everything optional, no card numbers allowed (SDK handles UI)
allowed_payment_instruments = serializers.ListField(
child=serializers.CharField(), required=False
)
default_payment_instrument = serializers.CharField(required=False)
allowed_swifts = serializers.ListField(child=serializers.CharField(), required=False)
default_swift = serializers.CharField(required=False)
class CallbackSerializer(serializers.Serializer):
return_url = serializers.URLField()
notification_url = serializers.URLField(required=False)
def validate(self, attrs):
# Default notification_url from settings if not provided
if not attrs.get("notification_url"):
attrs["notification_url"] = getattr(
settings, "GOPAY_NOTIFICATION_URL", "http://localhost:8000/api/payments/gopay/webhook"
)
return attrs
class ItemSerializer(serializers.Serializer):
name = serializers.CharField()
amount = serializers.IntegerField(min_value=1, help_text="Minor units")
type = serializers.CharField(required=False, default="ITEM")
count = serializers.IntegerField(required=False, min_value=1, default=1)
class AdditionalParamSerializer(serializers.Serializer):
name = serializers.CharField()
value = serializers.CharField()
class PaymentBodySerializer(serializers.Serializer):
# Minimal required
amount = serializers.IntegerField(min_value=1, help_text="Minor units (e.g. 100 CZK = 10000)")
currency = serializers.CharField()
order_number = serializers.CharField()
# Optional
order_description = serializers.CharField(required=False, allow_blank=True)
payer = PayerSerializer()
callback = CallbackSerializer()
items = ItemSerializer(many=True, required=False)
additional_params = AdditionalParamSerializer(many=True, required=False)
lang = serializers.CharField(required=False)
preauthorize = serializers.BooleanField(required=False)
def validate(self, attrs):
# Explicitly reject any card details if someone tries to sneak them in
forbidden_keys = {"card", "card_data", "pan", "cvv", "expiry", "card_number"}
for key in list(attrs.keys()):
if key in forbidden_keys:
raise serializers.ValidationError({key: "Card details must not be sent to the server."})
return attrs
class PaymentCreateSerializer(serializers.Serializer):
# Frontend posts { payment: { ... } }
payment = PaymentBodySerializer()
# Optional: local metadata only (not sent to GoPay)
user_id = serializers.IntegerField(required=False) user_id = serializers.IntegerField(required=False)

View File

@@ -13,12 +13,44 @@ from drf_spectacular.utils import (
OpenApiExample, OpenApiExample,
OpenApiTypes, 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 . import client as goclient
from .models import GoPayPayment, GoPayRefund from .models import GoPayPayment, GoPayRefund
from .serializers import ( from .serializers import (
PaymentCreateSerializer, PaymentCreateSerializer,
RefundSerializer, RefundSerializer,
) )
@@ -35,43 +67,38 @@ class CreatePaymentView(APIView):
@extend_schema( @extend_schema(
tags=["GoPay"], tags=["GoPay"],
operation_id="gopay_create_payment", operation_id="gopay_create_payment",
summary="Vytvořit platbu", summary="Vytvořit platbu (minimální vstup)",
description="Vytvoří platbu v GoPay. Objekt 'payment' je předán SDK beze změn.", 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, request=PaymentCreateSerializer,
responses={ responses={
201: OpenApiResponse( 201: OpenApiResponse(
response=OpenApiTypes.OBJECT, response=OpenApiTypes.OBJECT,
description="Platba vytvořena (odpověď z GoPay)." description="Platba vytvořena. Vrací gw_url pro přesměrování na platební bránu."
), ),
502: OpenApiResponse(description="Chyba upstream GoPay"), 502: OpenApiResponse(description="Chyba upstream GoPay"),
}, },
examples=[ examples=[
OpenApiExample( OpenApiExample(
"Základní platba kartou", "Minimální platba",
value={ value={
"payment": { "payment": {
"amount": 19900, "amount": 10000,
"currency": "CZK", "currency": "CZK",
"order_number": "ORDER-1001", "order_number": "123456",
"items": [{"name": "T-Shirt", "amount": 19900, "count": 1}], "payer": {"contact": {"email": "john.doe@example.com"}},
"callback": {"return_url": "https://shop.example.com/return"}, "callback": {
"return_url": "https://example.com/your-return-url",
"notification_url": "https://example.com/your-notify-url"
}
} }
}, },
) )
], ],
extensions={
"x-codeSamples": [
{
"lang": "typescript",
"label": "Client.auth (frontend)",
"source": 'await Client.auth.post("/api/payments/gopay/create/", { payment });',
},
{
"lang": "curl",
"source": 'curl -X POST http://localhost:8000/api/payments/gopay/create/ -H "Content-Type: application/json" -d \'{"payment": {"amount":19900,"currency":"CZK"}}\'',
},
]
},
) )
def post(self, request): def post(self, request):
ser = PaymentCreateSerializer(data=request.data) ser = PaymentCreateSerializer(data=request.data)
@@ -79,29 +106,39 @@ class CreatePaymentView(APIView):
payload = ser.validated_data["payment"] payload = ser.validated_data["payment"]
try: try:
res = goclient.create_payment(payload) # Expecting dict-like response res = payments_client().create_payment(payload) # Expecting dict-like/dict response
except Exception as e: except Exception as e:
return Response({"detail": str(e)}, status=status.HTTP_502_BAD_GATEWAY) return Response({"detail": str(e)}, status=status.HTTP_502_BAD_GATEWAY)
# Map fields defensively # Map fields defensively
gopay_id = str((res.get("id") if isinstance(res, dict) else getattr(res, "id", "")) or "") as_dict = res if isinstance(res, dict) else getattr(res, "__dict__", {}) or {}
status_text = (res.get("state") if isinstance(res, dict) else getattr(res, "state", "")) or "" gopay_id = str(as_dict.get("id", ""))
amount = int((res.get("amount") if isinstance(res, dict) else getattr(res, "amount", 0)) or payload.get("amount") or 0) status_text = str(as_dict.get("state", ""))
currency = (res.get("currency") if isinstance(res, dict) else getattr(res, "currency", "")) or payload.get("currency", "") 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( payment = GoPayPayment.objects.create(
user=request.user if request.user and request.user.is_authenticated else None, user=request.user if request.user and request.user.is_authenticated else None,
gopay_id=gopay_id or payload.get("id", ""), gopay_id=gopay_id or payload.get("id", ""),
order_number=str(payload.get("order_number", "")), order_number=str(payload.get("order_number", "")),
amount=amount, amount=amount,
currency=currency, currency=currency or "",
status=status_text, status=status_text,
preauthorized=bool(payload.get("preauthorize", False)), preauthorized=bool(payload.get("preauthorize", False)),
request_payload=payload, request_payload=payload,
response_payload=res if isinstance(res, dict) else {"raw": str(res)}, response_payload=as_dict or {"raw": str(res)},
) )
return Response({"payment": payment.response_payload}, status=status.HTTP_201_CREATED) 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): class PaymentStatusView(APIView):
@@ -127,22 +164,37 @@ class PaymentStatusView(APIView):
) )
def get(self, request, payment_id: str | int): def get(self, request, payment_id: str | int):
try: try:
res = goclient.get_status(payment_id) res = payments_client().get_status(payment_id)
except Exception as e: except Exception as e:
return Response({"detail": str(e)}, status=status.HTTP_502_BAD_GATEWAY) 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 # Update local status if we have it
try: try:
local = GoPayPayment.objects.get(gopay_id=str(payment_id)) local = GoPayPayment.objects.get(gopay_id=str(payment_id))
status_text = (res.get("state") if isinstance(res, dict) else getattr(res, "state", "")) or "" if state_val:
if status_text: local.status = state_val
local.status = status_text local.response_payload = body if isinstance(body, dict) else {"raw": str(body)}
local.response_payload = res if isinstance(res, dict) else {"raw": str(res)}
local.save(update_fields=["status", "response_payload", "updated_at"]) local.save(update_fields=["status", "response_payload", "updated_at"])
except GoPayPayment.DoesNotExist: except GoPayPayment.DoesNotExist:
pass pass
return Response(res, status=status.HTTP_200_OK) return Response(body, status=status.HTTP_200_OK)
class RefundPaymentView(APIView): class RefundPaymentView(APIView):
@@ -173,11 +225,12 @@ class RefundPaymentView(APIView):
amount = ser.validated_data["amount"] amount = ser.validated_data["amount"]
try: try:
res = goclient.refund_payment(payment_id, amount) res = payments_client().refund_payment(payment_id, amount)
except Exception as e: except Exception as e:
return Response({"detail": str(e)}, status=status.HTTP_502_BAD_GATEWAY) return Response({"detail": str(e)}, status=status.HTTP_502_BAD_GATEWAY)
payment = GoPayPayment.objects.filter(gopay_id=str(payment_id)).first() payment = GoPayPayment.objects.filter(gopay_id=str(payment_id)).first()
ref = GoPayRefund.objects.create( ref = GoPayRefund.objects.create(
payment=payment, payment=payment,
gopay_refund_id=str((res.get("id") if isinstance(res, dict) else getattr(res, "id", "")) or ""), gopay_refund_id=str((res.get("id") if isinstance(res, dict) else getattr(res, "id", "")) or ""),