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.
456 lines
12 KiB
Python
456 lines
12 KiB
Python
from rest_framework import serializers
|
|
|
|
from backend.thirdparty.stripe.client import StripeClient
|
|
|
|
from .models import Refund, Order, Invoice
|
|
|
|
|
|
class RefundCreatePublicSerializer(serializers.Serializer):
|
|
email = serializers.EmailField()
|
|
invoice_number = serializers.CharField(required=False, allow_blank=True)
|
|
order_id = serializers.IntegerField(required=False)
|
|
|
|
# Optional reason fields
|
|
reason_choice = serializers.ChoiceField(
|
|
choices=Refund.Reason.choices, required=False
|
|
)
|
|
reason_text = serializers.CharField(required=False, allow_blank=True)
|
|
|
|
def validate(self, attrs):
|
|
email = attrs.get("email")
|
|
invoice_number = (attrs.get("invoice_number") or "").strip()
|
|
order_id = attrs.get("order_id")
|
|
|
|
if not invoice_number and not order_id:
|
|
raise serializers.ValidationError(
|
|
"Provide either invoice_number or order_id."
|
|
)
|
|
|
|
order = None
|
|
if invoice_number:
|
|
try:
|
|
invoice = Invoice.objects.get(invoice_number=invoice_number)
|
|
order = invoice.order
|
|
except Invoice.DoesNotExist:
|
|
raise serializers.ValidationError({"invoice_number": "Invoice not found."})
|
|
except Order.DoesNotExist:
|
|
raise serializers.ValidationError({"invoice_number": "Order for invoice not found."})
|
|
|
|
if order_id and order is None:
|
|
try:
|
|
order = Order.objects.get(id=order_id)
|
|
except Order.DoesNotExist:
|
|
raise serializers.ValidationError({"order_id": "Order not found."})
|
|
|
|
# Verify email matches order's email or user's email
|
|
if not order:
|
|
raise serializers.ValidationError("Order could not be resolved.")
|
|
|
|
order_email = (order.email or "").strip().lower()
|
|
user_email = (getattr(order.user, "email", "") or "").strip().lower()
|
|
provided = email.strip().lower()
|
|
if provided not in {order_email, user_email}:
|
|
raise serializers.ValidationError({"email": "Email does not match the order."})
|
|
|
|
attrs["order"] = order
|
|
return attrs
|
|
|
|
def create(self, validated_data):
|
|
order = validated_data["order"]
|
|
reason_choice = validated_data.get("reason_choice") or Refund.Reason.OTHER
|
|
reason_text = validated_data.get("reason_text", "")
|
|
|
|
refund = Refund.objects.create(
|
|
order=order,
|
|
reason_choice=reason_choice,
|
|
reason_text=reason_text,
|
|
)
|
|
return refund
|
|
|
|
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,
|
|
Product,
|
|
ProductImage,
|
|
DiscountCode,
|
|
Refund,
|
|
Order,
|
|
OrderItem,
|
|
Carrier,
|
|
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 -----------------
|
|
|
|
#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 Meta:
|
|
model = Carrier
|
|
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
|
|
|
|
|
|
#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
|
|
|
|
|
|
|
|
# -- PAYMENT --
|
|
class PaymentSerializer(serializers.ModelSerializer):
|
|
|
|
class Meta:
|
|
model = Payment
|
|
fields = [
|
|
"id",
|
|
"payment_method",
|
|
"stripe",
|
|
"stripe_session_id",
|
|
"stripe_payment_intent",
|
|
"stripe_session_url",
|
|
]
|
|
read_only_fields = [
|
|
"id",
|
|
"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")
|
|
|
|
with transaction.atomic():
|
|
payment = Payment.objects.create(
|
|
order=order,
|
|
carrier=carrier,
|
|
**validated_data
|
|
)
|
|
|
|
# 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 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 structures
|
|
#produkty
|
|
items = OrderItemCreateSerializer(many=True)
|
|
|
|
#doprava/vyzvednutí + zasilkovna input (serializer)
|
|
carrier = OrderCarrierSerializer()
|
|
|
|
payment = PaymentSerializer()
|
|
|
|
#slevové kódy
|
|
discount_codes = serializers.ListField(
|
|
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 -----------------
|
|
|
|
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 = "__all__"
|
|
read_only_fields = ["created_at", "updated_at"]
|
|
|
|
|
|
class DiscountCodeSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = DiscountCode
|
|
fields = "__all__"
|
|
read_only_fields = ["used_count"]
|
|
|
|
|
|
class RefundSerializer(serializers.ModelSerializer):
|
|
class Meta:
|
|
model = Refund
|
|
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))
|
|
|
|
|