Refactor order creation and add configuration endpoints

Refactored order creation logic to use new serializers and transaction handling, improving validation and modularity. Introduced admin and public endpoints for shop configuration with sensitive fields protected. Enhanced Zásilkovna (Packeta) integration, including packet widget template, new API fields, and improved error handling. Added django-silk for profiling, updated requirements and settings, and improved frontend Orval config for API client generation.
This commit is contained in:
David Bruno Vontor
2025-12-08 18:19:20 +01:00
parent 5b066e2770
commit 946f86db7e
18 changed files with 606 additions and 309 deletions

View File

@@ -1,5 +1,7 @@
from rest_framework import serializers
from backend.thirdparty.stripe.client import StripeClient
from .models import Refund, Order, Invoice
@@ -69,6 +71,9 @@ from rest_framework import serializers
from drf_spectacular.utils import extend_schema_field
from decimal import Decimal
from django.contrib.auth import get_user_model
from django.db import transaction
from django.core.exceptions import ValidationError
from .models import (
Category,
@@ -82,155 +87,242 @@ from .models import (
Payment,
)
from thirdparty.stripe.models import StripeModel, StripePayment
from thirdparty.zasilkovna.serializers import ZasilkovnaPacketSerializer
from thirdparty.zasilkovna.models import ZasilkovnaPacket
User = get_user_model()
# ----------------- CREATING ORDER SERIALIZER -----------------
class UserBriefSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ["id", "first_name", "last_name", "email"]
#correct
# -- CARRIER --
class OrderCarrierSerializer(serializers.ModelSerializer):
# vstup: jen ID adresy z widgetu (write-only)
packeta_address_id = serializers.IntegerField(required=False, write_only=True)
# výstup: serializovaný packet
zasilkovna = ZasilkovnaPacketSerializer(many=True, read_only=True)
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"]
fields = ["shipping_method", "state", "zasilkovna", "shipping_price", "packeta_address_id"]
read_only_fields = ["state", "shipping_price", "zasilkovna"]
def create(self, validated_data):
packeta_address_id = validated_data.pop("packeta_address_id", None)
carrier = Carrier.objects.create(**validated_data)
if packeta_address_id is not None:
# vytvoříme nový packet s danou addressId
packet = ZasilkovnaPacket.objects.create(addressId=packeta_address_id)
carrier.zasilkovna.add(packet)
return carrier
class PaymentReadSerializer(serializers.Serializer):
payment_method = serializers.CharField(read_only=True)
stripe_id = serializers.IntegerField(source="stripe.id", read_only=True, allow_null=True)
#correct
# -- ORDER ITEMs --
class OrderItemCreateSerializer(serializers.Serializer):
product_id = serializers.IntegerField()
quantity = serializers.IntegerField(min_value=1, default=1)
def validate(self, attrs):
product_id = attrs.get("product_id")
try:
product = Product.objects.get(pk=product_id)
except Product.DoesNotExist:
raise serializers.ValidationError({"product_id": "Product not found."})
attrs["product"] = product
return attrs
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
# -- PAYMENT --
class PaymentSerializer(serializers.ModelSerializer):
class Meta:
model = Order
model = Payment
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",
"payment_method",
"stripe",
"stripe_session_id",
"stripe_payment_intent",
"stripe_session_url",
]
read_only_fields = [
"id",
"status",
"total_price",
"currency",
"created_at",
"updated_at",
"stripe",
"stripe_session_id",
"stripe_payment_intent",
"stripe_session_url",
]
def create(self, validated_data):
order = self.context.get("order") # musíš ho předat při inicializaci serializeru
carrier = self.context.get("carrier")
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)
with transaction.atomic():
payment = Payment.objects.create(
order=order,
carrier=carrier,
**validated_data
)
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'",
)
# pokud je Stripe, vytvoříme checkout session
if payment.payment_method == Payment.PAYMENT.SHOP and carrier.shipping_method != Carrier.SHIPPING.STORE:
raise serializers.ValidationError("Platba v obchodě je možná pouze pro osobní odběr.")
elif payment.payment_method == Payment.PAYMENT.CASH_ON_DELIVERY and carrier.shipping_method == Carrier.SHIPPING.STORE:
raise ValidationError("Dobírka není možná pro osobní odběr.")
if payment.payment_method == Payment.PAYMENT.STRIPE:
session = StripeClient.create_checkout_session(order)
payment.stripe_session_id = session.id
payment.stripe_payment_intent = session.payment_intent
payment.stripe_session_url = session.url
payment.save(update_fields=[
"stripe_session_id",
"stripe_payment_intent",
"stripe_session_url",
])
return payment
# -- ORDER CREATE SERIALIZER --
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")
# Customer/billing data (optional when authenticated)
first_name = serializers.CharField(required=False)
last_name = serializers.CharField(required=False)
email = serializers.EmailField(required=False)
phone = serializers.CharField(required=False, allow_blank=True)
address = serializers.CharField(required=False)
city = serializers.CharField(required=False)
postal_code = serializers.CharField(required=False)
country = serializers.CharField(required=False, default="Czech Republic")
note = serializers.CharField(required=False, allow_blank=True)
# Nested
# Nested structures
#produkty
items = OrderItemCreateSerializer(many=True)
carrier = CarrierCreateSerializer()
payment = PaymentCreateSerializer()
#doprava/vyzvednutí + zasilkovna input (serializer)
carrier = OrderCarrierSerializer()
payment = PaymentSerializer()
#slevové kódy
discount_codes = serializers.ListField(
child=serializers.CharField(), required=False, allow_empty=True, label="Discount Codes"
child=serializers.CharField(), required=False, allow_empty=True
)
def validate(self, attrs):
request = self.context.get("request")
#kontrola jestli je uzivatel valid/prihlasen
is_auth = bool(getattr(getattr(request, "user", None), "is_authenticated", False))
# pokud není, tak se musí vyplnit povinné údaje
required_fields = [
"first_name",
"last_name",
"email",
"address",
"city",
"postal_code",
]
if not is_auth:
missing_fields = []
# přidame fieldy, které nejsou vyplněné
for field in required_fields:
if attrs.get(field) not in required_fields:
missing_fields.append(field)
if missing_fields:
raise serializers.ValidationError({"billing": f"Missing fields: {', '.join(missing_fields)}"})
# pokud chybí itemy:
if not attrs.get("items"):
raise serializers.ValidationError({"items": "At least one item is required."})
return attrs
def create(self, validated_data):
items_data = validated_data.pop("items", [])
carrier_data = validated_data.pop("carrier")
payment_data = validated_data.pop("payment")
codes = validated_data.pop("discount_codes", [])
request = self.context.get("request")
user = getattr(request, "user", None)
is_auth = bool(getattr(user, "is_authenticated", False))
with transaction.atomic():
# Create Order (user data imported on save if user is set)
order = Order(
user=user if is_auth else None,
first_name=validated_data.get("first_name", ""),
last_name=validated_data.get("last_name", ""),
email=validated_data.get("email", ""),
phone=validated_data.get("phone", ""),
address=validated_data.get("address", ""),
city=validated_data.get("city", ""),
postal_code=validated_data.get("postal_code", ""),
country=validated_data.get("country", "Czech Republic"),
note=validated_data.get("note", ""),
)
# Order.save se postara o to jestli má doplnit data z usera
order.save()
# Vytvoření Carrier skrz serializer
carrier = OrderCarrierSerializer(data=carrier_data)
carrier.is_valid(raise_exception=True)
carrier = carrier.save()
order.carrier = carrier
order.save(update_fields=["carrier", "updated_at"]) # will recalc total later
# Vytvořit Order Items individualně, aby se spustila kontrola položek na skladu
for item in items_data:
product = item["product"] # OrderItemCreateSerializer.validate
quantity = int(item.get("quantity", 1))
OrderItem.objects.create(order=order, product=product, quantity=quantity)
# -- Slevové kódy --
if codes:
discounts = list(DiscountCode.objects.filter(code__in=codes))
if discounts:
order.discount.add(*discounts)
# -- Payment --
payment_serializer = PaymentSerializer(
data=payment_data,
context={"order": order, "carrier": carrier}
)
payment_serializer.is_valid(raise_exception=True)
payment = payment_serializer.save()
# přiřadíme k orderu
order.payment = payment
order.save(update_fields=["payment"])
return order
# ----------------- ADMIN/READ MODELS -----------------
@@ -262,55 +354,102 @@ class ProductImageSerializer(serializers.ModelSerializer):
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",
]
fields = "__all__"
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",
]
fields = "__all__"
read_only_fields = ["used_count"]
class RefundSerializer(serializers.ModelSerializer):
class Meta:
model = Refund
fields = [
"id",
"order",
"reason_choice",
"reason_text",
"verified",
"created_at",
]
fields = "__all__"
read_only_fields = ["id", "verified", "created_at"]
# ----------------- READ SERIALIZERS USED BY VIEWS -----------------
class ZasilkovnaPacketReadSerializer(ZasilkovnaPacketSerializer):
class Meta(ZasilkovnaPacketSerializer.Meta):
fields = getattr(ZasilkovnaPacketSerializer.Meta, "fields", None)
class CarrierReadSerializer(serializers.ModelSerializer):
zasilkovna = ZasilkovnaPacketReadSerializer(many=True, read_only=True)
class Meta:
model = Carrier
fields = ["shipping_method", "state", "zasilkovna", "shipping_price"]
read_only_fields = fields
class ProductMiniSerializer(serializers.ModelSerializer):
class Meta:
model = Product
fields = ["id", "name", "price"]
class OrderItemReadSerializer(serializers.ModelSerializer):
product = ProductMiniSerializer(read_only=True)
class Meta:
model = OrderItem
fields = ["id", "product", "quantity"]
read_only_fields = fields
class PaymentReadSerializer(serializers.ModelSerializer):
class Meta:
model = Payment
fields = ["payment_method"]
read_only_fields = fields
class OrderMiniSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ["id", "status", "total_price", "created_at"]
read_only_fields = fields
class OrderReadSerializer(serializers.ModelSerializer):
items = OrderItemReadSerializer(many=True, read_only=True)
carrier = CarrierReadSerializer(read_only=True)
payment = PaymentReadSerializer(read_only=True)
discount_codes = serializers.SerializerMethodField()
class Meta:
model = Order
fields = [
"id",
"status",
"total_price",
"currency",
"user",
"first_name",
"last_name",
"email",
"phone",
"address",
"city",
"postal_code",
"country",
"note",
"created_at",
"updated_at",
"items",
"carrier",
"payment",
"discount_codes",
]
read_only_fields = fields
def get_discount_codes(self, obj: Order):
return list(obj.discount.values_list("code", flat=True))