import { beforeEach, describe, expect, it, vi } from 'vitest' import { http } from '@/shared/api/http' import { createExceptionOrder, listExceptionOrders, resolveExceptionRelation, getExceptionDetail, commandException, uploadExceptionEvidence, normalizeExceptionDetail, type ExceptionDetail } from './exceptionOrders' vi.mock('@/shared/api/http', async (importOriginal) => ({ ...(await importOriginal()), http: { get: vi.fn(), post: vi.fn() } })) describe('exception order API', () => { it('preserves receipt and dispatch business numbers while normalizing all actual history quantities', () => { const raw = { order: { version: '2' }, returnSources: [], returnReceipts: [{ receiptNo: 'RSR-9007199254740993', receivedPackages: '2', receivedAt: '2026-10-03T23:24:13', redispatches: [{ dispatchNo: 'DS/1', status: 'DEPARTED', selectedPackages: '2', pickedPackages: '2', deliveries: [{ deliveryNo: 'DL/1', status: 'DELIVERED' }] }] }] } as unknown as ExceptionDetail const result = normalizeExceptionDetail(raw) expect(result.returnReceipts[0]).toMatchObject({ receiptNo: 'RSR-9007199254740993', receivedPackages: 2, redispatches: [{ dispatchNo: 'DS/1', selectedPackages: 2, pickedPackages: 2 }] }) raw.returnReceipts[0]!.receivedPackages = null as unknown as number expect(() => normalizeExceptionDetail(raw)).toThrow('历史实收数量无效') }) it('normalizes actual string versions and quantities without converting business references to numbers', () => { const raw = { order: { version: '7', evidenceFileIds: ['190000000000000099'] }, returnSources: [{ outboundPalletItemId: '190000000000000098', inventoryPositionId: '190000000000000097', maxReturnPackages: '2' }], returnReceipts: [] } as unknown as ExceptionDetail const result = normalizeExceptionDetail(raw) expect(result.order.version).toBe(7); expect(result.returnSources[0]?.maxReturnPackages).toBe(2) expect(result.returnSources[0]?.outboundPalletItemId).toBe('190000000000000098') for (const version of ['', '-1', '1.5', '9007199254740993', undefined]) expect(() => normalizeExceptionDetail({ ...raw, order: { ...raw.order, version } } as unknown as ExceptionDetail)).toThrow() }) it('loads authoritative detail and scopes every versioned command and evidence upload to its customer and business number', async () => { vi.mocked(http.get).mockResolvedValue({ code: '200', data: { order: { version: '7', allowedActions: ['COMPLETE_EXCEPTION'] }, returnSources: [], returnReceipts: [] } }) await getExceptionDetail('100', 'EX/1') expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/exception-orders/EX%2F1', { params: { customerId: '100' } }) vi.mocked(http.post).mockResolvedValue({ code: '200', data: { exceptionNo: 'EX/1', version: '8', restoredPackages: '0' } }) for (const action of ['start-processing', 'cancel', 'complete'] as const) { const body = { expectedVersion: 7, idempotencyKey: `original-${action}` } await commandException('100', 'EX/1', action, body) expect(http.post).toHaveBeenLastCalledWith(`/platform/v1/last-leg/exception-orders/EX%2F1/${action}`, body, { params: { customerId: '100' } }) } const file = new File(['evidence'], 'receipt.txt') await uploadExceptionEvidence('100', 'EX/1', file) const call = vi.mocked(http.post).mock.calls.at(-1)! expect(call[0]).toBe('/platform/v1/last-leg/exception-orders/EX%2F1/evidence') expect((call[1] as FormData).get('file')).toBe(file); expect(call[2]).toEqual({ params: { customerId: '100' } }) }) beforeEach(() => { vi.mocked(http.get).mockReset(); vi.mocked(http.post).mockReset() }) it('queries the real paginated exception endpoint', async () => { vi.mocked(http.get).mockResolvedValue({ code: '200', data: { records: [], page: '1', size: '20', total: '2', pages: '1' } }) const result = await listExceptionOrders({ page: 1, size: 20, keyword: 'EXC' }) expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/exception-orders', { params: { page: 1, size: 20, keyword: 'EXC' } }) expect(result.total).toBe(2) }) it('resolves relation before creating an exception document', async () => { vi.mocked(http.get).mockResolvedValue({ code: '200', data: { inboundOrderId: '11', inboundCode: 'IN-1', lastMileOrderId: '12', lastMileOrderNo: 'LM-1', customerId: '1' } }) await resolveExceptionRelation({ inboundCode: 'IN-1', lastMileOrderNo: 'LM-1' }) expect(http.get).toHaveBeenCalledWith('/platform/v1/last-leg/exception-orders/resolve', { params: { inboundCode: 'IN-1', lastMileOrderNo: 'LM-1' } }) vi.mocked(http.post).mockResolvedValue({ code: '200', data: { id: '99', exceptionNo: 'EXC-1', status: 'PENDING' } }) await createExceptionOrder({ inboundCode: 'IN-1', lastMileOrderNo: 'LM-1', exceptionType: 'RETURN', reason: '拒收' }) expect(http.post).toHaveBeenCalledWith('/platform/v1/last-leg/exception-orders', expect.objectContaining({ inboundCode: 'IN-1', lastMileOrderNo: 'LM-1' })) }) })