Files
baiyue-logistics-chat/tests/test_order_tracking.py
T

103 lines
6.1 KiB
Python

"""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', '[]'))