fix(admin): expose container customs downloads
This commit is contained in:
@@ -1,32 +1,134 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { flushPromises, mount } from '@vue/test-utils'
|
||||
import ElementPlus from 'element-plus'
|
||||
import { ApiResponseError } from '@/shared/api/http'
|
||||
import type { ContainerCustomsStatus, ContainerCustomsView } from '../api/platformContainerCustoms'
|
||||
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/container-loading/components/ContainerCustomsPanel.vue'), 'utf8')
|
||||
const api = vi.hoisted(() => ({
|
||||
get: vi.fn(),
|
||||
start: vi.fn(),
|
||||
completeAllBuy: vi.fn(),
|
||||
upload: vi.fn(),
|
||||
downloadSource: vi.fn(),
|
||||
downloadCompletion: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('ContainerCustomsPanel contract', () => {
|
||||
it('renders compact coverage, missing inbound and frozen document facts with operation first', () => {
|
||||
expect(source).toContain('退税总票')
|
||||
expect(source).toContain('缺资料清单')
|
||||
expect(source).toContain('<th>操作</th><th>关联入仓码</th><th>版本</th><th>客户</th>')
|
||||
expect(source).toContain("item.taxMode === 'TAX_REFUND' && !item.covered")
|
||||
vi.mock('../api/platformContainerCustoms', () => ({
|
||||
getContainerCustoms: api.get,
|
||||
startContainerCustoms: api.start,
|
||||
completeAllBuyContainerCustoms: api.completeAllBuy,
|
||||
uploadContainerCustomsCompletion: api.upload,
|
||||
downloadContainerCustomsDocument: api.downloadSource,
|
||||
downloadContainerCustomsCompletion: api.downloadCompletion,
|
||||
}))
|
||||
vi.mock('@/modules/downloads', () => ({ createPlatformExportTask: vi.fn() }))
|
||||
|
||||
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, version: 3, taxRefundCount: 1, coveredTaxRefundCount: 1,
|
||||
inbounds: [{ inboundId: '2092438843630391404', orderId: '2092438843630391405', orderNo: 'O-1', inboundCode: 'RK-1', tenantId: '2092438843630391406', customerName: '客户一', taxMode: 'TAX_REFUND', covered: true }],
|
||||
documents: [{
|
||||
documentId: '2092438843630391407', tenantId: '2092438843630391406',
|
||||
source: { fileId: '2092438843630391408', fileName: '客户退税资料.pdf', fileHash: 'source-hash', sizeBytes: 12, version: 2, uploadedAt: '2026-08-29T10:00:00' },
|
||||
inboundIds: ['2092438843630391404'], inboundCodes: ['RK-1'], frozen: true,
|
||||
completion: hasCompletion ? { fileId: '2092438843630391409', fileName: '报关完成文件.pdf', fileHash: 'completion-hash', sizeBytes: 13, version: 1, uploadedAt: '2026-08-29T11:00:00' } : null,
|
||||
}],
|
||||
startedAt: status === 'DECLARING' || status === 'COMPLETED' ? '2026-08-29T10:30:00' : null,
|
||||
completedAt: status === 'COMPLETED' ? '2026-08-29T11:30:00' : null,
|
||||
}
|
||||
}
|
||||
|
||||
async function render(status: ContainerCustomsStatus, canManage = true, hasCompletion = false) {
|
||||
api.get.mockResolvedValue(customs(status, hasCompletion))
|
||||
const wrapper = mount(ContainerCustomsPanel, {
|
||||
props: { loadingPlanId: '2092438843630391402', canList: true, canManage, departureState: 'WAITING_OTHER_GATES' },
|
||||
global: { plugins: [ElementPlus] },
|
||||
})
|
||||
await flushPromises()
|
||||
return wrapper
|
||||
}
|
||||
|
||||
function buttonTexts(wrapper: Awaited<ReturnType<typeof render>>) {
|
||||
return wrapper.findAll('button').map((button) => button.text())
|
||||
}
|
||||
|
||||
describe('ContainerCustomsPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
api.downloadSource.mockResolvedValue(new Blob(['source']))
|
||||
api.downloadCompletion.mockResolvedValue(new Blob(['completion']))
|
||||
Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: vi.fn(() => 'blob:customs-file') })
|
||||
Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: vi.fn() })
|
||||
vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => undefined)
|
||||
})
|
||||
|
||||
it('gates start by server status and confirms frozen facts', () => {
|
||||
expect(source).toContain("customs.status === 'READY_TO_START'")
|
||||
expect(source).toContain('冻结客户当前资料版本、关联入仓码覆盖范围及平台买单准备事实')
|
||||
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('客户退税资料.pdf')
|
||||
expect(wrapper.find('[aria-label="下载源资料 客户退税资料.pdf"]').exists()).toBe(true)
|
||||
expect(buttonTexts(wrapper)).not.toContain('上传完成文件')
|
||||
expect(buttonTexts(wrapper)).not.toContain('覆盖完成文件')
|
||||
expect(buttonTexts(wrapper)).not.toContain('提交')
|
||||
})
|
||||
|
||||
it('requires a second confirmation for the final missing completion and keeps departure independent', () => {
|
||||
expect(source).toContain('missingCompletionCount.value === 1')
|
||||
expect(source).toContain('确认上传并完成')
|
||||
expect(source).toContain("departureState === 'DEPARTED' ? '已自动离仓' : '报关已完成,待其他离仓条件'")
|
||||
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('提交')
|
||||
wrapper.unmount()
|
||||
|
||||
wrapper = await render('DECLARING', true, true)
|
||||
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('提交')
|
||||
})
|
||||
|
||||
it('creates the all-buy XLSX task with loadingPlanId as its only parameter', () => {
|
||||
expect(source).toContain("exportType: 'LOADING_LIST_BUY_DECLARATION_ITEMS', format: 'XLSX'")
|
||||
expect(source).toContain('parameters: { loadingPlanId: String(props.loadingPlanId) }')
|
||||
expect(source).not.toContain('fileType')
|
||||
expect(source).not.toContain('documentType')
|
||||
it('keeps COMPLETED strictly read-only with both files visible and downloadable', async () => {
|
||||
const wrapper = await render('COMPLETED', true, true)
|
||||
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('提交')
|
||||
})
|
||||
|
||||
it('routes source and completion downloads independently by the long document id and original names', async () => {
|
||||
const wrapper = await render('COMPLETED', false, true)
|
||||
await wrapper.get('[aria-label="下载源资料 客户退税资料.pdf"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(api.downloadSource).toHaveBeenCalledWith('2092438843630391407')
|
||||
expect(api.downloadCompletion).not.toHaveBeenCalled()
|
||||
|
||||
await wrapper.get('[aria-label="下载完成文件 报关完成文件.pdf"]').trigger('click')
|
||||
await flushPromises()
|
||||
expect(api.downloadCompletion).toHaveBeenCalledWith('2092438843630391407')
|
||||
expect(HTMLAnchorElement.prototype.click).toHaveBeenCalledTimes(2)
|
||||
expect(URL.createObjectURL).toHaveBeenCalledTimes(2)
|
||||
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:customs-file')
|
||||
})
|
||||
|
||||
it('preserves backend error code, request id and trace id when one download fails', async () => {
|
||||
api.downloadSource.mockRejectedValueOnce(new ApiResponseError('文件不可用', 'CUSTOMS_FILE_404', 404, undefined, 'request-customs-download', 'trace-customs-download'))
|
||||
const wrapper = await render('COMPLETED', false, true)
|
||||
|
||||
await wrapper.get('[aria-label="下载源资料 客户退税资料.pdf"]').trigger('click')
|
||||
await flushPromises()
|
||||
|
||||
expect(wrapper.text()).toContain('文件不可用')
|
||||
expect(wrapper.text()).toContain('CUSTOMS_FILE_404')
|
||||
expect(wrapper.text()).toContain('request-customs-download')
|
||||
expect(wrapper.text()).toContain('trace-customs-download')
|
||||
expect(api.downloadCompletion).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user