fix(overseas): expose permission-gated cargo discard review

This commit is contained in:
2026-10-04 16:57:16 +08:00
parent 1d73a40a07
commit cc973c72c0
6 changed files with 152 additions and 1 deletions
@@ -1,4 +1,4 @@
import ElementPlus from 'element-plus'
import ElementPlus, { ElMessageBox, ElMessage } from 'element-plus'
import { readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { createPinia } from 'pinia'
@@ -17,6 +17,7 @@ import {
getContainerPlanProgress,
listContainerPlanProgress,
listLastMileOrders,
reviewCargoDiscard,
transferLastMileOrders,
getLastMileOrderManualNodes,
updateLastMileOrderHold,
@@ -34,6 +35,7 @@ vi.mock('../api/lastMileOrders', () => ({
createManualInboundPlan: vi.fn(), convertLastMileContainerToInboundPlans: vi.fn(), getContainerInboundPlanPreview: vi.fn(), transferLastMileOrders: vi.fn(),
getLastMileOrderManualNodes: vi.fn(), updateLastMileOrderHold: vi.fn(), updateLastMileOrderManualNodes: vi.fn(),
downloadLastMileOrders: vi.fn(),
reviewCargoDiscard: vi.fn(),
}))
class ResizeObserverStub implements ResizeObserver { observe() {} unobserve() {} disconnect() {} }
@@ -257,6 +259,8 @@ describe('LastMileOrdersView pending order and container conversion', () => {
})
beforeEach(() => {
vi.restoreAllMocks()
vi.mocked(reviewCargoDiscard).mockReset().mockResolvedValue({})
canAccess.mockReset().mockReturnValue(true)
vi.stubGlobal('ResizeObserver', ResizeObserverStub)
vi.stubGlobal('localStorage', memoryStorage())
@@ -280,6 +284,92 @@ describe('LastMileOrdersView pending order and container conversion', () => {
vi.mocked(transferLastMileOrders).mockReset().mockResolvedValue(2)
})
async function openDiscard(overrides: Record<string, unknown> = {}) {
vi.mocked(listLastMileOrders).mockResolvedValue({ records: [{
id: '2092873463568596994', customerId, orderNo: 'CD-TEST-1', sourceOrderNo: 'CD-TEST-1',
orderType: 'CARGO_DISCARD', currentStatus: 'PENDING_REVIEW', inboundOrderNo: 'CD-TEST-1', ...overrides,
}], page: 1, size: 10, total: 1, pages: 1 })
return mountView()
}
it.each([
['lastleg:cargo-discard:approve', 'PENDING_REVIEW', true],
['platform:overseas-warehouse:last-mile-order:edit', 'PENDING_REVIEW', false],
['lastleg:cargo-discard:approve', 'APPROVED', false],
['lastleg:cargo-discard:approve', 'CANCELLED', false],
])('gates discard review on exact permission %s and status %s', async (code, currentStatus, visible) => {
canAccess.mockImplementation(permissionCode => permissionCode === code)
const wrapper = await openDiscard({ currentStatus })
expect(Boolean(button(wrapper, '审核'))).toBe(visible)
expect(reviewCargoDiscard).not.toHaveBeenCalled()
})
it.each([{ orderNo: '' }, { customerId: '' }])('does not expose discard review without a business target %j', async (overrides) => {
const wrapper = await openDiscard(overrides)
expect(button(wrapper, '审核')).toBeUndefined()
expect(reviewCargoDiscard).not.toHaveBeenCalled()
})
it('approves the selected discard once and reloads the server status', async () => {
const confirm = vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as Awaited<ReturnType<typeof ElMessageBox.confirm>>)
const wrapper = await openDiscard()
vi.mocked(listLastMileOrders).mockResolvedValue({ records: [{
id: '2092873463568596994', customerId, orderNo: 'CD-TEST-1',
orderType: 'CARGO_DISCARD', currentStatus: 'APPROVED', inboundOrderNo: 'CD-TEST-1',
}], page: 1, size: 10, total: 1, pages: 1 })
await button(wrapper, '审核')!.trigger('click')
await flushPromises()
expect(confirm).toHaveBeenCalledWith(expect.stringContaining('CD-TEST-1'), '废弃申请审核', expect.objectContaining({ distinguishCancelAndClose: true }))
expect(reviewCargoDiscard).toHaveBeenCalledExactlyOnceWith('CD-TEST-1', customerId, 'APPROVE', undefined)
expect(button(wrapper, '审核')).toBeUndefined()
})
it('rejects with a trimmed mandatory reason without approving first', async () => {
vi.spyOn(ElMessageBox, 'confirm').mockRejectedValue('cancel')
const prompt = vi.spyOn(ElMessageBox, 'prompt').mockResolvedValue({ value: ' 箱数不符 ', action: 'confirm' } as Awaited<ReturnType<typeof ElMessageBox.prompt>>)
const wrapper = await openDiscard()
await button(wrapper, '审核')!.trigger('click')
await flushPromises()
const options = prompt.mock.calls[0]?.[2]
expect(options?.inputValidator?.(' ')).not.toBe(true)
expect(options?.inputValidator?.('甲'.repeat(1001))).not.toBe(true)
expect(options?.inputValidator?.('箱数不符')).toBe(true)
expect(reviewCargoDiscard).toHaveBeenCalledExactlyOnceWith('CD-TEST-1', customerId, 'REJECT', '箱数不符')
})
it('closing review or cancelling the reason makes no request', async () => {
vi.spyOn(ElMessageBox, 'confirm').mockRejectedValueOnce('close').mockRejectedValueOnce('cancel')
vi.spyOn(ElMessageBox, 'prompt').mockRejectedValue('cancel')
const wrapper = await openDiscard()
await button(wrapper, '审核')!.trigger('click')
await flushPromises()
await button(wrapper, '审核')!.trigger('click')
await flushPromises()
expect(reviewCargoDiscard).not.toHaveBeenCalled()
expect(listLastMileOrders).toHaveBeenCalledTimes(1)
})
it('locks repeat clicks while review is pending and allows retry after an API failure', async () => {
vi.spyOn(ElMessageBox, 'confirm').mockResolvedValue('confirm' as Awaited<ReturnType<typeof ElMessageBox.confirm>>)
const errorToast = vi.spyOn(ElMessage, 'error')
let rejectRequest!: (error: Error) => void
vi.mocked(reviewCargoDiscard).mockImplementationOnce(() => new Promise((_, reject) => { rejectRequest = reject }))
const wrapper = await openDiscard()
await button(wrapper, '审核')!.trigger('click')
await flushPromises()
expect((button(wrapper, '审核')!.element as HTMLButtonElement).disabled).toBe(true)
await button(wrapper, '审核')!.trigger('click')
expect(reviewCargoDiscard).toHaveBeenCalledTimes(1)
rejectRequest(new Error('审批请求失败'))
await flushPromises()
expect(errorToast).toHaveBeenCalled()
expect((button(wrapper, '审核')!.element as HTMLButtonElement).disabled).toBe(false)
expect(listLastMileOrders).toHaveBeenCalledTimes(1)
await button(wrapper, '审核')!.trigger('click')
await flushPromises()
expect(reviewCargoDiscard).toHaveBeenCalledTimes(2)
})
it.each([
['PENDING_ASSIGNMENT', false],
['INBOUND_COMPLETED', true],