import { beforeEach, describe, expect, it, vi } from 'vitest' import { http } from '@/shared/api/http' import { sendTerminalDrayageEmail, getTerminalDrayageEmails, retryTerminalDrayageEmail, getTerminalDrayageHistory, listTerminalDrayages, recordTerminalDrayageEmptyReturn, scheduleTerminalDrayagePickup, selectTerminalDrayageFleet, getDirectDeliveries, recordDirectDelivery, recordTerminalPickup, } from './terminalDrayages' it('records only the original actual PICKED_UP node with an immutable observed time and idempotency header', async () => { const command = { standardNode: 'PICKED_UP' as const, observedAt: '2026-10-15T09:00:00', estimatedAt: null, actualAt: '2026-10-14T23:00:00', locationCode: 'TEST-PORT', locationName: '测试码头', reason: '司机确认已提柜' } vi.mocked(http.post).mockResolvedValue({ data: {} }) await recordTerminalPickup('9007199254740993', command, 'same-pickup-key') expect(http.post).toHaveBeenCalledWith('/platform/v1/container-shipping-nodes/9007199254740993', command, { headers: { 'Idempotency-Key': 'same-pickup-key' } }) }) it('uses the actual direct-delivery source and stable idempotency key without coercing long IDs', async () => { vi.mocked(http.get).mockResolvedValue({ data: [] }); await getDirectDeliveries('9007199254740993') expect(http.get).toHaveBeenCalledWith('/platform/v1/terminal-drayages/9007199254740993/direct-deliveries') const request = { expectedVersion: 0, actualAt: '2026-10-16T01:00:00', timezone: 'America/Los_Angeles', receivedBy: '测试签收人', evidenceNo: 'POD-001', remark: null } vi.mocked(http.post).mockResolvedValue({ data: { requirementKey: 'INBOUND:9007199254740999', version: 1 } }) await recordDirectDelivery('9007199254740993', 'INBOUND:9007199254740999', request, 'retry-same-key') expect(http.post).toHaveBeenCalledWith('/platform/v1/terminal-drayages/9007199254740993/direct-deliveries/9007199254740999', request, { headers: { 'Idempotency-Key': 'retry-same-key' } }) await expect(recordDirectDelivery('1', 'ORDER:1', request, 'bad')).rejects.toThrow('标识无效') }) vi.mock('@/shared/api/http', () => ({ http: { get: vi.fn(), post: vi.fn() }, unwrapApiResponse: (response: { data: T }) => response.data, })) const view = { containerBookingId: '9007199254740993', containerNo: 'MSCU1234567', oceanBookingId: '9007199254740994', status: 'UNLOADED', version: '3', fleetSupplierId: '9007199254740995', fleetSupplierName: '港运车队', pickupAppointmentAt: '2026-09-07T08:00:00', actualPickupAt: '2026-09-07T09:00:00', estimatedEmptyReturnAt: '2026-09-10T10:00:00', actualEmptyReturnAt: null, unloadingCompletedAt: '2026-09-07T10:00:00', emptyReturnWarning: { status: 'OVERDUE', deadlineAt: '2026-09-10T10:00:00', overdueHours: '26', overdueDays: '1' }, allowedActions: ['SELECT_FLEET', 'RECORD_EMPTY_RETURN'], updatedAt: '2026-09-11T12:00:00', } describe('terminal drayages API', () => { beforeEach(() => vi.clearAllMocks()) it('queues pickup email with version and idempotency and scopes retries to the container', async () => { vi.mocked(http.post).mockResolvedValueOnce({ data: view }) await sendTerminalDrayageEmail('9007199254740993', 3) expect(http.post).toHaveBeenCalledWith('/platform/v1/terminal-drayages/9007199254740993/pickup-email', { expectedVersion: 3 }, { headers: { 'Idempotency-Key': expect.any(String) } }) vi.mocked(http.get).mockResolvedValueOnce({ data: { deliveryEnabled: false, records: [{ id: '9007199254740999', status: 'FAILED' }] } }) expect((await getTerminalDrayageEmails('9007199254740993')).records[0].id).toBe('9007199254740999') vi.mocked(http.post).mockResolvedValueOnce({ data: null }) await retryTerminalDrayageEmail('9007199254740993', '9007199254740999') expect(http.post).toHaveBeenLastCalledWith('/platform/v1/terminal-drayages/9007199254740993/emails/9007199254740999/retry') }) it('sends repeated container ids and preserves long identifiers as strings', async () => { vi.mocked(http.get).mockResolvedValueOnce({ data: [view] }) const result = await listTerminalDrayages(['9007199254740993', '9007199254740994']) const config = vi.mocked(http.get).mock.calls[0][1] as { params: URLSearchParams } expect(config.params.getAll('containerBookingIds')).toEqual(['9007199254740993', '9007199254740994']) expect(result[0]).toMatchObject({ containerBookingId: '9007199254740993', oceanBookingId: '9007199254740994', fleetSupplierId: '9007199254740995', version: 3 }) expect(result[0].emptyReturnWarning).toMatchObject({ overdueHours: 26, overdueDays: 1 }) }) it('uses frozen command paths, versions and idempotency keys', async () => { vi.mocked(http.post).mockResolvedValue({ data: view }) await selectTerminalDrayageFleet('101', { expectedVersion: 3, fleetSupplierId: '501', fleetSupplierName: '港运车队' }) await scheduleTerminalDrayagePickup('101', { expectedVersion: 4, pickupAppointmentAt: '2026-09-07T08:00:00' }) await recordTerminalDrayageEmptyReturn('101', { expectedVersion: 5, actualAt: '2026-09-10T09:00:00', remark: '实际还柜' }) expect(http.post).toHaveBeenNthCalledWith(1, '/platform/v1/terminal-drayages/101/fleet', expect.objectContaining({ expectedVersion: 3, fleetSupplierId: '501' }), expect.objectContaining({ headers: { 'Idempotency-Key': expect.stringContaining('terminal-drayage-fleet-') } })) expect(http.post).toHaveBeenNthCalledWith(2, '/platform/v1/terminal-drayages/101/pickup-appointment', expect.objectContaining({ expectedVersion: 4 }), expect.any(Object)) expect(http.post).toHaveBeenNthCalledWith(3, '/platform/v1/terminal-drayages/101/empty-return', expect.objectContaining({ expectedVersion: 5, remark: '实际还柜' }), expect.objectContaining({ headers: { 'Idempotency-Key': expect.stringContaining('terminal-drayage-empty-return-') } })) }) it('loads operation history from the container business id', async () => { vi.mocked(http.get).mockResolvedValueOnce({ data: [{ action: 'SELECT_FLEET', fromStatus: 'ARRIVED', toStatus: 'ARRIVED', fromVersion: 0, toVersion: 1, reason: null, operatorId: '9007199254740993', operatorName: '平台管理员', createdAt: '2026-09-07T08:00:00' }] }) const result = await getTerminalDrayageHistory('101') expect(http.get).toHaveBeenCalledWith('/platform/v1/terminal-drayages/101/history') expect(result[0].operatorId).toBe('9007199254740993') }) })