fix(overseas): restore pending resource confirmation from original command
This commit is contained in:
@@ -91,6 +91,26 @@ const preview = {
|
||||
}],
|
||||
}],
|
||||
}
|
||||
// Recorded LOCAL_COMMITTED shape: complete local plan/pallet, zero new work, PLANNED,
|
||||
// and an explicit server resourceRetry holding the original command identity.
|
||||
const resourceCommittedPreview = {
|
||||
...preview, snapshotVersion: 10, snapshotHash: 'b'.repeat(64),
|
||||
totalOrderCount: 1, readyOrderCount: 1, pendingOrderCount: 0, pendingPalletCount: 0,
|
||||
sourcePalletCount: 0, supplementPalletCount: 0, existingPlannedPalletCount: 1,
|
||||
totalSuggestedPalletCount: 1, totalPlannedPalletCount: 1,
|
||||
groups: [{
|
||||
...preview.groups[0], destinationType: 'FBA' as const,
|
||||
orderCount: 1, pendingOrderCount: 0, pendingPalletCount: 0, sourcePalletCount: 0,
|
||||
supplementPalletCount: 0, existingPlannedPalletCount: 1, suggestedPalletCount: 1, plannedPalletCount: 1,
|
||||
orders: [{ ...preview.groups[0].orders[0], destinationType: 'FBA' as const, fbaWarehouseCode: 'ONT8',
|
||||
packageCount: 1, suggestedPalletCount: 1, sourcePalletCount: 1, targetPalletCount: 1,
|
||||
supplementPalletCount: 0, plannedPalletCount: 1, existingPlanNo: 'LIP-ORIGINAL',
|
||||
planStatus: 'PENDING_ASSIGNMENT', readiness: 'PLANNED' as const, blockers: [],
|
||||
}],
|
||||
}],
|
||||
resourceRetry: { idempotencyKey: 'original-container-command', snapshotVersion: 9, snapshotHash: 'a'.repeat(64) },
|
||||
}
|
||||
const originalResourcePlans = [{ planNo: 'LIP-ORIGINAL', pallets: [{ id: 'PALLET-ORIGINAL', palletNo: 'PLT-ORIGINAL' }] }]
|
||||
const receivingWarehouse = {
|
||||
...fbaWarehouse,
|
||||
id: '9007199254740996', warehouseCode: 'LAX02', warehouseName: '洛杉矶二号接柜仓', warehouseType: 'OVERSEAS',
|
||||
@@ -144,6 +164,9 @@ async function selectRows(wrapper: Awaited<ReturnType<typeof mountView>>, rows:
|
||||
function button(wrapper: Awaited<ReturnType<typeof mountView>>, text: string) {
|
||||
return wrapper.findAll('button').find((item) => item.text().includes(text))
|
||||
}
|
||||
function cabinetDialog(wrapper: Awaited<ReturnType<typeof mountView>>) {
|
||||
return wrapper.findAllComponents(AppDialog).find(dialog => dialog.props('title') === '按柜生成入库计划预览')!
|
||||
}
|
||||
|
||||
async function openContainerDialog(wrapper: Awaited<ReturnType<typeof mountView>>) {
|
||||
await button(wrapper, '按柜批量生成')!.trigger('click')
|
||||
@@ -729,32 +752,126 @@ describe('LastMileOrdersView pending order and container conversion', () => {
|
||||
records: [{ ...summary, pendingPlanCount: 0, resourcePendingPlanCount: 1, allPlansReady: true }],
|
||||
page: 1, size: 10, total: 1, pages: 1,
|
||||
})
|
||||
const reopenedPreview = {
|
||||
...preview, snapshotVersion: 10, snapshotHash: 'b'.repeat(64),
|
||||
groups: preview.groups.map((group) => ({
|
||||
...group, destinationType: 'FBA' as const,
|
||||
orders: group.orders.map((order) => ({
|
||||
...order, destinationType: 'FBA' as const, fbaWarehouseCode: 'ONT8',
|
||||
readiness: 'RESOURCE_PENDING' as const, existingPlanNo: 'LIP-ORIGINAL', plannedPalletCount: 5,
|
||||
})),
|
||||
})),
|
||||
resourceRetry: { idempotencyKey: 'original-container-command', snapshotVersion: 9, snapshotHash: 'a'.repeat(64) },
|
||||
}
|
||||
vi.mocked(getContainerInboundPlanPreview).mockResolvedValueOnce(reopenedPreview)
|
||||
vi.mocked(getContainerInboundPlanPreview).mockResolvedValueOnce(resourceCommittedPreview)
|
||||
vi.mocked(convertLastMileContainerToInboundPlans).mockResolvedValue(originalResourcePlans as never)
|
||||
const wrapper = await mountView()
|
||||
await openHiddenContainerWorkbench(wrapper)
|
||||
await button(wrapper, '重试生成托盘码')!.trigger('click')
|
||||
await flushPromises()
|
||||
const confirm = [...document.body.querySelectorAll('button')]
|
||||
.find((item) => item.textContent?.includes('确认生成')) as HTMLButtonElement
|
||||
expect(confirm.disabled).toBe(false)
|
||||
confirm.click()
|
||||
expect(cabinetDialog(wrapper).props('confirmDisabled')).toBe(false)
|
||||
expect(cabinetDialog(wrapper).props('confirmText')).toBe('重试资源确认')
|
||||
cabinetDialog(wrapper).vm.$emit('confirm')
|
||||
await flushPromises()
|
||||
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledWith({
|
||||
sourceContainerBookingId: bookingId, snapshotVersion: 9, snapshotHash: 'a'.repeat(64),
|
||||
}, 'original-container-command')
|
||||
})
|
||||
|
||||
it('reopens a complete local plan and confirms only the original resource key/version/hash without showing zero generation', async () => {
|
||||
const success = vi.spyOn(ElMessage, 'success').mockImplementation(() => ({ close: vi.fn() } as never))
|
||||
vi.mocked(getContainerInboundPlanPreview)
|
||||
.mockResolvedValueOnce(resourceCommittedPreview)
|
||||
.mockResolvedValueOnce(resourceCommittedPreview)
|
||||
.mockResolvedValue({ ...resourceCommittedPreview, resourceRetry: undefined })
|
||||
vi.mocked(convertLastMileContainerToInboundPlans).mockResolvedValue(originalResourcePlans as never)
|
||||
const wrapper = await openPending()
|
||||
await openContainerDialog(wrapper); await chooseContainer(wrapper)
|
||||
cabinetDialog(wrapper).vm.$emit('update:modelValue', false); await flushPromises()
|
||||
await openContainerDialog(wrapper); await chooseContainer(wrapper)
|
||||
const dialog = cabinetDialog(wrapper)
|
||||
expect(dialog.props('confirmDisabled')).toBe(false)
|
||||
expect(dialog.props('confirmText')).toBe('重试资源确认')
|
||||
expect(document.body.textContent).toContain('资源仍待确认')
|
||||
expect(document.body.textContent).not.toContain('最终生成 0 个托盘')
|
||||
expect(document.body.textContent).not.toContain('最终 0 托')
|
||||
dialog.vm.$emit('confirm'); await flushPromises()
|
||||
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledExactlyOnceWith({
|
||||
sourceContainerBookingId: bookingId, snapshotVersion: 9, snapshotHash: 'a'.repeat(64),
|
||||
}, 'original-container-command')
|
||||
expect(document.body.textContent).toContain('资源确认完成')
|
||||
expect(document.body.textContent).toContain('PLT-ORIGINAL')
|
||||
expect(cabinetDialog(wrapper).props('confirmText')).toBe('已确认')
|
||||
expect(cabinetDialog(wrapper).props('confirmDisabled')).toBe(true)
|
||||
expect(success).toHaveBeenCalledWith('已完成 1 个已有计划的资源确认')
|
||||
expect(createManualInboundPlan).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps ordinary zero-pending previews without resourceRetry disabled in both UI and handler', async () => {
|
||||
vi.mocked(getContainerInboundPlanPreview).mockResolvedValue({ ...resourceCommittedPreview, resourceRetry: undefined })
|
||||
const wrapper = await openPending(); await openContainerDialog(wrapper); await chooseContainer(wrapper)
|
||||
expect(cabinetDialog(wrapper).props('confirmDisabled')).toBe(true)
|
||||
expect(cabinetDialog(wrapper).props('confirmText')).toBe('确认生成')
|
||||
cabinetDialog(wrapper).vm.$emit('confirm'); await flushPromises()
|
||||
expect(convertLastMileContainerToInboundPlans).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses the visible missing-container label in the preview tooltip without exposing the booking ID', async () => {
|
||||
vi.mocked(getContainerInboundPlanPreview).mockResolvedValue({ ...preview, containerNo: null } as never)
|
||||
const wrapper = await openPending(); await openContainerDialog(wrapper); await chooseContainer(wrapper)
|
||||
const identity = document.body.querySelector('.container-preview__identity')!
|
||||
expect(identity.textContent).toBe('未录入货柜号')
|
||||
expect(identity.getAttribute('title')).toBe('未录入货柜号')
|
||||
expect(identity.getAttribute('title')).not.toContain(bookingId)
|
||||
})
|
||||
|
||||
it('keeps server readiness blocks effective even for a resource retry', async () => {
|
||||
vi.mocked(getContainerInboundPlanPreview).mockResolvedValue({ ...resourceCommittedPreview, ready: false, blockedOrderCount: 1 })
|
||||
const wrapper = await openPending(); await openContainerDialog(wrapper); await chooseContainer(wrapper)
|
||||
expect(cabinetDialog(wrapper).props('confirmDisabled')).toBe(true)
|
||||
cabinetDialog(wrapper).vm.$emit('confirm'); await flushPromises()
|
||||
expect(convertLastMileContainerToInboundPlans).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('guards resource retry at the handler when the caller lacks create permission', async () => {
|
||||
canAccess.mockImplementation(code => code !== 'platform:overseas-warehouse:inbound-order:create')
|
||||
vi.mocked(getContainerInboundPlanPreview).mockResolvedValue(resourceCommittedPreview)
|
||||
const wrapper = await openPending()
|
||||
const view = wrapper.vm as unknown as { openContainerConversion(): void }
|
||||
view.openContainerConversion(); await flushPromises(); await chooseContainer(wrapper)
|
||||
expect(cabinetDialog(wrapper).props('confirmDisabled')).toBe(true)
|
||||
cabinetDialog(wrapper).vm.$emit('confirm'); await flushPromises()
|
||||
expect(convertLastMileContainerToInboundPlans).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps preview and candidate loading guards effective for recovery commands', async () => {
|
||||
let resolvePreview!: (value: typeof resourceCommittedPreview) => void
|
||||
let resolveOptions!: (value: Awaited<ReturnType<typeof listContainerPlanProgress>>) => void
|
||||
vi.mocked(getContainerInboundPlanPreview).mockReturnValue(new Promise(resolve => { resolvePreview = resolve }))
|
||||
vi.mocked(listContainerPlanProgress).mockReturnValue(new Promise(resolve => { resolveOptions = resolve }))
|
||||
const wrapper = await openPending(); await openContainerDialog(wrapper); await chooseContainer(wrapper)
|
||||
expect(cabinetDialog(wrapper).props('confirmDisabled')).toBe(true)
|
||||
cabinetDialog(wrapper).vm.$emit('confirm'); await flushPromises()
|
||||
expect(convertLastMileContainerToInboundPlans).not.toHaveBeenCalled()
|
||||
resolvePreview(resourceCommittedPreview); await flushPromises()
|
||||
expect(cabinetDialog(wrapper).props('confirmDisabled')).toBe(true)
|
||||
cabinetDialog(wrapper).vm.$emit('confirm'); await flushPromises()
|
||||
expect(convertLastMileContainerToInboundPlans).not.toHaveBeenCalled()
|
||||
resolveOptions({ records: [summary], page: 1, size: 50, total: 1, pages: 1 }); await flushPromises()
|
||||
expect(cabinetDialog(wrapper).props('confirmDisabled')).toBe(false)
|
||||
})
|
||||
|
||||
it('preserves the original recovery command on failure and blocks duplicate or completed submissions', async () => {
|
||||
const success = vi.spyOn(ElMessage, 'success').mockImplementation(() => ({ close: vi.fn() } as never))
|
||||
vi.mocked(getContainerInboundPlanPreview).mockResolvedValue(resourceCommittedPreview)
|
||||
vi.mocked(convertLastMileContainerToInboundPlans).mockRejectedValueOnce(new ApiResponseError('资源确认失败', 'S0503', 503))
|
||||
const wrapper = await openPending(); await openContainerDialog(wrapper); await chooseContainer(wrapper)
|
||||
cabinetDialog(wrapper).vm.$emit('confirm'); await flushPromises()
|
||||
expect(cabinetDialog(wrapper).props('modelValue')).toBe(true)
|
||||
expect(cabinetDialog(wrapper).props('confirmDisabled')).toBe(false)
|
||||
expect(success).not.toHaveBeenCalled()
|
||||
let resolveCommand!: (value: Awaited<ReturnType<typeof convertLastMileContainerToInboundPlans>>) => void
|
||||
vi.mocked(convertLastMileContainerToInboundPlans).mockReturnValueOnce(new Promise(resolve => { resolveCommand = resolve }))
|
||||
cabinetDialog(wrapper).vm.$emit('confirm'); cabinetDialog(wrapper).vm.$emit('confirm'); await flushPromises()
|
||||
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledTimes(2)
|
||||
expect(cabinetDialog(wrapper).props('confirmDisabled')).toBe(true)
|
||||
resolveCommand(originalResourcePlans as never); await flushPromises()
|
||||
cabinetDialog(wrapper).vm.$emit('confirm'); await flushPromises()
|
||||
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledTimes(2)
|
||||
for (const args of vi.mocked(convertLastMileContainerToInboundPlans).mock.calls) expect(args).toEqual([
|
||||
{ sourceContainerBookingId: bookingId, snapshotVersion: 9, snapshotHash: 'a'.repeat(64) }, 'original-container-command',
|
||||
])
|
||||
})
|
||||
|
||||
it('renders the all-cabinet empty state and unit', async () => {
|
||||
vi.mocked(listContainerPlanProgress).mockResolvedValueOnce({ records: [], page: 1, size: 10, total: 0, pages: 0 })
|
||||
const wrapper = await mountView()
|
||||
|
||||
Reference in New Issue
Block a user