Major refactor of commerce and Stripe integration

Refactored commerce models to support refunds, invoices, and improved carrier/payment logic. Added new serializers and viewsets for products, categories, images, discount codes, and refunds. Introduced Stripe client integration and removed legacy Stripe admin/model code. Updated Dockerfile for PDF generation dependencies. Removed obsolete migration files and updated configuration app initialization. Added invoice template and tasks for order cleanup.
This commit is contained in:
2025-11-18 01:00:03 +01:00
parent 7a715efeda
commit b8a1a594b2
35 changed files with 1215 additions and 332 deletions

View File

@@ -1,23 +1,2 @@
from django.contrib import admin
from .models import Order
@admin.register(Order)
class OrderAdmin(admin.ModelAdmin):
list_display = ("id", "amount", "currency", "status", "created_at")
list_filter = ("status", "currency", "created_at")
search_fields = ("id", "stripe_session_id", "stripe_payment_intent")
readonly_fields = ("created_at", "stripe_session_id", "stripe_payment_intent")
fieldsets = (
(None, {
"fields": ("amount", "currency", "status")
}),
("Stripe info", {
"fields": ("stripe_session_id", "stripe_payment_intent"),
"classes": ("collapse",),
}),
("Metadata", {
"fields": ("created_at",),
}),
)
ordering = ("-created_at",)

54
backend/thirdparty/stripe/client.py vendored Normal file
View File

@@ -0,0 +1,54 @@
import stripe
from django.conf import settings
import json
import os
FRONTEND_URL = os.getenv("FRONTEND_URL") if not settings.DEBUG else os.getenv("DEBUG_DOMAIN")
SSL = "https://" if os.getenv("USE_SSL") == "true" else "http://"
stripe.api_key = os.getenv("STRIPE_SECRET_KEY")
class StripeClient:
def create_checkout_session(order):
"""
Vytvoří Stripe Checkout Session pro danou objednávku.
Args:
order (Order): Instance objednávky pro kterou se vytváří session.
Returns:
stripe.checkout.Session: Vytvořená Stripe Checkout Session.
"""
session = stripe.checkout.Session.create(
mode="payment",
payment_method_types=["card"],
success_url=f"{SSL}{FRONTEND_URL}/payment/success?order={order.id}", #jenom na grafickou část (webhook reálně ověří stav)
cancel_url=f"{SSL}{FRONTEND_URL}/payment/cancel?order={order.id}",
client_reference_id=str(order.id),
line_items=[{
"price_data": {
"currency": "czk",
"product_data": {
"name": f"Objednávka {order.id}",
},
"unit_amount": int(order.total_price * 100), # cena v haléřích
},
"quantity": 1,
}],
)
return session
def refund_order(stripe_payment_intent):
try:
refund = stripe.Refund.create(
payment_intent=stripe_payment_intent
)
return refund
except Exception as e:
return json.dumps({"error": str(e)})

View File

@@ -1,26 +0,0 @@
# Generated by Django 5.2.7 on 2025-10-28 22:28
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Order',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(decimal_places=2, max_digits=10)),
('currency', models.CharField(default='czk', max_length=10)),
('status', models.CharField(choices=[('pending', 'Pending'), ('paid', 'Paid'), ('failed', 'Failed'), ('cancelled', 'Cancelled')], default='pending', max_length=20)),
('stripe_session_id', models.CharField(blank=True, max_length=255, null=True)),
('stripe_payment_intent', models.CharField(blank=True, max_length=255, null=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
],
),
]

View File

@@ -1,25 +1,68 @@
from django.db import models
from django.apps import apps
# Create your models here.
#TODO: logika a interakce bude na stripu (třeba aktualizovaní objednávky na zaplacenou apod.)
class StripePayment(models.Model):
STATUS_CHOICES = [
("pending", "Pending"),
("paid", "Paid"),
("failed", "Failed"),
("cancelled", "Cancelled"),
]
amount = models.DecimalField(max_digits=10, decimal_places=2)
currency = models.CharField(max_length=10, default="czk")
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="pending")
from .client import StripeClient
class StripeModel(models.Model):
class STATUS_CHOICES(models.TextChoices):
PENDING = "pending", "Čeká se na platbu"
PAID = "paid", "Zaplaceno"
FAILED = "failed", "Neúspěšné"
CANCELLED = "cancelled", "Zrušeno"
REFUNDING = "refunding", "Platba se vrací"
REFUNDED = "refunded", "Platba úspěšně vrácena"
status = models.CharField(max_length=20, choices=STATUS_CHOICES.choices, default=STATUS_CHOICES.PENDING)
stripe_session_id = models.CharField(max_length=255, blank=True, null=True)
stripe_payment_intent = models.CharField(max_length=255, blank=True, null=True)
stripe_session_url = models.URLField(blank=True, null=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True, null=True, blank=True)
def __str__(self):
return f"Order {self.id} - {self.status}"
return f"Order {self.id} - {self.status}"
def save(self, *args, **kwargs):
#if new
if self.pk:
Order = apps.get_model('commerce', 'Order')
Payment = apps.get_model('commerce', 'Payment')
order = Order.objects.get(payment=Payment.objects.get(stripe=self))
session = StripeClient.create_checkout_session(order)# <-- předáme self.StripePayment
self.stripe_session_id = session.id
self.stripe_payment_intent = session.payment_intent
self.stripe_session_url = session.url
else:
self.updated_at = models.DateTimeField(auto_now=True)
super().save(*args, **kwargs)
def paid(self):
self.status = self.STATUS_CHOICES.PAID
self.save()
def refund(self):
StripeClient.refund_order(self.stripe_payment_intent)
self.status = self.STATUS_CHOICES.REFUNDING
self.save()
def refund_confirmed(self):
self.status = self.STATUS_CHOICES.REFUNDED
self.save()
def cancel(self):
StripeClient.cancel_checkout_session(self.stripe_session_id)
self.status = self.STATUS_CHOICES.CANCELLED
self.save()

