added public api for shipments
Some checks failed
/ test (push) Failing after 0s

This commit is contained in:
Jan Felix Wiebe 2025-04-11 22:13:05 +02:00
parent 6a5d954980
commit 3d01f16750
2 changed files with 370 additions and 2 deletions

View file

@ -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'),
]

View file

@ -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)