""" Unit tests for Logistics Models Tests for logistics module models: - Shipment - Vehicle Author: Claude """ import pytest from django.test import TestCase from django.contrib.auth import get_user_model from django.utils import timezone from decimal import Decimal from datetime import date, time, timedelta from backend.src.core.models.tenant import Tenant from backend.src.core.models.user import User from backend.src.modules.logistics.models.shipment import Shipment from backend.src.modules.logistics.models.vehicle import Vehicle User = get_user_model() class ShipmentModelTest(TestCase): """Test cases for Shipment model""" def setUp(self): self.tenant = Tenant.objects.create( name='Test Logistics Sdn Bhd', schema_name='test_logistics', domain='testlogistics.com', business_type='logistics' ) self.user = User.objects.create_user( username='dispatcher', email='dispatcher@test.com', password='test123', tenant=self.tenant, role='staff' ) self.shipment_data = { 'tenant': self.tenant, 'tracking_number': 'TRK-2024010001-MY', 'order_number': 'ORD-2024010001', 'sender_name': 'Test Sender', 'sender_company': 'Test Company', 'sender_phone': '+60123456789', 'sender_email': 'sender@test.com', 'sender_address': '123 Sender Street', 'sender_city': 'Kuala Lumpur', 'sender_state': 'KUL', 'sender_postal_code': '50000', 'receiver_name': 'Test Receiver', 'receiver_company': 'Test Receiver Company', 'receiver_phone': '+60123456788', 'receiver_email': 'receiver@test.com', 'receiver_address': '456 Receiver Street', 'receiver_city': 'Penang', 'receiver_state': 'PNG', 'receiver_postal_code': '10000', 'origin_state': 'KUL', 'destination_state': 'PNG', 'service_type': 'express', 'package_type': 'document', 'weight': Decimal('1.5'), 'length': Decimal('30.0'), 'width': Decimal('20.0'), 'height': Decimal('10.0'), 'declared_value': Decimal('100.00'), 'currency': 'MYR', 'shipping_cost': Decimal('15.00'), 'payment_method': 'cash', 'payment_status': 'paid', 'status': 'processing', 'priority': 'normal', 'special_instructions': 'Handle with care', 'insurance_required': False, 'insurance_amount': Decimal('0.00'), 'estimated_delivery': date.today() + timedelta(days=2), 'actual_delivery': None, 'proof_of_delivery': '', 'delivery_confirmation': False, 'created_by': self.user } def test_create_shipment(self): """Test creating a new shipment""" shipment = Shipment.objects.create(**self.shipment_data) self.assertEqual(shipment.tenant, self.tenant) self.assertEqual(shipment.tracking_number, self.shipment_data['tracking_number']) self.assertEqual(shipment.order_number, self.shipment_data['order_number']) self.assertEqual(shipment.sender_name, self.shipment_data['sender_name']) self.assertEqual(shipment.receiver_name, self.shipment_data['receiver_name']) self.assertEqual(shipment.service_type, self.shipment_data['service_type']) self.assertEqual(shipment.weight, self.shipment_data['weight']) self.assertEqual(shipment.shipping_cost, self.shipment_data['shipping_cost']) self.assertEqual(shipment.status, self.shipment_data['status']) def test_shipment_string_representation(self): """Test shipment string representation""" shipment = Shipment.objects.create(**self.shipment_data) self.assertEqual(str(shipment), f"{shipment.tracking_number} - {shipment.sender_name} to {shipment.receiver_name}") def test_shipment_volume_calculation(self): """Test shipment volume calculation""" shipment = Shipment.objects.create(**self.shipment_data) # Volume = length × width × height (in cm) expected_volume = Decimal('6000.0') # 30.0 × 20.0 × 10.0 self.assertEqual(shipment.volume, expected_volume) def test_shipment_delivery_status(self): """Test shipment delivery status""" shipment = Shipment.objects.create(**self.shipment_data) # Not delivered yet self.assertFalse(shipment.is_delivered) # Mark as delivered shipment.status = 'delivered' shipment.actual_delivery = date.today() shipment.delivery_confirmation = True shipment.save() self.assertTrue(shipment.is_delivered) def test_shipment_delayed_status(self): """Test shipment delayed status""" shipment = Shipment.objects.create(**self.shipment_data) # Not delayed (estimated delivery is in future) self.assertFalse(shipment.is_delayed) # Mark as delayed (past estimated delivery) shipment.estimated_delivery = date.today() - timedelta(days=1) shipment.status = 'in_transit' shipment.save() self.assertTrue(shipment.is_delayed) def test_shipment_service_type_choices(self): """Test shipment service type validation""" invalid_data = self.shipment_data.copy() invalid_data['service_type'] = 'invalid_service' with self.assertRaises(Exception): Shipment.objects.create(**invalid_data) def test_shipment_package_type_choices(self): """Test shipment package type validation""" invalid_data = self.shipment_data.copy() invalid_data['package_type'] = 'invalid_package' with self.assertRaises(Exception): Shipment.objects.create(**invalid_data) def test_shipment_status_choices(self): """Test shipment status validation""" invalid_data = self.shipment_data.copy() invalid_data['status'] = 'invalid_status' with self.assertRaises(Exception): Shipment.objects.create(**invalid_data) def test_shipment_priority_choices(self): """Test shipment priority validation""" invalid_data = self.shipment_data.copy() invalid_data['priority'] = 'invalid_priority' with self.assertRaises(Exception): Shipment.objects.create(**invalid_data) def test_shipment_malaysian_phone_validation(self): """Test Malaysian phone number validation""" # Valid Malaysian phone numbers shipment = Shipment.objects.create(**self.shipment_data) self.assertEqual(shipment.sender_phone, self.shipment_data['sender_phone']) self.assertEqual(shipment.receiver_phone, self.shipment_data['receiver_phone']) # Invalid sender phone invalid_data = self.shipment_data.copy() invalid_data['sender_phone'] = '12345' with self.assertRaises(Exception): Shipment.objects.create(**invalid_data) # Invalid receiver phone invalid_data = self.shipment_data.copy() invalid_data['receiver_phone'] = '67890' with self.assertRaises(Exception): Shipment.objects.create(**invalid_data) def test_shipment_malaysian_state_validation(self): """Test Malaysian state validation""" # Valid Malaysian states shipment = Shipment.objects.create(**self.shipment_data) self.assertEqual(shipment.sender_state, self.shipment_data['sender_state']) self.assertEqual(shipment.receiver_state, self.shipment_data['receiver_state']) # Invalid sender state invalid_data = self.shipment_data.copy() invalid_data['sender_state'] = 'XX' with self.assertRaises(Exception): Shipment.objects.create(**invalid_data) def test_shipment_weight_validation(self): """Test shipment weight validation""" # Valid weight shipment = Shipment.objects.create(**self.shipment_data) self.assertEqual(shipment.weight, self.shipment_data['weight']) # Invalid weight (negative) invalid_data = self.shipment_data.copy() invalid_data['weight'] = Decimal('-1.0') with self.assertRaises(Exception): Shipment.objects.create(**invalid_data) def test_shipment_tracking_number_format(self): """Test shipment tracking number format""" shipment = Shipment.objects.create(**self.shipment_data) # Should end with -MY for Malaysia self.assertTrue(shipment.tracking_number.endswith('-MY')) # Should be unique with self.assertRaises(Exception): Shipment.objects.create(**self.shipment_data) def test_shipment_dimensions_validation(self): """Test shipment dimensions validation""" # Valid dimensions shipment = Shipment.objects.create(**self.shipment_data) self.assertEqual(shipment.length, self.shipment_data['length']) self.assertEqual(shipment.width, self.shipment_data['width']) self.assertEqual(shipment.height, self.shipment_data['height']) # Invalid dimensions (negative) invalid_data = self.shipment_data.copy() invalid_data['length'] = Decimal('-1.0') with self.assertRaises(Exception): Shipment.objects.create(**invalid_data) class VehicleModelTest(TestCase): """Test cases for Vehicle model""" def setUp(self): self.tenant = Tenant.objects.create( name='Test Logistics Sdn Bhd', schema_name='test_logistics', domain='testlogistics.com', business_type='logistics' ) self.user = User.objects.create_user( username='manager', email='manager@test.com', password='test123', tenant=self.tenant, role='admin' ) self.vehicle_data = { 'tenant': self.tenant, 'vehicle_number': 'V1234', 'registration_number': 'WAB1234', 'vehicle_type': 'van', 'make': 'Toyota', 'model': 'Hiace', 'year': 2020, 'color': 'White', 'chassis_number': 'MR0HE3CD5L123456', 'engine_number': '2TR123456', 'capacity': 1000, # kg 'volume_capacity': 10.0, # cubic meters 'fuel_type': 'petrol', 'fuel_capacity': 70, # liters 'current_fuel': 50, # liters 'purchase_date': date(2020, 1, 1), 'purchase_price': Decimal('120000.00'), 'insurance_policy': 'INS-2024-001234', 'insurance_expiry': date.today() + timedelta(days=365), 'road_tax_expiry': date.today() + timedelta(days=180), 'inspection_expiry': date.today() + timedelta(days=90), 'current_mileage': 50000, 'last_service_mileage': 45000, 'next_service_mileage': 55000, 'status': 'active', 'assigned_driver': None, 'gps_device_id': 'GPS001234', 'is_active': True, 'notes': 'Well-maintained vehicle', 'created_by': self.user } def test_create_vehicle(self): """Test creating a new vehicle""" vehicle = Vehicle.objects.create(**self.vehicle_data) self.assertEqual(vehicle.tenant, self.tenant) self.assertEqual(vehicle.vehicle_number, self.vehicle_data['vehicle_number']) self.assertEqual(vehicle.registration_number, self.vehicle_data['registration_number']) self.assertEqual(vehicle.vehicle_type, self.vehicle_data['vehicle_type']) self.assertEqual(vehicle.make, self.vehicle_data['make']) self.assertEqual(vehicle.model, self.vehicle_data['model']) self.assertEqual(vehicle.year, self.vehicle_data['year']) self.assertEqual(vehicle.capacity, self.vehicle_data['capacity']) self.assertEqual(vehicle.status, self.vehicle_data['status']) self.assertTrue(vehicle.is_active) def test_vehicle_string_representation(self): """Test vehicle string representation""" vehicle = Vehicle.objects.create(**self.vehicle_data) self.assertEqual(str(vehicle), f"{vehicle.make} {vehicle.model} ({vehicle.registration_number})") def test_vehicle_age(self): """Test vehicle age calculation""" vehicle = Vehicle.objects.create(**self.vehicle_data) # Age should be calculated based on purchase date today = date.today() expected_age = today.year - vehicle.purchase_date.year if today.month < vehicle.purchase_date.month or (today.month == vehicle.purchase_date.month and today.day < vehicle.purchase_date.day): expected_age -= 1 self.assertEqual(vehicle.age, expected_age) def test_vehicle_service_due(self): """Test vehicle service due status""" vehicle = Vehicle.objects.create(**self.vehicle_data) # Service not due yet self.assertFalse(vehicle.service_due) # Mark as service due vehicle.current_mileage = 56000 vehicle.save() self.assertTrue(vehicle.service_due) def test_vehicle_insurance_expiry_status(self): """Test vehicle insurance expiry status""" vehicle = Vehicle.objects.create(**self.vehicle_data) # Insurance not expired self.assertFalse(vehicle.insurance_expired) # Mark as expired vehicle.insurance_expiry = date.today() - timedelta(days=1) vehicle.save() self.assertTrue(vehicle.insurance_expired) def test_vehicle_road_tax_expiry_status(self): """Test vehicle road tax expiry status""" vehicle = Vehicle.objects.create(**self.vehicle_data) # Road tax not expired self.assertFalse(vehicle.road_tax_expired) # Mark as expired vehicle.road_tax_expiry = date.today() - timedelta(days=1) vehicle.save() self.assertTrue(vehicle.road_tax_expired) def test_vehicle_inspection_expiry_status(self): """Test vehicle inspection expiry status""" vehicle = Vehicle.objects.create(**self.vehicle_data) # Inspection not expired self.assertFalse(vehicle.inspection_expired) # Mark as expired vehicle.inspection_expiry = date.today() - timedelta(days=1) vehicle.save() self.assertTrue(vehicle.inspection_expired) def test_vehicle_type_choices(self): """Test vehicle type validation""" invalid_data = self.vehicle_data.copy() invalid_data['vehicle_type'] = 'invalid_type' with self.assertRaises(Exception): Vehicle.objects.create(**invalid_data) def test_vehicle_fuel_type_choices(self): """Test vehicle fuel type validation""" invalid_data = self.vehicle_data.copy() invalid_data['fuel_type'] = 'invalid_fuel' with self.assertRaises(Exception): Vehicle.objects.create(**invalid_data) def test_vehicle_status_choices(self): """Test vehicle status validation""" invalid_data = self.vehicle_data.copy() invalid_data['status'] = 'invalid_status' with self.assertRaises(Exception): Vehicle.objects.create(**invalid_data) def test_vehicle_malaysian_registration_validation(self): """Test Malaysian vehicle registration validation""" # Valid registration number vehicle = Vehicle.objects.create(**self.vehicle_data) self.assertEqual(vehicle.registration_number, self.vehicle_data['registration_number']) # Invalid registration number format invalid_data = self.vehicle_data.copy() invalid_data['registration_number'] = 'ABC123' with self.assertRaises(Exception): Vehicle.objects.create(**invalid_data) def test_vehicle_mileage_validation(self): """Test vehicle mileage validation""" # Valid mileage vehicle = Vehicle.objects.create(**self.vehicle_data) self.assertEqual(vehicle.current_mileage, self.vehicle_data['current_mileage']) # Invalid mileage (negative) invalid_data = self.vehicle_data.copy() invalid_data['current_mileage'] = -1000 with self.assertRaises(Exception): Vehicle.objects.create(**invalid_data) def test_vehicle_capacity_validation(self): """Test vehicle capacity validation""" # Valid capacity vehicle = Vehicle.objects.create(**self.vehicle_data) self.assertEqual(vehicle.capacity, self.vehicle_data['capacity']) # Invalid capacity (negative) invalid_data = self.vehicle_data.copy() invalid_data['capacity'] = -100 with self.assertRaises(Exception): Vehicle.objects.create(**invalid_data) def test_vehicle_year_validation(self): """Test vehicle year validation""" # Valid year vehicle = Vehicle.objects.create(**self.vehicle_data) self.assertEqual(vehicle.year, self.vehicle_data['year']) # Invalid year (too old) invalid_data = self.vehicle_data.copy() invalid_data['year'] = 1950 with self.assertRaises(Exception): Vehicle.objects.create(**invalid_data) def test_vehicle_fuel_level_validation(self): """Test vehicle fuel level validation""" # Valid fuel level vehicle = Vehicle.objects.create(**self.vehicle_data) self.assertEqual(vehicle.current_fuel, self.vehicle_data['current_fuel']) # Invalid fuel level (negative) invalid_data = self.vehicle_data.copy() invalid_data['current_fuel'] = -10 with self.assertRaises(Exception): Vehicle.objects.create(**invalid_data) # Invalid fuel level (exceeds capacity) invalid_data = self.vehicle_data.copy() invalid_data['current_fuel'] = 100 with self.assertRaises(Exception): Vehicle.objects.create(**invalid_data)