fix(overseas): restore pending resource confirmation from original command

This commit is contained in:
2026-10-05 05:40:21 +08:00
parent 28eb43893a
commit 578df0f927
2 changed files with 162 additions and 27 deletions
@@ -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()