Introduces a Review model for product reviews, including rating and comment fields. Adds a public serializer and a ModelViewSet for reviews with search and ordering capabilities. Also updates the frontend API client to use the correct token refresh endpoint and improves FormData handling.
473 lines
13 KiB
Python
473 lines
13 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,
|
|
)
|
|
|
|
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.Serializer):
|
|
|
|
class Meta:
|
|
model = Review
|
|
fields = "__all__" |