"""Tracking facts preserve actual/estimated meaning, corrections and order identity.""" import json import sqlite3 import unittest from contextlib import closing from logistics_chat.order_data import import_records, import_tracking_records from logistics_chat.order_tracking import build_tracking class OrderTrackingTests(unittest.TestCase): def fact(self, code, actual=None, estimated=None, **extra): return {'code': code, 'actual_at': actual, 'estimated_at': estimated, 'observed_at': '2026-10-29 12:00', 'shipment_key': 'container-1', **extra} def test_estimates_do_not_advance_actual_progress_or_fill_missing_milestones(self): result = build_tracking([ self.fact('DEPARTED_ORIGIN_PORT', '2026-10-25 10:00'), self.fact('ARRIVED_DESTINATION_PORT', estimated='2026-11-01 10:00'), ]) self.assertEqual(result['status'], '船舶已离开起运港') self.assertEqual([n['text'] for n in result['nodes']], ['船舶已离开起运港']) self.assertEqual(result['estimated_nodes'][0]['text'], '预计到达目的港') self.assertEqual(result['estimated_nodes'][0]['time'], '2026-11-01 10:00') def test_latest_observation_can_correct_actual_time_backwards(self): result = build_tracking([ self.fact('ARRIVED_DESTINATION_PORT', '2026-10-28 10:00'), self.fact('ARRIVED_DESTINATION_PORT', '2026-10-27 10:00', observed_at='2026-10-30 10:00'), self.fact('ARRIVED_DESTINATION_PORT', estimated='2026-10-31 10:00', observed_at='2026-10-31 10:00'), ]) self.assertEqual(len(result['nodes']), 1) self.assertEqual(result['nodes'][0]['time'], '2026-10-27 10:00') self.assertEqual(result['estimated_nodes'], []) def test_delayed_earlier_milestone_and_stale_forecast_do_not_regress_progress(self): result = build_tracking([ self.fact('PICKED_UP', '2026-10-28 10:00'), self.fact('AT_ORIGIN_PORT', '2026-10-29 10:00', observed_at='2026-10-30 10:00'), self.fact('ARRIVED_DESTINATION_PORT', estimated='2026-10-29 20:00'), ]) self.assertEqual(result['status'], '已提柜') self.assertEqual(len(result['nodes']), 2) self.assertEqual(result['estimated_nodes'], []) self.assertNotIn('签收', json.dumps(result, ensure_ascii=False)) def test_multiple_containers_keep_independent_progress_and_locations(self): result = build_tracking([ self.fact('PICKED_UP', '2026-10-28 10:00', shipment_no='BOX-A', place='码头 A'), self.fact('IN_TRANSIT', '2026-10-27 10:00', shipment_key='container-2', shipment_no='BOX-B'), self.fact('ARRIVED_DESTINATION_PORT', estimated='2026-11-02 10:00', shipment_key='container-2', shipment_no='BOX-B'), ]) self.assertEqual(result['status'], '货柜 BOX-A:已提柜;货柜 BOX-B:海运途中') self.assertEqual(result['nodes'][0]['place'], '货柜 BOX-A · 码头 A') self.assertEqual(result['estimated_nodes'][0]['place'], '货柜 BOX-B') def test_domestic_actual_is_retained_when_ship_has_only_forecasts(self): result = build_tracking([ self.fact('DOMESTIC_RECEIVED', '2026-10-28 10:00', shipment_key=''), self.fact('ARRIVED_DESTINATION_PORT', estimated='2026-11-02 10:00'), ]) self.assertEqual(result['status'], '已入国内仓') self.assertEqual(len(result['nodes']), 1) def test_estimated_only_unknown_codes_and_broken_values_are_safe(self): result = build_tracking([self.fact('PICKED_UP', estimated='2026-11-02 10:00'), {'code': []}, None, self.fact('UNRECOGNIZED', '2026-10-28 10:00'), self.fact('IN_TRANSIT', 'not a time')]) self.assertEqual(result['status'], '') self.assertEqual(result['nodes'], []) self.assertEqual(result['estimated_nodes'][0]['text'], '预计提柜') for value in (None, {}, 'broken json', '[null]', []): self.assertIsNone(build_tracking(value)) def test_tracking_import_preserves_manual_order_fields_and_scopes_by_identity(self): with closing(sqlite3.connect(':memory:')) as connection: orders = [{'order_no': 'ORDER2026001', 'source_order_id': '987654321123456789', 'tenant_id': tenant, 'gross_weight_kg': weight} for tenant, weight in [('10', '42'), ('20', '80')]] import_records(connection, orders) record = {**orders[0], 'tracking_json': [self.fact('IN_TRANSIT', '2026-10-28 10:00')]} self.assertEqual(import_tracking_records(connection, [record]), {'imported': 1, 'preserved': 0, 'missing': 0}) self.assertEqual(connection.execute('SELECT gross_weight_kg FROM order_test_data ' 'WHERE tenant_id=\'10\'').fetchone()[0], '42') self.assertEqual(connection.execute('SELECT tracking_json FROM order_test_data ' 'WHERE tenant_id=\'20\'').fetchone()[0], '[]') self.assertEqual(import_tracking_records(connection, [record])['preserved'], 1) self.assertEqual(import_tracking_records(connection, [record], replace=True)['imported'], 1) with self.assertRaises(ValueError): import_tracking_records(connection, [{**record, 'source_order_id': 'wrong'}], replace=True) self.assertEqual(import_tracking_records(connection, [{**record, 'order_no': 'MISSING2026001'}])['missing'], 1) def test_old_table_is_migrated_without_losing_data(self): with closing(sqlite3.connect(':memory:')) as connection: import_records(connection, [{'tenant_id': '10', 'order_no': 'ORDER2026001'}]) connection.execute('ALTER TABLE order_test_data DROP COLUMN tracking_json') import_tracking_records(connection, []) self.assertEqual(connection.execute('SELECT order_no,tracking_json FROM order_test_data').fetchone(), ('ORDER2026001', '[]'))