103 lines
6.1 KiB
Python
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', '[]'))
|