Add public refund creation endpoint and PDF generation
Introduces RefundPublicView for public refund creation via email and invoice/order ID, returning refund info and a base64-encoded PDF slip. Adds RefundCreatePublicSerializer for validation and creation, implements PDF generation in Refund model, and provides a customer-facing HTML template for the return slip. Updates URLs to expose the new endpoint.
This commit is contained in:
@@ -1,9 +1,20 @@
|
||||
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,
|
||||
@@ -21,6 +32,7 @@ from .serializers import (
|
||||
OrderMiniSerializer,
|
||||
OrderCreateSerializer,
|
||||
OrderItemReadSerializer,
|
||||
|
||||
CarrierReadSerializer,
|
||||
PaymentReadSerializer,
|
||||
ProductSerializer,
|
||||
@@ -301,82 +313,182 @@ class AdminOnlyForPatchOtherwisePublic(AllowAny.__class__):
|
||||
|
||||
# ---------- Public/admin viewsets ----------
|
||||
|
||||
# -- Product --
|
||||
@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 (admin)"),
|
||||
partial_update=extend_schema(tags=["Products"], summary="Update product (admin)"),
|
||||
update=extend_schema(tags=["Products"], summary="Replace product (admin)"),
|
||||
destroy=extend_schema(tags=["Products"], summary="Delete product (admin)"),
|
||||
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().order_by("-created_at")
|
||||
queryset = Product.objects.all()
|
||||
serializer_class = ProductSerializer
|
||||
permission_classes = [AdminWriteOnlyOrReadOnly]
|
||||
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
||||
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
|
||||
search_fields = ["name", "code"]
|
||||
ordering_fields = ["price", "name", "created_at"]
|
||||
ordering = ["price"]
|
||||
|
||||
|
||||
# -- Category --
|
||||
@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 (admin)"),
|
||||
partial_update=extend_schema(tags=["Categories"], summary="Update category (admin)"),
|
||||
update=extend_schema(tags=["Categories"], summary="Replace category (admin)"),
|
||||
destroy=extend_schema(tags=["Categories"], summary="Delete category (admin)"),
|
||||
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().order_by("name")
|
||||
queryset = Category.objects.all()
|
||||
serializer_class = CategorySerializer
|
||||
permission_classes = [AdminWriteOnlyOrReadOnly]
|
||||
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
||||
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
|
||||
search_fields = ["name", "description"]
|
||||
ordering_fields = ["name", "id"]
|
||||
ordering = ["name"]
|
||||
|
||||
|
||||
# -- Product Image --
|
||||
@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 (admin)"),
|
||||
partial_update=extend_schema(tags=["Product Images"], summary="Update product image (admin)"),
|
||||
update=extend_schema(tags=["Product Images"], summary="Replace product image (admin)"),
|
||||
destroy=extend_schema(tags=["Product Images"], summary="Delete product image (admin)"),
|
||||
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().order_by("-id")
|
||||
queryset = ProductImage.objects.all()
|
||||
serializer_class = ProductImageSerializer
|
||||
permission_classes = [AdminWriteOnlyOrReadOnly]
|
||||
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
|
||||
filter_backends = [filters.SearchFilter, filters.OrderingFilter]
|
||||
search_fields = ["alt_text", "product__name"]
|
||||
ordering_fields = ["id", "product__name"]
|
||||
ordering = ["-id"]
|
||||
|
||||
|
||||
# -- Discount Code --
|
||||
@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 (admin)"),
|
||||
partial_update=extend_schema(tags=["Discount Codes"], summary="Update discount code (admin)"),
|
||||
update=extend_schema(tags=["Discount Codes"], summary="Replace discount code (admin)"),
|
||||
destroy=extend_schema(tags=["Discount Codes"], summary="Delete discount code (admin)"),
|
||||
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().order_by("-id")
|
||||
queryset = DiscountCode.objects.all()
|
||||
serializer_class = DiscountCodeSerializer
|
||||
permission_classes = [AdminWriteOnlyOrReadOnly]
|
||||
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 --
|
||||
# -- Refund (Admin only) --
|
||||
@extend_schema_view(
|
||||
list=extend_schema(tags=["Refunds"], summary="List refunds (public)"),
|
||||
retrieve=extend_schema(tags=["Refunds"], summary="Retrieve refund (public)"),
|
||||
create=extend_schema(tags=["Refunds"], summary="Create refund (public)"),
|
||||
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(mixins.CreateModelMixin,
|
||||
mixins.ListModelMixin,
|
||||
mixins.RetrieveModelMixin,
|
||||
mixins.UpdateModelMixin,
|
||||
viewsets.GenericViewSet):
|
||||
class RefundViewSet(viewsets.ModelViewSet):
|
||||
queryset = Refund.objects.select_related("order").all().order_by("-created_at")
|
||||
serializer_class = RefundSerializer
|
||||
permission_classes = [AdminOnlyForPatchOtherwisePublic]
|
||||
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)
|
||||
Reference in New Issue
Block a user