From b8a1a594b29965f9a8ab12cb9c33fb5e240a2e5b Mon Sep 17 00:00:00 2001 From: Brunobrno Date: Tue, 18 Nov 2025 01:00:03 +0100 Subject: [PATCH] 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. --- backend/Dockerfile | 8 +- backend/account/migrations/0001_initial.py | 54 --- ...ser_email_verification_sent_at_and_more.py | 23 -- backend/account/views.py | 2 +- backend/commerce/migrations/0001_initial.py | 41 -- backend/commerce/models.py | 204 ++++++++-- backend/commerce/serializers.py | 247 ++++++++++- backend/commerce/tasks.py | 10 + backend/commerce/templates/invoice/Order.html | 41 ++ backend/commerce/urls.py | 19 +- backend/commerce/views.py | 384 +++++++++++++++++- backend/configuration/apps.py | 7 +- backend/social/chat/migrations/__init__.py | 0 .../downloader/migrations/0001_initial.py | 30 -- .../downloader/migrations/__init__.py | 0 backend/thirdparty/stripe/admin.py | 21 - backend/thirdparty/stripe/client.py | 54 +++ .../stripe/migrations/0001_initial.py | 26 -- .../thirdparty/stripe/migrations/__init__.py | 0 backend/thirdparty/stripe/models.py | 67 ++- backend/thirdparty/stripe/stripe.md | 9 + .../stripe/tasks.py} | 0 backend/thirdparty/stripe/views.py | 100 ++--- .../where to find webhooks settings.png | Bin 0 -> 111040 bytes .../zasilkovna/migrations/__init__.py | 0 backend/thirdparty/zasilkovna/models.py | 38 +- backend/thirdparty/zasilkovna/serializers.py | 38 ++ backend/thirdparty/zasilkovna/urls.py | 15 + backend/thirdparty/zasilkovna/views.py | 86 +++- backend/vontor_cz/asgi.py | 2 +- backend/vontor_cz/celery.py | 4 +- backend/vontor_cz/settings.py | 14 +- backend/vontor_cz/urls.py | 1 + .../backup-20251117-224608.sql | 0 docker-compose.yml | 2 +- 35 files changed, 1215 insertions(+), 332 deletions(-) delete mode 100644 backend/account/migrations/0001_initial.py delete mode 100644 backend/account/migrations/0002_customuser_email_verification_sent_at_and_more.py delete mode 100644 backend/commerce/migrations/0001_initial.py create mode 100644 backend/commerce/templates/invoice/Order.html delete mode 100644 backend/social/chat/migrations/__init__.py delete mode 100644 backend/thirdparty/downloader/migrations/0001_initial.py delete mode 100644 backend/thirdparty/downloader/migrations/__init__.py create mode 100644 backend/thirdparty/stripe/client.py delete mode 100644 backend/thirdparty/stripe/migrations/0001_initial.py delete mode 100644 backend/thirdparty/stripe/migrations/__init__.py create mode 100644 backend/thirdparty/stripe/stripe.md rename backend/{account/migrations/__init__.py => thirdparty/stripe/tasks.py} (100%) create mode 100644 backend/thirdparty/stripe/where to find webhooks settings.png delete mode 100644 backend/thirdparty/zasilkovna/migrations/__init__.py rename backend/commerce/migrations/__init__.py => backups/backup-20251117-224608.sql (100%) diff --git a/backend/Dockerfile b/backend/Dockerfile index f69e6ce..19e8268 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -2,7 +2,13 @@ FROM python:3.12-slim WORKDIR /app -RUN apt update && apt install ffmpeg -y +RUN apt update && apt install -y \ + weasyprint \ + libcairo2 \ + pango1.0-tools \ + libpango-1.0-0 \ + libgobject-2.0-0 \ + ffmpeg COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt diff --git a/backend/account/migrations/0001_initial.py b/backend/account/migrations/0001_initial.py deleted file mode 100644 index 41ba5ce..0000000 --- a/backend/account/migrations/0001_initial.py +++ /dev/null @@ -1,54 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-28 22:28 - -import account.models -import django.contrib.auth.validators -import django.core.validators -import django.utils.timezone -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), - ] - - operations = [ - migrations.CreateModel( - name='CustomUser', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('is_deleted', models.BooleanField(default=False)), - ('deleted_at', models.DateTimeField(blank=True, null=True)), - ('role', models.CharField(choices=[('admin', 'Admin'), ('mod', 'Moderator'), ('regular', 'Regular')], default='regular', max_length=20)), - ('phone_number', models.CharField(blank=True, max_length=16, null=True, unique=True, validators=[django.core.validators.RegexValidator('^\\+?\\d{9,15}$', message='Zadejte platné telefonní číslo.')])), - ('email_verified', models.BooleanField(default=False)), - ('email', models.EmailField(db_index=True, max_length=254, unique=True)), - ('gdpr', models.BooleanField(default=False)), - ('is_active', models.BooleanField(default=False)), - ('create_time', models.DateTimeField(auto_now_add=True)), - ('city', models.CharField(blank=True, max_length=100, null=True)), - ('street', models.CharField(blank=True, max_length=200, null=True)), - ('postal_code', models.CharField(blank=True, max_length=5, null=True, validators=[django.core.validators.RegexValidator(code='invalid_postal_code', message='Postal code must contain exactly 5 digits.', regex='^\\d{5}$')])), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to.', related_name='customuser_set', related_query_name='customuser', to='auth.group')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='customuser_set', related_query_name='customuser', to='auth.permission')), - ], - options={ - 'abstract': False, - }, - managers=[ - ('objects', account.models.CustomUserManager()), - ('active', account.models.ActiveUserManager()), - ], - ), - ] diff --git a/backend/account/migrations/0002_customuser_email_verification_sent_at_and_more.py b/backend/account/migrations/0002_customuser_email_verification_sent_at_and_more.py deleted file mode 100644 index 56b21b1..0000000 --- a/backend/account/migrations/0002_customuser_email_verification_sent_at_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-31 07:36 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('account', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='customuser', - name='email_verification_sent_at', - field=models.DateTimeField(blank=True, null=True), - ), - migrations.AddField( - model_name='customuser', - name='email_verification_token', - field=models.CharField(blank=True, db_index=True, max_length=128, null=True), - ), - ] diff --git a/backend/account/views.py b/backend/account/views.py index 4660148..74bc90e 100644 --- a/backend/account/views.py +++ b/backend/account/views.py @@ -6,7 +6,7 @@ from .serializers import * from .permissions import * from .models import CustomUser from .tokens import * -from .tasks import send_password_reset_email_task, send_email_verification_task, send_email_clerk_accepted_task +from .tasks import send_password_reset_email_task, send_email_verification_task, send_email_clerk_accepted_task # FIXME: send_email_clerk_accepted_task neexistuje !!! from django.conf import settings import logging logger = logging.getLogger(__name__) diff --git a/backend/commerce/migrations/0001_initial.py b/backend/commerce/migrations/0001_initial.py deleted file mode 100644 index 97b32df..0000000 --- a/backend/commerce/migrations/0001_initial.py +++ /dev/null @@ -1,41 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-28 22:28 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Carrier', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=100)), - ('base_price', models.DecimalField(decimal_places=2, default=0, max_digits=10)), - ('delivery_time', models.CharField(blank=True, max_length=100)), - ('is_active', models.BooleanField(default=True)), - ('logo', models.ImageField(blank=True, null=True, upload_to='carriers/')), - ('external_id', models.CharField(blank=True, max_length=50, null=True)), - ], - ), - migrations.CreateModel( - name='Product', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(max_length=200)), - ('description', models.TextField(blank=True)), - ('price', models.DecimalField(decimal_places=2, max_digits=10)), - ('currency', models.CharField(default='czk', max_length=10)), - ('stock', models.PositiveIntegerField(default=0)), - ('is_active', models.BooleanField(default=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('default_carrier', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_for_products', to='commerce.carrier')), - ], - ), - ] diff --git a/backend/commerce/models.py b/backend/commerce/models.py index 0c68cef..23e2b59 100644 --- a/backend/commerce/models.py +++ b/backend/commerce/models.py @@ -1,12 +1,18 @@ from django.db import models from django.conf import settings from django.utils import timezone -from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ValidationError from decimal import Decimal +from django.template.loader import render_to_string +from django.core.files.base import ContentFile +from django.core.validators import MaxValueValidator, MinValueValidator + +from weasyprint import HTML +import os from configuration.models import ShopConfiguration from thirdparty.zasilkovna.models import ZasilkovnaPacket -from thirdparty.stripe.models import StripePayment +from thirdparty.stripe.models import StripeModel #FIXME: přidat soft delete pro všchny modely !!!! @@ -40,6 +46,17 @@ class Product(models.Model): code = models.CharField(max_length=100, unique=True, blank=True, null=True) + variants = models.ManyToManyField( + "self", + symmetrical=True, + blank=True, + related_name="variant_of", + help_text=( + "Symetrické varianty produktu: pokud přidáte variantu A → B, " + "Django automaticky přidá i variantu B → A. " + "Všechny varianty jsou rovnocenné a zobrazí se vzájemně." + ), + ) category = models.ForeignKey(Category, related_name='products', on_delete=models.PROTECT) @@ -50,7 +67,7 @@ class Product(models.Model): stock = models.PositiveIntegerField(default=0) is_active = models.BooleanField(default=True) - #limitka (volitelné) + #časový limit (volitelné) limited_to = models.DateTimeField(null=True, blank=True) default_carrier = models.ForeignKey( @@ -85,21 +102,22 @@ class ProductImage(models.Model): class Order(models.Model): class Status(models.TextChoices): - PENDING = "pending", _("Čeká na platbu") - PAID = "paid", _("Zaplaceno") - CANCELLED = "cancelled", _("Zrušeno") - SHIPPED = "shipped", _("Odesláno") - #COMPLETED = "completed", _("Dokončeno") + CREATED = "created", "Vytvořeno" + CANCELLED = "cancelled", "Zrušeno" + COMPLETED = "completed", "Dokončeno" + + REFUNDING = "refunding", "Vrácení v procesu" + REFUNDED = "refunded", "Vráceno" status = models.CharField( - max_length=20, choices=Status.choices, default=Status.PENDING + max_length=20, choices=Status.choices, null=True, blank=True, default=Status.CREATED ) # Stored order grand total; recalculated on save total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0) currency = models.CharField(max_length=10, default="CZK") - # fakturační údaje (zkopírované z user profilu při objednávce) FIXME: rozhodnout se co dát do on_delete + # fakturační údaje (zkopírované z user profilu při objednávce) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, related_name="orders", null=True, blank=True ) @@ -134,6 +152,8 @@ class Order(models.Model): blank=True ) + invoice = models.OneToOneField("Invoice", on_delete=models.CASCADE, related_name="order", null=True, blank=True) + discount = models.ManyToManyField("DiscountCode", blank=True, related_name="orders") def calculate_total_price(self): @@ -181,15 +201,24 @@ class Order(models.Model): # ------------------ DOPRAVCI A ZPŮSOBY DOPRAVY ------------------ + class Carrier(models.Model): class SHIPPING(models.TextChoices): ZASILKOVNA = "packeta", "Zásilkovna" STORE = "store", "Osobní odběr" shipping_method = models.CharField(max_length=20, choices=SHIPPING.choices, default=SHIPPING.STORE) + class STATE(models.TextChoices): + PREPARING = "ordered", "Objednávka se připravuje" + SHIPPED = "shipped", "Odesláno" + DELIVERED = "delivered", "Doručeno" + READY_TO_PICKUP = "ready_to_pickup", "Připraveno k vyzvednutí" + #RETURNING = "returning", "Vracení objednávky" + state = models.CharField(max_length=20, choices=STATE.choices, default=STATE.PREPARING) + # prodejce to přidá později - zasilkovna = models.ForeignKey( - ZasilkovnaPacket, on_delete=models.DO_NOTHING, null=True, blank=True, related_name="carriers" + zasilkovna = models.ManyToManyField( + ZasilkovnaPacket, blank=True, related_name="carriers" ) weight = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, help_text="Hmotnost zásilky v kg") @@ -197,7 +226,6 @@ class Carrier(models.Model): returning = models.BooleanField(default=False, help_text="Zda je tato zásilka na vrácení") def save(self, *args, **kwargs): - super().save(*args, **kwargs) def get_price(self): @@ -210,18 +238,30 @@ class Carrier(models.Model): #tohle bude vyvoláno pomocí admina přes api!!! def start_ordering_shipping(self): if self.shipping_method == self.SHIPPING.ZASILKOVNA: - #už při vytvoření se volá na api Zásilkovny - self.zasilkovna = ZasilkovnaPacket.objects.create() + # Uživatel může objednat více zásilek pokud potřeba + self.zasilkovna.add(ZasilkovnaPacket.objects.create()) + self.returning = False + self.save() + + else: + raise ValidationError("Tato metoda dopravy nepodporuje objednání přepravy.") #... další logika pro jiné způsoby dopravy #TODO: přidat notifikace uživateli, jak pro zásilkovnu, tak pro vyzvednutí v obchodě! - def returning_shipping(self): - self.returning = True + def ready_to_pickup(self): + if self.shipping_method == self.SHIPPING.STORE: + self.state = self.STATE.READY_TO_PICKUP + self.save() + else: + raise ValidationError("Tato metoda dopravy nepodporuje připravení k vyzvednutí.") - if self.shipping_method == self.SHIPPING.ZASILKOVNA: - #volá se na api Zásilkovny - self.zasilkovna.returning_packet() + # def returning_shipping(self, int:id): + # self.returning = True + + # if self.shipping_method == self.SHIPPING.ZASILKOVNA: + # #volá se na api Zásilkovny + # self.zasilkovna.get(id=id).returning_packet() # ------------------ PLATEBNÍ MODELY ------------------ @@ -233,20 +273,33 @@ class Payment(models.Model): CASH_ON_DELIVERY = "cash_on_delivery", "Dobírka" payment_method = models.CharField(max_length=30, choices=PAYMENT.choices, default=PAYMENT.SHOP) - #active = models.BooleanField(default=True) - #FIXME: potvrdit že logika platby funguje správně #veškera logika a interakce bude na stripu (třeba aktualizovaní objednávky na zaplacenou apod.) stripe = models.OneToOneField( - StripePayment, on_delete=models.CASCADE, null=True, blank=True, related_name="payment" + StripeModel, on_delete=models.CASCADE, null=True, blank=True, related_name="payment" ) def save(self, *args, **kwargs): - order = Order.objects.get(payment=self) - - if self.payment_method == self.PAYMENT.SHOP and Carrier.objects.get(orders=order).shipping_method != Carrier.SHIPPING.STORE: - raise ValueError("Platba v obchodě je možná pouze pro osobní odběr.") + if self.order: + order = Order.objects.get(payment=self) + + #validace platebních metod + if self.payment_method == self.PAYMENT.SHOP and Carrier.objects.get(orders=order).shipping_method != Carrier.SHIPPING.STORE: + raise ValueError("Platba v obchodě je možná pouze pro osobní odběr.") + + #validace dobírky (jestli není použitá pro osobní odběr) + elif self.payment_method == self.PAYMENT.CASH_ON_DELIVERY and Carrier.objects.get(orders=order).shipping_method == Carrier.SHIPPING.STORE: + raise ValueError("Dobírka není možná pro osobní odběr.") + + #vytvoření platebních metod pokud nový objekt + if not self.pk: + if self.payment_method == self.PAYMENT.STRIPE: + self.stripe = StripePayment.objects.create(amount=order.total_price) + else: + self.stripe = None + + super().save(*args, **kwargs) @@ -257,7 +310,13 @@ class DiscountCode(models.Model): description = models.CharField(max_length=255, blank=True) # sleva v procentech (0–100) - percent = models.PositiveIntegerField(max_value=100, help_text="Procento sleva 0-100", null=True, blank=True) + percent = models.PositiveIntegerField( + validators=[MinValueValidator(0), MaxValueValidator(100)], + help_text="Procento sleva 0-100", + null=True, + blank=True + ) + # nebo fixní částka amount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, help_text="Fixní sleva v CZK") @@ -288,6 +347,8 @@ class DiscountCode(models.Model): if self.usage_limit and self.used_count >= self.usage_limit: return False + return True + def __str__(self): return f"{self.code} ({self.percent}% or {self.amount} CZK)" @@ -369,8 +430,10 @@ class OrderItem(models.Model): if applicable_amount_discounts: if config.addition_of_coupons_amount: total_amount = sum(applicable_amount_discounts) + else: total_amount = max(applicable_amount_discounts) + final_price = final_price - total_amount if final_price < Decimal('0'): @@ -379,4 +442,87 @@ class OrderItem(models.Model): return final_price.quantize(Decimal('0.01')) def __str__(self): - return f"{self.product.name} x{self.quantity}" \ No newline at end of file + return f"{self.product.name} x{self.quantity}" + + def save(self, *args, **kwargs): + if self.pk is None: + if self.order.payment.payment_method: + raise ValueError("Nelze upravit položky z objednávky s již zvolenou platební metodou.") + + else: + #nová položka objednávky, snížit skladové zásoby + if self.product.stock < self.quantity: + raise ValueError("Nedostatečný skladový zásob pro produkt.") + + self.product.stock -= self.quantity + self.product.save(update_fields=["stock"]) + + super().save(*args, **kwargs) + + +class Refund(models.Model): + order = models.ForeignKey(Order, related_name="refunds", on_delete=models.CASCADE) + + class Reason(models.TextChoices): + RETUNING_PERIOD = "retuning_before_fourteen_day_period", "Vrácení před uplynutím 14-ti denní lhůty" + DAMAGED_PRODUCT = "damaged_product", "Poškozený produkt" + WRONG_ITEM = "wrong_item", "Špatná položka" + OTHER = "other", "Jiný důvod" + reason_choice = models.CharField(max_length=30, choices=Reason.choices) + reason_text = models.TextField(blank=True) + + verified = models.BooleanField(default=False) + + created_at = models.DateTimeField(auto_now_add=True) + + #VRACENÍ ZÁSILKY, LOGIKA (DISABLED FOR NOW) + # def save(self, *args, **kwargs): + # # Automaticky aktualizovat stav objednávky na "vráceno" + # if self.pk is None: + # self.order.status = Order.Status.REFUNDING + # self.order.save(update_fields=["status", "updated_at"]) + + # shipping_method = self.order.carrier.shipping_method + + # if shipping_method == Carrier.SHIPPING.ZASILKOVNA: + + # carrier = self.order.carrier; + + # # poslední odeslána/vytvořená zásilka + # # Iniciovat vrácení přes Zásilkovnu + # carrier.zasilkovna.latest('created_at').returning_packet() + # carrier.save() + + # else: + # # Logika pro jiné způsoby dopravy + # pass + + # super().save(*args, **kwargs) + + def refund_completed(self): + # Aktualizovat stav objednávky na "vráceno" + self.order.payment.stripe.refund() + self.order.status = Order.Status.REFUNDED + self.order.save(update_fields=["status", "updated_at"]) + + +class Invoice(models.Model): + invoice_number = models.CharField(max_length=50, unique=True) + + issued_at = models.DateTimeField(auto_now_add=True) + due_date = models.DateTimeField() + + pdf_file = models.FileField(upload_to='invoices/') + + def __str__(self): + return f"Invoice {self.invoice_number} for Order {self.order.id}" + + def generate_invoice_pdf(self): + order = Order.objects.get(invoice=self) + # Render HTML + html_string = render_to_string("invoice/invoice.html", {"invoice": self}) + pdf_bytes = HTML(string=html_string).write_pdf() + + # Save directly to FileField + self.pdf_file.save(f"{self.invoice_number}.pdf", ContentFile(pdf_bytes)) + self.save() \ No newline at end of file diff --git a/backend/commerce/serializers.py b/backend/commerce/serializers.py index ad5ba6b..1a48f9c 100644 --- a/backend/commerce/serializers.py +++ b/backend/commerce/serializers.py @@ -1,4 +1,249 @@ from rest_framework import serializers from drf_spectacular.utils import extend_schema_field -from .models import Category, Product, ProductImage, DiscountCode, Order, OrderItem, Carrier +from decimal import Decimal +from django.contrib.auth import get_user_model + +from .models import ( + Category, + Product, + ProductImage, + DiscountCode, + Refund, + Order, + OrderItem, + Carrier, + Payment, +) + +User = get_user_model() + + +class UserBriefSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ["id", "first_name", "last_name", "email"] + + +class ProductBriefSerializer(serializers.ModelSerializer): + class Meta: + model = Product + fields = ["id", "name", "price"] + + +class OrderItemReadSerializer(serializers.ModelSerializer): + product = ProductBriefSerializer(read_only=True) + total_price = serializers.SerializerMethodField() + + def get_total_price(self, obj): + return obj.get_total_price(list(obj.order.discount.all()) if getattr(obj, "order", None) else None) + + class Meta: + model = OrderItem + fields = ["id", "product", "quantity", "total_price"] + read_only_fields = ["id", "total_price"] + + +class CarrierReadSerializer(serializers.ModelSerializer): + class Meta: + model = Carrier + fields = ["id", "shipping_method", "weight", "returning"] + read_only_fields = ["id"] + + +class PaymentReadSerializer(serializers.Serializer): + payment_method = serializers.CharField(read_only=True) + stripe_id = serializers.IntegerField(source="stripe.id", read_only=True, allow_null=True) + + +class OrderReadSerializer(serializers.ModelSerializer): + items = OrderItemReadSerializer(many=True, read_only=True) + carrier = CarrierReadSerializer(read_only=True) + payment = PaymentReadSerializer(source="payment", read_only=True) + user = serializers.SerializerMethodField() + + def get_user(self, obj): + request = self.context.get("request") if hasattr(self, "context") else None + if request and getattr(request, "user", None) and request.user.is_authenticated and obj.user: + return UserBriefSerializer(obj.user).data + return None + + class Meta: + model = Order + fields = [ + "id", + "status", + "total_price", + "currency", + "first_name", + "last_name", + "email", + "phone", + "address", + "city", + "postal_code", + "country", + "note", + "created_at", + "updated_at", + "carrier", + "payment", + "user", + "items", + ] + read_only_fields = [ + "id", + "status", + "total_price", + "currency", + "created_at", + "updated_at", + ] + + +class OrderMiniSerializer(serializers.ModelSerializer): + amount = serializers.DecimalField(max_digits=10, decimal_places=2, source="total_price", read_only=True) + shipping_method = serializers.CharField(source="carrier.shipping_method", read_only=True) + + class Meta: + model = Order + fields = ["id", "amount", "status", "email", "shipping_method"] + read_only_fields = fields + + +# ----------------- CREATE PAYLOAD SERIALIZERS (PUBLIC) ----------------- + +class OrderItemCreateSerializer(serializers.Serializer): + product_id = serializers.IntegerField(label="Product ID") + quantity = serializers.IntegerField(min_value=1, label="Quantity") + + +class CarrierCreateSerializer(serializers.Serializer): + shipping_method = serializers.ChoiceField( + choices=Carrier.SHIPPING.choices, + label="Shipping Method", + help_text="Choose 'store' (pickup) or 'packeta'", + ) + weight = serializers.DecimalField( + required=False, + max_digits=10, + decimal_places=2, + label="Weight (kg)", + ) + + +class PaymentCreateSerializer(serializers.Serializer): + payment_method = serializers.ChoiceField( + choices=Payment.PAYMENT.choices, + label="Payment Method", + help_text="Choose 'shop', 'stripe' or 'cash_on_delivery'", + ) + + +class OrderCreateSerializer(serializers.Serializer): + # Customer/billing + first_name = serializers.CharField(max_length=100, label="First Name") + last_name = serializers.CharField(max_length=100, label="Last Name") + email = serializers.EmailField(label="Email") + phone = serializers.CharField(max_length=20, required=False, allow_blank=True, label="Phone") + address = serializers.CharField(max_length=255, label="Address") + city = serializers.CharField(max_length=100, label="City") + postal_code = serializers.CharField(max_length=20, label="Postal Code") + country = serializers.CharField(max_length=100, required=False, default="Czech Republic", label="Country") + note = serializers.CharField(required=False, allow_blank=True, label="Note") + + # Nested + items = OrderItemCreateSerializer(many=True) + carrier = CarrierCreateSerializer() + payment = PaymentCreateSerializer() + discount_codes = serializers.ListField( + child=serializers.CharField(), required=False, allow_empty=True, label="Discount Codes" + ) + + def validate(self, attrs): + if not attrs.get("items"): + raise serializers.ValidationError({"items": "At least one item is required."}) + return attrs + + +# ----------------- ADMIN/READ MODELS ----------------- + +class CategorySerializer(serializers.ModelSerializer): + class Meta: + model = Category + fields = [ + "id", + "name", + "url", + "parent", + "description", + "image", + ] + + +class ProductImageSerializer(serializers.ModelSerializer): + class Meta: + model = ProductImage + fields = [ + "id", + "product", + "image", + "alt_text", + "is_main", + ] + + +class ProductSerializer(serializers.ModelSerializer): + class Meta: + model = Product + fields = [ + "id", + "name", + "description", + "code", + "category", + "price", + "url", + "stock", + "is_active", + "limited_to", + "default_carrier", + "created_at", + "updated_at", + ] + read_only_fields = ["created_at", "updated_at"] + + +class DiscountCodeSerializer(serializers.ModelSerializer): + class Meta: + model = DiscountCode + fields = [ + "id", + "code", + "description", + "percent", + "amount", + "valid_from", + "valid_to", + "active", + "usage_limit", + "used_count", + "specific_products", + "specific_categories", + ] + read_only_fields = ["used_count"] + + +class RefundSerializer(serializers.ModelSerializer): + class Meta: + model = Refund + fields = [ + "id", + "order", + "reason_choice", + "reason_text", + "verified", + "created_at", + ] + read_only_fields = ["id", "verified", "created_at"] + diff --git a/backend/commerce/tasks.py b/backend/commerce/tasks.py index e69de29..60a9829 100644 --- a/backend/commerce/tasks.py +++ b/backend/commerce/tasks.py @@ -0,0 +1,10 @@ +from .models import Order +from django.utils import timezone + +def delete_expired_orders(): + expired_orders = Order.objects.filter(status=Order.STATUS_CHOICES.CANCELLED, created_at__lt=timezone.now() - timezone.timedelta(hours=24)) + count = expired_orders.count() + expired_orders.delete() + return count + + diff --git a/backend/commerce/templates/invoice/Order.html b/backend/commerce/templates/invoice/Order.html new file mode 100644 index 0000000..fc0ee19 --- /dev/null +++ b/backend/commerce/templates/invoice/Order.html @@ -0,0 +1,41 @@ + + + + + Faktura {{ invoice.invoice_number }} + + + +

Faktura {{ invoice.invoice_number }}

+

Datum vystavení: {{ invoice.issue_date.strftime("%Y-%m-%d") }}

+

Zákazník: {{ invoice.order.customer_name }}

+ + + + + + + + + + + + {% for item in invoice.order.items.all %} + + + + + + + {% endfor %} + +
ProduktMnožstvíCenaCelkem
{{ item.product.name }}{{ item.quantity }}{{ item.price }}{{ item.price * item.quantity }}
+ +

Celkem k úhradě: {{ invoice.total_amount }} Kč

+ + diff --git a/backend/commerce/urls.py b/backend/commerce/urls.py index 5518a66..ecf49dd 100644 --- a/backend/commerce/urls.py +++ b/backend/commerce/urls.py @@ -1,15 +1,24 @@ from django.urls import path, include from rest_framework.routers import DefaultRouter -from .views import CategoryViewSet, ProductViewSet, DiscountCodeViewSet, OrderViewSet +from .views import ( + OrderViewSet, + ProductViewSet, + CategoryViewSet, + ProductImageViewSet, + DiscountCodeViewSet, + RefundViewSet, +) router = DefaultRouter() -router.register(r'categories', CategoryViewSet) -router.register(r'products', ProductViewSet) -router.register(r'discounts', DiscountCodeViewSet) router.register(r'orders', OrderViewSet) +router.register(r'products', ProductViewSet, basename='product') +router.register(r'categories', CategoryViewSet, basename='category') +router.register(r'product-images', ProductImageViewSet, basename='product-image') +router.register(r'discount-codes', DiscountCodeViewSet, basename='discount-code') +router.register(r'refunds', RefundViewSet, basename='refund') urlpatterns = [ path('', include(router.urls)), ] -# NOTE: Carrier endpoints intentionally omitted (TODO) +# NOTE: Other endpoints (categories/products/discounts) can be added later diff --git a/backend/commerce/views.py b/backend/commerce/views.py index e582197..13c2a8a 100644 --- a/backend/commerce/views.py +++ b/backend/commerce/views.py @@ -1,4 +1,382 @@ -from rest_framework import viewsets -from rest_framework.permissions import AllowAny -from drf_spectacular.utils import extend_schema, extend_schema_view +from django.db import transaction +from rest_framework import viewsets, mixins, status +from rest_framework.permissions import AllowAny, IsAdminUser, SAFE_METHODS +from rest_framework.decorators import action +from rest_framework.response import Response +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiExample + +from .models import ( + Order, + OrderItem, + Carrier, + Payment, + Product, + DiscountCode, + Category, + ProductImage, + Refund, +) +from .serializers import ( + OrderReadSerializer, + OrderMiniSerializer, + OrderCreateSerializer, + OrderItemReadSerializer, + CarrierReadSerializer, + PaymentReadSerializer, + ProductSerializer, + CategorySerializer, + ProductImageSerializer, + DiscountCodeSerializer, + RefundSerializer, +) + + +@extend_schema_view( + list=extend_schema(tags=["Orders"], summary="List Orders (public)"), + retrieve=extend_schema(tags=["Orders"], summary="Retrieve Order (public)"), +) +class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet): + queryset = Order.objects.select_related("carrier", "payment").prefetch_related( + "items__product", "discount" + ).order_by("-created_at") + permission_classes = [AllowAny] + + def get_serializer_class(self): + if self.action == "mini": + return OrderMiniSerializer + if self.action in ["list", "retrieve"]: + return OrderReadSerializer + if self.action == "create": + return OrderCreateSerializer + return OrderReadSerializer + + @extend_schema( + tags=["Orders"], + summary="Create Order (public)", + request=OrderCreateSerializer, + responses={201: OrderReadSerializer}, + examples=[ + OpenApiExample( + "Create order", + value={ + "first_name": "Jan", + "last_name": "Novak", + "email": "jan@example.com", + "phone": "+420123456789", + "address": "Ulice 1", + "city": "Praha", + "postal_code": "11000", + "country": "Czech Republic", + "note": "Prosím doručit odpoledne", + "items": [ + {"product_id": 1, "quantity": 2}, + {"product_id": 7, "quantity": 1}, + ], + "carrier": {"shipping_method": "store"}, + "payment": {"payment_method": "stripe"}, + "discount_codes": ["WELCOME10"], + }, + ) + ], + ) + def create(self, request, *args, **kwargs): + serializer = OrderCreateSerializer(data=request.data) + serializer.is_valid(raise_exception=True) + data = serializer.validated_data + user = request.user + + #VELMI DŮLEŽITELÉ: vše vytvořit v transakci, aby se nepřidávaly neúplné objednávky + with transaction.atomic(): + + # Create base order (customer details only for now) + order = Order.objects.create( + user=user if getattr(user, "is_authenticated", False) else None, + first_name=data["first_name"], + last_name=data["last_name"], + email=data["email"], + phone=data.get("phone", ""), + address=data["address"], + city=data["city"], + postal_code=data["postal_code"], + country=data.get("country", "Czech Republic"), + note=data.get("note", ""), + ) + + # Carrier + carrier_payload = data["carrier"] + carrier = Carrier.objects.create( + shipping_method=carrier_payload["shipping_method"], + weight=carrier_payload.get("weight"), + ) + order.carrier = carrier + order.save(update_fields=["carrier", "updated_at"]) # recalc later after items + + # Items + items_payload = data["items"] + order_items = [] + for item in items_payload: + product = Product.objects.get(pk=item["product_id"]) # raises 404 if missing + qty = int(item["quantity"]) + order_items.append(OrderItem(order=order, product=product, quantity=qty)) + OrderItem.objects.bulk_create(order_items) + + # Discount codes (optional) + codes = data.get("discount_codes") or [] + if codes: + discounts = list(DiscountCode.objects.filter(code__in=codes)) + order.discount.add(*discounts) + + # Recalculate now that items/discounts/carrier are linked + order.save() + + # Payment and validation + pay_payload = data["payment"] + payment_method = pay_payload["payment_method"] + + # Validate combos (mirror of Payment.save but here we have order) + if payment_method == Payment.PAYMENT.SHOP and order.carrier.shipping_method != Carrier.SHIPPING.STORE: + return Response( + {"payment": "Platba v obchodě je možná pouze pro osobní odběr."}, + status=status.HTTP_400_BAD_REQUEST, + ) + if payment_method == Payment.PAYMENT.CASH_ON_DELIVERY and order.carrier.shipping_method == Carrier.SHIPPING.STORE: + return Response( + {"payment": "Dobírka není možná pro osobní odběr."}, + status=status.HTTP_400_BAD_REQUEST, + ) + + # Create payment WITHOUT triggering Payment.save (which expects reverse link first) + payment = Payment(payment_method=payment_method) + # Bypass custom save by bulk_create + Payment.objects.bulk_create([payment]) + order.payment = payment + order.save(update_fields=["payment", "updated_at"]) + + # If Stripe, create StripePayment now and attach + if payment_method == Payment.PAYMENT.STRIPE: + from thirdparty.stripe.models import StripePayment + + stripe_obj = StripePayment.objects.create(amount=order.total_price) + payment.stripe = stripe_obj + payment.save(update_fields=["stripe"]) + + out = self.get_serializer(order) + return Response(out.data, status=status.HTTP_201_CREATED) + + # -- List mini orders -- (public) -- + @action(detail=False, methods=["get"], url_path="detail") + @extend_schema( + tags=["Orders"], + summary="List mini orders (public)", + responses={200: OrderMiniSerializer(many=True)}, + ) + def mini(self, request, *args, **kwargs): + qs = self.get_queryset() + page = self.paginate_queryset(qs) + + if page is not None: + ser = OrderMiniSerializer(page, many=True) + return self.get_paginated_response(ser.data) + + ser = OrderMiniSerializer(qs, many=True) + return Response(ser.data) + + # -- Get order items -- (public) -- + @action(detail=True, methods=["get"], url_path="items") + @extend_schema( + tags=["Orders"], + summary="List order items (public)", + responses={200: OrderItemReadSerializer(many=True)}, + ) + def items(self, request, pk=None): + order = self.get_object() + qs = order.items.select_related("product").all() + ser = OrderItemReadSerializer(qs, many=True) + return Response(ser.data) + + # -- Get order carrier -- (public) -- + @action(detail=True, methods=["get"], url_path="carrier") + @extend_schema( + tags=["Orders"], + summary="Get order carrier (public)", + responses={200: CarrierReadSerializer}, + ) + def carrier_detail(self, request, pk=None): + order = self.get_object() + ser = CarrierReadSerializer(order.carrier) + return Response(ser.data) + + # -- Get order payment -- (public) -- + @action(detail=True, methods=["get"], url_path="payment") + @extend_schema( + tags=["Orders"], + summary="Get order payment (public)", + responses={200: PaymentReadSerializer}, + ) + def payment_detail(self, request, pk=None): + order = self.get_object() + ser = PaymentReadSerializer(order.payment) + return Response(ser.data) + + # -- Mark carrier ready to pickup(store) (admin) -- + @action( + detail=True, + methods=["patch"], + url_path="carrier/ready-to-pickup", + permission_classes=[IsAdminUser], + ) + @extend_schema( + tags=["Orders"], + summary="Mark carrier ready to pickup (admin)", + request=None, + responses={200: CarrierReadSerializer}, + ) + def carrier_ready_to_pickup(self, request, pk=None): + order = self.get_object() + if not order.carrier: + return Response({"detail": "Carrier not set."}, status=400) + order.carrier.ready_to_pickup() + order.carrier.refresh_from_db() + ser = CarrierReadSerializer(order.carrier) + return Response(ser.data) + + # -- Start ordering shipping (admin) -- + @action( + detail=True, + methods=["patch"], + url_path="carrier/start-ordering-shipping", + permission_classes=[IsAdminUser], + ) + @extend_schema( + tags=["Orders"], + summary="Start ordering shipping (admin)", + request=None, + responses={200: CarrierReadSerializer}, + ) + def carrier_start_ordering_shipping(self, request, pk=None): + order = self.get_object() + if not order.carrier: + return Response({"detail": "Carrier not set."}, status=400) + order.carrier.start_ordering_shipping() + order.carrier.refresh_from_db() + ser = CarrierReadSerializer(order.carrier) + return Response(ser.data) + + +# -- Invoice PDF for Order -- +class OrderInvoice(viewsets.ViewSet): + @action(detail=True, methods=["get"], url_path="generate-invoice") + @extend_schema( + tags=["Orders"], + summary="Get Invoice PDF for Order (public)", + responses={200: "PDF File"}, + ) + def get(order_id, request): + try: + order = Order.objects.get(pk=order_id) + except Order.DoesNotExist: + return Response({"detail": "Order not found."}, status=status.HTTP_404_NOT_FOUND) + + return Response(order.invoice.pdf_file, content_type='application/pdf') + + +# ---------- Permissions helpers ---------- + +class AdminWriteOnlyOrReadOnly(AllowAny.__class__): + def has_permission(self, request, view): + if request.method in SAFE_METHODS: + return True + return IsAdminUser().has_permission(request, view) + + +class AdminOnlyForPatchOtherwisePublic(AllowAny.__class__): + def has_permission(self, request, view): + if request.method in SAFE_METHODS or request.method == "POST": + return True + if request.method == "PATCH": + return IsAdminUser().has_permission(request, view) + # default to admin for other unsafe + return IsAdminUser().has_permission(request, view) + + +# ---------- Public/admin viewsets ---------- + +# -- Product -- +@extend_schema_view( + list=extend_schema(tags=["Products"], summary="List products (public)"), + retrieve=extend_schema(tags=["Products"], summary="Retrieve product (public)"), + create=extend_schema(tags=["Products"], summary="Create product (admin)"), + partial_update=extend_schema(tags=["Products"], summary="Update product (admin)"), + update=extend_schema(tags=["Products"], summary="Replace product (admin)"), + destroy=extend_schema(tags=["Products"], summary="Delete product (admin)"), +) +class ProductViewSet(viewsets.ModelViewSet): + queryset = Product.objects.all().order_by("-created_at") + serializer_class = ProductSerializer + permission_classes = [AdminWriteOnlyOrReadOnly] + + +# -- Category -- +@extend_schema_view( + list=extend_schema(tags=["Categories"], summary="List categories (public)"), + retrieve=extend_schema(tags=["Categories"], summary="Retrieve category (public)"), + create=extend_schema(tags=["Categories"], summary="Create category (admin)"), + partial_update=extend_schema(tags=["Categories"], summary="Update category (admin)"), + update=extend_schema(tags=["Categories"], summary="Replace category (admin)"), + destroy=extend_schema(tags=["Categories"], summary="Delete category (admin)"), +) +class CategoryViewSet(viewsets.ModelViewSet): + queryset = Category.objects.all().order_by("name") + serializer_class = CategorySerializer + permission_classes = [AdminWriteOnlyOrReadOnly] + + +# -- Product Image -- +@extend_schema_view( + list=extend_schema(tags=["Product Images"], summary="List product images (public)"), + retrieve=extend_schema(tags=["Product Images"], summary="Retrieve product image (public)"), + create=extend_schema(tags=["Product Images"], summary="Create product image (admin)"), + partial_update=extend_schema(tags=["Product Images"], summary="Update product image (admin)"), + update=extend_schema(tags=["Product Images"], summary="Replace product image (admin)"), + destroy=extend_schema(tags=["Product Images"], summary="Delete product image (admin)"), +) +class ProductImageViewSet(viewsets.ModelViewSet): + queryset = ProductImage.objects.all().order_by("-id") + serializer_class = ProductImageSerializer + permission_classes = [AdminWriteOnlyOrReadOnly] + + +# -- Discount Code -- +@extend_schema_view( + list=extend_schema(tags=["Discount Codes"], summary="List discount codes (public)"), + retrieve=extend_schema(tags=["Discount Codes"], summary="Retrieve discount code (public)"), + create=extend_schema(tags=["Discount Codes"], summary="Create discount code (admin)"), + partial_update=extend_schema(tags=["Discount Codes"], summary="Update discount code (admin)"), + update=extend_schema(tags=["Discount Codes"], summary="Replace discount code (admin)"), + destroy=extend_schema(tags=["Discount Codes"], summary="Delete discount code (admin)"), +) +class DiscountCodeViewSet(viewsets.ModelViewSet): + queryset = DiscountCode.objects.all().order_by("-id") + serializer_class = DiscountCodeSerializer + permission_classes = [AdminWriteOnlyOrReadOnly] + + +# -- Refund -- +@extend_schema_view( + list=extend_schema(tags=["Refunds"], summary="List refunds (public)"), + retrieve=extend_schema(tags=["Refunds"], summary="Retrieve refund (public)"), + create=extend_schema(tags=["Refunds"], summary="Create refund (public)"), + partial_update=extend_schema(tags=["Refunds"], summary="Update refund (admin)"), +) +class RefundViewSet(mixins.CreateModelMixin, + mixins.ListModelMixin, + mixins.RetrieveModelMixin, + mixins.UpdateModelMixin, + viewsets.GenericViewSet): + queryset = Refund.objects.select_related("order").all().order_by("-created_at") + serializer_class = RefundSerializer + permission_classes = [AdminOnlyForPatchOtherwisePublic] + + + diff --git a/backend/configuration/apps.py b/backend/configuration/apps.py index d3265d0..8cc1282 100644 --- a/backend/configuration/apps.py +++ b/backend/configuration/apps.py @@ -1,6 +1,5 @@ from django.apps import AppConfig from django.db.utils import OperationalError, ProgrammingError -from .models import ShopConfiguration class ConfigurationConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' @@ -13,9 +12,11 @@ class ConfigurationConfig(AppConfig): makemigrations/migrate don't fail when the table does not yet exist. """ try: - ShopConfiguration.get_solo() + from .models import ShopConfiguration # local import to avoid premature app registry access + ShopConfiguration.get_solo() # creates if missing except (OperationalError, ProgrammingError): - ShopConfiguration.objects.create() + # DB not ready (e.g., before initial migrate); ignore silently + pass diff --git a/backend/social/chat/migrations/__init__.py b/backend/social/chat/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/thirdparty/downloader/migrations/0001_initial.py b/backend/thirdparty/downloader/migrations/0001_initial.py deleted file mode 100644 index 33b2b97..0000000 --- a/backend/thirdparty/downloader/migrations/0001_initial.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-29 14:53 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='DownloaderRecord', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('is_deleted', models.BooleanField(default=False)), - ('deleted_at', models.DateTimeField(blank=True, null=True)), - ('url', models.URLField()), - ('download_time', models.DateTimeField(auto_now_add=True)), - ('format', models.CharField(max_length=50)), - ('length_of_media', models.IntegerField(help_text='Length of media in seconds')), - ('file_size', models.BigIntegerField(help_text='File size in bytes')), - ], - options={ - 'abstract': False, - }, - ), - ] diff --git a/backend/thirdparty/downloader/migrations/__init__.py b/backend/thirdparty/downloader/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/thirdparty/stripe/admin.py b/backend/thirdparty/stripe/admin.py index d23cc8e..c6fe108 100644 --- a/backend/thirdparty/stripe/admin.py +++ b/backend/thirdparty/stripe/admin.py @@ -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",) diff --git a/backend/thirdparty/stripe/client.py b/backend/thirdparty/stripe/client.py new file mode 100644 index 0000000..d0c7e6e --- /dev/null +++ b/backend/thirdparty/stripe/client.py @@ -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)}) \ No newline at end of file diff --git a/backend/thirdparty/stripe/migrations/0001_initial.py b/backend/thirdparty/stripe/migrations/0001_initial.py deleted file mode 100644 index 9377f3e..0000000 --- a/backend/thirdparty/stripe/migrations/0001_initial.py +++ /dev/null @@ -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)), - ], - ), - ] diff --git a/backend/thirdparty/stripe/migrations/__init__.py b/backend/thirdparty/stripe/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/thirdparty/stripe/models.py b/backend/thirdparty/stripe/models.py index 4207bc1..f80f3a3 100644 --- a/backend/thirdparty/stripe/models.py +++ b/backend/thirdparty/stripe/models.py @@ -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}" \ No newline at end of file + 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() \ No newline at end of file diff --git a/backend/thirdparty/stripe/stripe.md b/backend/thirdparty/stripe/stripe.md new file mode 100644 index 0000000..11af82a --- /dev/null +++ b/backend/thirdparty/stripe/stripe.md @@ -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!!! \ No newline at end of file diff --git a/backend/account/migrations/__init__.py b/backend/thirdparty/stripe/tasks.py similarity index 100% rename from backend/account/migrations/__init__.py rename to backend/thirdparty/stripe/tasks.py diff --git a/backend/thirdparty/stripe/views.py b/backend/thirdparty/stripe/views.py index 561d29d..f940959 100644 --- a/backend/thirdparty/stripe/views.py +++ b/backend/thirdparty/stripe/views.py @@ -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.") diff --git a/backend/thirdparty/stripe/where to find webhooks settings.png b/backend/thirdparty/stripe/where to find webhooks settings.png new file mode 100644 index 0000000000000000000000000000000000000000..abcea4471c6044b253df3307ed1996413a0f02ab GIT binary patch literal 111040 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

|9gX1-qWm@zZrez4qC)tk(uXOE{0B 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 literal 0 HcmV?d00001 diff --git a/backend/thirdparty/zasilkovna/migrations/__init__.py b/backend/thirdparty/zasilkovna/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/backend/thirdparty/zasilkovna/models.py b/backend/thirdparty/zasilkovna/models.py index 831386a..21e9d09 100644 --- a/backend/thirdparty/zasilkovna/models.py +++ b/backend/thirdparty/zasilkovna/models.py @@ -21,10 +21,11 @@ from django.db import models from django.utils import timezone from django.core.validators import RegexValidator from django.core.files.base import ContentFile +from django.apps import apps from .client import PacketaAPI -from commerce.models import Order, Carrier -from configuration.models import Configuration + +from configuration.models import ShopConfiguration packeta_client = PacketaAPI() # single reusable instance @@ -55,6 +56,13 @@ class ZasilkovnaPacket(models.Model): help_text="Hmotnost zásilky v gramech" ) + # 🚚 návratové směrovací kódy (pro vrácení zásilky) + return_routing = models.JSONField( + default=list, + blank=True, + help_text="Seznam 2 routing stringů pro vrácení zásilky" + ) + class PDF_SIZE(models.TextChoices): A6_ON_A6 = ("A6 on A6", "105x148 mm (A6) label on a page of the same size") A7_ON_A7 = ("A7 on A7", "105x74 mm (A7) label on a page of the same size") @@ -63,19 +71,16 @@ class ZasilkovnaPacket(models.Model): A8_ON_A8 = ("A8 on A8", "50x74 mm (A8) label on a page of the same size") size_of_pdf = models.CharField(max_length=20, choices=PDF_SIZE.choices, default=PDF_SIZE.A6_ON_A6) - - # 🚚 návratové směrovací kódy (pro vrácení zásilky) - return_routing = models.JSONField( - default=list, - blank=True, - help_text="Seznam 2 routing stringů pro vrácení zásilky" - ) - def save(self, *args, **kwargs): - # On first save, create the packet remotely if packet_id is not set + # workaroud to avoid circular import + Carrier = apps.get_model('commerce', 'Carrier') + Order = apps.get_model('commerce', 'Order') + carrier = Carrier.objects.get(zasilkovna=self) order = Order.objects.get(carrier=carrier) + cash_on_delivery = order.payment.payment_method == order.payment.PAYMENT.CASH_ON_DELIVERY + if not self.packet_id: response = packeta_client.create_packet( address_id=self.addressId, @@ -85,14 +90,13 @@ class ZasilkovnaPacket(models.Model): surname=order.last_name, company=order.company, email=order.email, - addressId=Configuration.get_solo().zasilkovna_address_id, + addressId=ShopConfiguration.get_solo().zasilkovna_address_id, - #FIXME: udělat logiku pro počítaní dobírky a hodnoty zboží - cod=100.00, - value=100.00, + cod=order.total_price if cash_on_delivery else 0, # dobírka + value=order.total_price, - currency=Configuration.get_solo().currency, - eshop= Configuration.get_solo().name, + currency=ShopConfiguration.get_solo().currency, + eshop= ShopConfiguration.get_solo().name, ) self.packet_id = response['packet_id'] self.barcode = response['barcode'] diff --git a/backend/thirdparty/zasilkovna/serializers.py b/backend/thirdparty/zasilkovna/serializers.py index e69de29..d0fe9b8 100644 --- a/backend/thirdparty/zasilkovna/serializers.py +++ b/backend/thirdparty/zasilkovna/serializers.py @@ -0,0 +1,38 @@ +from rest_framework import serializers + +from .models import ZasilkovnaPacket, ZasilkovnaShipment + + +class ZasilkovnaPacketSerializer(serializers.ModelSerializer): + class Meta: + model = ZasilkovnaPacket + fields = [ + "id", + "created_at", + "packet_id", + "barcode", + "state", + "weight", + "return_routing", + ] + read_only_fields = fields + + +class TrackingURLSerializer(serializers.Serializer): + barcode = serializers.CharField(read_only=True) + tracking_url = serializers.URLField(read_only=True) + + +class ZasilkovnaShipmentSerializer(serializers.ModelSerializer): + packets = serializers.PrimaryKeyRelatedField(many=True, read_only=True) + + class Meta: + model = ZasilkovnaShipment + fields = [ + "id", + "created_at", + "shipment_id", + "barcode", + "packets", + ] + read_only_fields = fields diff --git a/backend/thirdparty/zasilkovna/urls.py b/backend/thirdparty/zasilkovna/urls.py index e69de29..cd0592a 100644 --- a/backend/thirdparty/zasilkovna/urls.py +++ b/backend/thirdparty/zasilkovna/urls.py @@ -0,0 +1,15 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter + +from .views import ZasilkovnaShipmentViewSet, ZasilkovnaPacketViewSet + + +router = DefaultRouter() +router.register(r"shipments", ZasilkovnaShipmentViewSet, basename="zasilkovna-shipment") +router.register(r"packets", ZasilkovnaPacketViewSet, basename="zasilkovna-packet") + +app_name = "zasilkovna" + +urlpatterns = [ + path("", include(router.urls)), +] diff --git a/backend/thirdparty/zasilkovna/views.py b/backend/thirdparty/zasilkovna/views.py index 04de252..022316c 100644 --- a/backend/thirdparty/zasilkovna/views.py +++ b/backend/thirdparty/zasilkovna/views.py @@ -1,8 +1,80 @@ -#views.py +from rest_framework import viewsets, mixins, status +from rest_framework.decorators import action +from rest_framework.response import Response -""" -TODO: OBJEDNAVANÍ SE VYVOLÁVA V CARRIER V COMMERCE.MODELS.PY - získaní labelu, - info o kurýrovi, vracení balíku, - vytvoření hromadné expedice -""" \ No newline at end of file +from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiResponse + +from .models import ZasilkovnaShipment, ZasilkovnaPacket +from .serializers import ( + ZasilkovnaShipmentSerializer, + ZasilkovnaPacketSerializer, + TrackingURLSerializer, +) + + +@extend_schema_view( + list=extend_schema( + tags=["Zásilkovna"], + summary="List shipments", + description="Returns a paginated list of Packeta (Zásilkovna) shipments.", + responses={200: ZasilkovnaShipmentSerializer}, + ), + retrieve=extend_schema( + tags=["Zásilkovna"], + summary="Retrieve a shipment", + description="Returns detail for a single shipment.", + responses={200: ZasilkovnaShipmentSerializer}, + ), +) +class ZasilkovnaShipmentViewSet(viewsets.ReadOnlyModelViewSet): + queryset = ZasilkovnaShipment.objects.all().order_by("-created_at") + serializer_class = ZasilkovnaShipmentSerializer + + + +@extend_schema_view( + retrieve=extend_schema( + tags=["Zásilkovna"], + summary="Retrieve a packet", + description="Returns detail for a single packet.", + responses={200: ZasilkovnaPacketSerializer}, + ) +) +class ZasilkovnaPacketViewSet(mixins.RetrieveModelMixin, viewsets.GenericViewSet): + queryset = ZasilkovnaPacket.objects.all() + serializer_class = ZasilkovnaPacketSerializer + + @extend_schema( + tags=["Zásilkovna"], + summary="Get public tracking URL", + description=( + "Returns the public Zásilkovna tracking URL derived from the packet's barcode." + ), + responses={200: OpenApiResponse(response=TrackingURLSerializer)}, + ) + @action(detail=True, methods=["get"], url_path="tracking-url") + def tracking_url(self, request, pk=None): + packet: ZasilkovnaPacket = self.get_object() + data = { + "barcode": packet.barcode, + "tracking_url": packet.get_tracking_url(), + } + return Response(data) + + + @extend_schema( + tags=["Zásilkovna"], + summary="Cancel packet", + description=( + "Cancels the packet through the Packeta API and updates its state to CANCELED. " + "No request body is required." + ), + request=None, + responses={200: OpenApiResponse(response=ZasilkovnaPacketSerializer)}, + ) + @action(detail=True, methods=["patch"], url_path="cancel") + def cancel(self, request, pk=None): + packet: ZasilkovnaPacket = self.get_object() + packet.cancel_packet() + serializer = self.get_serializer(packet) + return Response(serializer.data, status=status.HTTP_200_OK) diff --git a/backend/vontor_cz/asgi.py b/backend/vontor_cz/asgi.py index 626be7f..96c15cb 100644 --- a/backend/vontor_cz/asgi.py +++ b/backend/vontor_cz/asgi.py @@ -13,7 +13,7 @@ from channels.routing import ProtocolTypeRouter, URLRouter from channels.auth import AuthMiddlewareStack #import myapp.routing # your app's routing -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'trznice.settings') +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'vontor_cz.settings') application = ProtocolTypeRouter({ "http": get_asgi_application(), diff --git a/backend/vontor_cz/celery.py b/backend/vontor_cz/celery.py index 96a43e1..5d7dd4a 100644 --- a/backend/vontor_cz/celery.py +++ b/backend/vontor_cz/celery.py @@ -1,8 +1,8 @@ import os from celery import Celery -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "backend.settings") +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "vontor_cz.settings") -app = Celery("backend") +app = Celery("vontor_cz") app.config_from_object("django.conf:settings", namespace="CELERY") app.autodiscover_tasks() diff --git a/backend/vontor_cz/settings.py b/backend/vontor_cz/settings.py index 1c06467..3eb8ad5 100644 --- a/backend/vontor_cz/settings.py +++ b/backend/vontor_cz/settings.py @@ -51,12 +51,13 @@ DATETIME_INPUT_FORMATS = [ "%Y-%m-%dT%H:%M:%S", # '2025-07-25T14:30:59' ] -LANGUAGE_CODE = 'cs' +# -------------------- LOKALIZACE ------------------------- -TIME_ZONE = 'Europe/Prague' +LANGUAGE_CODE = os.getenv("LANGUAGE_CODE", "cs") +TIME_ZONE = os.getenv("TIME_ZONE", "Europe/Prague") USE_I18N = True - +USE_L10N = True USE_TZ = True @@ -313,6 +314,10 @@ REST_FRAMEWORK = { 'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', 'DEFAULT_FILTER_BACKENDS': ['django_filters.rest_framework.DjangoFilterBackend'], + # Enable default pagination so custom list actions (e.g., /orders/detail) paginate + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 20, + 'DEFAULT_THROTTLE_RATES': { 'anon': '100/hour', # unauthenticated 'user': '2000/hour', # authenticated @@ -326,7 +331,8 @@ REST_FRAMEWORK = { MY_CREATED_APPS = [ 'account', 'commerce', - + 'configuration', + 'social.chat', 'thirdparty.downloader', diff --git a/backend/vontor_cz/urls.py b/backend/vontor_cz/urls.py index ecce746..18e2e96 100644 --- a/backend/vontor_cz/urls.py +++ b/backend/vontor_cz/urls.py @@ -39,4 +39,5 @@ urlpatterns = [ path('api/trading212/', include('thirdparty.trading212.urls')), path('api/downloader/', include('thirdparty.downloader.urls')), path("api/payments/gopay/", include("thirdparty.gopay.urls", namespace="gopay")), + path('api/zasilkovna/', include('thirdparty.zasilkovna.urls')), ] diff --git a/backend/commerce/migrations/__init__.py b/backups/backup-20251117-224608.sql similarity index 100% rename from backend/commerce/migrations/__init__.py rename to backups/backup-20251117-224608.sql diff --git a/docker-compose.yml b/docker-compose.yml index ab1fe8e..bda87e4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: - ./backend:/app depends_on: - redis - command: daphne -b 0.0.0.0 -p 8000 backend.asgi:application + command: daphne -b 0.0.0.0 -p 8000 vontor_cz.asgi:application frontend: env_file: