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)

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

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

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

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

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

View File

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

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

View File

@@ -0,0 +1,12 @@
from rest_framework import serializers
class StripeCheckoutRequestSerializer(serializers.Serializer):
amount = serializers.DecimalField(max_digits=12, decimal_places=2, min_value=0.01)
product_name = serializers.CharField(required=False, default="Example Product")
success_url = serializers.URLField(required=False)
cancel_url = serializers.URLField(required=False)
class StripeCheckoutResponseSerializer(serializers.Serializer):
url = serializers.URLField()

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

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

6
backend/thirdparty/stripe/urls.py vendored Normal file
View File

@@ -0,0 +1,6 @@
from django.urls import path
from .views import StripeCheckoutCZKView
urlpatterns = [
path('checkout/', StripeCheckoutCZKView.as_view(), name='stripe-checkout-czk'),
]

71
backend/thirdparty/stripe/views.py vendored Normal file
View File

@@ -0,0 +1,71 @@
import stripe
import os
from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from drf_spectacular.utils import extend_schema, OpenApiResponse, OpenApiExample, OpenApiParameter
from .serializers import (
StripeCheckoutRequestSerializer,
StripeCheckoutResponseSerializer,
)
stripe.api_key = os.getenv("STRIPE_SECRET_KEY")
class StripeCheckoutCZKView(APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
tags=["Stripe"],
summary="Create Stripe Checkout session in CZK",
description="Creates a Stripe Checkout session for payment in Czech Koruna (CZK). Requires authentication.",
request=StripeCheckoutRequestSerializer,
responses={
200: OpenApiResponse(response=StripeCheckoutResponseSerializer, description="Stripe Checkout session URL returned successfully."),
400: OpenApiResponse(description="Amount is required or invalid."),
},
examples=[
OpenApiExample(
"Success",
value={"url": "https://checkout.stripe.com/pay/cs_test_123456"},
response_only=True,
status_codes=["200"],
),
OpenApiExample(
"Missing amount",
value={"error": "Amount is required"},
response_only=True,
status_codes=["400"],
),
]
)
def post(self, request):
serializer = StripeCheckoutRequestSerializer(data=request.data)
if not serializer.is_valid():
return Response(serializer.errors, status=400)
amount = serializer.validated_data.get("amount")
product_name = serializer.validated_data.get("product_name", "Example Product")
success_url = serializer.validated_data.get("success_url", "https://yourfrontend.com/success")
cancel_url = serializer.validated_data.get("cancel_url", "https://yourfrontend.com/cancel")
# Stripe expects amount in the smallest currency unit (haléř = 1/100 CZK)
amount_in_haler = int(amount * 100)
session = stripe.checkout.Session.create(
payment_method_types=['card'],
line_items=[{
'price_data': {
'currency': 'czk',
'product_data': {
'name': product_name,
},
'unit_amount': amount_in_haler,
},
'quantity': 1,
}],
mode='payment',
success_url=success_url,
cancel_url=cancel_url,
customer_email=getattr(request.user, 'email', None)
)
return Response({"url": session.url})

View File

View File

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

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

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

View File

View File

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

View File

@@ -0,0 +1,11 @@
# thirdparty/trading212/serializers.py
from rest_framework import serializers
class Trading212AccountCashSerializer(serializers.Serializer):
blocked = serializers.FloatField()
free = serializers.FloatField()
invested = serializers.FloatField()
pieCash = serializers.FloatField()
ppl = serializers.FloatField()
result = serializers.FloatField()
total = serializers.FloatField()

View File

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

6
backend/thirdparty/trading212/urls.py vendored Normal file
View File

@@ -0,0 +1,6 @@
from django.urls import path
from .views import YourTrading212View # Replace with actual view class
urlpatterns = [
path('your-endpoint/', YourTrading212View.as_view(), name='trading212-endpoint'),
]

37
backend/thirdparty/trading212/views.py vendored Normal file
View File

@@ -0,0 +1,37 @@
# thirdparty/trading212/views.py
import os
import requests
from decouple import config
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
from .serializers import Trading212AccountCashSerializer
from drf_spectacular.utils import extend_schema
class Trading212AccountCashView(APIView):
permission_classes = [IsAuthenticated]
@extend_schema(
summary="Get Trading212 account cash",
responses=Trading212AccountCashSerializer
)
def get(self, request):
api_key = os.getenv("API_KEY_TRADING212")
headers = {
"Authorization": f"Bearer {api_key}",
"Accept": "application/json",
}
url = "https://api.trading212.com/api/v0/equity/account/cash"
try:
resp = requests.get(url, headers=headers, timeout=10)
resp.raise_for_status()
except requests.RequestException as exc:
return Response({"error": str(exc)}, status=400)
data = resp.json()
serializer = Trading212AccountCashSerializer(data=data)
serializer.is_valid(raise_exception=True)
return Response(serializer.data)