import unittest import hashlib import json from pydantic import ValidationError from module_business.domain.enums import PalletEventType from module_business.entity.do.master_do import BizTunnel from module_business.entity.do.operation_do import BizPalletEvent, BizPalletInventory, BizReturnItem, BizUsageRecord from module_business.entity.vo.master_vo import PalletCreateModel, TunnelModel, TunnelPageQueryModel from module_business.entity.vo.operation_vo import FillingCreateModel, TunnelEntryCreateModel, UsageCreateModel from module_business.service.usage_service import UsageService class TestTunnelFlowContract(unittest.TestCase): def test_tunnel_master_data_belongs_to_site_and_has_scoped_code(self) -> None: self.assertEqual(BizTunnel.__tablename__, 'biz_tunnel') self.assertIn('site_id', BizTunnel.__table__.c) self.assertIn('tunnel_code', BizTunnel.__table__.c) self.assertIn('current_trace_no', BizTunnel.__table__.c) self.assertEqual( {constraint.name for constraint in BizTunnel.__table__.constraints if constraint.name}, { 'uq_biz_tunnel_site_code', 'ck_biz_tunnel_status', }, ) def test_tunnel_master_vo_supports_list_and_write_contract(self) -> None: tunnel = TunnelModel( siteId=9, tunnelCode='T-01', tunnelName='一号隧道', ) self.assertEqual(tunnel.model_dump(by_alias=True)['tunnelCode'], 'T-01') query = TunnelPageQueryModel(siteId=9, tunnelCode='T-01') self.assertEqual(query.site_id, 9) def test_usage_requires_tunnel_but_keeps_line_optional(self) -> None: usage = UsageCreateModel( qrToken='qr-1', tunnelId=9, usedPackages=20, idempotencyKey='usage-key', ) self.assertIsNone(usage.line_id) with self.assertRaises(ValidationError): UsageCreateModel( qrToken='qr-1', usedPackages=20, idempotencyKey='usage-key', ) def test_tunnel_entry_is_idempotent_command(self) -> None: command = TunnelEntryCreateModel( qrToken='qr-1', tunnelId=9, idempotencyKey='entry-key', ) self.assertEqual(command.tunnel_id, 9) def test_flow_records_keep_tunnel_and_trace_snapshots(self) -> None: self.assertIn('tunnel_id', BizPalletInventory.__table__.c) self.assertIn('trace_no', BizPalletInventory.__table__.c) self.assertIn('tunnel_id', BizUsageRecord.__table__.c) self.assertIn('trace_no', BizUsageRecord.__table__.c) self.assertIn('batch_no_snapshot', BizUsageRecord.__table__.c) self.assertIn('tunnel_id_snapshot', BizReturnItem.__table__.c) self.assertIn('trace_no_snapshot', BizReturnItem.__table__.c) self.assertIn('before_tunnel_id', BizPalletEvent.__table__.c) self.assertIn('after_tunnel_id', BizPalletEvent.__table__.c) self.assertIn('trace_no', BizPalletEvent.__table__.c) self.assertEqual(PalletEventType.TUNNEL_ENTERED.value, 'TUNNEL_ENTERED') def test_new_pallet_and_filling_are_fixed_to_one_hundred_packages(self) -> None: PalletCreateModel(palletCode='P-001', capacityPackages=100) FillingCreateModel( qrToken='qr-1', warehouseId=1, strainId=1, batchNo='B-1', productionDate='2026-10-01', expiryDate='2027-04-01', packageSpec='标准包', initialPackages=100, idempotencyKey='fill-key', ) with self.assertRaises(ValidationError): PalletCreateModel(palletCode='P-002', capacityPackages=20) with self.assertRaises(ValidationError): FillingCreateModel( qrToken='qr-1', warehouseId=1, strainId=1, batchNo='B-1', packageSpec='标准包', initialPackages=20, idempotencyKey='fill-key', ) def test_usage_idempotency_hash_includes_tunnel(self) -> None: command = UsageCreateModel( qrToken=' qr-1 ', tunnelId=9, lineId=None, usedPackages=20, idempotencyKey='usage-key', ) expected = hashlib.sha256( json.dumps( { 'qr_token': 'qr-1', 'tunnel_id': 9, 'line_id': None, 'used_packages': 20, }, sort_keys=True, separators=(',', ':'), ).encode() ).hexdigest() self.assertEqual(expected, UsageService._build_request_hash(command))