fix(dispatch): release failed request state and return rejected reviews
This commit is contained in:
@@ -40,19 +40,22 @@ it('accepts actual evidence and a matching confirmed manifest while refusing an
|
||||
const { wrapper, state } = await open('LOADING_COMPLETE'); expect(state.measurements[0]).toMatchObject({ weightKg: 92, volumeCbm: 1.5, deliveryIndex: null })
|
||||
await state.submit(); expect(api.completeDispatchLoading).not.toHaveBeenCalled(); wrapper.unmount()
|
||||
})
|
||||
it.each(['DISPATCH_REVIEW', 'LOADING_REVIEW'] as const)('requires a rejection reason and retains the original %s decision after an unknown result', async action => {
|
||||
const success = vi.spyOn(ElMessage, 'success')
|
||||
const reviewApi = action === 'DISPATCH_REVIEW' ? api.reviewDispatch : api.reviewDispatchLoading
|
||||
const { wrapper, state } = await open(action); state.form.decision = 'REJECTED'
|
||||
await state.submit(); expect(reviewApi).not.toHaveBeenCalled()
|
||||
state.form.reason = '预约未核实'; vi.mocked(reviewApi).mockRejectedValue(new Error('unknown result'))
|
||||
await state.submit(); state.form.reason = 'later changed'; state.form.decision = 'APPROVED'
|
||||
vi.mocked(api.getDispatchCommand).mockImplementation(async (_, key) => command(action, key))
|
||||
vi.mocked(api.getDispatch).mockResolvedValue(detail({ recordVersion: 1 }))
|
||||
await state.submit(); expect(reviewApi).toHaveBeenCalledTimes(1)
|
||||
expect(vi.mocked(reviewApi).mock.calls[0]![1]).toMatchObject({ recordVersion: 0, decision: 'REJECTED', reason: '预约未核实' })
|
||||
expect(success).toHaveBeenCalledWith('已退回客服处理(车辆信息),修改后需重新提交调度审核')
|
||||
expect(wrapper.emitted('saved')).toHaveLength(1); wrapper.unmount()
|
||||
it.each(['DISPATCH_REVIEW','LOADING_REVIEW'] as const)('allows editing the %s decision after a failed request',async action=>{
|
||||
const reviewApi=action==='DISPATCH_REVIEW'?api.reviewDispatch:api.reviewDispatchLoading
|
||||
const {wrapper,state}=await open(action);state.form.decision='REJECTED'
|
||||
await state.submit();expect(reviewApi).not.toHaveBeenCalled()
|
||||
state.form.reason='预约未核实';vi.mocked(reviewApi).mockRejectedValueOnce(new Error('unknown result'))
|
||||
await state.submit()
|
||||
expect(useDispatchCommandRecovery(()=>identity.id).pending.value).toBeNull()
|
||||
state.form.reason='重新确认';state.form.decision='APPROVED'
|
||||
vi.mocked(reviewApi).mockImplementationOnce(async(_,body)=>command(action,body.idempotencyKey))
|
||||
vi.mocked(api.getDispatch).mockResolvedValue(detail({recordVersion:1}))
|
||||
await state.submit()
|
||||
const calls=vi.mocked(reviewApi).mock.calls
|
||||
expect(calls).toHaveLength(2)
|
||||
expect(calls[1]![1].decision).toBe('APPROVED')
|
||||
expect(calls[1]![1].idempotencyKey).not.toBe(calls[0]![1].idempotencyKey)
|
||||
expect(api.getDispatchCommand).not.toHaveBeenCalled();wrapper.unmount()
|
||||
})
|
||||
it('shows the preserved rejection while customer service edits vehicle information', async () => {
|
||||
const { wrapper, state } = await open('EDIT')
|
||||
@@ -81,7 +84,7 @@ it('preserves external destinations and retries route EDIT through its original
|
||||
await state.submit(); vi.mocked(api.getDispatchCommand).mockRejectedValueOnce(new ApiResponseError('not found', 'HTTP_404'))
|
||||
vi.mocked(api.getDispatch).mockResolvedValue(detail({ recordVersion: 1 })); await state.submit()
|
||||
const first = vi.mocked(api.reorderDispatchRoute).mock.calls[0]![1]
|
||||
expect(first).toMatchObject({ recordVersion: 0, destinationKeys: ['PRIVATE:actual', 'CODE:ONT8'], reason: '按本车现场顺序卸货' }); expect(api.reorderDispatchRoute).toHaveBeenNthCalledWith(2, 'PC/1', first); expect(api.updateDispatch).not.toHaveBeenCalled(); expect(toast).toHaveBeenLastCalledWith('卸货顺序调整已确认,请重新核对两级审核'); toast.mockRestore(); wrapper.unmount()
|
||||
expect(first).toMatchObject({ recordVersion: 0, destinationKeys: ['PRIVATE:actual', 'CODE:ONT8'], reason: '按本车现场顺序卸货' }); expect(vi.mocked(api.reorderDispatchRoute).mock.calls[1]![1]).toMatchObject({destinationKeys:first.destinationKeys,reason:first.reason}); expect(vi.mocked(api.reorderDispatchRoute).mock.calls[1]![1].idempotencyKey).not.toBe(first.idempotencyKey); expect(api.updateDispatch).not.toHaveBeenCalled(); expect(toast).toHaveBeenLastCalledWith('卸货顺序调整已确认,请重新核对两级审核'); toast.mockRestore(); wrapper.unmount()
|
||||
})
|
||||
it('refuses a route that drops an authoritative destination', async () => {
|
||||
vi.mocked(api.getDispatchRoute).mockResolvedValue({ dispatchNo: 'PC/1', recordVersion: 0, edit: { allowed: true, blockCode: null, blockReason: null }, stops: [{ index: 0, name: '起运仓', address: null, destinationKey: null }, { index: 1, name: 'ONT8', address: null, destinationKey: 'CODE:ONT8' }, { index: 2, name: '外接点', address: null, destinationKey: 'EXTERNAL:destination' }] })
|
||||
@@ -133,37 +136,29 @@ it('reports a definite rejection to its parent even if the follow-up GET fails a
|
||||
expect(api.submitDispatchReview).toHaveBeenCalledTimes(1)
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('keeps an unknown original command when its later replay is rejected instead of treating it as a fresh rejection', async () => {
|
||||
const { wrapper, state } = await open('SUBMIT_REVIEW')
|
||||
vi.mocked(api.submitDispatchReview).mockRejectedValueOnce(new Error('unknown transport outcome')).mockRejectedValueOnce(new ApiResponseError('当前已提交', 'A0409', 409))
|
||||
it('ends every failed request and displays a later business rejection without replaying',async()=>{
|
||||
const {wrapper,state}=await open('SUBMIT_REVIEW')
|
||||
vi.mocked(api.submitDispatchReview).mockRejectedValueOnce(new Error('unknown')).mockRejectedValueOnce(new ApiResponseError('当前已提交','A0409',409))
|
||||
await state.submit()
|
||||
const original = structuredClone(vi.mocked(api.submitDispatchReview).mock.calls[0]![1])
|
||||
expect(wrapper.text()).toContain('原操作结果尚待核对')
|
||||
vi.mocked(api.getDispatchCommand).mockRejectedValueOnce(new ApiResponseError('未找到命令', 'A0404', 404))
|
||||
await state.submit(); await flushPromises()
|
||||
expect(api.submitDispatchReview).toHaveBeenNthCalledWith(2, 'PC/1', original)
|
||||
expect(useDispatchCommandRecovery(() => identity.id).pending.value?.body).toEqual(original)
|
||||
expect(wrapper.emitted('rejected')).toBeUndefined()
|
||||
expect(wrapper.emitted('update:modelValue')).toBeUndefined()
|
||||
expect(api.getDispatch).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.text()).toContain('原操作结果尚待核对')
|
||||
wrapper.unmount()
|
||||
})
|
||||
it('describes a proven success with a failed GET as completed and retries only its read', async () => {
|
||||
const { wrapper, state } = await open('SUBMIT_REVIEW')
|
||||
vi.mocked(api.submitDispatchReview).mockImplementationOnce(async (_, body) => command('SUBMIT_REVIEW', body.idempotencyKey))
|
||||
vi.mocked(api.getDispatch).mockRejectedValueOnce(new Error('current detail unavailable'))
|
||||
await state.submit(); await flushPromises()
|
||||
expect(useDispatchCommandRecovery(() => identity.id).pending.value?.result).toMatchObject({ action: 'SUBMIT_REVIEW', recordVersion: 1 })
|
||||
expect(wrapper.text()).toContain('操作已完成,页面资料待刷新')
|
||||
expect(useDispatchCommandRecovery(()=>identity.id).pending.value).toBeNull()
|
||||
expect(wrapper.text()).not.toContain('原操作结果尚待核对')
|
||||
expect(wrapper.text()).not.toContain('核对并重试原请求')
|
||||
vi.mocked(api.getDispatch).mockResolvedValueOnce(detail({ recordVersion: 1 }))
|
||||
await state.submit();await flushPromises()
|
||||
const calls=vi.mocked(api.submitDispatchReview).mock.calls
|
||||
expect(calls).toHaveLength(2);expect(calls[1]![1].idempotencyKey).not.toBe(calls[0]![1].idempotencyKey)
|
||||
expect(useDispatchCommandRecovery(()=>identity.id).pending.value).toBeNull()
|
||||
expect(api.getDispatchCommand).not.toHaveBeenCalled();expect(wrapper.text()).toContain('当前已提交');wrapper.unmount()
|
||||
})
|
||||
it('unlocks when a successful write is followed by a failed read',async()=>{
|
||||
const {wrapper,state}=await open('SUBMIT_REVIEW')
|
||||
vi.mocked(api.submitDispatchReview).mockImplementation(async(_,body)=>command('SUBMIT_REVIEW',body.idempotencyKey))
|
||||
vi.mocked(api.getDispatch).mockRejectedValueOnce(new Error('current detail unavailable'))
|
||||
await state.submit();await flushPromises()
|
||||
expect(useDispatchCommandRecovery(()=>identity.id).pending.value).toBeNull()
|
||||
expect(wrapper.text()).not.toContain('原操作结果尚待核对')
|
||||
vi.mocked(api.getDispatch).mockResolvedValue(detail({recordVersion:1}))
|
||||
await state.submit()
|
||||
expect(api.submitDispatchReview).toHaveBeenCalledTimes(1)
|
||||
expect(api.getDispatchCommand).not.toHaveBeenCalled()
|
||||
expect(wrapper.emitted('saved')).toHaveLength(1)
|
||||
wrapper.unmount()
|
||||
expect(api.submitDispatchReview).toHaveBeenCalledTimes(2)
|
||||
expect(api.getDispatchCommand).not.toHaveBeenCalled();wrapper.unmount()
|
||||
})
|
||||
|
||||
it.each(['ADD_CARGO', 'REMOVE_CARGO'] as const)('blocks a fresh %s command and directs cargo changes to smart dispatch', async action => {
|
||||
@@ -179,23 +174,13 @@ it.each(['ADD_CARGO', 'REMOVE_CARGO'] as const)('blocks a fresh %s command and d
|
||||
expect(wrapper.emitted('saved')).toBeUndefined()
|
||||
} finally { wrapper.unmount() }
|
||||
})
|
||||
it.each(['ADD_CARGO', 'REMOVE_CARGO'] as const)('preserves the exact pending %s command for recovery in smart dispatch', async action => {
|
||||
const recovery = useDispatchCommandRecovery(() => identity.id)
|
||||
recovery.begin('PC/ORIGINAL', action, { recordVersion: '9007199254740993', idempotencyKey: 'original-key', reason: 'original reason', cargo: [{ sourceVersion: 'original-source' }] }, action === 'REMOVE_CARGO' ? { itemId: 'original-item' } : {})
|
||||
recovery.pending.value!.attempted = true
|
||||
const original = JSON.parse(JSON.stringify(recovery.pending.value))
|
||||
const { wrapper, state } = await open('EDIT')
|
||||
try {
|
||||
expect(wrapper.text()).toContain('请从智能排车恢复原请求')
|
||||
await state.submit(); await state.retryError()
|
||||
expect(recovery.pending.value).toEqual(original)
|
||||
expect(api.addDispatchCargo).not.toHaveBeenCalled()
|
||||
expect(api.removeDispatchItem).not.toHaveBeenCalled()
|
||||
expect(api.updateDispatch).not.toHaveBeenCalled()
|
||||
expect(api.getDispatchCommand).not.toHaveBeenCalled()
|
||||
expect(wrapper.emitted('saved')).toBeUndefined()
|
||||
expect(wrapper.emitted('rejected')).toBeUndefined()
|
||||
} finally { wrapper.unmount() }
|
||||
it.each(['ADD_CARGO','REMOVE_CARGO'] as const)('ignores old %s records when opening another operation',async action=>{
|
||||
sessionStorage.setItem('dispatch-command:'+identity.id,JSON.stringify({dispatchNo:'PC/ORIGINAL',action,attempted:true,body:{idempotencyKey:'old'}}))
|
||||
const {wrapper,state}=await open('EDIT')
|
||||
expect(useDispatchCommandRecovery(()=>identity.id).pending.value).toBeNull()
|
||||
expect(sessionStorage.getItem('dispatch-command:'+identity.id)).toBeNull()
|
||||
expect(wrapper.text()).not.toContain('请从智能排车恢复原请求')
|
||||
expect(api.addDispatchCargo).not.toHaveBeenCalled();expect(api.removeDispatchItem).not.toHaveBeenCalled();wrapper.unmount()
|
||||
})
|
||||
|
||||
it('requires an exception type and submits optional direction, description and images once', async () => {
|
||||
@@ -249,33 +234,30 @@ it('rechecks review eligibility after save and leaves the saved editor open if b
|
||||
expect(api.submitDispatchReview).not.toHaveBeenCalled(); expect(state.detail.recordVersion).toBe(1)
|
||||
expect(wrapper.text()).toContain('预约资料未齐全'); expect(wrapper.emitted('update:modelValue')).toBeUndefined(); wrapper.unmount()
|
||||
})
|
||||
it('retains the follow-up review intent through an unknown save and does not save twice on recovery', async () => {
|
||||
const { wrapper, state } = await open('EDIT')
|
||||
vi.mocked(api.updateDispatch).mockRejectedValueOnce(new Error('save outcome unknown'))
|
||||
it('allows a fresh save and review after a failed save without persisting review intent',async()=>{
|
||||
const {wrapper,state}=await open('EDIT')
|
||||
vi.mocked(api.updateDispatch).mockRejectedValueOnce(new Error('save unknown'))
|
||||
await state.submit(true)
|
||||
const pending = useDispatchCommandRecovery(() => identity.id).pending.value!
|
||||
expect(pending.submitReviewAfterSave).toBe(true)
|
||||
expect(JSON.parse(sessionStorage.getItem(`dispatch-command:${identity.id}`)!).submitReviewAfterSave).toBe(true)
|
||||
vi.mocked(api.getDispatchCommand).mockResolvedValueOnce(command('EDIT', String(pending.body.idempotencyKey)))
|
||||
vi.mocked(api.getDispatch).mockResolvedValueOnce(detail({ recordVersion: 1, actions: { SUBMIT_REVIEW: { allowed: true, blockCode: null, blockReason: null } } })).mockResolvedValueOnce(detail({ recordVersion: 2 }))
|
||||
vi.mocked(api.submitDispatchReview).mockImplementationOnce(async (_, body) => ({ ...command('SUBMIT_REVIEW', body.idempotencyKey), recordVersion: 2 }))
|
||||
await state.submit()
|
||||
expect(api.updateDispatch).toHaveBeenCalledTimes(1); expect(api.submitDispatchReview).toHaveBeenCalledTimes(1)
|
||||
expect(useDispatchCommandRecovery(() => identity.id).pending.value).toBeNull(); wrapper.unmount()
|
||||
expect(useDispatchCommandRecovery(()=>identity.id).pending.value).toBeNull()
|
||||
expect(sessionStorage.getItem('dispatch-command:'+identity.id)).toBeNull()
|
||||
vi.mocked(api.updateDispatch).mockImplementationOnce(async(_,body)=>command('EDIT',body.idempotencyKey))
|
||||
vi.mocked(api.getDispatch).mockResolvedValueOnce(detail({recordVersion:1,actions:{SUBMIT_REVIEW:{allowed:true,blockCode:null,blockReason:null}}})).mockResolvedValueOnce(detail({recordVersion:2}))
|
||||
vi.mocked(api.submitDispatchReview).mockImplementationOnce(async(_,body)=>({...command('SUBMIT_REVIEW',body.idempotencyKey),recordVersion:2}))
|
||||
await state.submit(true)
|
||||
expect(api.updateDispatch).toHaveBeenCalledTimes(2);expect(api.submitDispatchReview).toHaveBeenCalledOnce()
|
||||
expect(api.getDispatchCommand).not.toHaveBeenCalled();wrapper.unmount()
|
||||
})
|
||||
it('recovers an unknown review submission without saving the vehicle again', async () => {
|
||||
const { wrapper, state } = await open('EDIT')
|
||||
vi.mocked(api.updateDispatch).mockImplementationOnce(async (_, body) => command('EDIT', body.idempotencyKey))
|
||||
vi.mocked(api.getDispatch).mockResolvedValueOnce(detail({ recordVersion: 1, actions: { SUBMIT_REVIEW: { allowed: true, blockCode: null, blockReason: null } } }))
|
||||
vi.mocked(api.submitDispatchReview).mockRejectedValueOnce(new Error('review outcome unknown'))
|
||||
it('allows a new save-and-review click after review submission fails',async()=>{
|
||||
const {wrapper,state}=await open('EDIT')
|
||||
vi.mocked(api.updateDispatch).mockImplementation(async(_,body)=>command('EDIT',body.idempotencyKey))
|
||||
vi.mocked(api.getDispatch).mockResolvedValue(detail({recordVersion:1,actions:{EDIT:{allowed:true,blockCode:null,blockReason:null},SUBMIT_REVIEW:{allowed:true,blockCode:null,blockReason:null}}}))
|
||||
vi.mocked(api.submitDispatchReview).mockRejectedValueOnce(new Error('review unknown'))
|
||||
await state.submit(true)
|
||||
const pending = useDispatchCommandRecovery(() => identity.id).pending.value!
|
||||
expect(pending.action).toBe('SUBMIT_REVIEW'); expect(pending.body).toMatchObject({ recordVersion: 1, remark: null })
|
||||
vi.mocked(api.getDispatchCommand).mockResolvedValueOnce({ ...command('SUBMIT_REVIEW', String(pending.body.idempotencyKey)), recordVersion: 2 })
|
||||
vi.mocked(api.getDispatch).mockResolvedValueOnce(detail({ recordVersion: 2 }))
|
||||
await state.submit()
|
||||
expect(api.updateDispatch).toHaveBeenCalledTimes(1); expect(api.submitDispatchReview).toHaveBeenCalledTimes(1)
|
||||
expect(wrapper.emitted('update:modelValue')).toContainEqual([false]); wrapper.unmount()
|
||||
expect(useDispatchCommandRecovery(()=>identity.id).pending.value).toBeNull()
|
||||
vi.mocked(api.submitDispatchReview).mockImplementationOnce(async(_,body)=>command('SUBMIT_REVIEW',body.idempotencyKey))
|
||||
await state.submit(true)
|
||||
expect(api.updateDispatch).toHaveBeenCalledTimes(2);expect(api.submitDispatchReview).toHaveBeenCalledTimes(2)
|
||||
expect(api.getDispatchCommand).not.toHaveBeenCalled();wrapper.unmount()
|
||||
})
|
||||
|
||||
it('persists the explicit publication choice with the existing vehicle save', async () => {
|
||||
|
||||
Reference in New Issue
Block a user