This commit is contained in:
2025-10-01 18:37:59 +02:00
parent 85b035fd27
commit 696d0e61f1
46 changed files with 1750 additions and 0 deletions

0
backend/thirdparty/gopay/__init__.py vendored Normal file
View File

3
backend/thirdparty/gopay/admin.py vendored Normal file
View File

@@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
backend/thirdparty/gopay/apps.py vendored Normal file
View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class GopayConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'gopay'

View File

3
backend/thirdparty/gopay/models.py vendored Normal file
View File

@@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

37
backend/thirdparty/gopay/serializers.py vendored Normal file
View File

@@ -0,0 +1,37 @@
from rest_framework import serializers
class GoPayCreatePaymentRequestSerializer(serializers.Serializer):
amount = serializers.DecimalField(max_digits=12, decimal_places=2, min_value=0.01)
currency = serializers.CharField(required=False, default="CZK")
order_number = serializers.CharField(required=False, allow_blank=True, default="order-001")
order_description = serializers.CharField(required=False, allow_blank=True, default="Example GoPay payment")
return_url = serializers.URLField(required=False)
notify_url = serializers.URLField(required=False)
preauthorize = serializers.BooleanField(required=False, default=False)
class GoPayPaymentCreatedResponseSerializer(serializers.Serializer):
id = serializers.IntegerField()
state = serializers.CharField()
gw_url = serializers.URLField(required=False, allow_null=True)
class GoPayStatusResponseSerializer(serializers.Serializer):
id = serializers.IntegerField()
state = serializers.CharField()
class GoPayRefundRequestSerializer(serializers.Serializer):
amount = serializers.DecimalField(max_digits=12, decimal_places=2, required=False, min_value=0.01)
class GoPayCaptureRequestSerializer(serializers.Serializer):
amount = serializers.DecimalField(max_digits=12, decimal_places=2, required=False, min_value=0.01)
class GoPayCreateRecurrenceRequestSerializer(serializers.Serializer):
amount = serializers.DecimalField(max_digits=12, decimal_places=2, min_value=0.01)
currency = serializers.CharField(required=False, default="CZK")
order_number = serializers.CharField(required=False, allow_blank=True, default="recur-001")
order_description = serializers.CharField(required=False, allow_blank=True, default="Recurring payment")

3
backend/thirdparty/gopay/tests.py vendored Normal file
View File

@@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

20
backend/thirdparty/gopay/urls.py vendored Normal file
View File

@@ -0,0 +1,20 @@
from django.urls import path
from .views import (
GoPayPaymentView,
GoPayPaymentStatusView,
GoPayRefundPaymentView,
GoPayCaptureAuthorizationView,
GoPayVoidAuthorizationView,
GoPayCreateRecurrenceView,
GoPayPaymentInstrumentsView,
)
urlpatterns = [
path('payment/', GoPayPaymentView.as_view(), name='gopay-payment'),
path('payment/<int:payment_id>/status/', GoPayPaymentStatusView.as_view(), name='gopay-payment-status'),
path('payment/<int:payment_id>/refund/', GoPayRefundPaymentView.as_view(), name='gopay-refund-payment'),
path('payment/<int:payment_id>/capture/', GoPayCaptureAuthorizationView.as_view(), name='gopay-capture-authorization'),
path('payment/<int:payment_id>/void/', GoPayVoidAuthorizationView.as_view(), name='gopay-void-authorization'),
path('payment/<int:payment_id>/recurrence/', GoPayCreateRecurrenceView.as_view(), name='gopay-create-recurrence'),
path('payment-instruments/', GoPayPaymentInstrumentsView.as_view(), name='gopay-payment-instruments'),
]

233
backend/thirdparty/gopay/views.py vendored Normal file
View File

