fix: align audit and dispatch workflows and release failed requests

This commit is contained in:
2026-10-22 23:10:09 +08:00
parent 9cdcb9c077
commit 4a3456756f
124 changed files with 1296 additions and 2120 deletions
@@ -23,41 +23,41 @@ describe('transfer movement UI', () => {
can.mockImplementation(code => code !== 'platform:overseas-warehouse:warehouse:list')
const denied = render(detail('OUTBOUNDED')); expect(denied.text()).not.toContain('登记目标仓实收'); expect(listTransferReceivingWarehouses).not.toHaveBeenCalled()
})
it.each([
{ cause: new ApiResponseError('目标不可用', 'A0409', 200), recorded: false, canReselect: true },
{ cause: new Error('response lost'), recorded: false, canReselect: false },
{ cause: new ApiResponseError('目标不可用', 'A0409', 200), recorded: true, canReselect: false },
])('allows target correction only after definite rejection plus authoritative no-command readback: $canReselect', async ({ cause, recorded, canReselect }) => {
const body = { sourceInventoryPositionId: 'hidden-source', destinationOverseasWarehouseId: 'NY', destinationOverseasWarehouseCode: 'NY-CODE', destinationOverseasWarehouseName: '纽约', destinationWarehouseCode: 'ABE4', destinationPalletId: 'P-NY', destinationPalletNo: 'PALLET-NY', warehouseLocationId: 'L-NY', warehouseLocationCode: 'LOC-NY', batchNo: 'WT-1', idempotencyKey: 'original-key' }
sessionStorage.setItem('warehouse-transfer-movement:customer:WT-1', JSON.stringify({ pending: { type: 'receipt', body } }))
const fresh = detail('OUTBOUNDED')
if (recorded) fresh.receiptCommands = [{ sourceInventoryPositionId: 'hidden-source', status: 'PREPARED', requestHash: 'hash', request: body, target: { warehouseId: 'NY', warehouseCode: 'NY-CODE', warehouseName: '纽约', palletId: 'P-NY', palletNo: 'PALLET-NY', locationId: 'L-NY', locationCode: 'LOC-NY', fbaCode: 'ABE4' } }]
vi.mocked(receiveWarehouseTransfer).mockRejectedValue(cause); vi.mocked(getWarehouseTransfer).mockResolvedValue(fresh)
const wrapper = render(detail('OUTBOUNDED')); await button(wrapper, '重试原操作').trigger('click'); await flushPromises()
expect(wrapper.text()).not.toContain('重新选择目标')
await button(wrapper, '只读核对当前状态').trigger('click'); await flushPromises()
expect(wrapper.text().includes('重新选择目标')).toBe(canReselect)
expect(receiveWarehouseTransfer).toHaveBeenCalledTimes(1)
expect(JSON.parse(sessionStorage.getItem('warehouse-transfer-movement:customer:WT-1')!).pending.body).toEqual(body)
if (canReselect) {
vi.mocked(listTransferReceivingWarehouses).mockResolvedValue({ records: [] })
await button(wrapper, '重新选择目标').trigger('click'); await flushPromises()
expect(JSON.parse(sessionStorage.getItem('warehouse-transfer-movement:customer:WT-1')!).pending).toBeNull()
expect(wrapper.text()).toContain('上次目标选择被拒绝:目标不可用')
expect(receiveWarehouseTransfer).toHaveBeenCalledTimes(1)
}
it.each(['{bad', JSON.stringify({pending:{type:'receipt',body:{idempotencyKey:'old'}}})])('ignores stored failed receipts without blocking current selection', async stored => {
sessionStorage.setItem('warehouse-transfer-movement:customer:WT-1', stored)
vi.mocked(listTransferReceivingWarehouses).mockResolvedValue({ records: [] })
const wrapper = render(detail('OUTBOUNDED')); await button(wrapper, '登记目标仓实收').trigger('click'); await flushPromises()
expect(wrapper.find('[aria-label="目标仓实收选择"]').exists()).toBe(true)
expect(receiveWarehouseTransfer).not.toHaveBeenCalled(); expect(wrapper.text()).not.toContain('重试原操作')
})
it('recovers a reserved original server command in a fresh browser and never queries arbitrary occupied resources', async () => {
it.each(['PREPARED', 'RESERVED'] as const)('continues a %s source from current server state and unlocks other sources after failure', async status => {
const command: WarehouseTransferReceiptCommandDto = { sourceInventoryPositionId: 'hidden-source', status: 'RESERVED', requestHash: 'server-hash', request: { sourceInventoryPositionId: 'hidden-source', destinationOverseasWarehouseId: 'NY', destinationOverseasWarehouseCode: 'NY-CODE', destinationOverseasWarehouseName: '纽约', destinationWarehouseCode: 'ABE4', destinationPalletId: 'P-NY', destinationPalletNo: 'PALLET-NY', warehouseLocationId: 'L-NY', warehouseLocationCode: 'LOC-NY', batchNo: 'WT-1', idempotencyKey: 'server-original' }, target: { warehouseId: 'NY', warehouseCode: 'NY-CODE', warehouseName: '纽约', palletId: 'P-NY', palletNo: 'PALLET-NY', locationId: 'L-NY', locationCode: 'LOC-NY', fbaCode: 'ABE4' } }
command.status = status
const d = detail('OUTBOUNDED'); d.receiptCommands = [command]
d.allocations!.push({ ...d.allocations![0]!, inventoryPositionId: 'other-source', inboundCode: 'IN-2' })
const completed = detail('RECEIVED'); completed.allocations![0]!.status = 'RECEIVED'; completed.receiptCommands = [{ ...command, status: 'COMPLETED' }]
vi.mocked(receiveWarehouseTransfer).mockResolvedValue(completed)
vi.mocked(getWarehouseTransfer).mockResolvedValue(d)
vi.mocked(receiveWarehouseTransfer).mockRejectedValueOnce(new Error('response lost')).mockResolvedValueOnce(completed)
const wrapper = render(d); await flushPromises()
expect(wrapper.text()).toContain('从本票服务端记录恢复'); expect(wrapper.text()).not.toContain('登记目标仓实收')
await button(wrapper, '重试原操作').trigger('click'); await flushPromises()
expect(wrapper.text()).not.toContain('重试原操作'); expect(wrapper.text()).toContain('登记目标仓实收')
expect(receiveWarehouseTransfer).not.toHaveBeenCalled()
expect(getWarehouseTransfer).not.toHaveBeenCalled()
await button(wrapper, '确认本来源实收').trigger('click'); await flushPromises()
expect(receiveWarehouseTransfer).toHaveBeenCalledExactlyOnceWith('customer', 'WT-1', command.request)
expect(wrapper.emitted('updated')?.[0]?.[0]).toMatchObject({ status: 'RECEIVED' })
expect(getWarehouseTransfer).toHaveBeenCalledTimes(1)
expect(button(wrapper, '登记目标仓实收').attributes('disabled')).toBeUndefined()
expect(button(wrapper, '确认本来源实收').attributes('disabled')).toBeUndefined()
expect(sessionStorage.length).toBe(0)
await button(wrapper, '确认本来源实收').trigger('click'); await flushPromises()
expect(getWarehouseTransfer).toHaveBeenCalledTimes(2)
expect(receiveWarehouseTransfer).toHaveBeenCalledTimes(2)
expect(wrapper.emitted('updated')?.at(-1)?.[0]).toEqual(completed)
// 服务端已完成时只展示最新结果,不再次提交。
vi.mocked(getWarehouseTransfer).mockResolvedValue(completed)
await button(wrapper, '确认本来源实收').trigger('click'); await flushPromises()
expect(receiveWarehouseTransfer).toHaveBeenCalledTimes(2)
expect(listPallets).not.toHaveBeenCalled(); expect(getPallet).not.toHaveBeenCalled()
expect(wrapper.text()).not.toContain('hidden-source')
})
it('does not use a browser remembered occupied target when server receipt history is missing', async () => {
sessionStorage.setItem('warehouse-transfer-movement:customer:WT-1', JSON.stringify({ pending: null, previous: { warehouse: { id: 'OTHER' }, pallet: { id: 'OTHER', status: 'OCCUPIED' } } }))
@@ -79,20 +79,18 @@ describe('transfer movement UI', () => {
expect(button(wrapper, '确认本来源实收').attributes('disabled')).toBeUndefined()
expect(receiveWarehouseTransfer).not.toHaveBeenCalled()
})
it('requires permission and explicit actual-outbound confirmation; unknown outcome survives remount with same body', async () => {
it('requires outbound permission and releases a failed confirmation for a fresh click', async () => {
vi.mocked(outboundWarehouseTransfer).mockRejectedValueOnce(new Error('response lost')).mockResolvedValueOnce(detail('OUTBOUNDED'))
const first = render(); await button(first, '确认本票出库').trigger('click'); expect(outboundWarehouseTransfer).not.toHaveBeenCalled()
await button(first, '确认实际出库').trigger('click'); await flushPromises()
const original = JSON.parse(JSON.stringify(vi.mocked(outboundWarehouseTransfer).mock.calls[0]))
expect(first.text()).toContain('原操作内容已保留'); first.unmount()
const second = render(detail('OUTBOUNDED')); await flushPromises()
expect(second.text()).not.toContain('hidden-source'); expect(second.text()).toContain('重试原操作')
await button(second, '重试原操作').trigger('click'); await flushPromises()
expect(vi.mocked(outboundWarehouseTransfer).mock.calls[1]).toEqual(original)
expect(second.emitted('updated')?.[0]?.[0]).toMatchObject({ status: 'OUTBOUNDED' })
const wrapper = render(); await button(wrapper, '确认本票出库').trigger('click'); expect(outboundWarehouseTransfer).not.toHaveBeenCalled()
await button(wrapper, '确认实际出库').trigger('click'); await flushPromises()
expect(button(wrapper, '确认本票出库').attributes('disabled')).toBeUndefined(); expect(sessionStorage.length).toBe(0)
const original = vi.mocked(outboundWarehouseTransfer).mock.calls[0]![2]
await button(wrapper, '确认实际出库').trigger('click'); await flushPromises()
expect(vi.mocked(outboundWarehouseTransfer).mock.calls[1]![2].idempotencyKey).not.toBe(original.idempotencyKey)
expect(wrapper.emitted('updated')?.[0]?.[0]).toMatchObject({ status: 'OUTBOUNDED' })
can.mockReturnValue(false); const denied = render(); expect(denied.text()).not.toContain('确认本票出库')
})
it('queries only selected warehouse page, reads exact bound location and keeps receipt request after response loss', async () => {
it('queries only selected warehouse page, reads exact bound location and unlocks the selection after response loss', async () => {
vi.mocked(listTransferReceivingWarehouses).mockResolvedValue({ records: [{ id: 'NY', customerId: '0', warehouseCode: 'NY-CODE', warehouseName: '纽约', warehouseType: 'OVERSEAS', status: 'ACTIVE' }] })
const p = { id: 'P-NY', customerId: '0', warehouseId: 'NY', locationId: 'L-NY', palletCode: 'PALLET-NY', palletType: 'STANDARD', fbaWarehouseCode: 'ABE4', capacityCbm: 2, unitPrice: 0, status: 'IDLE', version: 0 }
vi.mocked(listPallets).mockResolvedValue({ records: [p], page: 1, size: 20, total: 1, pages: 1 })
@@ -109,10 +107,10 @@ describe('transfer movement UI', () => {
await button(wrapper, '确认本来源实收').trigger('click'); await flushPromises()
const original = JSON.parse(JSON.stringify(vi.mocked(receiveWarehouseTransfer).mock.calls[0]))
expect(original[2]).toMatchObject({ sourceInventoryPositionId: 'hidden-source', destinationPalletNo: 'PALLET-NY', warehouseLocationCode: 'LOC-NY' })
await button(wrapper, '只读核对当前状态').trigger('click'); await flushPromises()
expect(button(wrapper, '查询目标托盘').attributes('disabled')).toBeUndefined()
expect(receiveWarehouseTransfer).toHaveBeenCalledTimes(1)
await button(wrapper, '重试原操作').trigger('click'); await flushPromises()
expect(vi.mocked(receiveWarehouseTransfer).mock.calls[1]).toEqual(original)
await button(wrapper, '确认本来源实收').trigger('click'); await flushPromises()
expect(vi.mocked(receiveWarehouseTransfer).mock.calls[1]![2].idempotencyKey).not.toBe(original[2].idempotencyKey)
expect(wrapper.text()).not.toContain('hidden-source')
})
it('does not request resources when permission is absent or source already received', async () => {