feat: add inbound hold evidence controls
This commit is contained in:
@@ -2,7 +2,7 @@ import { flushPromises, shallowMount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { getLoadingAssociationSummary, getOverseasInboundCodeRelationship } from '@/shared/api/inboundCodeRelations'
|
||||
import { downloadPlatformProductImage, getPlatformUnifiedOrder, listPlatformUnifiedOrders } from '@/modules/unified-orders/api/unifiedOrders'
|
||||
import { getPlatformInboundHold, holdPlatformInbound, releasePlatformInbound } from '@/modules/inbounds'
|
||||
import { downloadPlatformInboundHoldEvidence, getPlatformInboundHold, holdPlatformInbound, releasePlatformInbound, uploadPlatformInboundHoldEvidence } from '@/modules/inbounds'
|
||||
import UnifiedOrdersView from './UnifiedOrdersView.vue'
|
||||
|
||||
vi.mock('vue-router', () => ({
|
||||
@@ -15,7 +15,7 @@ vi.mock('@/modules/iam', () => ({ listPlatformTenants: vi.fn().mockResolvedValue
|
||||
vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn().mockResolvedValue([]) }))
|
||||
vi.mock('@/modules/inbounds', () => ({
|
||||
downloadWeightImage: vi.fn(), getInboundMeasurement: vi.fn(),
|
||||
getPlatformInboundHold: vi.fn(), holdPlatformInbound: vi.fn(), releasePlatformInbound: vi.fn(),
|
||||
downloadPlatformInboundHoldEvidence: vi.fn(), getPlatformInboundHold: vi.fn(), holdPlatformInbound: vi.fn(), releasePlatformInbound: vi.fn(), uploadPlatformInboundHoldEvidence: vi.fn(),
|
||||
InboundFeePanel: { template: '<div />' }, InboundMeasurementDialog: { template: '<div />' }, MeasurementEvidenceThumbnail: { template: '<div />' },
|
||||
}))
|
||||
vi.mock('@/modules/unified-orders/workflows', () => ({ UnifiedOrderWorkflowPanel: { template: '<div />' } }))
|
||||
@@ -78,6 +78,10 @@ describe('UnifiedOrdersView downstream fulfillment detail', () => {
|
||||
vi.mocked(getPlatformUnifiedOrder).mockResolvedValue(detail as never)
|
||||
vi.mocked(downloadPlatformProductImage).mockResolvedValue(new Blob(['image'], { type: 'image/png' }))
|
||||
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: [], page: 1, size: 20, total: 0, pages: 0 })
|
||||
vi.mocked(uploadPlatformInboundHoldEvidence).mockResolvedValue({
|
||||
fileId: '9007199254740999', fileName: '扣货说明.pdf', contentType: 'application/pdf', sizeBytes: 8,
|
||||
})
|
||||
vi.mocked(downloadPlatformInboundHoldEvidence).mockResolvedValue(new Blob(['evidence'], { type: 'application/pdf' }))
|
||||
vi.mocked(getPlatformInboundHold).mockResolvedValue({
|
||||
inboundId: '7001', tenantId: '6001', holdFlag: false, currentHoldReason: null, version: 3,
|
||||
canHold: true, canRelease: false, actionBlockReason: null,
|
||||
@@ -180,6 +184,226 @@ describe('UnifiedOrdersView downstream fulfillment detail', () => {
|
||||
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('exposes loading, empty and error states for the lazy hold request', async () => {
|
||||
let resolveHold!: (value: Awaited<ReturnType<typeof getPlatformInboundHold>>) => void
|
||||
vi.mocked(getPlatformInboundHold).mockImplementationOnce(() => new Promise((resolve) => { resolveHold = resolve }))
|
||||
const wrapper = mountDetail()
|
||||
await flushPromises()
|
||||
await wrapper.findAll('.detail-tabs button').find((button) => button.text().includes('其他'))!.trigger('click')
|
||||
expect((wrapper.vm as unknown as { holdLoading: boolean }).holdLoading).toBe(true)
|
||||
resolveHold({
|
||||
inboundId: '7001', tenantId: '6001', holdFlag: false, currentHoldReason: null, version: 3,
|
||||
canHold: true, canRelease: false, actionBlockReason: null, history: [],
|
||||
})
|
||||
await flushPromises()
|
||||
const loadedVm = wrapper.vm as unknown as { holdLoading: boolean; holdDetail: { history: unknown[] } }
|
||||
expect(loadedVm.holdLoading).toBe(false)
|
||||
expect(loadedVm.holdDetail.history).toEqual([])
|
||||
expect(wrapper.text()).toContain('扣货历史')
|
||||
|
||||
vi.mocked(getPlatformInboundHold).mockRejectedValueOnce(new Error('网络断开'))
|
||||
await (wrapper.vm as unknown as { loadHoldDetail(force: boolean): Promise<void> }).loadHoldDetail(true)
|
||||
expect(wrapper.text()).toContain('扣货状态与历史加载失败')
|
||||
expect((wrapper.vm as unknown as { holdError: { message: string } }).holdError.message).toBe('扣货状态与历史加载失败')
|
||||
})
|
||||
|
||||
it('rejects blank and over-limit reasons while accepting exactly 1000 characters', async () => {
|
||||
const wrapper = mountDetail()
|
||||
await flushPromises()
|
||||
await wrapper.findAll('.detail-tabs button').find((button) => button.text().includes('其他'))!.trigger('click')
|
||||
await flushPromises()
|
||||
const vm = wrapper.vm as unknown as {
|
||||
holdReason: string
|
||||
openHoldDialog(action: 'HOLD'): void
|
||||
submitHoldAction(): Promise<void>
|
||||
}
|
||||
vm.openHoldDialog('HOLD')
|
||||
vm.holdReason = ' '
|
||||
await vm.submitHoldAction()
|
||||
vm.holdReason = 'a'.repeat(1001)
|
||||
await vm.submitHoldAction()
|
||||
expect(holdPlatformInbound).not.toHaveBeenCalled()
|
||||
|
||||
vm.holdReason = 'a'.repeat(1000)
|
||||
await vm.submitHoldAction()
|
||||
expect(holdPlatformInbound).toHaveBeenCalledOnce()
|
||||
expect(holdPlatformInbound).toHaveBeenCalledWith('6001', '7001', {
|
||||
expectedVersion: 3,
|
||||
reason: 'a'.repeat(1000),
|
||||
})
|
||||
})
|
||||
|
||||
it('prevents duplicate submissions while the hold request is pending', async () => {
|
||||
let resolveCommand!: (value: Awaited<ReturnType<typeof holdPlatformInbound>>) => void
|
||||
vi.mocked(holdPlatformInbound).mockImplementationOnce(() => new Promise((resolve) => { resolveCommand = resolve }))
|
||||
const wrapper = mountDetail()
|
||||
await flushPromises()
|
||||
await wrapper.findAll('.detail-tabs button').find((button) => button.text().includes('其他'))!.trigger('click')
|
||||
await flushPromises()
|
||||
const vm = wrapper.vm as unknown as {
|
||||
holdReason: string
|
||||
holdSaving: boolean
|
||||
openHoldDialog(action: 'HOLD'): void
|
||||
submitHoldAction(): Promise<void>
|
||||
}
|
||||
vm.openHoldDialog('HOLD')
|
||||
vm.holdReason = '等待客户补款'
|
||||
const first = vm.submitHoldAction()
|
||||
const duplicate = vm.submitHoldAction()
|
||||
await duplicate
|
||||
expect(vm.holdSaving).toBe(true)
|
||||
expect(holdPlatformInbound).toHaveBeenCalledOnce()
|
||||
resolveCommand({
|
||||
inboundId: '7001', tenantId: '6001', holdFlag: true, currentHoldReason: '等待客户补款', version: 4,
|
||||
canHold: false, canRelease: true, actionBlockReason: null, history: [],
|
||||
})
|
||||
await first
|
||||
expect(vm.holdSaving).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps the dialog and reason when a stale-version submission fails', async () => {
|
||||
vi.mocked(holdPlatformInbound).mockRejectedValueOnce(Object.assign(new Error('扣货状态已变化,请刷新后重试'), { code: 'A0409' }))
|
||||
const wrapper = mountDetail()
|
||||
await flushPromises()
|
||||
await wrapper.findAll('.detail-tabs button').find((button) => button.text().includes('其他'))!.trigger('click')
|
||||
await flushPromises()
|
||||
const vm = wrapper.vm as unknown as {
|
||||
holdReason: string
|
||||
holdDialogOpen: boolean
|
||||
openHoldDialog(action: 'HOLD'): void
|
||||
submitHoldAction(): Promise<void>
|
||||
}
|
||||
vm.openHoldDialog('HOLD')
|
||||
vm.holdReason = '等待客户补款'
|
||||
await vm.submitHoldAction()
|
||||
expect(holdPlatformInbound).toHaveBeenCalledWith('6001', '7001', { expectedVersion: 3, reason: '等待客户补款' })
|
||||
expect(vm.holdDialogOpen).toBe(true)
|
||||
expect(vm.holdReason).toBe('等待客户补款')
|
||||
expect(getPlatformUnifiedOrder).toHaveBeenCalledOnce()
|
||||
expect(listPlatformUnifiedOrders).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does not submit the command when evidence upload fails and preserves the form', async () => {
|
||||
vi.mocked(uploadPlatformInboundHoldEvidence).mockRejectedValueOnce(new Error('文件服务不可用'))
|
||||
const wrapper = mountDetail()
|
||||
await flushPromises()
|
||||
await wrapper.findAll('.detail-tabs button').find((button) => button.text().includes('其他'))!.trigger('click')
|
||||
await flushPromises()
|
||||
const evidence = new File(['evidence'], '扣货说明.pdf', { type: 'application/pdf' })
|
||||
const vm = wrapper.vm as unknown as {
|
||||
holdReason: string
|
||||
holdDialogOpen: boolean
|
||||
holdEvidenceFile: File | null
|
||||
holdEvidenceError: { message: string } | null
|
||||
openHoldDialog(action: 'HOLD'): void
|
||||
selectHoldEvidenceFile(upload: { raw: File }): void
|
||||
submitHoldAction(): Promise<void>
|
||||
}
|
||||
vm.openHoldDialog('HOLD')
|
||||
vm.holdReason = '等待客户补款'
|
||||
vm.selectHoldEvidenceFile({ raw: evidence })
|
||||
await vm.submitHoldAction()
|
||||
expect(uploadPlatformInboundHoldEvidence).toHaveBeenCalledWith('6001', '7001', evidence)
|
||||
expect(holdPlatformInbound).not.toHaveBeenCalled()
|
||||
expect(vm.holdDialogOpen).toBe(true)
|
||||
expect(vm.holdReason).toBe('等待客户补款')
|
||||
expect(vm.holdEvidenceFile).toBe(evidence)
|
||||
expect(vm.holdEvidenceError?.message).toBe('关联文件上传失败,请稍后重试')
|
||||
})
|
||||
|
||||
it('uploads optional evidence once and submits its long identifier', async () => {
|
||||
const wrapper = mountDetail()
|
||||
await flushPromises()
|
||||
await wrapper.findAll('.detail-tabs button').find((button) => button.text().includes('其他'))!.trigger('click')
|
||||
await flushPromises()
|
||||
const evidence = new File(['evidence'], '扣货说明.pdf', { type: 'application/pdf' })
|
||||
const vm = wrapper.vm as unknown as {
|
||||
holdReason: string
|
||||
openHoldDialog(action: 'HOLD'): void
|
||||
selectHoldEvidenceFile(upload: { raw: File }): void
|
||||
submitHoldAction(): Promise<void>
|
||||
}
|
||||
vm.openHoldDialog('HOLD')
|
||||
vm.holdReason = '等待客户补款'
|
||||
vm.selectHoldEvidenceFile({ raw: evidence })
|
||||
await vm.submitHoldAction()
|
||||
expect(uploadPlatformInboundHoldEvidence).toHaveBeenCalledOnce()
|
||||
expect(holdPlatformInbound).toHaveBeenCalledWith('6001', '7001', {
|
||||
expectedVersion: 3,
|
||||
reason: '等待客户补款',
|
||||
evidenceFileId: '9007199254740999',
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects unsafe evidence extensions, empty files and files over 10MB', async () => {
|
||||
const wrapper = mountDetail()
|
||||
await flushPromises()
|
||||
await wrapper.findAll('.detail-tabs button').find((button) => button.text().includes('其他'))!.trigger('click')
|
||||
await flushPromises()
|
||||
const vm = wrapper.vm as unknown as {
|
||||
holdEvidenceFile: File | null
|
||||
holdEvidenceError: { code: string } | null
|
||||
openHoldDialog(action: 'HOLD'): void
|
||||
selectHoldEvidenceFile(upload: { raw: File }): void
|
||||
}
|
||||
vm.openHoldDialog('HOLD')
|
||||
vm.selectHoldEvidenceFile({ raw: new File(['run'], '恶意程序.exe') })
|
||||
expect(vm.holdEvidenceFile).toBeNull()
|
||||
expect(vm.holdEvidenceError?.code).toBe('FILE_TYPE_INVALID')
|
||||
|
||||
vm.selectHoldEvidenceFile({ raw: new File([], '空文件.txt', { type: 'text/plain' }) })
|
||||
expect(vm.holdEvidenceError?.code).toBe('FILE_SIZE_INVALID')
|
||||
const oversized = new File(['x'], '过大文件.png', { type: 'image/png' })
|
||||
Object.defineProperty(oversized, 'size', { value: 10 * 1024 * 1024 + 1 })
|
||||
vm.selectHoldEvidenceFile({ raw: oversized })
|
||||
expect(vm.holdEvidenceError?.code).toBe('FILE_SIZE_INVALID')
|
||||
expect(uploadPlatformInboundHoldEvidence).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('downloads associated evidence once while a repeated click is pending', async () => {
|
||||
let resolveDownload!: (value: Blob) => void
|
||||
vi.mocked(downloadPlatformInboundHoldEvidence).mockImplementationOnce(() => new Promise((resolve) => { resolveDownload = resolve }))
|
||||
Object.defineProperty(URL, 'createObjectURL', { configurable: true, value: vi.fn(() => 'blob:hold-evidence') })
|
||||
Object.defineProperty(URL, 'revokeObjectURL', { configurable: true, value: vi.fn() })
|
||||
const click = vi.spyOn(HTMLAnchorElement.prototype, 'click').mockImplementation(() => undefined)
|
||||
const wrapper = mountDetail()
|
||||
await flushPromises()
|
||||
const vm = wrapper.vm as unknown as { downloadHoldEvidence(fileId: string): Promise<void>; holdDownloadingFileId: string }
|
||||
const first = vm.downloadHoldEvidence('9007199254740999')
|
||||
const duplicate = vm.downloadHoldEvidence('9007199254740999')
|
||||
await duplicate
|
||||
expect(downloadPlatformInboundHoldEvidence).toHaveBeenCalledOnce()
|
||||
expect(vm.holdDownloadingFileId).toBe('9007199254740999')
|
||||
resolveDownload(new Blob(['evidence'], { type: 'application/pdf' }))
|
||||
await first
|
||||
expect(downloadPlatformInboundHoldEvidence).toHaveBeenCalledWith('6001', '7001', '9007199254740999')
|
||||
expect(click).toHaveBeenCalledOnce()
|
||||
expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:hold-evidence')
|
||||
expect(vm.holdDownloadingFileId).toBe('')
|
||||
})
|
||||
|
||||
it('submits release with the current server version and refreshes detail and list', async () => {
|
||||
vi.mocked(getPlatformInboundHold).mockResolvedValueOnce({
|
||||
inboundId: '7001', tenantId: '6001', holdFlag: true, currentHoldReason: '等待客户补款', version: 8,
|
||||
canHold: false, canRelease: true, actionBlockReason: null, history: [],
|
||||
})
|
||||
const wrapper = mountDetail()
|
||||
await flushPromises()
|
||||
await wrapper.findAll('.detail-tabs button').find((button) => button.text().includes('其他'))!.trigger('click')
|
||||
await flushPromises()
|
||||
const vm = wrapper.vm as unknown as {
|
||||
holdReason: string
|
||||
openHoldDialog(action: 'RELEASE'): void
|
||||
submitHoldAction(): Promise<void>
|
||||
}
|
||||
vm.openHoldDialog('RELEASE')
|
||||
vm.holdReason = '货款已核对'
|
||||
await vm.submitHoldAction()
|
||||
expect(releasePlatformInbound).toHaveBeenCalledWith('6001', '7001', { expectedVersion: 8, reason: '货款已核对' })
|
||||
expect(getPlatformUnifiedOrder).toHaveBeenCalledTimes(2)
|
||||
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('stays read-only without the independent hold permission', async () => {
|
||||
canPermission.mockImplementation((...permissions: string[]) => permissions[0] !== 'platform:firstleg:inbound:hold')
|
||||
const wrapper = mountDetail()
|
||||
|
||||
Reference in New Issue
Block a user