import { beforeEach, describe, expect, it, vi } from 'vitest' import { ApiResponseError, http } from '@/shared/api/http' import { createDeclarationBatch, submitDeclarationBatch, recordClearanceResult, type DeclarationBatch } from './platformContainerCustoms' import { completeAllBuyContainerCustoms, downloadContainerCustomsReceipt, downloadContainerCustomsDocument, downloadContainerCustomsFinal, getContainerCustoms, sendContainerCustomsToBroker, startContainerCustoms, uploadContainerCustomsReceipt } from './platformContainerCustoms' const customs = { aggregateId: 2092438843630391401n, loadingPlanId: 2092438843630391402n, containerBookingId: 2092438843630391403n, planNo: 'LP-1', cabinetNo: 'CB-1', containerNo: 'MSCU-1', status: 'READY_TO_START', hasActualCargo: true, version: '3', taxRefundCount: 1, coveredTaxRefundCount: 1, actualBoxCount: 12, inbounds: [{ inboundId: 2092438843630391404n, orderId: 2092438843630391405n, orderNo: 'O-1', inboundCode: 'RK-1', tenantId: 2092438843630391406n, customerName: '客户一', taxMode: 'TAX_REFUND', covered: true }], documents: [{ documentId: 2092438843630391407n, tenantId: 2092438843630391406n, source: { fileId: '2092438843630391408', fileName: 'source.pdf', fileHash: 'hash', sizeBytes: 1, version: 2, uploadedAt: '2026-08-28T10:00:00' }, inboundIds: ['2092438843630391404'], inboundCodes: ['RK-1'], frozen: true, receipt: null, finalDocument: null, confirmationHistory: [] }], brokerSubmission: null, startedAt: null, completedAt: null, } describe('platform container customs API', () => { beforeEach(() => vi.restoreAllMocks()) it('preserves the server batch number when creating and submitting a declaration', async () => { const batch = { id: '123', customsContainerId: '456', loadingPlanId: '789', batchNo: 'DECL202610200001', sourceVersion: 1, version: 0, results: [] } const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', data: batch }) const created = await createDeclarationBatch('789', 3) expect(created.batchNo).toBe('DECL202610200001') expect(post.mock.calls[0]?.slice(0, 2)).toEqual([ '/platform/v1/customs/loading-plans/789/declaration-batches', { expectedVersion: 3, remark: null }, ]) const submitted = await submitDeclarationBatch(created, ' CUSTOMS-EXTERNAL-1 ') expect(submitted.batchNo).toBe(created.batchNo) expect(post.mock.calls[1]?.slice(0, 2)).toEqual([ '/platform/v1/customs/loading-plans/declaration-batches/123/submit', { expectedVersion: 0, customsEntryNo: 'CUSTOMS-EXTERNAL-1', remark: null }, ]) }) it('records domestic customs time in Shanghai even when UTC falls on the previous day', async () => { vi.useFakeTimers() try { vi.setSystemTime(new Date('2026-09-29T16:30:00Z')) const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', data: { id: '1' } }) await recordClearanceResult({ id: '123', version: 1 } as DeclarationBatch, 'OTHER', 'test') expect(post).toHaveBeenCalledWith('/platform/v1/customs/loading-plans/declaration-batches/123/results', expect.objectContaining({ occurredAt: '2026-09-30T00:30:00', resultType: 'OTHER', resolution: 'test' }), expect.objectContaining({ headers: { 'Idempotency-Key': expect.any(String) } })) } finally { vi.useRealTimers() } }) it('uses the frozen customs-v1 paths and preserves long ids as strings', async () => { const get = vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: customs }) const result = await getContainerCustoms('2092438843630391402') expect(result).not.toBeNull() if (!result) throw new Error('expected completed loading plan customs view') expect(get).toHaveBeenCalledWith('/platform/v1/customs/loading-plans/2092438843630391402') expect(result).toMatchObject({ aggregateId: '2092438843630391401', loadingPlanId: '2092438843630391402', containerBookingId: '2092438843630391403' }) expect(result.inbounds[0]).toMatchObject({ inboundId: '2092438843630391404', orderId: '2092438843630391405', tenantId: '2092438843630391406' }) expect(result.documents[0]?.documentId).toBe('2092438843630391407') }) it('returns null while the loading plan is not completed', async () => { vi.spyOn(http, 'get').mockResolvedValue({ code: '200', message: 'ok', data: null }) await expect(getContainerCustoms('2097922185010147329')).resolves.toBeNull() }) it('sends confirmed start and all-buy completion with optimistic version and idempotency', async () => { const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: customs }) await startContainerCustoms('2092438843630391402', 3) await completeAllBuyContainerCustoms('2092438843630391402', 4) expect(post.mock.calls[0]?.slice(0, 2)).toEqual(['/platform/v1/customs/loading-plans/2092438843630391402/start', { expectedVersion: 3, confirm: true, remark: null }]) expect(post.mock.calls[1]?.slice(0, 2)).toEqual(['/platform/v1/customs/loading-plans/2092438843630391402/complete-all-buy', { expectedVersion: 4, confirm: true, remark: null }]) expect(post.mock.calls[0]?.[2]).toMatchObject({ headers: { 'Idempotency-Key': expect.any(String) } }) }) it('sends the frozen source documents to the broker through the first-send action', async () => { const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: customs }) await sendContainerCustomsToBroker('2092438843630391402', 3) expect(post.mock.calls[0]?.slice(0, 2)).toEqual(['/platform/v1/customs/loading-plans/2092438843630391402/send-to-broker', { expectedVersion: 3, confirm: true, remark: '已将冻结退税报关资料首次发送给报关行' }]) expect(post.mock.calls[0]?.[2]).toMatchObject({ headers: { 'Idempotency-Key': expect.any(String) } }) }) it('uploads one receipt file without completing the container', async () => { const document = { ...customs.documents[0], documentId: '2092438843630391407', tenantId: '2092438843630391406' } const post = vi.spyOn(http, 'post').mockResolvedValue({ code: '200', message: 'ok', data: document }) const completion = new File(['done'], 'done.pdf') await uploadContainerCustomsReceipt('2092438843630391407', completion, 0) expect(post.mock.calls[0]?.[0]).toBe('/platform/v1/customs/loading-plans/documents/2092438843630391407/receipt/upload') expect((post.mock.calls[0]?.[1] as FormData).get('file')).toBe(completion) expect(post.mock.calls[0]?.[2]).toMatchObject({ params: { expectedVersion: 0 }, headers: { 'Content-Type': 'multipart/form-data', 'Idempotency-Key': expect.any(String) } }) }) it('keeps source and completion downloads isolated on the long document id', async () => { const source = new Blob(['source']) const completion = new Blob(['completion']) const get = vi.spyOn(http, 'get').mockResolvedValueOnce(source).mockResolvedValueOnce(completion) const documentId = '2092438843630391407' await expect(downloadContainerCustomsDocument(documentId)).resolves.toBe(source) await expect(downloadContainerCustomsReceipt(documentId)).resolves.toBe(completion) expect(get.mock.calls).toEqual([ [`/platform/v1/customs/loading-plans/documents/${documentId}/download`, { responseType: 'blob' }], [`/platform/v1/customs/loading-plans/documents/${documentId}/completion/download`, { responseType: 'blob' }], ]) }) it.each([downloadContainerCustomsDocument, downloadContainerCustomsReceipt, downloadContainerCustomsFinal])('拒绝 HTTP 200 中的真实 JSON Blob 业务错误 %#', async (download) => { const body = new Blob([JSON.stringify({ code: 'A0404', message: '预录单不存在', data: null, requestId: 'request-customs-blob', traceId: 'trace-customs-blob' })], { type: 'application/json;charset=UTF-8' }) vi.spyOn(http, 'get').mockResolvedValue(body) const error: unknown = await download('2092438843630391407').catch((reason: unknown) => reason) expect(error).toBeInstanceOf(ApiResponseError) expect(error).toMatchObject({ code: 'A0404', message: '预录单不存在', status: 200, requestId: 'request-customs-blob', traceId: 'trace-customs-blob' }) }) it('非 2xx 的 JSON Blob 也保留业务错误和诊断信息', async () => { const body = new Blob([JSON.stringify({ code: 'A0403', message: '无权下载预录单', data: null, requestId: 'request-forbidden', traceId: 'trace-forbidden' })], { type: 'application/json' }) vi.spyOn(http, 'get').mockRejectedValue({ isAxiosError: true, response: { status: 403, data: body } }) await expect(downloadContainerCustomsReceipt('7')).rejects.toMatchObject({ code: 'A0403', message: '无权下载预录单', status: 403, requestId: 'request-forbidden', traceId: 'trace-forbidden' }) }) it.each([ ['application/pdf', '%PDF-1.7 原文件'], ['application/octet-stream', '原始二进制'], ['application/json', '{"product":"报关资料","code":"ITEM-1"}'], ['application/json', '{invalid-json'], ])('保留原文件内容与对象:%s', async (type, contents) => { const body = new Blob([contents], { type }) vi.spyOn(http, 'get').mockResolvedValue(body) await expect(downloadContainerCustomsFinal('7')).resolves.toBe(body) }) it('非 JSON 的传输错误沿用原始错误', async () => { const error = { isAxiosError: true, response: { status: 502, data: new Blob(['bad gateway'], { type: 'text/html' }) } } vi.spyOn(http, 'get').mockRejectedValue(error) await expect(downloadContainerCustomsReceipt('7')).rejects.toBe(error) }) })