from django.urls import re_path from rest_framework import routers, viewsets, status from rest_framework.decorators import api_view, permission_classes, action from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework import serializers from django.shortcuts import get_object_or_404 from .models import Shipment from inventory.models import Item from tickets.models import IssueThread class ShipmentSerializer(serializers.ModelSerializer): related_items = serializers.PrimaryKeyRelatedField( many=True, queryset=Item.objects.all(), required=False ) related_tickets = serializers.PrimaryKeyRelatedField( many=True, queryset=IssueThread.objects.all(), required=False ) class Meta: model = Shipment fields = [ 'id', 'public_secret', 'state', 'review_requirement', 'recipient_name', 'address_supplements', 'street_address', 'city', 'state_province', 'postal_code', 'country', 'related_items', 'related_tickets', 'created_at', 'updated_at', 'shipped_at', 'completed_at' ] read_only_fields = ['id', 'public_secret', 'created_at', 'updated_at', 'shipped_at', 'completed_at'] class PublicShipmentSerializer(serializers.ModelSerializer): related_items = serializers.PrimaryKeyRelatedField( many=True, read_only=True ) class Meta: model = Shipment fields = [ 'id', 'public_secret', 'state', 'review_requirement', 'recipient_name', 'address_supplements', 'street_address', 'city', 'state_province', 'postal_code', 'country', 'related_items', 'created_at' ] read_only_fields = ['id', 'public_secret', 'state', 'review_requirement', 'created_at'] class ShipmentAddressUpdateSerializer(serializers.ModelSerializer): class Meta: model = Shipment fields = [ 'recipient_name', 'address_supplements', 'street_address', 'city', 'state_province', 'postal_code', 'country' ] class ShipmentViewSet(viewsets.ModelViewSet): serializer_class = ShipmentSerializer queryset = Shipment.objects.all() permission_classes = [IsAuthenticated] @action(detail=True, methods=['post']) def add_items(self, request, pk=None): shipment = self.get_object() review_requirement = request.data.get('review_requirement') item_ids = request.data.get('item_ids', []) try: items = Item.objects.filter(id__in=item_ids) shipment.related_items.add(*items) shipment.add_items(review_requirement) return Response(ShipmentSerializer(shipment).data) except Exception as e: return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) @action(detail=True, methods=['post']) def approve(self, request, pk=None): shipment = self.get_object() try: shipment.approve() return Response(ShipmentSerializer(shipment).data) except Exception as e: return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) @action(detail=True, methods=['post']) def reject(self, request, pk=None): shipment = self.get_object() try: shipment.reject() return Response(ShipmentSerializer(shipment).data) except Exception as e: return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) @action(detail=True, methods=['post']) def mark_shipped(self, request, pk=None): shipment = self.get_object() try: # Validate state transition before attempting to mark as shipped if shipment.state != Shipment.READY_FOR_SHIPPING: return Response( {'error': f'Invalid state transition: Cannot mark shipment as shipped from {shipment.state} state. Shipment must be in READY_FOR_SHIPPING state.'}, status=status.HTTP_400_BAD_REQUEST ) shipment.mark_shipped() return Response(ShipmentSerializer(shipment).data) except Exception as e: return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) @action(detail=True, methods=['post']) def mark_completed(self, request, pk=None): shipment = self.get_object() try: # Validate state transition before attempting to mark as completed if shipment.state != Shipment.SHIPPED: return Response( {'error': f'Invalid state transition: Cannot mark shipment as completed from {shipment.state} state. Shipment must be in SHIPPED state.'}, status=status.HTTP_400_BAD_REQUEST ) shipment.mark_completed() return Response(ShipmentSerializer(shipment).data) except Exception as e: return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) # Public API endpoints for shipment recipients @api_view(['GET']) @permission_classes([AllowAny]) def public_shipment_detail(request, public_secret): """ Retrieve shipment details using public_secret. Only accessible when shipment is in PENDING_REVIEW state. """ try: shipment = Shipment.objects.get(public_secret=public_secret) # Only allow access to shipments in PENDING_REVIEW state if shipment.state != Shipment.PENDING_REVIEW: return Response(status=status.HTTP_404_NOT_FOUND) serializer = PublicShipmentSerializer(shipment) return Response(serializer.data) except Shipment.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) @api_view(['POST']) @permission_classes([AllowAny]) def public_shipment_update(request, public_secret): """ Update shipment with address information and transition state. Only accessible when shipment is in PENDING_REVIEW state. """ try: shipment = Shipment.objects.get(public_secret=public_secret) # Only allow updates to shipments in PENDING_REVIEW state if shipment.state != Shipment.PENDING_REVIEW: return Response(status=status.HTTP_404_NOT_FOUND) serializer = ShipmentAddressUpdateSerializer(shipment, data=request.data, partial=True) if serializer.is_valid(): # Update address fields for field, value in serializer.validated_data.items(): setattr(shipment, field, value) # Update the shipment state based on review requirement try: if shipment.review_requirement == Shipment.REVIEW_ADDRESS: # If only address review was required, we can approve directly shipment.approve() elif shipment.review_requirement == Shipment.REVIEW_BOTH: # If both items and address review were required, we need to check if items exist if not shipment.related_items.exists(): return Response( {'error': 'Items must be added before approval'}, status=status.HTTP_400_BAD_REQUEST ) shipment.approve() else: # For ITEMS review requirement, we just save the address but don't change state shipment.save() return Response(PublicShipmentSerializer(shipment).data) except Exception as e: return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) except Shipment.DoesNotExist: return Response(status=status.HTTP_404_NOT_FOUND) router = routers.SimpleRouter() router.register(r'shipments', ShipmentViewSet, basename='shipments') urlpatterns = router.urls + [ re_path(r'^public/shipments/(?P[0-9a-f-]+)/$', public_shipment_detail, name='public-shipment-detail'), re_path(r'^public/shipments/(?P[0-9a-f-]+)/update/$', public_shipment_update, name='public-shipment-update'), ]