gopay done
This commit is contained in:
74
backend/thirdparty/gopay/client.py
vendored
74
backend/thirdparty/gopay/client.py
vendored
@@ -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)
|
|
||||||
81
backend/thirdparty/gopay/serializers.py
vendored
81
backend/thirdparty/gopay/serializers.py
vendored
@@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
131
backend/thirdparty/gopay/views.py
vendored
131
backend/thirdparty/gopay/views.py
vendored
@@ -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 ""),
|
||||||
|
|||||||
Reference in New Issue
Block a user