Files
ShiErFeng/shierfeng-fastapi-backend/tests/module_business/test_shipment_receipt.py
T

141 lines
6.5 KiB
Python

import unittest
from datetime import datetime
from unittest.mock import AsyncMock, MagicMock, patch
from sqlalchemy.ext.asyncio import AsyncSession
from exceptions.exception import PermissionException, ServiceException
from module_business.dao.access_dao import AccessDao
from module_business.dao.inventory_dao import InventoryDao
from module_business.dao.master_dao import MasterDao
from module_business.dao.operation_dao import OperationDao
from module_business.domain.test_batch_scope import TestBatchScope
from module_business.entity.do.master_do import BizCustomerSite
from module_business.entity.vo.shipment_receipt_vo import ShipmentReceiptCommandModel
from module_business.service.shipment_receipt_service import ShipmentReceiptService
from tests.module_business.test_shipment_service import make_inventory, make_pallet, make_shipment, make_user
class TestShipmentReceipt(unittest.IsolatedAsyncioTestCase):
def setUp(self) -> None:
self.db = MagicMock(spec=AsyncSession)
self.user = make_user()
self.scope = TestBatchScope.formal()
self.order = make_shipment(status='CONFIRMED', pallet_ids=(2, 5))
self.order.receipt_state = 'PENDING'
self.order.confirmed_at = datetime(2026, 10, 8, 8)
self.inventories = [
make_inventory(i, warehouse_id=None, site_id=9, flow_state='IN_TRANSIT', remaining_packages=100)
for i in (2, 5)
]
for item in self.order.items:
item.active_filling_id = 100 + item.pallet_id
item.package_qty = 100
self.events = []
self.site_access = AsyncMock(return_value=True)
self.patches = [
patch.object(AccessDao, 'list_user_site_ids', new=AsyncMock(return_value=[9])),
patch.object(AccessDao, 'user_has_site', new=self.site_access),
patch.object(OperationDao, 'get_shipment_for_update', new=AsyncMock(return_value=self.order)),
patch.object(
MasterDao,
'get_site',
new=AsyncMock(return_value=BizCustomerSite(site_id=9, status='0', test_batch_id=None)),
),
patch.object(InventoryDao, 'get_inventories_for_update', new=AsyncMock(return_value=self.inventories)),
patch.object(
InventoryDao,
'get_pallets_by_ids',
new=AsyncMock(return_value=[make_pallet(i, capacity=100) for i in (2, 5)]),
),
patch.object(InventoryDao, 'add_event', new=AsyncMock(side_effect=self.capture_event)),
]
for patcher in self.patches:
patcher.start()
self.addCleanup(patcher.stop)
async def capture_event(self, _db, inventory, event, _scope):
self.events.append(event)
return event
def command(self, ids=None, key='receipt-key'):
return ShipmentReceiptCommandModel(scannedPalletIds=ids if ids is not None else [5, 2], idempotencyKey=key)
async def receive(self, command=None):
return await ShipmentReceiptService.confirm_services(
self.db, 12, command or self.command(), self.user, self.scope
)
async def test_receipt_keeps_original_pallet_filling_and_quantity(self) -> None:
await self.receive()
self.assertEqual(self.order.receipt_state, 'RECEIVED')
self.assertEqual(self.order.received_by, 7)
self.assertIsInstance(self.order.received_at, datetime)
self.assertEqual(
[(i.pallet_id, i.active_filling_id, i.remaining_packages, i.flow_state) for i in self.inventories],
[
(2, 102, 100, 'AT_SITE_UNUSED'),
(5, 105, 100, 'AT_SITE_UNUSED'),
],
)
self.assertTrue(all(i.site_arrived_at == self.order.received_at for i in self.inventories))
self.assertEqual(
[(e.event_type, e.before_state, e.after_state) for e in self.events],
[
('SHIPMENT_RECEIVED', 'IN_TRANSIT', 'AT_SITE_UNUSED'),
('SHIPMENT_RECEIVED', 'IN_TRANSIT', 'AT_SITE_UNUSED'),
],
)
self.db.commit.assert_awaited_once()
async def test_retry_after_later_consumption_does_not_restore_quantity(self) -> None:
await self.receive()
self.inventories[0].remaining_packages = 80
self.inventories[0].flow_state = 'AT_SITE_PARTIAL'
await self.receive(self.command(ids=[2, 5]))
self.assertEqual(self.inventories[0].remaining_packages, 80)
self.assertEqual(len(self.events), 2)
self.db.commit.assert_awaited_once()
async def test_received_order_rejects_different_confirmation_key(self) -> None:
await self.receive()
with self.assertRaises(ServiceException):
await self.receive(self.command(key='different'))
self.assertEqual(len(self.events), 2)
async def test_missing_or_unexpected_scans_do_not_change_inventory(self) -> None:
for ids in ([2], [2, 5, 6], [2, 6]):
with self.subTest(ids=ids), self.assertRaises(ServiceException):
await self.receive(self.command(ids=ids))
self.assertEqual(self.order.receipt_state, 'PENDING')
self.assertFalse(self.events)
self.db.commit.assert_not_awaited()
async def test_target_site_access_is_required_even_with_warehouse_permissions(self) -> None:
self.site_access.return_value = False
with self.assertRaises(PermissionException):
await self.receive()
self.assertFalse(self.events)
self.db.commit.assert_not_awaited()
async def test_draft_and_legacy_cannot_be_received_as_new_shipments(self) -> None:
for status, receipt_state in [('DRAFT', 'PENDING'), ('CONFIRMED', 'LEGACY')]:
self.order.status = status
self.order.receipt_state = receipt_state
with self.subTest(status=status, receipt_state=receipt_state), self.assertRaises(ServiceException):
await self.receive()
self.assertFalse(self.events)
async def test_changed_material_snapshot_is_rejected_before_any_mutation(self) -> None:
self.inventories[1].active_filling_id = 999
with self.assertRaises(ServiceException):
await self.receive()
self.assertEqual(self.inventories[0].flow_state, 'IN_TRANSIT')
self.assertFalse(self.events)
async def test_cross_test_batch_inventory_is_rejected(self) -> None:
self.inventories[1].test_batch_id = 3
with self.assertRaises(ServiceException):
await self.receive()
self.assertFalse(self.events)