From c0bd24ee5e6cc9fdc0bf53484ebeae94e870d99e Mon Sep 17 00:00:00 2001 From: Brunobrno Date: Tue, 20 Jan 2026 23:45:21 +0100 Subject: [PATCH] Refactor commerce models and enhance payment logic Refactored commerce models to remove language prefixes from status choices, improved order and payment validation, and enforced business rules for payment and shipping combinations. Updated order item and cart calculations to use VAT-inclusive prices, added unique constraints and indexes to reviews, and improved stock management logic. Added new Stripe client methods for session and refund management, and updated Zasilkovna and Deutsche Post models for consistency. Minor fixes and improvements across related tasks, URLs, and configuration models. --- backend/advertisement/tasks.py | 13 ++ backend/commerce/models.py | 214 ++++++++++++++++------ backend/commerce/tasks.py | 26 ++- backend/commerce/urls.py | 4 +- backend/configuration/models.py | 8 +- backend/thirdparty/deutschepost/models.py | 20 +- backend/thirdparty/stripe/client.py | 126 ++++++++++++- backend/thirdparty/stripe/models.py | 19 +- backend/thirdparty/zasilkovna/client.py | 5 +- backend/thirdparty/zasilkovna/models.py | 14 +- 10 files changed, 353 insertions(+), 96 deletions(-) diff --git a/backend/advertisement/tasks.py b/backend/advertisement/tasks.py index e9d97c5..d9f6874 100644 --- a/backend/advertisement/tasks.py +++ b/backend/advertisement/tasks.py @@ -2,6 +2,7 @@ from account.tasks import send_email_with_context from configuration.models import SiteConfiguration from celery import shared_task +from celery.schedules import crontab @shared_task def send_contact_me_email_task(client_email, message_content): @@ -14,4 +15,16 @@ def send_contact_me_email_task(client_email, message_content): subject="Poptávka z kontaktního formuláře!!!", template_path="email/contact_me.html", context=context, + ) + + +def send_newly_added_items_to_store_email_task_last_week(item_id): + + send_email_with_context( + recipients=SiteConfiguration.get_solo().contact_email, + subject="Nový produkt přidán do obchodu", + template_path="email/new_item_added.html", + context={ + "item": item, + } ) \ No newline at end of file diff --git a/backend/commerce/models.py b/backend/commerce/models.py index cb53fd2..8aad58b 100644 --- a/backend/commerce/models.py +++ b/backend/commerce/models.py @@ -1,3 +1,5 @@ +from ast import Or +import dis from django.db import models from django.conf import settings from django.utils import timezone @@ -5,7 +7,7 @@ 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 django.core.validators import MaxValueValidator, MinValueValidator, validate_email try: from weasyprint import HTML @@ -146,12 +148,12 @@ class ProductImage(models.Model): class Order(models.Model): class OrderStatus(models.TextChoices): - CREATED = "created", "cz#Vytvořeno" - CANCELLED = "cancelled", "cz#Zrušeno" - COMPLETED = "completed", "cz#Dokončeno" + CREATED = "created", "Vytvořeno" + CANCELLED = "cancelled", "Zrušeno" + COMPLETED = "completed", "Dokončeno" - REFUNDING = "refunding", "cz#Vrácení v procesu" - REFUNDED = "refunded", "cz#Vráceno" + REFUNDING = "refunding", "Vrácení v procesu" + REFUNDED = "refunded", "Vráceno" status = models.CharField( max_length=20, choices=OrderStatus.choices, null=True, blank=True, default=OrderStatus.CREATED @@ -216,10 +218,10 @@ class Order(models.Model): else: total = Decimal('0.0') - # getting all prices from order items (without discount) - + # getting all prices from order items (without discount) - using VAT-inclusive prices + for item in self.items.all(): - total = total + (item.product.price * item.quantity) + total = total + (item.product.get_price_with_vat() * item.quantity) return total + carrier_price @@ -234,6 +236,24 @@ class Order(models.Model): self.postal_code = self.user.postal_code self.country = self.user.country + def clean(self): + """Validate order data""" + # Validate required fields + required_fields = ['first_name', 'last_name', 'email', 'address', 'city', 'postal_code'] + for field in required_fields: + if not getattr(self, field): + raise ValidationError(f"{field.replace('_', ' ').title()} is required.") + + # Validate email format + try: + validate_email(self.email) + except ValidationError: + raise ValidationError("Invalid email format.") + + # Validate order has items + if self.pk and not self.items.exists(): + raise ValidationError("Order must have at least one item.") + def save(self, *args, **kwargs): # Keep total_price always in sync with items and discount self.total_price = self.calculate_total_price() @@ -255,16 +275,16 @@ class Order(models.Model): class Carrier(models.Model): class SHIPPING(models.TextChoices): - ZASILKOVNA = "packeta", "cz#Zásilkovna" - DEUTSCHEPOST = "deutschepost", "cz#Deutsche Post" - STORE = "store", "cz#Osobní odběr" + ZASILKOVNA = "packeta", "Zásilkovna" + DEUTSCHEPOST = "deutschepost", "Deutsche Post" + STORE = "store", "Osobní odběr" shipping_method = models.CharField(max_length=20, choices=SHIPPING.choices, default=SHIPPING.STORE) class STATE(models.TextChoices): - PREPARING = "ordered", "cz#Objednávka se připravuje" - SHIPPED = "shipped", "cz#Odesláno" - DELIVERED = "delivered", "cz#Doručeno" - READY_TO_PICKUP = "ready_to_pickup", "cz#Připraveno k vyzvednutí" + 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) @@ -285,27 +305,44 @@ class Carrier(models.Model): shipping_price = models.DecimalField(max_digits=10, decimal_places=2, default=Decimal('0.00')) def save(self, *args, **kwargs): - if self.pk is None: - - if self.shipping_price is None: - self.shipping_price = self.get_price() + # Set shipping price for new carriers + if self.pk is None and self.shipping_price is None: + # For new carriers, we might not have an order yet + self.shipping_price = self.get_price(order=None) + # Check if state changed to ready for pickup + old_state = None if self.pk: - if self.STATE == self.STATE.READY_TO_PICKUP and self.shipping_method == self.SHIPPING.STORE: - notify_Ready_to_pickup.delay(order=self.order, user=self.order.user) - pass - else: - pass + old_carrier = Carrier.objects.filter(pk=self.pk).first() + old_state = old_carrier.state if old_carrier else None super().save(*args, **kwargs) - def get_price(self): + # Send notification if state changed to ready for pickup + if (old_state != self.STATE.READY_TO_PICKUP and + self.state == self.STATE.READY_TO_PICKUP and + self.shipping_method == self.SHIPPING.STORE): + + if hasattr(self, 'order') and self.order: + notify_Ready_to_pickup.delay(order=self.order, user=self.order.user) + + def get_price(self, order=None): if self.shipping_method == self.SHIPPING.ZASILKOVNA: return SiteConfiguration.get_solo().zasilkovna_shipping_price elif self.shipping_method == self.SHIPPING.DEUTSCHEPOST: return SiteConfiguration.get_solo().deutschepost_shipping_price - else: + elif self.shipping_method == self.SHIPPING.STORE: + # Store pickup is always free return Decimal('0.0') + else: + # Check for free shipping based on order total + if order is None: + order = Order.objects.filter(carrier=self).first() + + if order and order.total_price >= SiteConfiguration.get_solo().free_shipping_over: + return Decimal('0.0') + else: + return SiteConfiguration.get_solo().default_shipping_price or Decimal('50.0') # fallback price #tohle bude vyvoláno pomocí admina přes api!!! @@ -363,9 +400,9 @@ class Carrier(models.Model): class Payment(models.Model): class PAYMENT(models.TextChoices): - SHOP = "shop", "cz#Platba v obchodě", "de#Bezahlung im Geschäft" - STRIPE = "stripe", "cz#Platební Brána", "de#Zahlungsgateway" - CASH_ON_DELIVERY = "cash_on_delivery", "cz#Dobírka", "de#Nachnahme" + SHOP = "shop", "Platba v obchodě" + STRIPE = "stripe", "Platební Brána" + CASH_ON_DELIVERY = "cash_on_delivery", "Dobírka" payment_method = models.CharField(max_length=30, choices=PAYMENT.choices, default=PAYMENT.SHOP) #FIXME: potvrdit že logika platby funguje správně @@ -373,16 +410,56 @@ class Payment(models.Model): stripe = models.OneToOneField( StripeModel, on_delete=models.CASCADE, null=True, blank=True, related_name="payment" ) + payed_at_shop = models.BooleanField(default=False, null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) def clean(self): """Validate payment and shipping method combinations""" - # TODO: Add validation logic for invalid payment/shipping combinations - # TODO: Skip GoPay integration for now + # Validate payment method consistency + if self.payment_method == self.PAYMENT.STRIPE and not self.stripe: + raise ValidationError("Stripe payment method requires a linked StripeModel instance.") + + elif self.payment_method == self.PAYMENT.SHOP and self.stripe: + raise ValidationError("Shop payment method should not have a linked StripeModel instance.") + + # Validate payment and shipping compatibility + if self.payment_method == self.PAYMENT.SHOP: + # SHOP payment only works with STORE pickup - customer pays at physical store + if Order.objects.filter(payment=self).exists(): + order = Order.objects.get(payment=self) + + if order.carrier and order.carrier.shipping_method != Carrier.SHIPPING.STORE: + raise ValidationError( + "Shop payment is only compatible with store pickup. " + "For shipping orders, use Stripe or Cash on Delivery payment methods." + ) + + elif self.payment_method == self.PAYMENT.CASH_ON_DELIVERY: + # Cash on delivery only works with shipping methods (not store pickup) + if Order.objects.filter(payment=self).exists(): + order = Order.objects.get(payment=self) + + if order.carrier and order.carrier.shipping_method == Carrier.SHIPPING.STORE: + raise ValidationError( + "Cash on delivery is not compatible with store pickup. " + "For store pickup, use shop payment method." + ) + + # STRIPE payment works with all shipping methods - no additional validation needed + super().clean() + def payed_manually(self): + """Mark payment as completed""" + if self.payment_method == self.PAYMENT.SHOP: + self.payed_at_shop = True + self.save() + else: + raise ValidationError("Manuální platba je povolena pouze pro platbu v obchodě.") + + # ------------------ SLEVOVÉ KÓDY ------------------ @@ -441,7 +518,7 @@ class OrderItem(models.Model): product = models.ForeignKey("commerce.Product", on_delete=models.PROTECT) quantity = models.PositiveIntegerField(default=1) - def get_total_price(self, discounts: list[DiscountCode] = None): + def get_total_price(self, discounts: list[DiscountCode] = list()): """Vrátí celkovou cenu položky po aplikaci relevantních kupónů. Logika dle SiteConfiguration: @@ -453,12 +530,12 @@ class OrderItem(models.Model): - Kombinace: nejprve procentuální část, poté odečtení fixní částky. - Sleva se nikdy nesmí dostat pod 0. """ - base_price = self.product.price * self.quantity + # Use VAT-inclusive price for customer-facing calculations + base_price = self.product.get_price_with_vat() * self.quantity - if not discounts: + if not discounts or discounts == []: return base_price - config = SiteConfiguration.get_solo() #seznám slev @@ -527,16 +604,19 @@ class OrderItem(models.Model): 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.") + # Check if order already has a processed payment + if (self.order.payment and + self.order.payment.payment_method and + self.order.payment.payment_method != Payment.PAYMENT.SHOP): + raise ValueError("Cannot modify items from order with processed payment method.") - 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.") + # Validate stock availability + if self.product.stock < self.quantity: + raise ValueError(f"Insufficient stock for product {self.product.name}. Available: {self.product.stock}") - self.product.stock -= self.quantity - self.product.save(update_fields=["stock"]) + # Reduce stock + self.product.stock -= self.quantity + self.product.save(update_fields=["stock"]) super().save(*args, **kwargs) @@ -545,10 +625,10 @@ 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", "cz#Vrácení před uplynutím 14-ti denní lhůty" - DAMAGED_PRODUCT = "damaged_product", "cz#Poškozený produkt" - WRONG_ITEM = "wrong_item", "cz#Špatná položka" - OTHER = "other", "cz#Jiný důvod" + 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=40, choices=Reason.choices) reason_text = models.TextField(blank=True) @@ -635,17 +715,15 @@ class Refund(models.Model): "return_reason": return_reason, } - - carrier = models.OneToOneField( - "Carrier", - on_delete=models.CASCADE, - related_name="refund", - null=True, - blank=True - ) - - html_string = render_to_string("refund/customer_in_package_returning_form.html", context) + + # Import WeasyPrint lazily to avoid startup failures when system + # libraries (Pango/GObject) are not present on Windows. + if HTML is None: + raise RuntimeError( + "WeasyPrint is not available. Install its system dependencies (Pango/GTK) or run the backend in Docker." + ) + pdf_bytes = HTML(string=html_string).write_pdf() return pdf_bytes @@ -665,6 +743,7 @@ class Invoice(models.Model): order = Order.objects.get(invoice=self) # Render HTML html_string = render_to_string("invoice/Order.html", {"invoice": self, "order": order}) + # Import WeasyPrint lazily to avoid startup failures when system # libraries (Pango/GObject) are not present on Windows. if HTML is None: @@ -693,6 +772,19 @@ class Review(models.Model): created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) + class Meta: + unique_together = ('product', 'user') # Prevent multiple reviews per user per product + indexes = [ + models.Index(fields=['product', 'rating']), + models.Index(fields=['created_at']), + ] + + def clean(self): + """Validate that user hasn't already reviewed this product""" + if self.pk is None: # Only for new reviews + if Review.objects.filter(product=self.product, user=self.user).exists(): + raise ValidationError("User has already reviewed this product.") + def __str__(self): return f"Review for {self.product.name} by {self.user.username}" @@ -727,7 +819,7 @@ class Cart(models.Model): return f"Anonymous cart ({self.session_key})" def get_total(self): - """Calculate total price of all items in cart""" + """Calculate total price of all items in cart including VAT""" total = Decimal('0.0') for item in self.items.all(): total += item.get_subtotal() @@ -754,8 +846,8 @@ class CartItem(models.Model): return f"{self.quantity}x {self.product.name} in cart" def get_subtotal(self): - """Calculate subtotal for this cart item""" - return self.product.price * self.quantity + """Calculate subtotal for this cart item including VAT""" + return self.product.get_price_with_vat() * self.quantity def clean(self): """Validate that product has enough stock""" diff --git a/backend/commerce/tasks.py b/backend/commerce/tasks.py index ac1d5e6..a7199bb 100644 --- a/backend/commerce/tasks.py +++ b/backend/commerce/tasks.py @@ -83,6 +83,24 @@ def notify_order_successfuly_created(order = None, user = None, **kwargs): pass +@shared_task +def notify_order_payed(order = None, user = None, **kwargs): + if not order or not user: + raise ValueError("Order and User must be provided for notification.") + + if kwargs: + print("Additional kwargs received in notify_order_paid:", kwargs) + + send_email_with_context( + recipients=user.email, + subject="Your order has been paid", + template_path="email/order_paid.html", + context={ + "user": user, + "order": order, + }) + + pass @shared_task def notify_about_missing_payment(order = None, user = None, **kwargs): @@ -103,6 +121,9 @@ def notify_about_missing_payment(order = None, user = None, **kwargs): pass + +# -- NOTIFICATIONS REFUND -- + @shared_task def notify_refund_items_arrived(order = None, user = None, **kwargs): if not order or not user: @@ -141,4 +162,7 @@ def notify_refund_accepted(order = None, user = None, **kwargs): "order": order, }) - pass \ No newline at end of file + pass + + +# \ No newline at end of file diff --git a/backend/commerce/urls.py b/backend/commerce/urls.py index 6a8c2da..4c8d90f 100644 --- a/backend/commerce/urls.py +++ b/backend/commerce/urls.py @@ -13,7 +13,7 @@ from .views import ( CartViewSet, WishlistViewSet, AdminWishlistViewSet, - AnalyticsViewSet, + AnalyticsView, ) router = DefaultRouter() @@ -27,10 +27,10 @@ router.register(r'reviews', ReviewPublicViewSet, basename='review') router.register(r'cart', CartViewSet, basename='cart') router.register(r'wishlist', WishlistViewSet, basename='wishlist') router.register(r'admin/wishlists', AdminWishlistViewSet, basename='admin-wishlist') -router.register(r'analytics', AnalyticsViewSet, basename='analytics') urlpatterns = [ path('', include(router.urls)), path('refunds/public/', RefundPublicView.as_view(), name='RefundPublicView'), path('reviews/create/', ReviewPostPublicView.as_view(), name='ReviewCreate'), + path('analytics/', AnalyticsView.as_view(), name='analytics'), ] diff --git a/backend/configuration/models.py b/backend/configuration/models.py index 6857af6..6abf4bf 100644 --- a/backend/configuration/models.py +++ b/backend/configuration/models.py @@ -44,8 +44,8 @@ class SiteConfiguration(models.Model): addition_of_coupons_amount = models.BooleanField(default=False, help_text="Sčítání slevových kupónů v objednávce (ano/ne), pokud ne tak se použije pouze nejvyšší slevový kupón") class CURRENCY(models.TextChoices): - CZK = "CZK", "cz#Czech Koruna" - EUR = "EUR", "cz#Euro" + CZK = "CZK", "Czech Koruna" + EUR = "EUR", "Euro" currency = models.CharField(max_length=10, default=CURRENCY.CZK, choices=CURRENCY.choices) class Meta: @@ -87,6 +87,10 @@ class VATRate(models.Model): default=False, help_text="Default rate for new products" ) + is_active = models.BooleanField( + default=True, + help_text="Whether this VAT rate is active and available for use" + ) created_at = models.DateTimeField(auto_now_add=True) class Meta: diff --git a/backend/thirdparty/deutschepost/models.py b/backend/thirdparty/deutschepost/models.py index 90398be..63a4a63 100644 --- a/backend/thirdparty/deutschepost/models.py +++ b/backend/thirdparty/deutschepost/models.py @@ -42,12 +42,12 @@ class DeutschePostOrder(models.Model): created_at = models.DateTimeField(auto_now_add=True) class STATE(models.TextChoices): - CREATED = "CREATED", "cz#Vytvořeno" - FINALIZED = "FINALIZED", "cz#Dokončeno" - SHIPPED = "SHIPPED", "cz#Odesláno" - DELIVERED = "DELIVERED", "cz#Doručeno" - CANCELLED = "CANCELLED", "cz#Zrušeno" - ERROR = "ERROR", "cz#Chyba" + CREATED = "CREATED", "Vytvořeno" + FINALIZED = "FINALIZED", "Dokončeno" + SHIPPED = "SHIPPED", "Odesláno" + DELIVERED = "DELIVERED", "Doručeno" + CANCELLED = "CANCELLED", "Zrušeno" + ERROR = "ERROR", "Chyba" state = models.CharField(max_length=20, choices=STATE.choices, default=STATE.CREATED) @@ -280,10 +280,10 @@ class DeutschePostBulkOrder(models.Model): created_at = models.DateTimeField(auto_now_add=True) class STATUS(models.TextChoices): - CREATED = "CREATED", "cz#Vytvořeno" - PROCESSING = "PROCESSING", "cz#Zpracovává se" - COMPLETED = "COMPLETED", "cz#Dokončeno" - ERROR = "ERROR", "cz#Chyba" + CREATED = "CREATED", "Vytvořeno" + PROCESSING = "PROCESSING", "Zpracovává se" + COMPLETED = "COMPLETED", "Dokončeno" + ERROR = "ERROR", "Chyba" status = models.CharField(max_length=20, choices=STATUS.choices, default=STATUS.CREATED) diff --git a/backend/thirdparty/stripe/client.py b/backend/thirdparty/stripe/client.py index d0c7e6e..ce5ad4e 100644 --- a/backend/thirdparty/stripe/client.py +++ b/backend/thirdparty/stripe/client.py @@ -10,6 +10,7 @@ stripe.api_key = os.getenv("STRIPE_SECRET_KEY") class StripeClient: + @staticmethod def create_checkout_session(order): """ Vytvoří Stripe Checkout Session pro danou objednávku. @@ -42,8 +43,64 @@ class StripeClient: return session + @staticmethod + def cancel_checkout_session(session_id): + """ + Zruší Stripe Checkout Session. + Args: + session_id (str): ID Stripe Checkout Session k zrušení. + + Returns: + stripe.checkout.Session: Zrušená Stripe Checkout Session. + """ + try: + session = stripe.checkout.Session.expire(session_id) + return session + except Exception as e: + return {"error": str(e)} + @staticmethod + def get_checkout_session(session_id): + """ + Získá informace o Stripe Checkout Session. + Args: + session_id (str): ID Stripe Checkout Session. + + Returns: + stripe.checkout.Session: Stripe Checkout Session objekt. + """ + try: + session = stripe.checkout.Session.retrieve(session_id) + return session + except Exception as e: + return {"error": str(e)} + + @staticmethod + def get_payment_intent(payment_intent_id): + """ + Získá informace o Stripe Payment Intent. + Args: + payment_intent_id (str): ID Stripe Payment Intent. + + Returns: + stripe.PaymentIntent: Stripe Payment Intent objekt. + """ + try: + payment_intent = stripe.PaymentIntent.retrieve(payment_intent_id) + return payment_intent + except Exception as e: + return {"error": str(e)} + + @staticmethod def refund_order(stripe_payment_intent): + """ + Vrátí platbu pro danou objednávku. + Args: + stripe_payment_intent (str): ID Stripe Payment Intent k vrácení. + + Returns: + stripe.Refund: Vytvořený refund objekt nebo chyba. + """ try: refund = stripe.Refund.create( payment_intent=stripe_payment_intent @@ -51,4 +108,71 @@ class StripeClient: return refund except Exception as e: - return json.dumps({"error": str(e)}) \ No newline at end of file + return {"error": str(e)} + + @staticmethod + def partial_refund_order(stripe_payment_intent, amount): + """ + Částečně vrátí platbu pro danou objednávku. + Args: + stripe_payment_intent (str): ID Stripe Payment Intent k vrácení. + amount (int): Částka k vrácení v haléřích. + + Returns: + stripe.Refund: Vytvořený refund objekt nebo chyba. + """ + try: + refund = stripe.Refund.create( + payment_intent=stripe_payment_intent, + amount=amount + ) + return refund + + except Exception as e: + return {"error": str(e)} + + @staticmethod + def create_customer(email, name=None, phone=None): + """ + Vytvoří Stripe Customer. + Args: + email (str): Email zákazníka. + name (str, optional): Jméno zákazníka. + phone (str, optional): Telefon zákazníka. + + Returns: + stripe.Customer: Vytvořený Stripe Customer. + """ + try: + customer_data = {"email": email} + if name: + customer_data["name"] = name + if phone: + customer_data["phone"] = phone + + customer = stripe.Customer.create(**customer_data) + return customer + except Exception as e: + return {"error": str(e)} + + @staticmethod + def webhook_verify_signature(payload, sig_header, endpoint_secret): + """ + Ověří webhook signature od Stripe. + Args: + payload: Raw webhook payload. + sig_header: Stripe signature header. + endpoint_secret: Webhook endpoint secret. + + Returns: + stripe.Event: Ověřený event objekt. + """ + try: + event = stripe.Webhook.construct_event( + payload, sig_header, endpoint_secret + ) + return event + except ValueError as e: + return {"error": "Invalid payload"} + except stripe.error.SignatureVerificationError as e: + return {"error": "Invalid signature"} \ No newline at end of file diff --git a/backend/thirdparty/stripe/models.py b/backend/thirdparty/stripe/models.py index cf82a90..49b8b05 100644 --- a/backend/thirdparty/stripe/models.py +++ b/backend/thirdparty/stripe/models.py @@ -1,5 +1,6 @@ from django.db import models from django.apps import apps +from django.utils import timezone # Create your models here. @@ -29,23 +30,25 @@ class StripeModel(models.Model): def save(self, *args, **kwargs): - #if new - if self.pk: + # If new (no primary key yet) + if self.pk is None: + super().save(*args, **kwargs) # Save first to get 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 + session = StripeClient.create_checkout_session(order) self.stripe_session_id = session.id self.stripe_payment_intent = session.payment_intent self.stripe_session_url = session.url - + + # Save again with Stripe data + super().save(update_fields=['stripe_session_id', 'stripe_payment_intent', 'stripe_session_url', 'updated_at']) else: - self.updated_at = models.DateTimeField(auto_now=True) - - super().save(*args, **kwargs) + self.updated_at = timezone.now() + super().save(*args, **kwargs) def paid(self): self.status = self.STATUS_CHOICES.PAID diff --git a/backend/thirdparty/zasilkovna/client.py b/backend/thirdparty/zasilkovna/client.py index 76a3005..5d0ebb9 100644 --- a/backend/thirdparty/zasilkovna/client.py +++ b/backend/thirdparty/zasilkovna/client.py @@ -3,7 +3,7 @@ from zeep.exceptions import Fault from zeep.transports import Transport from zeep.cache import SqliteCache -from configuration.models import Configuration +from configuration.models import SiteConfiguration import tempfile import base64 @@ -17,9 +17,6 @@ logger = logging.getLogger(__name__) WSDL_URL = os.getenv("PACKETA_WSDL_URL", "https://www.zasilkovna.cz/api/soap.wsdl") PACKETA_API_PASSWORD = os.getenv("PACKETA_API_PASSWORD") -Configuration.g - - # --- 1. Singleton pro klienta (aby se WSDL stáhlo jen jednou pro celý proces) --- @lru_cache(maxsize=1) diff --git a/backend/thirdparty/zasilkovna/models.py b/backend/thirdparty/zasilkovna/models.py index ff568d3..75bfdd4 100644 --- a/backend/thirdparty/zasilkovna/models.py +++ b/backend/thirdparty/zasilkovna/models.py @@ -35,14 +35,14 @@ class ZasilkovnaPacket(models.Model): created_at = models.DateTimeField(auto_now_add=True) class STATE(models.TextChoices): - WAITING_FOR_ORDER = "WAITING_FOR_ORDERING_SHIPMENT", "cz#Čeká na objednání zásilkovny" - PENDING = "PENDING", "cz#Podáno" - SENDED = "SENDED", "cz#Odesláno" - ARRIVED = "ARRIVED", "cz#Doručeno" - CANCELED = "CANCELED", "cz#Zrušeno" + WAITING_FOR_ORDER = "WAITING_FOR_ORDERING_SHIPMENT", "Čeká na objednání zásilkovny" + PENDING = "PENDING", "Podáno" + SENDED = "SENDED", "Odesláno" + ARRIVED = "ARRIVED", "Doručeno" + CANCELED = "CANCELED", "Zrušeno" - RETURNING = "RETURNING", "cz#Posláno zpátky" - RETURNED = "RETURNED", "cz#Vráceno" + RETURNING = "RETURNING", "Posláno zpátky" + RETURNED = "RETURNED", "Vráceno" state = models.CharField(max_length=35, choices=STATE.choices, default=STATE.PENDING) # ------- API -------