Compare commits
No commits in common. "3d01f167503611858202a34fcdb8a439a4d38eff" and "8d830bda644b9b2c34d8ca3f7a23080149615e02" have entirely different histories.
3d01f16750
...
8d830bda64
8 changed files with 4 additions and 1068 deletions
|
@ -30,7 +30,6 @@ urlpatterns = [
|
|||
path('api/2/', include('mail.api_v2')),
|
||||
path('api/2/', include('notify_sessions.api_v2')),
|
||||
path('api/2/', include('authentication.api_v2')),
|
||||
path('api/2/', include('shipments.api_v2')),
|
||||
path('api/', get_info),
|
||||
path('', include('django_prometheus.urls')),
|
||||
]
|
||||
|
|
|
@ -16,57 +16,3 @@ This module handles shipments that are processed by the lost&found team.
|
|||
- automatic state change after confirmation
|
||||
- **tracking**: the lostee recieves a email with publicly accessible link, on this page the user can enter his address
|
||||
- Returning parcels could be managed to (e.g. if a shipment is marked as returned, the items will be *unfound* again)
|
||||
|
||||
## State Model
|
||||
The shipment process follows a defined state model to ensure proper workflow and validation:
|
||||
|
||||
### States
|
||||
1. **CREATED** (Initial State)
|
||||
- Shipment is created without address, items, or tickets
|
||||
- No validation requirements
|
||||
|
||||
2. **PENDING_REVIEW**
|
||||
- Items have been added to the shipment
|
||||
- Waiting for requester to verify items and/or address
|
||||
- Requires specification of what needs to be reviewed:
|
||||
- Items only
|
||||
- Address only
|
||||
- Both items and address
|
||||
- Validation requirements depend on review type
|
||||
|
||||
3. **READY_FOR_SHIPPING**
|
||||
- All required reviews completed
|
||||
- All required fields validated
|
||||
- Ready for shipping process
|
||||
|
||||
4. **REJECTED**
|
||||
- Shipment was reviewed and rejected
|
||||
- Terminal state (no further transitions possible)
|
||||
|
||||
5. **SHIPPED**
|
||||
- Package has been shipped
|
||||
- Requires complete address information
|
||||
- Timestamp of shipping is recorded
|
||||
|
||||
6. **COMPLETED**
|
||||
- Package has been delivered
|
||||
- Terminal state (no further transitions possible)
|
||||
- Timestamp of completion is recorded
|
||||
|
||||
### State Transitions
|
||||
- CREATED → PENDING_REVIEW (when items are added, requires specification of review type)
|
||||
- PENDING_REVIEW → READY_FOR_SHIPPING (when approved and all required reviews completed)
|
||||
- PENDING_REVIEW → REJECTED (when rejected)
|
||||
- READY_FOR_SHIPPING → SHIPPED (when marked as shipped)
|
||||
- SHIPPED → COMPLETED (when marked as delivered)
|
||||
|
||||
### Validation Rules
|
||||
- Review requirement must be specified when entering PENDING_REVIEW state
|
||||
- Address fields are required for shipping-related states (READY_FOR_SHIPPING, SHIPPED, COMPLETED)
|
||||
- At least one item is required for any state beyond CREATED
|
||||
- Invalid state transitions will raise validation errors
|
||||
- Terminal states (REJECTED, COMPLETED) cannot transition to other states
|
||||
- Approval requires completion of all specified review requirements:
|
||||
- For ITEMS review: items must be added
|
||||
- For ADDRESS review: all address fields must be complete
|
||||
- For BOTH review: both items and complete address are required
|
|
@ -1,205 +0,0 @@
|
|||
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<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'),
|
||||
]
|
|
@ -1,68 +0,0 @@
|
|||
# Generated by Django 4.2.7 on 2025-04-11 15:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('shipments', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='shipment',
|
||||
name='address_supplements',
|
||||
field=models.TextField(blank=True, default='', max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shipment',
|
||||
name='city',
|
||||
field=models.CharField(blank=True, default='', max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shipment',
|
||||
name='completed_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shipment',
|
||||
name='country',
|
||||
field=models.CharField(blank=True, default='', max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shipment',
|
||||
name='postal_code',
|
||||
field=models.CharField(blank=True, default='', max_length=20),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shipment',
|
||||
name='recipient_name',
|
||||
field=models.CharField(blank=True, default='', max_length=255),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shipment',
|
||||
name='review_requirement',
|
||||
field=models.CharField(blank=True, choices=[('ITEMS', 'Items Review Required'), ('ADDRESS', 'Address Review Required'), ('BOTH', 'Both Items and Address Review Required')], max_length=10, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shipment',
|
||||
name='shipped_at',
|
||||
field=models.DateTimeField(blank=True, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shipment',
|
||||
name='state',
|
||||
field=models.CharField(choices=[('CREATED', 'Created'), ('PENDING_REVIEW', 'Pending Review'), ('READY_FOR_SHIPPING', 'Ready for Shipping'), ('REJECTED', 'Rejected'), ('SHIPPED', 'Shipped'), ('COMPLETED', 'Completed')], default='CREATED', max_length=20),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shipment',
|
||||
name='state_province',
|
||||
field=models.CharField(blank=True, default='', max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='shipment',
|
||||
name='street_address',
|
||||
field=models.CharField(blank=True, default='', max_length=255),
|
||||
),
|
||||
]
|
|
@ -1,162 +1,21 @@
|
|||
import uuid
|
||||
|
||||
from django.db import models
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.utils import timezone
|
||||
from django_softdelete.models import SoftDeleteModel, SoftDeleteManager
|
||||
from inventory.models import Item
|
||||
from tickets.models import IssueThread
|
||||
|
||||
class Shipment(SoftDeleteModel):
|
||||
# State choices
|
||||
CREATED = 'CREATED'
|
||||
PENDING_REVIEW = 'PENDING_REVIEW'
|
||||
READY_FOR_SHIPPING = 'READY_FOR_SHIPPING'
|
||||
REJECTED = 'REJECTED'
|
||||
SHIPPED = 'SHIPPED'
|
||||
COMPLETED = 'COMPLETED'
|
||||
|
||||
SHIPMENT_STATES = [
|
||||
(CREATED, 'Created'),
|
||||
(PENDING_REVIEW, 'Pending Review'),
|
||||
(READY_FOR_SHIPPING, 'Ready for Shipping'),
|
||||
(REJECTED, 'Rejected'),
|
||||
(SHIPPED, 'Shipped'),
|
||||
(COMPLETED, 'Completed'),
|
||||
]
|
||||
|
||||
# Review requirement choices
|
||||
REVIEW_ITEMS = 'ITEMS'
|
||||
REVIEW_ADDRESS = 'ADDRESS'
|
||||
REVIEW_BOTH = 'BOTH'
|
||||
|
||||
REVIEW_REQUIREMENTS = [
|
||||
(REVIEW_ITEMS, 'Items Review Required'),
|
||||
(REVIEW_ADDRESS, 'Address Review Required'),
|
||||
(REVIEW_BOTH, 'Both Items and Address Review Required'),
|
||||
]
|
||||
|
||||
id = models.AutoField(primary_key=True)
|
||||
public_secret = models.UUIDField(default = uuid.uuid4)
|
||||
state = models.CharField(
|
||||
max_length=20,
|
||||
choices=SHIPMENT_STATES,
|
||||
default=CREATED
|
||||
)
|
||||
review_requirement = models.CharField(
|
||||
max_length=10,
|
||||
choices=REVIEW_REQUIREMENTS,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
|
||||
# Shipping address fields
|
||||
recipient_name = models.CharField(max_length=255, blank=True, default='')
|
||||
address_supplements = models.TextField(max_length=255, blank=True, default='')
|
||||
street_address = models.CharField(max_length=255, blank=True, default='')
|
||||
city = models.CharField(max_length=100, blank=True, default='')
|
||||
state_province = models.CharField(max_length=100, blank=True, default='')
|
||||
postal_code = models.CharField(max_length=20, blank=True, default='')
|
||||
country = models.CharField(max_length=100, blank=True, default='')
|
||||
|
||||
related_items = models.ManyToManyField(Item)
|
||||
related_tickets = models.ManyToManyField(IssueThread)
|
||||
|
||||
created_at = models.DateTimeField(null=True, auto_now_add=True)
|
||||
updated_at = models.DateTimeField(blank=True, null=True)
|
||||
shipped_at = models.DateTimeField(blank=True, null=True)
|
||||
completed_at = models.DateTimeField(blank=True, null=True)
|
||||
|
||||
all_objects = models.Manager()
|
||||
|
||||
def __str__(self):
|
||||
return f'[{self.id}] {self.state}'
|
||||
|
||||
def clean(self):
|
||||
"""Validate state transitions and required fields"""
|
||||
if not self.pk: # New instance
|
||||
return
|
||||
|
||||
old_instance = Shipment.objects.get(pk=self.pk)
|
||||
|
||||
# Validate state transitions
|
||||
valid_transitions = {
|
||||
self.CREATED: [self.PENDING_REVIEW],
|
||||
self.PENDING_REVIEW: [self.READY_FOR_SHIPPING, self.REJECTED],
|
||||
self.READY_FOR_SHIPPING: [self.SHIPPED],
|
||||
self.SHIPPED: [self.COMPLETED],
|
||||
self.REJECTED: [], # No transitions from rejected
|
||||
self.COMPLETED: [], # No transitions from completed
|
||||
}
|
||||
|
||||
if (self.state != old_instance.state and
|
||||
self.state not in valid_transitions[old_instance.state]):
|
||||
raise ValidationError(f'Invalid state transition from {old_instance.state} to {self.state}')
|
||||
|
||||
# Validate review requirement when entering PENDING_REVIEW
|
||||
if self.state == self.PENDING_REVIEW and old_instance.state == self.CREATED:
|
||||
if not self.review_requirement:
|
||||
raise ValidationError('Review requirement must be specified when entering PENDING_REVIEW state')
|
||||
|
||||
# Validate required fields based on state and review requirement
|
||||
if self.state in [self.READY_FOR_SHIPPING, self.SHIPPED, self.COMPLETED]:
|
||||
if not all([self.recipient_name, self.street_address, self.city,
|
||||
self.state_province, self.postal_code, self.country]):
|
||||
raise ValidationError('All address fields are required for shipping')
|
||||
|
||||
if self.state != self.CREATED and not self.related_items.exists():
|
||||
raise ValidationError('Shipment must have at least one item')
|
||||
|
||||
def add_items(self, review_requirement):
|
||||
"""Move shipment to PENDING_REVIEW after items are added"""
|
||||
if self.state == self.CREATED and self.related_items.exists():
|
||||
if not review_requirement:
|
||||
raise ValidationError('Review requirement must be specified when adding items')
|
||||
if review_requirement not in [self.REVIEW_ITEMS, self.REVIEW_ADDRESS, self.REVIEW_BOTH]:
|
||||
raise ValidationError(f'Invalid review requirement: {review_requirement}')
|
||||
|
||||
self.review_requirement = review_requirement
|
||||
self.state = self.PENDING_REVIEW
|
||||
self.save()
|
||||
|
||||
def approve(self):
|
||||
"""Approve shipment for shipping"""
|
||||
if self.state == self.PENDING_REVIEW:
|
||||
# Validate that all required reviews are completed
|
||||
if self.review_requirement == self.REVIEW_ITEMS and not self.related_items.exists():
|
||||
raise ValidationError('Items must be added before approval')
|
||||
elif self.review_requirement == self.REVIEW_ADDRESS and not all([self.recipient_name, self.street_address, self.city,
|
||||
self.state_province, self.postal_code, self.country]):
|
||||
raise ValidationError('Address must be complete before approval')
|
||||
elif self.review_requirement == self.REVIEW_BOTH:
|
||||
if not self.related_items.exists():
|
||||
raise ValidationError('Items must be added before approval')
|
||||
if not all([self.recipient_name, self.street_address, self.city,
|
||||
self.state_province, self.postal_code, self.country]):
|
||||
raise ValidationError('Address must be complete before approval')
|
||||
|
||||
self.state = self.READY_FOR_SHIPPING
|
||||
self.save()
|
||||
|
||||
def reject(self):
|
||||
"""Reject shipment"""
|
||||
if self.state == self.PENDING_REVIEW:
|
||||
self.state = self.REJECTED
|
||||
self.save()
|
||||
|
||||
def mark_shipped(self):
|
||||
"""Mark shipment as shipped"""
|
||||
if self.state == self.READY_FOR_SHIPPING:
|
||||
self.state = self.SHIPPED
|
||||
self.shipped_at = timezone.now()
|
||||
self.save()
|
||||
|
||||
def mark_completed(self):
|
||||
"""Mark shipment as completed"""
|
||||
if self.state == self.SHIPPED:
|
||||
self.state = self.COMPLETED
|
||||
self.completed_at = timezone.now()
|
||||
self.save()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.clean()
|
||||
super().save(*args, **kwargs)
|
||||
return '[' + str(self.id) + ']' + self.description
|
|
@ -1,595 +0,0 @@
|
|||
from django.test import TestCase, Client
|
||||
from django.contrib.auth.models import Permission
|
||||
from knox.models import AuthToken
|
||||
from django.utils import timezone
|
||||
|
||||
from authentication.models import ExtendedUser
|
||||
from inventory.models import Event, Item, Container
|
||||
from tickets.models import IssueThread
|
||||
from shipments.models import Shipment
|
||||
|
||||
|
||||
class ShipmentApiTest(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
# Create a test user with all permissions
|
||||
self.user = ExtendedUser.objects.create_user('testuser', 'test@example.com', 'test')
|
||||
self.user.user_permissions.add(*Permission.objects.all())
|
||||
self.user.save()
|
||||
|
||||
# Create authentication token
|
||||
self.token = AuthToken.objects.create(user=self.user)
|
||||
self.client = Client(headers={'Authorization': 'Token ' + self.token[1]})
|
||||
|
||||
# Create test event and container
|
||||
self.event = Event.objects.create(slug='test-event', name='Test Event')
|
||||
self.container = Container.objects.create(name='Test Container')
|
||||
|
||||
# Create test items with container
|
||||
self.item1 = Item.objects.create(description='Test Item 1', event=self.event, container=self.container)
|
||||
self.item2 = Item.objects.create(description='Test Item 2', event=self.event, container=self.container)
|
||||
|
||||
# Create test ticket
|
||||
self.ticket = IssueThread.objects.create(name='Test Ticket')
|
||||
|
||||
def test_list_shipments_empty(self):
|
||||
"""Test listing shipments when none exist"""
|
||||
response = self.client.get('/api/2/shipments/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json(), [])
|
||||
|
||||
def test_create_shipment(self):
|
||||
"""Test creating a new shipment"""
|
||||
data = {
|
||||
'recipient_name': 'John Smith',
|
||||
'street_address': '123 Oxford Street',
|
||||
'city': 'London',
|
||||
'state_province': 'Greater London',
|
||||
'postal_code': 'W1D 2HG',
|
||||
'country': 'United Kingdom',
|
||||
'related_items': [],
|
||||
'related_tickets': []
|
||||
}
|
||||
response = self.client.post('/api/2/shipments/', data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
self.assertEqual(response.json()['recipient_name'], 'John Smith')
|
||||
self.assertEqual(response.json()['state'], Shipment.CREATED)
|
||||
self.assertEqual(response.json()['review_requirement'], None)
|
||||
|
||||
# Retrieve the created shipment and verify its details
|
||||
shipment_id = response.json()['id']
|
||||
get_response = self.client.get(f'/api/2/shipments/{shipment_id}/')
|
||||
self.assertEqual(get_response.status_code, 200)
|
||||
self.assertEqual(get_response.json()['id'], shipment_id)
|
||||
self.assertEqual(get_response.json()['recipient_name'], 'John Smith')
|
||||
self.assertEqual(get_response.json()['street_address'], '123 Oxford Street')
|
||||
self.assertEqual(get_response.json()['city'], 'London')
|
||||
self.assertEqual(get_response.json()['state_province'], 'Greater London')
|
||||
self.assertEqual(get_response.json()['postal_code'], 'W1D 2HG')
|
||||
self.assertEqual(get_response.json()['country'], 'United Kingdom')
|
||||
self.assertEqual(get_response.json()['state'], Shipment.CREATED)
|
||||
self.assertEqual(get_response.json()['review_requirement'], None)
|
||||
|
||||
def test_get_shipment(self):
|
||||
"""Test retrieving a specific shipment"""
|
||||
# Create a shipment first
|
||||
shipment = Shipment.objects.create(
|
||||
recipient_name='Maria Garcia',
|
||||
street_address='Calle Gran Via 28',
|
||||
city='Madrid',
|
||||
state_province='Madrid',
|
||||
postal_code='28013',
|
||||
country='Spain'
|
||||
)
|
||||
|
||||
response = self.client.get(f'/api/2/shipments/{shipment.id}/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json()['id'], shipment.id)
|
||||
self.assertEqual(response.json()['recipient_name'], 'Maria Garcia')
|
||||
|
||||
def test_update_shipment(self):
|
||||
"""Test updating a shipment"""
|
||||
# Create a shipment first
|
||||
shipment = Shipment.objects.create(
|
||||
recipient_name='Hans Mueller',
|
||||
street_address='Kurfürstendamm 216',
|
||||
city='Berlin',
|
||||
state_province='Berlin',
|
||||
postal_code='10719',
|
||||
country='Germany'
|
||||
)
|
||||
|
||||
data = {
|
||||
'recipient_name': 'Hans-Jürgen Mueller',
|
||||
'address_supplements': 'Apartment 4B'
|
||||
}
|
||||
response = self.client.patch(f'/api/2/shipments/{shipment.id}/', data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json()['recipient_name'], 'Hans-Jürgen Mueller')
|
||||
self.assertEqual(response.json()['address_supplements'], 'Apartment 4B')
|
||||
|
||||
def test_delete_shipment(self):
|
||||
"""Test deleting a shipment"""
|
||||
# Create a shipment first
|
||||
shipment = Shipment.objects.create(
|
||||
recipient_name='Sophie Dubois',
|
||||
street_address='15 Rue de Rivoli',
|
||||
city='Paris',
|
||||
state_province='Île-de-France',
|
||||
postal_code='75001',
|
||||
country='France'
|
||||
)
|
||||
|
||||
response = self.client.delete(f'/api/2/shipments/{shipment.id}/')
|
||||
self.assertEqual(response.status_code, 204)
|
||||
|
||||
# Verify it's soft deleted - use all_objects manager to see deleted items
|
||||
self.assertTrue(Shipment.all_objects.filter(id=shipment.id).exists())
|
||||
# Check if it's marked as deleted
|
||||
shipment.refresh_from_db()
|
||||
self.assertIsNotNone(shipment.deleted_at)
|
||||
|
||||
def test_add_items(self):
|
||||
"""Test adding items to a shipment"""
|
||||
# Create a shipment first
|
||||
shipment = Shipment.objects.create(
|
||||
recipient_name='Alessandro Romano',
|
||||
street_address='Via del Corso 123',
|
||||
city='Rome',
|
||||
state_province='Lazio',
|
||||
postal_code='00186',
|
||||
country='Italy'
|
||||
)
|
||||
|
||||
data = {
|
||||
'item_ids': [self.item1.id, self.item2.id],
|
||||
'review_requirement': Shipment.REVIEW_ITEMS
|
||||
}
|
||||
response = self.client.post(f'/api/2/shipments/{shipment.id}/add_items/', data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json()['state'], Shipment.PENDING_REVIEW)
|
||||
self.assertEqual(response.json()['review_requirement'], Shipment.REVIEW_ITEMS)
|
||||
|
||||
# Verify items were added
|
||||
shipment.refresh_from_db()
|
||||
self.assertEqual(shipment.related_items.count(), 2)
|
||||
|
||||
def test_approve_shipment(self):
|
||||
"""Test approving a shipment"""
|
||||
# Create a shipment and add items
|
||||
shipment = Shipment.objects.create(
|
||||
recipient_name='Yuki Tanaka',
|
||||
street_address='1-1-1 Shibuya',
|
||||
city='Tokyo',
|
||||
state_province='Tokyo',
|
||||
postal_code='150-0002',
|
||||
country='Japan'
|
||||
)
|
||||
shipment.related_items.add(self.item1)
|
||||
shipment.review_requirement = Shipment.REVIEW_ITEMS
|
||||
shipment.state = Shipment.PENDING_REVIEW
|
||||
shipment.save()
|
||||
|
||||
response = self.client.post(f'/api/2/shipments/{shipment.id}/approve/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json()['state'], Shipment.READY_FOR_SHIPPING)
|
||||
|
||||
def test_reject_shipment(self):
|
||||
"""Test rejecting a shipment"""
|
||||
# Create a shipment and add items
|
||||
shipment = Shipment.objects.create(
|
||||
recipient_name='Carlos Rodriguez',
|
||||
street_address='Avenida Insurgentes Sur 1602',
|
||||
city='Mexico City',
|
||||
state_province='CDMX',
|
||||
postal_code='03940',
|
||||
country='Mexico'
|
||||
)
|
||||
shipment.related_items.add(self.item1)
|
||||
shipment.review_requirement = Shipment.REVIEW_ITEMS
|
||||
shipment.state = Shipment.PENDING_REVIEW
|
||||
shipment.save()
|
||||
|
||||
response = self.client.post(f'/api/2/shipments/{shipment.id}/reject/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json()['state'], Shipment.REJECTED)
|
||||
|
||||
def test_mark_shipped(self):
|
||||
"""Test marking a shipment as shipped"""
|
||||
# Create a shipment and set it to READY_FOR_SHIPPING
|
||||
shipment = Shipment.objects.create(
|
||||
recipient_name='Anna Kowalski',
|
||||
street_address='ul. Marszałkowska 126',
|
||||
city='Warsaw',
|
||||
state_province='Mazowieckie',
|
||||
postal_code='00-008',
|
||||
country='Poland',
|
||||
state=Shipment.READY_FOR_SHIPPING
|
||||
)
|
||||
shipment.related_items.add(self.item1)
|
||||
|
||||
response = self.client.post(f'/api/2/shipments/{shipment.id}/mark_shipped/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json()['state'], Shipment.SHIPPED)
|
||||
self.assertIsNotNone(response.json()['shipped_at'])
|
||||
|
||||
def test_mark_completed(self):
|
||||
"""Test marking a shipment as completed"""
|
||||
# Create a shipment and set it to SHIPPED
|
||||
shipment = Shipment.objects.create(
|
||||
recipient_name='Lars Andersen',
|
||||
street_address='Strøget 1',
|
||||
city='Copenhagen',
|
||||
state_province='Capital Region',
|
||||
postal_code='1095',
|
||||
country='Denmark',
|
||||
state=Shipment.SHIPPED,
|
||||
shipped_at=timezone.now()
|
||||
)
|
||||
shipment.related_items.add(self.item1)
|
||||
|
||||
response = self.client.post(f'/api/2/shipments/{shipment.id}/mark_completed/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(response.json()['state'], Shipment.COMPLETED)
|
||||
self.assertIsNotNone(response.json()['completed_at'])
|
||||
|
||||
def test_invalid_state_transition(self):
|
||||
"""Test that invalid state transitions are rejected"""
|
||||
# Create a shipment in CREATED state
|
||||
shipment = Shipment.objects.create(
|
||||
recipient_name='Ahmed Hassan',
|
||||
street_address='26 July Street',
|
||||
city='Cairo',
|
||||
state_province='Cairo Governorate',
|
||||
postal_code='11511',
|
||||
country='Egypt'
|
||||
)
|
||||
|
||||
# Try to mark it as shipped directly (invalid transition)
|
||||
response = self.client.post(f'/api/2/shipments/{shipment.id}/mark_shipped/')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn('error', response.json())
|
||||
self.assertIn('Invalid state transition', response.json()['error'])
|
||||
|
||||
# Verify state hasn't changed
|
||||
shipment.refresh_from_db()
|
||||
self.assertEqual(shipment.state, Shipment.CREATED)
|
||||
|
||||
def test_add_items_without_review_requirement(self):
|
||||
"""Test adding items without specifying review requirement"""
|
||||
# Create a shipment first
|
||||
shipment = Shipment.objects.create(
|
||||
recipient_name='Wei Chen',
|
||||
street_address='88 Century Avenue',
|
||||
city='Shanghai',
|
||||
state_province='Shanghai',
|
||||
postal_code='200120',
|
||||
country='China'
|
||||
)
|
||||
|
||||
data = {
|
||||
'item_ids': [self.item1.id, self.item2.id]
|
||||
}
|
||||
response = self.client.post(f'/api/2/shipments/{shipment.id}/add_items/', data, content_type='application/json')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn('error', response.json())
|
||||
|
||||
# Verify state hasn't changed
|
||||
shipment.refresh_from_db()
|
||||
self.assertEqual(shipment.state, Shipment.CREATED)
|
||||
|
||||
def test_approve_without_items(self):
|
||||
"""Test approving a shipment without items"""
|
||||
# Create a shipment with REVIEW_ITEMS requirement
|
||||
shipment = Shipment.objects.create(
|
||||
recipient_name='Raj Patel',
|
||||
street_address='123 MG Road',
|
||||
city='Mumbai',
|
||||
state_province='Maharashtra',
|
||||
postal_code='400001',
|
||||
country='India',
|
||||
state=Shipment.PENDING_REVIEW,
|
||||
review_requirement=Shipment.REVIEW_ITEMS
|
||||
)
|
||||
|
||||
response = self.client.post(f'/api/2/shipments/{shipment.id}/approve/')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn('error', response.json())
|
||||
|
||||
# Verify state hasn't changed
|
||||
shipment.refresh_from_db()
|
||||
self.assertEqual(shipment.state, Shipment.PENDING_REVIEW)
|
||||
|
||||
def test_approve_without_address(self):
|
||||
"""Test approving a shipment without complete address"""
|
||||
# Create a shipment with REVIEW_ADDRESS requirement
|
||||
shipment = Shipment.objects.create(
|
||||
recipient_name='Sofia Oliveira',
|
||||
street_address='Avenida Paulista 1000',
|
||||
city='São Paulo',
|
||||
state_province='São Paulo',
|
||||
postal_code='01310-100',
|
||||
country='', # Missing country
|
||||
state=Shipment.PENDING_REVIEW,
|
||||
review_requirement=Shipment.REVIEW_ADDRESS
|
||||
)
|
||||
shipment.related_items.add(self.item1)
|
||||
|
||||
response = self.client.post(f'/api/2/shipments/{shipment.id}/approve/')
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn('error', response.json())
|
||||
|
||||
# Verify state hasn't changed
|
||||
shipment.refresh_from_db()
|
||||
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