Files
vontor-cz/backend/commerce/serializers.py
Brunobrno b279ac36d5 Add shopping cart and product review features
Introduces Cart and CartItem models, admin, serializers, and API endpoints for shopping cart management for both authenticated and anonymous users. Adds Review model, serializers, and endpoints for product reviews, including public creation and retrieval. Updates ProductImage ordering, enhances order save logic with notification, and improves product and order endpoints with new actions and filters. Includes related migrations for commerce, configuration, social chat, and Deutsche Post integration.
2026-01-17 02:38:02 +01:00

526 lines
14 KiB
Python

from rest_framework import serializers
from thirdparty.stripe.client import StripeClient
from .models import Refund, Order, Invoice, Review
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,
Cart,
CartItem,
)
from thirdparty.stripe.models import StripeModel
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):
stripe_session_id = serializers.CharField(source='stripe.stripe_session_id', read_only=True)
stripe_payment_intent = serializers.CharField(source='stripe.stripe_payment_intent', read_only=True)
stripe_session_url = serializers.URLField(source='stripe.stripe_session_url', read_only=True)
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)
stripe_instance = StripeModel.objects.create(
stripe_session_id=session.id,
stripe_payment_intent=session.payment_intent,
stripe_session_url=session.url,
status=StripeModel.STATUS_CHOICES.PENDING
)
payment.stripe = stripe_instance
payment.save(update_fields=["stripe"])
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):
status = serializers.ChoiceField(
choices=Order.OrderStatus.choices,
read_only=True
)
class Meta:
model = Order
fields = ["id", "status", "total_price", "created_at"]
read_only_fields = fields
class OrderReadSerializer(serializers.ModelSerializer):
status = serializers.ChoiceField(
choices=Order.OrderStatus.choices,
read_only=True
)
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))
class ReviewSerializerPublic(serializers.ModelSerializer):
class Meta:
model = Review
fields = "__all__"
read_only_fields = ['user', 'created_at', 'updated_at']
# ----------------- CART SERIALIZERS -----------------
class CartItemSerializer(serializers.ModelSerializer):
product_name = serializers.CharField(source='product.name', read_only=True)
product_price = serializers.DecimalField(source='product.price', max_digits=10, decimal_places=2, read_only=True)
subtotal = serializers.SerializerMethodField()
class Meta:
model = CartItem
fields = ['id', 'product', 'product_name', 'product_price', 'quantity', 'subtotal', 'added_at']
read_only_fields = ['id', 'added_at']
def get_subtotal(self, obj):
return obj.get_subtotal()
def validate_quantity(self, value):
if value < 1:
raise serializers.ValidationError("Quantity must be at least 1")
return value
class CartItemCreateSerializer(serializers.Serializer):
product_id = serializers.IntegerField()
quantity = serializers.IntegerField(min_value=1, default=1)
def validate_product_id(self, value):
try:
Product.objects.get(pk=value, is_active=True)
except Product.DoesNotExist:
raise serializers.ValidationError("Product not found or inactive.")
return value
class CartSerializer(serializers.ModelSerializer):
items = CartItemSerializer(many=True, read_only=True)
total = serializers.SerializerMethodField()
items_count = serializers.SerializerMethodField()
class Meta:
model = Cart
fields = ['id', 'user', 'items', 'total', 'items_count', 'created_at', 'updated_at']
read_only_fields = ['id', 'user', 'created_at', 'updated_at']
def get_total(self, obj):
return obj.get_total()
def get_items_count(self, obj):
return obj.get_items_count()