fix(admin): prevent duplicate actual carrier nodes

This commit is contained in:
2026-08-26 15:41:08 +08:00
parent b8948fdff0
commit b907881740
3 changed files with 81 additions and 3 deletions
@@ -132,6 +132,47 @@ describe('CarrierScheduleQueryPanel loading states', () => {
expect(wrapper.text()).toContain('人工补录标准节点')
})
it('shows only the latest duplicate actual node in the primary trajectory and keeps the older fact expandable', async () => {
const task = { ...queryView.query, latestActualNode: 'PICKED_UP' as const, latestActualAt: '2026-09-24T10:00:00', resultPortScheduleId: null }
const older = { id: 'pickup-1', providerCode: 'MANUAL', providerEventId: 'pickup-old', providerNodeCode: 'PICKED_UP', standardNode: 'PICKED_UP' as const, nodeSequence: 90, observedAt: '2026-09-09T10:05:00', estimatedAt: null, actualAt: '2026-09-09T10:00:00', locationCode: 'USLAX', locationName: 'Los Angeles', sourceReference: '人工提柜-旧', rawPayloadHash: '1'.repeat(64), createdAt: '2026-09-09T10:06:00' }
const latest = { ...older, id: 'pickup-2', providerEventId: 'pickup-latest', observedAt: '2026-09-24T10:05:00', actualAt: '2026-09-24T10:00:00', sourceReference: '人工提柜-最新', rawPayloadHash: '2'.repeat(64), createdAt: '2026-09-24T10:06:00' }
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [{ ...queryView, query: task, expired: false }], page: 1, size: 200, total: 1, pages: 1 })
vi.mocked(getCarrierScheduleQuery).mockResolvedValue({ query: { ...queryView, query: task, expired: false }, nodes: [older, latest], exceptions: [], histories: [] })
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2' }, global: { plugins: [ElementPlus, pinia] } })
await flushPromises()
const primaryPickupRows = wrapper.findAll('.fulfillment-timeline li').filter((row) => row.text().includes('已提柜'))
expect(primaryPickupRows).toHaveLength(1)
expect(primaryPickupRows[0]!.text()).toContain('2026-09-24 10:00')
expect(primaryPickupRows[0]!.text()).not.toContain('2026-09-09 10:00')
expect(wrapper.find('.duplicate-node-history').text()).toContain('历史重复实际节点(1)')
expect(wrapper.find('.duplicate-node-history').text()).toContain('2026-09-09 10:00')
expect(wrapper.find('.duplicate-node-history').text()).toContain('人工提柜-旧')
})
it('disables same and earlier actual nodes and rejects an invalid actual submission defensively', async () => {
const task = { ...queryView.query, latestActualNode: 'PICKED_UP' as const, latestActualAt: '2026-09-24T10:00:00', resultPortScheduleId: null }
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [{ ...queryView, query: task, expired: false }], page: 1, size: 200, total: 1, pages: 1 })
vi.mocked(getCarrierScheduleQuery).mockResolvedValue({ query: { ...queryView, query: task, expired: false }, nodes: [], exceptions: [], histories: [] })
const wrapper = mount(CarrierScheduleQueryPanel, { props: { oceanBookingId: '2' }, global: { plugins: [ElementPlus, pinia] } })
await flushPromises()
await wrapper.findAll('button').find((button) => button.text().includes('人工补录节点'))!.trigger('click')
const pickers = wrapper.findAllComponents({ name: 'ElDatePicker' })
pickers[1]!.vm.$emit('update:modelValue', '2026-09-25T10:00:00')
await flushPromises()
const options = wrapper.findAllComponents({ name: 'ElOption' })
expect(options.find((option) => option.props('value') === 'DEPARTED_WAREHOUSE')!.props('disabled')).toBe(true)
expect(options.find((option) => option.props('value') === 'PICKED_UP')!.props('disabled')).toBe(true)
expect(options.find((option) => option.props('value') === 'EMPTY_RETURNED')!.props('disabled')).toBe(false)
wrapper.findComponent({ name: 'ElSelect' }).vm.$emit('update:modelValue', 'PICKED_UP')
wrapper.findAllComponents({ name: 'ElInput' }).find((input) => input.props('type') === 'textarea')!.vm.$emit('update:modelValue', '重复提柜事实')
await wrapper.findComponent({ name: 'AppDialog' }).vm.$emit('confirm'); await flushPromises()
expect(wrapper.text()).toContain('实际补录只能选择严格晚于当前实际进度的节点')
expect(appendCarrierScheduleManualNode).not.toHaveBeenCalled()
})
it('hides the manual action without the frozen permission while retaining fulfillment facts', async () => {
useAuthStore().permissions = []
vi.mocked(listCarrierScheduleQueries).mockResolvedValue({ records: [{ ...queryView, query: { ...queryView.query, resultPortScheduleId: null }, expired: false }], page: 1, size: 200, total: 1, pages: 1 })