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.
417 lines
14 KiB
Python
417 lines
14 KiB
Python
from rest_framework.views import APIView
|
|
from rest_framework.permissions import AllowAny
|
|
from rest_framework.response import Response
|
|
from rest_framework import status
|
|
import base64
|
|
|
|
from .models import Refund
|
|
from .serializers import RefundCreatePublicSerializer
|
|
|
|
|
|
from django.db import transaction
|
|
from rest_framework import viewsets, mixins, status
|
|
from rest_framework.permissions import AllowAny, IsAdminUser, SAFE_METHODS
|
|
from rest_framework.decorators import action
|
|
from rest_framework.response import Response
|
|
from drf_spectacular.utils import extend_schema, extend_schema_view, OpenApiExample
|
|
from rest_framework import filters, permissions
|
|
|
|
from .models import (
|
|
Order,
|
|
OrderItem,
|
|
Carrier,
|
|
Payment,
|
|
Product,
|
|
DiscountCode,
|
|
Category,
|
|
ProductImage,
|
|
Refund,
|
|
)
|
|
from .serializers import (
|
|
OrderReadSerializer,
|
|
OrderMiniSerializer,
|
|
OrderCreateSerializer,
|
|
OrderItemReadSerializer,
|
|
|
|
CarrierReadSerializer,
|
|
PaymentReadSerializer,
|
|
ProductSerializer,
|
|
CategorySerializer,
|
|
ProductImageSerializer,
|
|
DiscountCodeSerializer,
|
|
RefundSerializer,
|
|
)
|
|
|
|
#FIXME: uravit view na nový order serializer
|
|
@extend_schema_view(
|
|
list=extend_schema(tags=["Orders"], summary="List Orders (public)"),
|
|
retrieve=extend_schema(tags=["Orders"], summary="Retrieve Order (public)"),
|
|
)
|
|
class OrderViewSet(mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet):
|
|
queryset = Order.objects.select_related("carrier", "payment").prefetch_related(
|
|
"items__product", "discount"
|
|
).order_by("-created_at")
|
|
permission_classes = [AllowAny]
|
|
|
|
def get_serializer_class(self):
|
|
if self.action == "mini":
|
|
return OrderMiniSerializer
|
|
if self.action in ["list", "retrieve"]:
|
|
return OrderReadSerializer
|
|
if self.action == "create":
|
|
return OrderCreateSerializer
|
|
return OrderReadSerializer
|
|
|
|
@extend_schema(
|
|
tags=["Order"],
|
|
summary="Create Order (public)",
|
|
request=OrderCreateSerializer,
|
|
responses={201: OrderReadSerializer},
|
|
examples=[
|
|
OpenApiExample(
|
|
"Create order",
|
|
value={
|
|
"first_name": "Jan",
|
|
"last_name": "Novak",
|
|
"email": "jan@example.com",
|
|
"phone": "+420123456789",
|
|
"address": "Ulice 1",
|
|
"city": "Praha",
|
|
"postal_code": "11000",
|
|
"country": "Czech Republic",
|
|
"note": "Prosím doručit odpoledne",
|
|
"items": [
|
|
{"product_id": 1, "quantity": 2},
|
|
{"product_id": 7, "quantity": 1},
|
|
],
|
|
"carrier": {"shipping_method": "store"},
|
|
"payment": {"payment_method": "stripe"},
|
|
"discount_codes": ["WELCOME10"],
|
|
},
|
|
)
|
|
],
|
|
)
|
|
def create(self, request, *args, **kwargs):
|
|
serializer = OrderCreateSerializer(data=request.data, context={"request": request})
|
|
serializer.is_valid(raise_exception=True)
|
|
order = serializer.save()
|
|
out = OrderReadSerializer(order)
|
|
return Response(out.data, status=status.HTTP_201_CREATED)
|
|
|
|
# -- List mini orders -- (public) --
|
|
@action(detail=False, methods=["get"], url_path="detail")
|
|
@extend_schema(
|
|
tags=["Orders"],
|
|
summary="List mini orders (public)",
|
|
responses={200: OrderMiniSerializer(many=True)},
|
|
)
|
|
def mini(self, request, *args, **kwargs):
|
|
qs = self.get_queryset()
|
|
page = self.paginate_queryset(qs)
|
|
|
|
if page is not None:
|
|
ser = OrderMiniSerializer(page, many=True)
|
|
return self.get_paginated_response(ser.data)
|
|
|
|
ser = OrderMiniSerializer(qs, many=True)
|
|
return Response(ser.data)
|
|
|
|
# -- Get order items -- (public) --
|
|
@action(detail=True, methods=["get"], url_path="items")
|
|
@extend_schema(
|
|
tags=["Orders"],
|
|
summary="List order items (public)",
|
|
responses={200: OrderItemReadSerializer(many=True)},
|
|
)
|
|
def items(self, request, pk=None):
|
|
order = self.get_object()
|
|
qs = order.items.select_related("product").all()
|
|
ser = OrderItemReadSerializer(qs, many=True)
|
|
return Response(ser.data)
|
|
|
|
# -- Get order carrier -- (public) --
|
|
@action(detail=True, methods=["get"], url_path="carrier")
|
|
@extend_schema(
|
|
tags=["Orders"],
|
|
summary="Get order carrier (public)",
|
|
responses={200: CarrierReadSerializer},
|
|
)
|
|
def carrier_detail(self, request, pk=None):
|
|
order = self.get_object()
|
|
ser = CarrierReadSerializer(order.carrier)
|
|
return Response(ser.data)
|
|
|
|
# -- Get order payment -- (public) --
|
|
@action(detail=True, methods=["get"], url_path="payment")
|
|
@extend_schema(
|
|
tags=["Orders"],
|
|
summary="Get order payment (public)",
|
|
responses={200: PaymentReadSerializer},
|
|
)
|
|
def payment_detail(self, request, pk=None):
|
|
order = self.get_object()
|
|
ser = PaymentReadSerializer(order.payment)
|
|
return Response(ser.data)
|
|
|
|
# -- Mark carrier ready to pickup(store) (admin) --
|
|
@action(
|
|
detail=True,
|
|
methods=["patch"],
|
|
url_path="carrier/ready-to-pickup",
|
|
permission_classes=[IsAdminUser],
|
|
)
|
|
@extend_schema(
|
|
tags=["Orders"],
|
|
summary="Mark carrier ready to pickup (admin)",
|
|
request=None,
|
|
responses={200: CarrierReadSerializer},
|
|
)
|
|
def carrier_ready_to_pickup(self, request, pk=None):
|
|
order = self.get_object()
|
|
if not order.carrier:
|
|
return Response({"detail": "Carrier not set."}, status=400)
|
|
order.carrier.ready_to_pickup()
|
|
order.carrier.refresh_from_db()
|
|
ser = CarrierReadSerializer(order.carrier)
|
|
return Response(ser.data)
|
|
|
|
# -- Start ordering shipping (admin) --
|
|
@action(
|
|
detail=True,
|
|
methods=["patch"],
|
|
url_path="carrier/start-ordering-shipping",
|
|
permission_classes=[IsAdminUser],
|
|
)
|
|
@extend_schema(
|
|
tags=["Orders"],
|
|
summary="Start ordering shipping (admin)",
|
|
request=None,
|
|
responses={200: CarrierReadSerializer},
|
|
)
|
|
def carrier_start_ordering_shipping(self, request, pk=None):
|
|
order = self.get_object()
|
|
if not order.carrier:
|
|
return Response({"detail": "Carrier not set."}, status=400)
|
|
order.carrier.start_ordering_shipping()
|
|
order.carrier.refresh_from_db()
|
|
ser = CarrierReadSerializer(order.carrier)
|
|
return Response(ser.data)
|
|
|
|
|
|
# -- Invoice PDF for Order --
|
|
class OrderInvoice(viewsets.ViewSet):
|
|
@action(detail=True, methods=["get"], url_path="generate-invoice")
|
|
@extend_schema(
|
|
tags=["Orders"],
|
|
summary="Get Invoice PDF for Order (public)",
|
|
responses={200: "PDF File"},
|
|
)
|
|
def get(order_id, request):
|
|
try:
|
|
order = Order.objects.get(pk=order_id)
|
|
except Order.DoesNotExist:
|
|
return Response({"detail": "Order not found."}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
return Response(order.invoice.pdf_file, content_type='application/pdf')
|
|
|
|
|
|
# ---------- Permissions helpers ----------
|
|
|
|
class AdminWriteOnlyOrReadOnly(AllowAny.__class__):
|
|
def has_permission(self, request, view):
|
|
if request.method in SAFE_METHODS:
|
|
return True
|
|
return IsAdminUser().has_permission(request, view)
|
|
|
|
|
|
class AdminOnlyForPatchOtherwisePublic(AllowAny.__class__):
|
|
def has_permission(self, request, view):
|
|
if request.method in SAFE_METHODS or request.method == "POST":
|
|
return True
|
|
if request.method == "PATCH":
|
|
return IsAdminUser().has_permission(request, view)
|
|
# default to admin for other unsafe
|
|
return IsAdminUser().has_permission(request, view)
|
|
|
|
|
|
# ---------- Public/admin viewsets ----------
|
|
|
|
@extend_schema_view(
|
|
list=extend_schema(tags=["Products"], summary="List products (public)"),
|
|
retrieve=extend_schema(tags=["Products"], summary="Retrieve product (public)"),
|
|
create=extend_schema(tags=["Products"], summary="Create product (auth required)"),
|
|
partial_update=extend_schema(tags=["Products"], summary="Update product (auth required)"),
|
|
update=extend_schema(tags=["Products"], summary="Replace product (auth required)"),
|
|
destroy=extend_schema(tags=["Products"], summary="Delete product (auth required)"),
|
|
)
|
|
class ProductViewSet(viewsets.ModelViewSet):
|
|
queryset = Product.objects.all()
|
|
serializer_class = ProductSerializer
|
|
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
|
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
|
|
search_fields = ["name", "code"]
|
|
ordering_fields = ["price", "name", "created_at"]
|
|
ordering = ["price"]
|
|
|
|
|
|
@extend_schema_view(
|
|
list=extend_schema(tags=["Categories"], summary="List categories (public)"),
|
|
retrieve=extend_schema(tags=["Categories"], summary="Retrieve category (public)"),
|
|
create=extend_schema(tags=["Categories"], summary="Create category (auth required)"),
|
|
partial_update=extend_schema(tags=["Categories"], summary="Update category (auth required)"),
|
|
update=extend_schema(tags=["Categories"], summary="Replace category (auth required)"),
|
|
destroy=extend_schema(tags=["Categories"], summary="Delete category (auth required)"),
|
|
)
|
|
class CategoryViewSet(viewsets.ModelViewSet):
|
|
queryset = Category.objects.all()
|
|
serializer_class = CategorySerializer
|
|
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
|
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
|
|
search_fields = ["name", "description"]
|
|
ordering_fields = ["name", "id"]
|
|
ordering = ["name"]
|
|
|
|
|
|
@extend_schema_view(
|
|
list=extend_schema(tags=["Product Images"], summary="List product images (public)"),
|
|
retrieve=extend_schema(tags=["Product Images"], summary="Retrieve product image (public)"),
|
|
create=extend_schema(tags=["Product Images"], summary="Create product image (auth required)"),
|
|
partial_update=extend_schema(tags=["Product Images"], summary="Update product image (auth required)"),
|
|
update=extend_schema(tags=["Product Images"], summary="Replace product image (auth required)"),
|
|
destroy=extend_schema(tags=["Product Images"], summary="Delete product image (auth required)"),
|
|
)
|
|
class ProductImageViewSet(viewsets.ModelViewSet):
|
|
queryset = ProductImage.objects.all()
|
|
serializer_class = ProductImageSerializer
|
|
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
|
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
|
|
search_fields = ["alt_text", "product__name"]
|
|
ordering_fields = ["id", "product__name"]
|
|
ordering = ["-id"]
|
|
|
|
|
|
@extend_schema_view(
|
|
list=extend_schema(tags=["Discount Codes"], summary="List discount codes (public)"),
|
|
retrieve=extend_schema(tags=["Discount Codes"], summary="Retrieve discount code (public)"),
|
|
create=extend_schema(tags=["Discount Codes"], summary="Create discount code (auth required)"),
|
|
partial_update=extend_schema(tags=["Discount Codes"], summary="Update discount code (auth required)"),
|
|
update=extend_schema(tags=["Discount Codes"], summary="Replace discount code (auth required)"),
|
|
destroy=extend_schema(tags=["Discount Codes"], summary="Delete discount code (auth required)"),
|
|
)
|
|
class DiscountCodeViewSet(viewsets.ModelViewSet):
|
|
queryset = DiscountCode.objects.all()
|
|
serializer_class = DiscountCodeSerializer
|
|
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
|
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
|
|
search_fields = ["code", "description"]
|
|
ordering_fields = ["percent", "amount", "valid_from", "valid_to"]
|
|
ordering = ["-valid_from"]
|
|
|
|
|
|
# -- Refund (Admin only) --
|
|
@extend_schema_view(
|
|
list=extend_schema(tags=["Refunds"], summary="List refunds (admin)"),
|
|
retrieve=extend_schema(tags=["Refunds"], summary="Retrieve refund (admin)"),
|
|
create=extend_schema(tags=["Refunds"], summary="Create refund (admin)"),
|
|
partial_update=extend_schema(tags=["Refunds"], summary="Update refund (admin)"),
|
|
update=extend_schema(tags=["Refunds"], summary="Replace refund (admin)"),
|
|
destroy=extend_schema(tags=["Refunds"], summary="Delete refund (admin)"),
|
|
)
|
|
class RefundViewSet(viewsets.ModelViewSet):
|
|
queryset = Refund.objects.select_related("order").all().order_by("-created_at")
|
|
serializer_class = RefundSerializer
|
|
permission_classes = [IsAdminUser]
|
|
|
|
|
|
|
|
|
|
|
|
class RefundPublicView(APIView):
|
|
"""Public endpoint to create and fetch refund objects.
|
|
|
|
POST: Create a refund given email and invoice_number or order_id.
|
|
Returns JSON with refund info, order items, and a base64 PDF payload.
|
|
GET: Return a refund object by id (query param `id`).
|
|
"""
|
|
|
|
permission_classes = [AllowAny]
|
|
|
|
def get(self, request):
|
|
rid = request.query_params.get("id")
|
|
if not rid:
|
|
return Response({"detail": "Missing 'id' query parameter."}, status=status.HTTP_400_BAD_REQUEST)
|
|
try:
|
|
refund = Refund.objects.select_related("order").get(id=rid)
|
|
except Refund.DoesNotExist:
|
|
return Response({"detail": "Refund not found."}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
order = refund.order
|
|
items = []
|
|
for it in order.items.select_related('product').all():
|
|
items.append({
|
|
"product_name": getattr(it.product, "name", "Item"),
|
|
"sku": getattr(it.product, "code", None),
|
|
"quantity": it.quantity,
|
|
})
|
|
|
|
data = {
|
|
"refund": {
|
|
"id": refund.id,
|
|
"order_id": order.id,
|
|
"reason_choice": refund.reason_choice,
|
|
"reason_text": refund.reason_text,
|
|
"verified": refund.verified,
|
|
"created_at": refund.created_at,
|
|
},
|
|
"order": {
|
|
"id": order.id,
|
|
"email": order.email,
|
|
"created_at": order.created_at,
|
|
"items": items,
|
|
},
|
|
}
|
|
return Response(data)
|
|
|
|
def post(self, request):
|
|
serializer = RefundCreatePublicSerializer(data=request.data)
|
|
if not serializer.is_valid():
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
refund = serializer.save()
|
|
order = refund.order
|
|
|
|
# Build items list for response
|
|
items = []
|
|
for it in order.items.select_related('product').all():
|
|
items.append({
|
|
"product_name": getattr(it.product, "name", "Item"),
|
|
"sku": getattr(it.product, "code", None),
|
|
"quantity": it.quantity,
|
|
})
|
|
|
|
# Generate PDF bytes using model helper
|
|
pdf_bytes = refund.generate_refund_pdf_for_customer()
|
|
pdf_b64 = base64.b64encode(pdf_bytes).decode('ascii')
|
|
|
|
resp = {
|
|
"refund": {
|
|
"id": refund.id,
|
|
"order_id": order.id,
|
|
"reason_choice": refund.reason_choice,
|
|
"reason_text": refund.reason_text,
|
|
"verified": refund.verified,
|
|
"created_at": refund.created_at,
|
|
},
|
|
"order": {
|
|
"id": order.id,
|
|
"email": order.email,
|
|
"created_at": order.created_at,
|
|
"items": items,
|
|
},
|
|
"pdf": {
|
|
"filename": f"refund_{refund.id}.pdf",
|
|
"content_type": "application/pdf",
|
|
"base64": pdf_b64,
|
|
},
|
|
}
|
|
return Response(resp, status=status.HTTP_201_CREATED) |