diff --git a/src/modules/container-loading/api/platformContainerCustoms.test.ts b/src/modules/container-loading/api/platformContainerCustoms.test.ts index 199831eb..2cfc01a1 100644 --- a/src/modules/container-loading/api/platformContainerCustoms.test.ts +++ b/src/modules/container-loading/api/platformContainerCustoms.test.ts @@ -4,7 +4,7 @@ import { completeAllBuyContainerCustoms, downloadContainerCustomsCompletion, dow 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, + 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, completion: null }], startedAt: null, completedAt: null, @@ -39,14 +39,14 @@ describe('platform container customs API', () => { expect(post.mock.calls[0]?.[2]).toMatchObject({ headers: { 'Idempotency-Key': expect.any(String) } }) }) - it('uploads one completion file with final-confirmation query parameters', async () => { + 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 uploadContainerCustomsCompletion('2092438843630391407', completion, 0, true) + await uploadContainerCustomsCompletion('2092438843630391407', completion, 0) expect(post.mock.calls[0]?.[0]).toBe('/platform/v1/customs/loading-plans/documents/2092438843630391407/completion/upload') expect((post.mock.calls[0]?.[1] as FormData).get('file')).toBe(completion) - expect(post.mock.calls[0]?.[2]).toMatchObject({ params: { expectedVersion: 0, finalConfirmation: true }, headers: { 'Content-Type': 'multipart/form-data', 'Idempotency-Key': expect.any(String) } }) + 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 () => { diff --git a/src/modules/container-loading/api/platformContainerCustoms.ts b/src/modules/container-loading/api/platformContainerCustoms.ts index ccfaa157..e1d7bad2 100644 --- a/src/modules/container-loading/api/platformContainerCustoms.ts +++ b/src/modules/container-loading/api/platformContainerCustoms.ts @@ -31,6 +31,8 @@ export interface ContainerCustomsDocument { inboundCodes: string[] frozen: boolean completion: ContainerCustomsFile | null + receiptStatus?: 'PENDING' | 'CONFIRMED' | 'REJECTED' | null + receiptRejectReason?: string | null } export interface ContainerCustomsView { @@ -45,6 +47,7 @@ export interface ContainerCustomsView { version: number taxRefundCount: number coveredTaxRefundCount: number + actualBoxCount: number inbounds: ContainerCustomsInbound[] documents: ContainerCustomsDocument[] startedAt: string | null @@ -105,13 +108,13 @@ export function completeAllBuyContainerCustoms(loadingPlanId: string, expectedVe return action(loadingPlanId, 'complete-all-buy', expectedVersion) } -export async function uploadContainerCustomsCompletion(documentId: string, fileValue: File, expectedVersion: number, finalConfirmation: boolean) { +export async function uploadContainerCustomsCompletion(documentId: string, fileValue: File, expectedVersion: number) { const body = new FormData() body.append('file', fileValue) const response = await http.post, ApiEnvelope>( `${basePath}/documents/${encodeURIComponent(documentId)}/completion/upload`, body, - { params: { expectedVersion, finalConfirmation }, headers: { 'Content-Type': 'multipart/form-data', 'Idempotency-Key': operationId('container-customs-completion') } }, + { params: { expectedVersion }, headers: { 'Content-Type': 'multipart/form-data', 'Idempotency-Key': operationId('container-customs-completion') } }, ) const result = unwrapApiResponse(response) return { ...result, documentId: String(result.documentId), tenantId: String(result.tenantId), source: file(result.source)!, completion: file(result.completion) } diff --git a/src/modules/container-loading/components/ContainerCustomsPanel.test.ts b/src/modules/container-loading/components/ContainerCustomsPanel.test.ts index 2e63b4d2..ef943649 100644 --- a/src/modules/container-loading/components/ContainerCustomsPanel.test.ts +++ b/src/modules/container-loading/components/ContainerCustomsPanel.test.ts @@ -28,7 +28,7 @@ import ContainerCustomsPanel from './ContainerCustomsPanel.vue' function customs(status: ContainerCustomsStatus, hasCompletion = false): ContainerCustomsView { return { aggregateId: '2092438843630391401', loadingPlanId: '2092438843630391402', containerBookingId: '2092438843630391403', - planNo: 'LP-1', cabinetNo: 'MCR08290001', containerNo: 'MSCU-1', status, hasActualCargo: true, version: 3, taxRefundCount: 1, coveredTaxRefundCount: 1, + planNo: 'LP-1', cabinetNo: 'MCR08290001', containerNo: 'MSCU-1', status, hasActualCargo: true, version: 3, taxRefundCount: 1, coveredTaxRefundCount: 1, actualBoxCount: 12, inbounds: [{ inboundId: '2092438843630391404', orderId: '2092438843630391405', orderNo: 'O-1', inboundCode: 'RK-1', tenantId: '2092438843630391406', customerName: '客户一', taxMode: 'TAX_REFUND', covered: true }], documents: [{ documentId: '2092438843630391407', tenantId: '2092438843630391406', @@ -68,18 +68,19 @@ describe('ContainerCustomsPanel', () => { it('keeps READY_TO_START read-only while exposing source file name and download', async () => { const wrapper = await render('READY_TO_START') expect(wrapper.text()).toContain('开始报关') + expect(wrapper.text()).toContain('实际箱数 12') expect(wrapper.text()).toContain('客户退税资料.pdf') expect(wrapper.text()).toContain('客户退税资料') - expect(wrapper.text()).toContain('报关完成文件') + expect(wrapper.text()).toContain('报关行回执') expect(wrapper.text()).not.toContain('客户报关资料') expect(wrapper.find('[aria-label="下载客户退税资料 客户退税资料.pdf"]').exists()).toBe(true) - expect(buttonTexts(wrapper)).not.toContain('上传报关完成文件') - expect(buttonTexts(wrapper)).not.toContain('覆盖报关完成文件') + expect(buttonTexts(wrapper)).not.toContain('上传报关行回执') + expect(buttonTexts(wrapper)).not.toContain('覆盖报关行回执') expect(buttonTexts(wrapper)).not.toContain('提交') }) it('shows a successful empty state and hides customs actions when the completed plan has no actual cargo', async () => { - api.get.mockResolvedValue({ ...customs('READY_TO_START'), hasActualCargo: false, taxRefundCount: 0, coveredTaxRefundCount: 0, inbounds: [], documents: [] }) + api.get.mockResolvedValue({ ...customs('READY_TO_START'), hasActualCargo: false, taxRefundCount: 0, coveredTaxRefundCount: 0, actualBoxCount: 0, inbounds: [], documents: [] }) const wrapper = mount(ContainerCustomsPanel, { props: { loadingPlanId: '2092438843630391402', canList: true, canManage: true, departureState: 'WAITING_OTHER_GATES' }, global: { plugins: [ElementPlus] }, @@ -106,19 +107,19 @@ describe('ContainerCustomsPanel', () => { it('allows completion upload and overwrite only while DECLARING with manage permission', async () => { let wrapper = await render('DECLARING', true, false) - expect(wrapper.text()).toContain('上传报关完成文件') + expect(wrapper.text()).toContain('上传报关行回执') expect(wrapper.text()).toContain('提交') wrapper.unmount() wrapper = await render('DECLARING', true, true) - expect(wrapper.text()).toContain('覆盖报关完成文件') + expect(wrapper.text()).toContain('覆盖报关行回执') expect(wrapper.text()).toContain('报关完成文件.pdf') wrapper.unmount() wrapper = await render('DECLARING', false, true) expect(wrapper.text()).toContain('只读') - expect(buttonTexts(wrapper)).not.toContain('上传报关完成文件') - expect(buttonTexts(wrapper)).not.toContain('覆盖报关完成文件') + expect(buttonTexts(wrapper)).not.toContain('上传报关行回执') + expect(buttonTexts(wrapper)).not.toContain('覆盖报关行回执') expect(buttonTexts(wrapper)).not.toContain('提交') }) @@ -127,8 +128,8 @@ describe('ContainerCustomsPanel', () => { expect(wrapper.text()).toContain('客户退税资料.pdf') expect(wrapper.text()).toContain('报关完成文件.pdf') expect(wrapper.text()).toContain('报关已完成,待其他离仓条件') - expect(buttonTexts(wrapper)).not.toContain('上传报关完成文件') - expect(buttonTexts(wrapper)).not.toContain('覆盖报关完成文件') + expect(buttonTexts(wrapper)).not.toContain('上传报关行回执') + expect(buttonTexts(wrapper)).not.toContain('覆盖报关行回执') expect(buttonTexts(wrapper)).not.toContain('提交') }) @@ -139,7 +140,7 @@ describe('ContainerCustomsPanel', () => { expect(api.downloadSource).toHaveBeenCalledWith('2092438843630391407') expect(api.downloadCompletion).not.toHaveBeenCalled() - await wrapper.get('[aria-label="下载报关完成文件 报关完成文件.pdf"]').trigger('click') + await wrapper.get('[aria-label="下载报关行回执 报关完成文件.pdf"]').trigger('click') await flushPromises() expect(api.downloadCompletion).toHaveBeenCalledWith('2092438843630391407') expect(HTMLAnchorElement.prototype.click).toHaveBeenCalledTimes(2) diff --git a/src/modules/container-loading/components/ContainerCustomsPanel.vue b/src/modules/container-loading/components/ContainerCustomsPanel.vue index 312ceb4b..6002af16 100644 --- a/src/modules/container-loading/components/ContainerCustomsPanel.vue +++ b/src/modules/container-loading/components/ContainerCustomsPanel.vue @@ -38,7 +38,6 @@ const completionFiles = reactive>({}) const missingInbounds = computed(() => customs.value?.inbounds.filter((item) => item.taxMode === 'TAX_REFUND' && !item.covered) || []) const missingCoverageCount = computed(() => Math.max(0, (customs.value?.taxRefundCount || 0) - (customs.value?.coveredTaxRefundCount || 0))) const allBuy = computed(() => customs.value?.hasActualCargo === true && customs.value.taxRefundCount === 0) -const missingCompletionCount = computed(() => customs.value?.documents.filter((item) => !item.completion).length || 0) const statusText: Record = { WAITING_CUSTOMER_DOCUMENTS: '待客户补齐退税资料', READY_TO_START: '可开始报关', DECLARING: '报关中', COMPLETED: '报关已完成', } @@ -76,19 +75,13 @@ async function uploadCompletion(document: ContainerCustomsDocument) { if (!props.canManage || customs.value?.status !== 'DECLARING') return const selected = completionFiles[document.documentId] if (!selected) { ElMessage.warning('请选择对应的报关完成文件'); return } - const finalConfirmation = !document.completion && missingCompletionCount.value === 1 - if (finalConfirmation) { - try { - await ElMessageBox.confirm('这是最后一份待上传的报关完成文件;确认后系统将自动完成当前混合/退税柜报关。是否继续?', '完成整柜报关', { customClass: 'admin-density-confirm', type: 'warning', confirmButtonText: '确认上传并完成' }) - } catch { return } - } actionLoading.value = true; actionError.value = null try { - await uploadContainerCustomsCompletion(document.documentId, selected, document.completion?.version || 0, finalConfirmation) + await uploadContainerCustomsCompletion(document.documentId, selected, document.completion?.version || 0) completionFiles[document.documentId] = null await load() emit('refreshed') - ElMessage.success(finalConfirmation ? '报关完成文件已上传,整柜报关已完成' : '报关完成文件已上传') + ElMessage.success('报关行回执已上传,等待客户确认') } catch (error) { actionError.value = getApiErrorDetails(error, '报关完成文件上传失败') } finally { actionLoading.value = false } } @@ -168,19 +161,19 @@ onMounted(load)