fix: align audit and dispatch workflows and release failed requests
This commit is contained in:
@@ -9,7 +9,6 @@ import AppDialog from '@/shared/components/AppDialog.vue'
|
||||
import ProcurementNodeActions from './ProcurementNodeActions.vue'
|
||||
import { listOverseasPurchaseWarehouseOptions } from '../api/platformOverseasBusiness'
|
||||
import { submitProcurementCommand, uploadProcurementFile } from '../api/procurementCommands'
|
||||
import { readPendingProcurement } from '../procurementPending'
|
||||
import type { PlatformProcurement } from '../procurementTypes'
|
||||
vi.mock('../api/platformOverseasBusiness', () => ({ listOverseasPurchaseWarehouseOptions: vi.fn() }))
|
||||
vi.mock('../api/procurementCommands', () => ({ submitProcurementCommand: vi.fn(), uploadProcurementFile: vi.fn() }))
|
||||
@@ -74,7 +73,7 @@ describe('procurement purchase node operations', () => {
|
||||
expect(dialog.props('confirmDisabled')).toBe(false)
|
||||
expect(submitProcurementCommand).not.toHaveBeenCalled()
|
||||
})
|
||||
it('saves and restores a separate correction while the immutable original request is pending, scoped to operator/customer/order', async () => {
|
||||
it('keeps ordinary correction drafts scoped to operator/customer/order while ignoring old requests', async () => {
|
||||
const value = fixture('FINANCE_ADJUST', false)
|
||||
value.fees = [{ id: '11', feeNo: 'PF-11', purpose: 'SAMPLE_PAYMENT', confirmationOrigin: 'BUSINESS', feeItemCode: 'SAMPLE_PAYMENT', feeItemName: '样品货款', feeVersion: 1, active: true, determination: 'POSITIVE', amount: '70.00', currency: 'CNY', originalAmount: '10.00', originalCurrency: 'USD', exchangeRate: '7', exchangeRateMonth: '2020-01', businessDate: '2020-01-01', parentFeeId: null, sourceFeeId: null, voucherFileId: '501', voucherFileName: 'invoice.pdf', walletStatus: 'PENDING', walletLedgerId: null, walletErrorCode: null, walletErrorMessage: null, remark: null }]
|
||||
const original = JSON.stringify({ command: 'fees/correct-target', payload: { version: 3, idempotencyKey: 'original-request', feeId: '11', targetOriginalAmount: '11.00', reason: '原请求', voucherFileId: '501' } })
|
||||
@@ -90,12 +89,11 @@ describe('procurement purchase node operations', () => {
|
||||
expect(submitProcurementCommand).not.toHaveBeenCalled()
|
||||
expect(sessionStorage.getItem(storageKey)).toBe(original)
|
||||
expect(JSON.parse(sessionStorage.getItem(`${storageKey}:correction-draft`)!)).toMatchObject({ targetOriginalAmount: '12.00', feeId: '11', reason: '准备下一版本' })
|
||||
expect(wrapper.findAll('button').find(button => button.text() === '核对并继续确认')!.attributes('disabled')).toBeDefined()
|
||||
expect(wrapper.findAll('button').find(button => button.text() === '核对并继续确认')!.attributes('disabled')).toBeUndefined()
|
||||
wrapper.unmount(); wrapper = render(value)
|
||||
await click('恢复费用更正草稿')
|
||||
expect((wrapper.findComponent(AppDialog).find('input[placeholder="填写原费用链正确总额,最多两位小数"]').element as HTMLInputElement).value).toBe('12.00')
|
||||
await click('确认上次提交结果')
|
||||
expect(submitProcurementCommand).toHaveBeenCalledWith('OP-100', '10001', 'fees/correct-target', JSON.parse(original).payload)
|
||||
expect(submitProcurementCommand).not.toHaveBeenCalled()
|
||||
expect(sessionStorage.getItem(`${storageKey}:correction-draft`)).not.toBeNull()
|
||||
const latest = structuredClone(value)
|
||||
latest.actions[0].allowed = true; latest.customerView.order.version = 9
|
||||
@@ -104,7 +102,7 @@ describe('procurement purchase node operations', () => {
|
||||
await click('恢复费用更正草稿'); await click('核对并继续确认')
|
||||
wrapper.findComponent(AppDialog).findAllComponents({ name: 'ElFormItem' }).find(row => row.props('label') === '本单凭证')!.findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', '501')
|
||||
await confirm(wrapper)
|
||||
expect(submitProcurementCommand).toHaveBeenCalledTimes(1)
|
||||
expect(submitProcurementCommand).not.toHaveBeenCalled()
|
||||
expect(wrapper.findComponent(AppDialog).text()).toContain('原实际费用及来源关联尚未完成')
|
||||
latest.fees[0].walletStatus = 'SETTLED'; latest.fees[0].walletLedgerId = 'LEDGER-1'
|
||||
latest.customerView.fees = [{ feeNo: 'PF-11', purpose: 'SAMPLE_PAYMENT', name: '样品货款', version: 1, active: true, determination: 'POSITIVE', amount: '70.00', currency: 'CNY', originalAmount: '10.00', originalCurrency: 'USD', exchangeRate: '7', exchangeRateMonth: '2020-01', businessDate: '2020-01-01', parentFeeNo: null, walletStatus: 'SETTLED', sourceLinked: true }]
|
||||
@@ -251,22 +249,20 @@ describe('procurement purchase node operations', () => {
|
||||
await confirm(wrapper)
|
||||
expect(submitProcurementCommand).toHaveBeenCalledWith('OP-100', '10001', 'purchase', expect.objectContaining({ productName: '样品商品', specification: '黑色', quantity: 2, amount: '0.00', currency: 'USD', voucherFileId: '101', purchasedAt: '2026-10-08T10:00:00' }))
|
||||
})
|
||||
it('recovers an uncertain request after reload without changing its body or key', async () => {
|
||||
it('unknown request leaves inputs editable and a later click submits current input', async () => {
|
||||
vi.mocked(submitProcurementCommand).mockRejectedValueOnce(new Error('timeout'))
|
||||
let wrapper = render(fixture('CANCEL')); await open(wrapper); await explain(wrapper, '客户取消'); await confirm(wrapper)
|
||||
const original = vi.mocked(submitProcurementCommand).mock.calls[0][3]; expect(sessionStorage.getItem(storageKey)).not.toBeNull(); wrapper.unmount()
|
||||
wrapper = render(fixture('CANCEL')); await flushPromises()
|
||||
const retry = wrapper.findAll('button').find(button => button.text() === '确认上次提交结果')!; await retry.trigger('click'); await flushPromises()
|
||||
expect(vi.mocked(submitProcurementCommand).mock.calls[1][3]).toEqual(original); expect(sessionStorage.getItem(storageKey)).toBeNull()
|
||||
const wrapper = render(fixture('CANCEL')); await open(wrapper); await explain(wrapper, '客户取消'); await confirm(wrapper)
|
||||
const original = vi.mocked(submitProcurementCommand).mock.calls[0]![3]
|
||||
expect(sessionStorage.getItem(storageKey)).toBeNull(); await explain(wrapper, '修改后的原因'); await confirm(wrapper)
|
||||
const next = vi.mocked(submitProcurementCommand).mock.calls[1]![3]
|
||||
expect(next.idempotencyKey).not.toBe(original.idempotencyKey); expect(next.reason).toBe('修改后的原因')
|
||||
})
|
||||
it('preserves an earlier uncertain financial or physical command when replay is rejected', async () => {
|
||||
it('consecutive failures never retain a request or block another click', async () => {
|
||||
vi.mocked(submitProcurementCommand).mockRejectedValueOnce(new Error('timeout')).mockRejectedValueOnce(new ApiResponseError('资源正在处理中', 'A0409'))
|
||||
const wrapper = render(fixture('CANCEL')); await open(wrapper); await explain(wrapper); await confirm(wrapper)
|
||||
const saved = sessionStorage.getItem(storageKey); expect(saved).not.toBeNull()
|
||||
await confirm(wrapper)
|
||||
expect(sessionStorage.getItem(storageKey)).toBe(saved)
|
||||
expect(vi.mocked(submitProcurementCommand).mock.calls[1][3]).toEqual(vi.mocked(submitProcurementCommand).mock.calls[0][3])
|
||||
expect(wrapper.emitted('changed')).toBeUndefined()
|
||||
const wrapper = render(fixture('CANCEL')); await open(wrapper); await explain(wrapper); await confirm(wrapper); await confirm(wrapper)
|
||||
const calls = vi.mocked(submitProcurementCommand).mock.calls
|
||||
expect(calls).toHaveLength(2); expect(calls[1]![3].idempotencyKey).not.toBe(calls[0]![3].idempotencyKey)
|
||||
expect(sessionStorage.getItem(storageKey)).toBeNull(); expect(wrapper.emitted('changed')).toBeUndefined()
|
||||
})
|
||||
it('keeps a version conflict visible, releases the failed command and does not report success', async () => {
|
||||
vi.mocked(submitProcurementCommand).mockRejectedValue(new ApiResponseError('订单版本冲突,请刷新', 'A0409'))
|
||||
@@ -300,9 +296,9 @@ describe('procurement purchase node operations', () => {
|
||||
wrapper.findComponent(AppDialog).vm.$emit('confirm'); wrapper.findComponent(AppDialog).vm.$emit('confirm'); await flushPromises()
|
||||
expect(submitProcurementCommand).toHaveBeenCalledTimes(1); complete(); await flushPromises()
|
||||
})
|
||||
it('rejects malformed recovery records instead of allowing a fresh competing operation', async () => {
|
||||
it('old malformed request storage does not disable current actions', async () => {
|
||||
sessionStorage.setItem(storageKey, '{bad')
|
||||
const wrapper = render(); expect(wrapper.text()).toContain('上次操作记录无法读取'); expect(wrapper.find('.purchase-actions button').attributes('disabled')).toBeDefined()
|
||||
expect(() => readPendingProcurement(JSON.stringify({ command: 'purchase', payload: { version: 3, idempotencyKey: 'a', amount: '-2' } }))).toThrow()
|
||||
const wrapper = render(); expect(wrapper.text()).not.toContain('上次操作记录无法读取')
|
||||
expect(wrapper.find('.purchase-actions button').attributes('disabled')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user