feat: confirm container inbound plans

This commit is contained in:
2026-09-09 00:44:07 +08:00
parent 695db084e2
commit cf8727a809
5 changed files with 243 additions and 27 deletions
@@ -13,6 +13,7 @@ import {
changeContainerReceivingWarehouse,
createManualInboundPlan,
convertLastMileContainerToInboundPlans,
getContainerInboundPlanPreview,
getContainerPlanProgress,
listContainerPlanProgress,
listLastMileOrders,
@@ -30,7 +31,7 @@ vi.mock('@/modules/warehouses', () => ({ listAllPlatformWarehouses: vi.fn() }))
vi.mock('../api/lastMileOrders', () => ({
listLastMileOrders: vi.fn(), listContainerPlanProgress: vi.fn(), getContainerPlanProgress: vi.fn(),
changeContainerReceivingWarehouse: vi.fn(),
createManualInboundPlan: vi.fn(), convertLastMileContainerToInboundPlans: vi.fn(), transferLastMileOrders: vi.fn(),
createManualInboundPlan: vi.fn(), convertLastMileContainerToInboundPlans: vi.fn(), getContainerInboundPlanPreview: vi.fn(), transferLastMileOrders: vi.fn(),
getLastMileOrderManualNodes: vi.fn(), updateLastMileOrderHold: vi.fn(), updateLastMileOrderManualNodes: vi.fn(),
downloadLastMileOrders: vi.fn(),
}))
@@ -65,6 +66,26 @@ const fbaWarehouse = {
},
addressSummary: 'CA Ontario',
}
const preview = {
sourceContainerBookingId: bookingId, containerNo: 'CONT-15',
overseasWarehouseId: summary.overseasWarehouseId, overseasWarehouseCode: summary.overseasWarehouseCode,
overseasWarehouseName: summary.overseasWarehouseName, snapshotVersion: 9, snapshotHash: 'a'.repeat(64),
totalOrderCount: 5, readyOrderCount: 5, blockedOrderCount: 0, pendingOrderCount: 5,
pendingPalletCount: 5, existingPlannedPalletCount: 0, totalSuggestedPalletCount: 5,
totalPlannedPalletCount: 0, ready: true,
groups: [{
destinationType: 'FBA', destinationWarehouseId: null, destinationWarehouseCode: 'ONT8',
destinationWarehouseName: 'Ontario FBA', orderCount: 5, pendingOrderCount: 5, pendingPalletCount: 5,
existingPlannedPalletCount: 0, suggestedPalletCount: 5, plannedPalletCount: 0,
orders: [{
customerId, customerName: '测试客户', orderId: '2092873463568596994', businessOrderNo: 'LMO-1',
inboundCode: 'IN-1', orderType: 'FIRST_LEG', destinationType: 'FBA', destinationWarehouseId: null,
destinationWarehouseCode: 'ONT8', destinationWarehouseName: 'Ontario FBA', packageCount: 10,
volumeCbm: 1, suggestedPalletCount: 5, plannedPalletCount: null, existingPlanNo: null,
planStatus: null, orderVersion: 3, readiness: 'READY', blockers: [],
}],
}],
}
const receivingWarehouse = {
...fbaWarehouse,
id: '9007199254740996', warehouseCode: 'LAX02', warehouseName: '洛杉矶二号接柜仓', warehouseType: 'OVERSEAS',
@@ -118,6 +139,8 @@ async function chooseContainer(wrapper: Awaited<ReturnType<typeof mountView>>) {
const containerSelect = selects[selects.length - 1]
expect(containerSelect).toBeDefined()
containerSelect.vm.$emit('update:modelValue', bookingId)
containerSelect.vm.$emit('change', bookingId)
await flushPromises()
await wrapper.vm.$nextTick()
}
@@ -133,10 +156,14 @@ describe('LastMileOrdersView pending order and container conversion', () => {
orders: [{ orderId: '2092873463568596994', customerId, orderNo: 'LMO-1', inboundCode: 'IN-1', orderStatus: 'INBOUND_PLANNED', planId: '2092873463568597010', planNo: 'LIP-1', planStatus: 'PENDING_RECEIVING', planValid: true, plannedArrivalAt: '2026-09-01T08:00:00' }],
})
vi.mocked(createManualInboundPlan).mockReset().mockResolvedValue({} as never)
vi.mocked(getContainerInboundPlanPreview).mockReset().mockResolvedValue(preview as never)
vi.mocked(getLastMileOrderManualNodes).mockReset().mockResolvedValue({ orderId: '2092873463568596994', manualOverrideEnabled: false, nodes: [], version: 0 })
vi.mocked(updateLastMileOrderHold).mockReset().mockResolvedValue({ orderId: '2092873463568596994', held: true, chargeable: true, version: 1 })
vi.mocked(updateLastMileOrderManualNodes).mockReset().mockResolvedValue({ orderId: '2092873463568596994', manualOverrideEnabled: true, nodes: [], version: 1 })
vi.mocked(convertLastMileContainerToInboundPlans).mockReset().mockResolvedValue([{ planNo: 'LIP-1' }, { planNo: 'LIP-2' }] as never)
vi.mocked(convertLastMileContainerToInboundPlans).mockReset().mockResolvedValue([
{ planNo: 'LIP-1', pallets: [{ id: 'PALLET-1', palletNo: 'PLT-001' }] },
{ planNo: 'LIP-2', pallets: [{ id: 'PALLET-2', palletNo: 'PLT-002' }] },
] as never)
vi.mocked(changeContainerReceivingWarehouse).mockReset().mockResolvedValue({ ...summary, overseasWarehouseId: '9007199254740996', overseasWarehouseCode: 'LAX02', overseasWarehouseName: '洛杉矶二号接柜仓', assignmentVersion: 8 })
vi.mocked(listAllPlatformWarehouses).mockReset().mockResolvedValue([fbaWarehouse] as never)
vi.mocked(transferLastMileOrders).mockReset().mockResolvedValue(2)
@@ -253,7 +280,8 @@ describe('LastMileOrdersView pending order and container conversion', () => {
expect(listContainerPlanProgress).toHaveBeenCalledTimes(1)
expect(listContainerPlanProgress).toHaveBeenCalledWith({ page: 1, size: 50, containerNo: undefined })
expect(document.body.textContent).toContain('选择待分配柜')
expect(document.body.textContent).toContain('将为所选柜内所有待分配尾程订单分别生成独立入库计划')
expect(document.body.textContent).toContain('确认后整柜只提交一次生成命令')
expect(getContainerInboundPlanPreview).not.toHaveBeenCalled()
expect(convertLastMileContainerToInboundPlans).not.toHaveBeenCalled()
})
@@ -261,14 +289,22 @@ describe('LastMileOrdersView pending order and container conversion', () => {
const wrapper = await openPending()
await openContainerDialog(wrapper)
await chooseContainer(wrapper)
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认按柜生成')) as HTMLButtonElement
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认生成')) as HTMLButtonElement
expect(confirm.disabled).toBe(false)
confirm.click()
await flushPromises()
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledTimes(1)
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledWith({ sourceContainerBookingId: bookingId })
expect(getContainerInboundPlanPreview).toHaveBeenCalledTimes(2)
expect(getContainerInboundPlanPreview).toHaveBeenNthCalledWith(1, bookingId)
expect(getContainerInboundPlanPreview).toHaveBeenNthCalledWith(2, bookingId)
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledWith({
sourceContainerBookingId: bookingId,
snapshotVersion: 9,
snapshotHash: 'a'.repeat(64),
}, expect.stringContaining('container-inbound-plan-'))
expect(createManualInboundPlan).not.toHaveBeenCalled()
expect(document.body.textContent).toContain('已按柜生成 2 个独立入库计划')
expect(document.body.textContent).toContain('新建 2 个 / 已有 0 个')
expect(document.body.textContent).toContain('PLT-001')
expect(listLastMileOrders).toHaveBeenCalledTimes(3)
})
@@ -279,7 +315,7 @@ describe('LastMileOrdersView pending order and container conversion', () => {
const wrapper = await openPending()
await openContainerDialog(wrapper)
await chooseContainer(wrapper)
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认按柜生成')) as HTMLButtonElement
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认生成')) as HTMLButtonElement
confirm.click()
await flushPromises()
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledTimes(1)
@@ -288,6 +324,51 @@ describe('LastMileOrdersView pending order and container conversion', () => {
expect(wrapper.text()).toContain(`trace-${code}`)
})
it('keeps the latest cabinet preview when an older request resolves last', async () => {
let resolveFirst!: (value: typeof preview) => void
let resolveSecond!: (value: typeof preview) => void
vi.mocked(getContainerInboundPlanPreview).mockImplementation((id) => new Promise((resolve) => {
if (id === bookingId) resolveFirst = resolve
else resolveSecond = resolve
}) as never)
const wrapper = await openPending()
await openContainerDialog(wrapper)
const selects = wrapper.findAllComponents({ name: 'ElSelect' })
const containerSelect = selects[selects.length - 1]
containerSelect.vm.$emit('update:modelValue', bookingId)
containerSelect.vm.$emit('change', bookingId)
await wrapper.vm.$nextTick()
containerSelect.vm.$emit('update:modelValue', '2092873463568597002')
containerSelect.vm.$emit('change', '2092873463568597002')
await wrapper.vm.$nextTick()
resolveSecond({ ...preview, sourceContainerBookingId: '2092873463568597002', containerNo: 'CONT-NEW', pendingPalletCount: 2 })
await flushPromises()
expect(document.body.querySelector('.container-preview__identity')?.textContent).toContain('CONT-NEW')
resolveFirst(preview)
await flushPromises()
expect(document.body.querySelector('.container-preview__identity')?.textContent).toContain('CONT-NEW')
})
it('reuses the same idempotency key when an uncertain confirmation is retried', async () => {
vi.mocked(convertLastMileContainerToInboundPlans)
.mockRejectedValueOnce(new Error('network uncertain'))
.mockResolvedValueOnce([{ planNo: 'LIP-RETRY', pallets: [{ id: 'PALLET-R', palletNo: 'PLT-RETRY' }] }] as never)
const wrapper = await openPending()
await openContainerDialog(wrapper)
await chooseContainer(wrapper)
const findConfirm = () => [...document.body.querySelectorAll('button')]
.find((item) => item.textContent?.includes('确认生成')) as HTMLButtonElement
findConfirm().click()
await flushPromises()
findConfirm().click()
await flushPromises()
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledTimes(2)
expect(vi.mocked(convertLastMileContainerToInboundPlans).mock.calls[0][1]).toBe(
vi.mocked(convertLastMileContainerToInboundPlans).mock.calls[1][1],
)
expect(document.body.textContent).toContain('PLT-RETRY')
})
it('renders all cabinets as an arrival-ordered workbench with the remaining inbound-code count', async () => {
const nullArrival = { ...summary, sourceContainerBookingId: '2092873463568597002', containerNo: 'CONT-NULL', plannedArrivalAt: null, planCount: 0, pendingPlanCount: 15 }
vi.mocked(listContainerPlanProgress).mockResolvedValueOnce({ records: [summary, nullArrival], page: 1, size: 10, total: 2, pages: 1 })
@@ -369,12 +450,16 @@ describe('LastMileOrdersView pending order and container conversion', () => {
await flushPromises()
await button(wrapper, '生成入库计划')!.trigger('click')
await flushPromises()
expect(document.body.textContent).toContain('确认后将为该柜剩余 5 个入仓码分别生成独立入库计划')
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认按柜生成')) as HTMLButtonElement
expect(document.body.textContent).toContain('预览已就绪,本次将新增 5 个计划、5 个托盘码')
const confirm = [...document.body.querySelectorAll('button')].find((item) => item.textContent?.includes('确认生成')) as HTMLButtonElement
expect(confirm.disabled).toBe(false)
confirm.click()
await flushPromises()
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledWith({ sourceContainerBookingId: bookingId })
expect(convertLastMileContainerToInboundPlans).toHaveBeenCalledWith({
sourceContainerBookingId: bookingId,
snapshotVersion: 9,
snapshotHash: 'a'.repeat(64),
}, expect.stringContaining('container-inbound-plan-'))
expect(getContainerPlanProgress).not.toHaveBeenCalled()
})