This commit is contained in:
parent
6a5d954980
commit
3d01f16750
2 changed files with 370 additions and 2 deletions
|
@ -2,8 +2,9 @@ from django.urls import re_path
|
||||||
from rest_framework import routers, viewsets, status
|
from rest_framework import routers, viewsets, status
|
||||||
from rest_framework.decorators import api_view, permission_classes, action
|
from rest_framework.decorators import api_view, permission_classes, action
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated, AllowAny
|
||||||
from rest_framework import serializers
|
from rest_framework import serializers
|
||||||
|
from django.shortcuts import get_object_or_404
|
||||||
|
|
||||||
from .models import Shipment
|
from .models import Shipment
|
||||||
from inventory.models import Item
|
from inventory.models import Item
|
||||||
|
@ -34,6 +35,32 @@ class ShipmentSerializer(serializers.ModelSerializer):
|
||||||
read_only_fields = ['id', 'public_secret', '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):
|
class ShipmentViewSet(viewsets.ModelViewSet):
|
||||||
serializer_class = ShipmentSerializer
|
serializer_class = ShipmentSerializer
|
||||||
queryset = Shipment.objects.all()
|
queryset = Shipment.objects.all()
|
||||||
|
@ -102,7 +129,77 @@ class ShipmentViewSet(viewsets.ModelViewSet):
|
||||||
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
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 = routers.SimpleRouter()
|
||||||
router.register(r'shipments', ShipmentViewSet, basename='shipments')
|
router.register(r'shipments', ShipmentViewSet, basename='shipments')
|
||||||
|
|
||||||
urlpatterns = router.urls
|
urlpatterns = router.urls + [
|
||||||
|
re_path(r'^public/shipments/(?P<public_secret>[0-9a-f-]+)/$', public_shipment_detail, name='public-shipment-detail'),
|
||||||
|
re_path(r'^public/shipments/(?P<public_secret>[0-9a-f-]+)/update/$', public_shipment_update, name='public-shipment-update'),
|
||||||
|
]
|
||||||
|
|
|
@ -322,3 +322,274 @@ class ShipmentApiTest(TestCase):
|
||||||
# Verify state hasn't changed
|
# Verify state hasn't changed
|
||||||
shipment.refresh_from_db()
|
shipment.refresh_from_db()
|
||||||
self.assertEqual(shipment.state, Shipment.PENDING_REVIEW)
|
self.assertEqual(shipment.state, Shipment.PENDING_REVIEW)
|
||||||
|
|
||||||
|
def test_public_shipment_detail_success(self):
|
||||||
|
"""Test retrieving shipment details via public API with valid public_secret"""
|
||||||
|
# Create a shipment in PENDING_REVIEW state
|
||||||
|
shipment = Shipment.objects.create(
|
||||||
|
recipient_name='John Doe',
|
||||||
|
street_address='123 Main St',
|
||||||
|
city='Anytown',
|
||||||
|
state_province='State',
|
||||||
|
postal_code='12345',
|
||||||
|
country='Country',
|
||||||
|
state=Shipment.PENDING_REVIEW,
|
||||||
|
review_requirement=Shipment.REVIEW_ADDRESS
|
||||||
|
)
|
||||||
|
shipment.related_items.add(self.item1)
|
||||||
|
|
||||||
|
# Create a client without authentication
|
||||||
|
public_client = Client()
|
||||||
|
|
||||||
|
# Test retrieving shipment details
|
||||||
|
response = public_client.get(f'/api/2/public/shipments/{shipment.public_secret}/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json()['id'], shipment.id)
|
||||||
|
self.assertEqual(response.json()['recipient_name'], 'John Doe')
|
||||||
|
self.assertEqual(response.json()['state'], Shipment.PENDING_REVIEW)
|
||||||
|
self.assertEqual(len(response.json()['related_items']), 1)
|
||||||
|
|
||||||
|
def test_public_shipment_detail_not_found(self):
|
||||||
|
"""Test retrieving shipment details with non-existent public_secret"""
|
||||||
|
# Create a client without authentication
|
||||||
|
public_client = Client()
|
||||||
|
|
||||||
|
# Test with non-existent public_secret
|
||||||
|
response = public_client.get('/api/2/public/shipments/00000000-0000-0000-0000-000000000000/')
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_public_shipment_detail_wrong_state(self):
|
||||||
|
"""Test retrieving shipment details when shipment is not in PENDING_REVIEW state"""
|
||||||
|
# Create a shipment in CREATED state
|
||||||
|
shipment = Shipment.objects.create(
|
||||||
|
recipient_name='Jane Smith',
|
||||||
|
street_address='456 Oak Ave',
|
||||||
|
city='Somewhere',
|
||||||
|
state_province='Province',
|
||||||
|
postal_code='67890',
|
||||||
|
country='Nation',
|
||||||
|
state=Shipment.CREATED
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a client without authentication
|
||||||
|
public_client = Client()
|
||||||
|
|
||||||
|
# Test retrieving shipment details
|
||||||
|
response = public_client.get(f'/api/2/public/shipments/{shipment.public_secret}/')
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_public_shipment_update_success_address_only(self):
|
||||||
|
"""Test updating shipment with address information when review_requirement is ADDRESS"""
|
||||||
|
# Create a shipment in PENDING_REVIEW state with ADDRESS review requirement
|
||||||
|
shipment = Shipment.objects.create(
|
||||||
|
recipient_name='Alice Johnson',
|
||||||
|
street_address='789 Pine Rd',
|
||||||
|
city='Nowhere',
|
||||||
|
state_province='Region',
|
||||||
|
postal_code='13579',
|
||||||
|
country='Land',
|
||||||
|
state=Shipment.PENDING_REVIEW,
|
||||||
|
review_requirement=Shipment.REVIEW_ADDRESS
|
||||||
|
)
|
||||||
|
shipment.related_items.add(self.item1)
|
||||||
|
|
||||||
|
# Create a client without authentication
|
||||||
|
public_client = Client()
|
||||||
|
|
||||||
|
# Test updating shipment with address information
|
||||||
|
data = {
|
||||||
|
'recipient_name': 'Alice Johnson',
|
||||||
|
'street_address': '789 Pine Rd, Apt 4B',
|
||||||
|
'city': 'Nowhere',
|
||||||
|
'state_province': 'Region',
|
||||||
|
'postal_code': '13579',
|
||||||
|
'country': 'Land'
|
||||||
|
}
|
||||||
|
response = public_client.post(
|
||||||
|
f'/api/2/public/shipments/{shipment.public_secret}/update/',
|
||||||
|
data,
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json()['state'], Shipment.READY_FOR_SHIPPING)
|
||||||
|
self.assertEqual(response.json()['street_address'], '789 Pine Rd, Apt 4B')
|
||||||
|
|
||||||
|
# Verify state has changed
|
||||||
|
shipment.refresh_from_db()
|
||||||
|
self.assertEqual(shipment.state, Shipment.READY_FOR_SHIPPING)
|
||||||
|
|
||||||
|
def test_public_shipment_update_success_both_review(self):
|
||||||
|
"""Test updating shipment with address information when review_requirement is BOTH"""
|
||||||
|
# Create a shipment in PENDING_REVIEW state with BOTH review requirement
|
||||||
|
shipment = Shipment.objects.create(
|
||||||
|
recipient_name='Bob Wilson',
|
||||||
|
street_address='321 Elm St',
|
||||||
|
city='Elsewhere',
|
||||||
|
state_province='Territory',
|
||||||
|
postal_code='24680',
|
||||||
|
country='Realm',
|
||||||
|
state=Shipment.PENDING_REVIEW,
|
||||||
|
review_requirement=Shipment.REVIEW_BOTH
|
||||||
|
)
|
||||||
|
shipment.related_items.add(self.item1)
|
||||||
|
|
||||||
|
# Create a client without authentication
|
||||||
|
public_client = Client()
|
||||||
|
|
||||||
|
# Test updating shipment with address information
|
||||||
|
data = {
|
||||||
|
'recipient_name': 'Bob Wilson',
|
||||||
|
'street_address': '321 Elm St, Suite 7',
|
||||||
|
'city': 'Elsewhere',
|
||||||
|
'state_province': 'Territory',
|
||||||
|
'postal_code': '24680',
|
||||||
|
'country': 'Realm'
|
||||||
|
}
|
||||||
|
response = public_client.post(
|
||||||
|
f'/api/2/public/shipments/{shipment.public_secret}/update/',
|
||||||
|
data,
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json()['state'], Shipment.READY_FOR_SHIPPING)
|
||||||
|
self.assertEqual(response.json()['street_address'], '321 Elm St, Suite 7')
|
||||||
|
|
||||||
|
# Verify state has changed
|
||||||
|
shipment.refresh_from_db()
|
||||||
|
self.assertEqual(shipment.state, Shipment.READY_FOR_SHIPPING)
|
||||||
|
|
||||||
|
def test_public_shipment_update_success_items_only(self):
|
||||||
|
"""Test updating shipment with address information when review_requirement is ITEMS"""
|
||||||
|
# Create a shipment in PENDING_REVIEW state with ITEMS review requirement
|
||||||
|
shipment = Shipment.objects.create(
|
||||||
|
recipient_name='Carol Davis',
|
||||||
|
street_address='654 Maple Dr',
|
||||||
|
city='Anywhere',
|
||||||
|
state_province='District',
|
||||||
|
postal_code='97531',
|
||||||
|
country='Kingdom',
|
||||||
|
state=Shipment.PENDING_REVIEW,
|
||||||
|
review_requirement=Shipment.REVIEW_ITEMS
|
||||||
|
)
|
||||||
|
shipment.related_items.add(self.item1)
|
||||||
|
|
||||||
|
# Create a client without authentication
|
||||||
|
public_client = Client()
|
||||||
|
|
||||||
|
# Test updating shipment with address information
|
||||||
|
data = {
|
||||||
|
'recipient_name': 'Carol Davis',
|
||||||
|
'street_address': '654 Maple Dr, Unit 3',
|
||||||
|
'city': 'Anywhere',
|
||||||
|
'state_province': 'District',
|
||||||
|
'postal_code': '97531',
|
||||||
|
'country': 'Kingdom'
|
||||||
|
}
|
||||||
|
response = public_client.post(
|
||||||
|
f'/api/2/public/shipments/{shipment.public_secret}/update/',
|
||||||
|
data,
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertEqual(response.json()['state'], Shipment.PENDING_REVIEW) # State should not change
|
||||||
|
self.assertEqual(response.json()['street_address'], '654 Maple Dr, Unit 3')
|
||||||
|
|
||||||
|
# Verify state has not changed
|
||||||
|
shipment.refresh_from_db()
|
||||||
|
self.assertEqual(shipment.state, Shipment.PENDING_REVIEW)
|
||||||
|
|
||||||
|
def test_public_shipment_update_not_found(self):
|
||||||
|
"""Test updating shipment with non-existent public_secret"""
|
||||||
|
# Create a client without authentication
|
||||||
|
public_client = Client()
|
||||||
|
|
||||||
|
# Test with non-existent public_secret
|
||||||
|
data = {
|
||||||
|
'recipient_name': 'Unknown Person',
|
||||||
|
'street_address': '123 Fake St',
|
||||||
|
'city': 'Fake City',
|
||||||
|
'state_province': 'Fake State',
|
||||||
|
'postal_code': '12345',
|
||||||
|
'country': 'Fake Country'
|
||||||
|
}
|
||||||
|
response = public_client.post(
|
||||||
|
'/api/2/public/shipments/00000000-0000-0000-0000-000000000000/update/',
|
||||||
|
data,
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
def test_public_shipment_update_wrong_state(self):
|
||||||
|
"""Test updating shipment when it's not in PENDING_REVIEW state"""
|
||||||
|
# Create a shipment in CREATED state
|
||||||
|
shipment = Shipment.objects.create(
|
||||||
|
recipient_name='David Lee',
|
||||||
|
street_address='987 Cedar Ln',
|
||||||
|
city='Nowhere',
|
||||||
|
state_province='Province',
|
||||||
|
postal_code='13579',
|
||||||
|
country='Country',
|
||||||
|
state=Shipment.CREATED
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a client without authentication
|
||||||
|
public_client = Client()
|
||||||
|
|
||||||
|
# Test updating shipment
|
||||||
|
data = {
|
||||||
|
'recipient_name': 'David Lee',
|
||||||
|
'street_address': '987 Cedar Ln, Apt 2C',
|
||||||
|
'city': 'Nowhere',
|
||||||
|
'state_province': 'Province',
|
||||||
|
'postal_code': '13579',
|
||||||
|
'country': 'Country'
|
||||||
|
}
|
||||||
|
response = public_client.post(
|
||||||
|
f'/api/2/public/shipments/{shipment.public_secret}/update/',
|
||||||
|
data,
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 404)
|
||||||
|
|
||||||
|
# Verify state hasn't changed
|
||||||
|
shipment.refresh_from_db()
|
||||||
|
self.assertEqual(shipment.state, Shipment.CREATED)
|
||||||
|
|
||||||
|
def test_public_shipment_update_both_review_missing_items(self):
|
||||||
|
"""Test updating shipment with BOTH review requirement when items are missing"""
|
||||||
|
# Create a shipment in PENDING_REVIEW state with BOTH review requirement but no items
|
||||||
|
shipment = Shipment.objects.create(
|
||||||
|
recipient_name='Eve Brown',
|
||||||
|
street_address='147 Birch Ave',
|
||||||
|
city='Somewhere',
|
||||||
|
state_province='Region',
|
||||||
|
postal_code='24680',
|
||||||
|
country='Land',
|
||||||
|
state=Shipment.PENDING_REVIEW,
|
||||||
|
review_requirement=Shipment.REVIEW_BOTH
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a client without authentication
|
||||||
|
public_client = Client()
|
||||||
|
|
||||||
|
# Test updating shipment with address information
|
||||||
|
data = {
|
||||||
|
'recipient_name': 'Eve Brown',
|
||||||
|
'street_address': '147 Birch Ave, Suite 5',
|
||||||
|
'city': 'Somewhere',
|
||||||
|
'state_province': 'Region',
|
||||||
|
'postal_code': '24680',
|
||||||
|
'country': 'Land'
|
||||||
|
}
|
||||||
|
response = public_client.post(
|
||||||
|
f'/api/2/public/shipments/{shipment.public_secret}/update/',
|
||||||
|
data,
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 400)
|
||||||
|
self.assertIn('error', response.json())
|
||||||
|
self.assertIn('Items must be added before approval', response.json()['error'])
|
||||||
|
|
||||||
|
# Verify state hasn't changed
|
||||||
|
shipment.refresh_from_db()
|
||||||
|
self.assertEqual(shipment.state, Shipment.PENDING_REVIEW)
|
||||||
|
|
Loading…
Add table
Reference in a new issue