@@ -0,0 +1,233 @@
from django.shortcuts import render
# Create your views here.
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
import gopay
from gopay.enums import TokenScope, Language
import os
from drf_spectacular.utils import extend_schema, OpenApiResponse, OpenApiExample, OpenApiParameter
from .serializers import (
GoPayCreatePaymentRequestSerializer,
GoPayPaymentCreatedResponseSerializer,
GoPayStatusResponseSerializer,
GoPayRefundRequestSerializer,
GoPayCaptureRequestSerializer,
GoPayCreateRecurrenceRequestSerializer,
)
class GoPayClientMixin:
"""Shared helpers for configuring GoPay client and formatting responses."""
def get_gopay_client(self):
gateway_url = os.getenv("GOPAY_GATEWAY_URL", "https://gw.sandbox.gopay.com/api")
return gopay.payments({
"goid": os.getenv("GOPAY_GOID"),
"client_id": os.getenv("GOPAY_CLIENT_ID"),
"client_secret": os.getenv("GOPAY_CLIENT_SECRET"),
"gateway_url": gateway_url,
"scope": TokenScope.ALL,
"language": Language.CZECH,
})
def _to_response(self, sdk_response):
# The GoPay SDK returns a response object with has_succeed(), json, errors, status_code
try:
if hasattr(sdk_response, "has_succeed") and sdk_response.has_succeed():
return Response(getattr(sdk_response, "json", {}))
status = getattr(sdk_response, "status_code", 400)
errors = getattr(sdk_response, "errors", None)
if errors is None and hasattr(sdk_response, "json"):
errors = sdk_response.json
if errors is None:
errors = {"detail": "GoPay request failed"}
return Response({"errors": errors}, status=status)
except Exception as e:
return Response({"errors": str(e)}, status=500)
class GoPayPaymentView(GoPayClientMixin, APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["GoPay"],
summary="Create GoPay payment",
description="Creates a GoPay payment and returns gateway URL and payment info.",
request=GoPayCreatePaymentRequestSerializer,
responses={
200: OpenApiResponse(response=GoPayPaymentCreatedResponseSerializer, description="Payment created"),
400: OpenApiResponse(description="Validation error or SDK error"),
},
examples=[
OpenApiExample(
"Create payment",
value={
"amount": 123.45,
"currency": "CZK",
"order_number": "order-001",
"order_description": "Example GoPay payment",
"return_url": "https://yourfrontend.com/success",
"notify_url": "https://yourbackend.com/gopay/notify",
"preauthorize": False,
},
request_only=True,
)
]
)
def post(self, request):
amount = request.data.get("amount")
currency = request.data.get("currency", "CZK")
order_number = request.data.get("order_number", "order-001")
order_description = request.data.get("order_description", "Example GoPay payment")
return_url = request.data.get("return_url", "https://yourfrontend.com/success")
notify_url = request.data.get("notify_url", "https://yourbackend.com/gopay/notify")
preauthorize = bool(request.data.get("preauthorize", False))
if not amount:
return Response({"error": "Amount is required"}, status=400)
payments = self.get_gopay_client()
payment_data = {
"payer": {
"allowed_payment_instruments": ["PAYMENT_CARD"],
"default_payment_instrument": "PAYMENT_CARD",
"allowed_swifts": ["FIOB"],
"contact": {
"first_name": getattr(request.user, "first_name", ""),
"last_name": getattr(request.user, "last_name", ""),
"email": getattr(request.user, "email", ""),
},
},
"amount": int(float(amount) * 100), # GoPay expects amount in cents
"currency": currency,
"order_number": order_number,
"order_description": order_description,
"items": [
{"name": "Example Item", "amount": int(float(amount) * 100)}
],
"callback": {"return_url": return_url, "notify_url": notify_url},
"preauthorize": preauthorize,
}
resp = payments.create_payment(payment_data)
return self._to_response(resp)
class GoPayPaymentStatusView(GoPayClientMixin, APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["GoPay"],
summary="Get GoPay payment status",
parameters=[OpenApiParameter(name="payment_id", required=True, type=int, location=OpenApiParameter.PATH)],
responses={200: OpenApiResponse(response=GoPayStatusResponseSerializer, description="Payment status")},
)
def get(self, request, payment_id: int):
payments = self.get_gopay_client()
resp = payments.get_status(payment_id)
return self._to_response(resp)
class GoPayRefundPaymentView(GoPayClientMixin, APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["GoPay"],
summary="Refund GoPay payment",
parameters=[OpenApiParameter(name="payment_id", required=True, type=int, location=OpenApiParameter.PATH)],
request=GoPayRefundRequestSerializer,
responses={200: OpenApiResponse(description="Refund processed")},
)
def post(self, request, payment_id: int):
amount = request.data.get("amount") # optional for full refund
payments = self.get_gopay_client()
if amount is None or amount == "":
# Full refund
resp = payments.refund_payment(payment_id)
else:
resp = payments.refund_payment(payment_id, int(float(amount) * 100))
return self._to_response(resp)
class GoPayCaptureAuthorizationView(GoPayClientMixin, APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["GoPay"],
summary="Capture GoPay authorization",
parameters=[OpenApiParameter(name="payment_id", required=True, type=int, location=OpenApiParameter.PATH)],
request=GoPayCaptureRequestSerializer,
responses={200: OpenApiResponse(description="Capture processed")},
)
def post(self, request, payment_id: int):
amount = request.data.get("amount") # optional for partial capture
payments = self.get_gopay_client()
if amount is None or amount == "":
resp = payments.capture_authorization(payment_id)
else:
resp = payments.capture_authorization(payment_id, int(float(amount) * 100))
return self._to_response(resp)
class GoPayVoidAuthorizationView(GoPayClientMixin, APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["GoPay"],
summary="Void GoPay authorization",
parameters=[OpenApiParameter(name="payment_id", required=True, type=int, location=OpenApiParameter.PATH)],
responses={200: OpenApiResponse(description="Authorization voided")},
)
def post(self, request, payment_id: int):
payments = self.get_gopay_client()
resp = payments.void_authorization(payment_id)
return self._to_response(resp)
class GoPayCreateRecurrenceView(GoPayClientMixin, APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["GoPay"],
summary="Create GoPay recurrence",
parameters=[OpenApiParameter(name="payment_id", required=True, type=int, location=OpenApiParameter.PATH)],
request=GoPayCreateRecurrenceRequestSerializer,
responses={200: OpenApiResponse(description="Recurrence created")},
)
def post(self, request, payment_id: int):
amount = request.data.get("amount")
currency = request.data.get("currency", "CZK")
order_number = request.data.get("order_number", "recur-001")
order_description = request.data.get("order_description", "Recurring payment")
if not amount:
return Response({"error": "Amount is required"}, status=400)
payments = self.get_gopay_client()
recurrence_payload = {
"amount": int(float(amount) * 100),
"currency": currency,
"order_number": order_number,
"order_description": order_description,
}
resp = payments.create_recurrence(payment_id, recurrence_payload)
return self._to_response(resp)
class GoPayPaymentInstrumentsView(GoPayClientMixin, APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["GoPay"],
summary="Get GoPay payment instruments",
parameters=[OpenApiParameter(name="currency", required=False, type=str, location=OpenApiParameter.QUERY)],
responses={200: OpenApiResponse(description="Available payment instruments returned")},
)
def get(self, request):
currency = request.query_params.get("currency", "CZK")
goid = os.getenv("GOPAY_GOID")
if not goid:
return Response({"error": "GOPAY_GOID is not configured"}, status=500)
payments = self.get_gopay_client()
resp = payments.get_payment_instruments(goid, currency)
return self._to_response(resp)