9
backend/thirdparty/stripe/stripe.md vendored Normal file
View File

@@ -0,0 +1,9 @@
# Stripe Tutorial
## Example of redirecting the webhook events to local Django endpoint
```
stripe listen --forward-to localhost:8000/api/stripe/webhook/
```
# POUŽÍVEJTE SANDBOX/TESING REŽIM PŘI DEVELOPMENTU!!!

View File

@@ -6,73 +6,73 @@ from rest_framework import generics
from rest_framework.response import Response
from rest_framework.views import APIView
from drf_spectacular.utils import extend_schema
from .models import Order
from .serializers import OrderSerializer
import os
import logging
from .models import StripeTransaction
from commerce.models import Order, Payment
logger = logging.getLogger(__name__)
import stripe
stripe.api_key = os.getenv("STRIPE_SECRET_KEY")
class CreateCheckoutSessionView(APIView):
class StripeWebhook(APIView):
@extend_schema(
tags=["stripe"],
)
def post(self, request):
serializer = OrderSerializer(data=request.data) #obecný serializer
serializer.is_valid(raise_exception=True)
payload = request.body
sig_header = request.META['HTTP_STRIPE_SIGNATURE']
order = Order.objects.create(
amount=serializer.validated_data["amount"],
currency=serializer.validated_data.get("currency", "czk"),
)
try:
#build stripe event
event = stripe.Webhook.construct_event(
payload, sig_header, os.getenv("STRIPE_WEBHOOK_SECRET")
)
# Vytvoření Stripe Checkout Session
session = stripe.checkout.Session.create(
payment_method_types=["card"],
line_items=[{
"price_data": {
"currency": order.currency,
"product_data": {"name": f"Order {order.id}"},
"unit_amount": int(order.amount * 100), # v centech
},
"quantity": 1,
}],
mode="payment",
success_url=request.build_absolute_uri(f"/payment/success/{order.id}"),
cancel_url=request.build_absolute_uri(f"/payment/cancel/{order.id}"),
)
except ValueError as e:
logger.error(f"Invalid payload: {e}")
return HttpResponse(status=400)
except stripe.error as e:
# stripe error
logger.error(f"Stripe error: {e}")
return HttpResponse(status=400)
order.stripe_session_id = session.id
order.stripe_payment_intent = session.payment_intent
order.save()
data = OrderSerializer(order).data
data["checkout_url"] = session.url
return Response(data)
session = event['data']['object']
# ZAPLACENO
if event['type'] == 'checkout.session.completed':
stripe_transaction = StripeTransaction.objects.get(stripe_session_id=session.id)
if stripe_transaction:
stripe_transaction.paid()
@csrf_exempt
def stripe_webhook(request):
payload = request.body
sig_header = request.META.get("HTTP_STRIPE_SIGNATURE")
event = None
logger.info(f"Transaction {stripe_transaction.id} marked as paid.")
try:
event = stripe.Webhook.construct_event(
payload, sig_header, settings.STRIPE_WEBHOOK_SECRET
)
except stripe.error.SignatureVerificationError:
return HttpResponse(status=400)
else:
logger.warning(f"No transaction found for session ID: {session.id}")
if event["type"] == "checkout.session.completed":
session = event["data"]["object"]
order = Order.objects.filter(stripe_session_id=session.get("id")).first()
if order:
order.status = "paid"
# EXPIRACE (zrušení objednávky) uživatel nezaplatil do 24 hodin!
elif event['type'] == 'checkout.session.expired':
order = Order.objects.get(payment=Payment.objects.get(stripe=StripeTransaction.objects.get(stripe_session_id=session.id)))
order.status = Order.STATUS_CHOICES.CANCELLED
order.save()
return HttpResponse(status=200)
elif event['type'] == 'payment_intent.payment_failed':
#nothing to do for now
pass
# REFUND POTVRZEN
elif event['type'] == 'payment_intent.refunded':
session = event['data']['object']
stripe_transaction = StripeTransaction.objects.get(stripe_payment_intent=session.id)
if stripe_transaction:
stripe_transaction.refund_confirmed()
logger.info(f"Transaction {stripe_transaction.id} marked as refunded.")

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB