fix(admin): validate co-load routes by business code
This commit is contained in:
@@ -71,11 +71,11 @@ describe('unified orders API contract', () => {
|
|||||||
|
|
||||||
it('normalizes C02 row IDs and keeps inboundId for measurement routing', async () => {
|
it('normalizes C02 row IDs and keeps inboundId for measurement routing', async () => {
|
||||||
vi.mocked(http.get).mockResolvedValue({ data: {
|
vi.mocked(http.get).mockResolvedValue({ data: {
|
||||||
records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3, mainStatus: 'WAIT_MEASURE', locationCodes: 'A0-01, B2-03', actualGrossWeightKg: '12.5', actualVolumeCbm: '0.45', totalPalletCount: '3', totalNetWeightKg: '11.2', chargeableVolumeCbm: '0.5', totalQuantity: '48', skuNos: ['SKU-1', ''], requestedPickupAt: '2026-08-18T09:30:00', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: 90001, parentOrderId: null, splitSequence: null, childCount: '2' }, measurement: null, preload: null, loading: null, delivery: null, transport: null }],
|
records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3, mainStatus: 'WAIT_MEASURE', channelCode: ' NY_EXX ', channelName: '纽约渠道', locationCodes: 'A0-01, B2-03', actualGrossWeightKg: '12.5', actualVolumeCbm: '0.45', totalPalletCount: '3', totalNetWeightKg: '11.2', chargeableVolumeCbm: '0.5', totalQuantity: '48', skuNos: ['SKU-1', ''], requestedPickupAt: '2026-08-18T09:30:00', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: 90001, parentOrderId: null, splitSequence: null, childCount: '2' }, measurement: null, preload: null, loading: null, delivery: null, transport: null }],
|
||||||
page: 1, size: 20, total: 1, pages: 1,
|
page: 1, size: 20, total: 1, pages: 1,
|
||||||
} })
|
} })
|
||||||
await expect(listPlatformUnifiedOrders({ page: 1, size: 20 })).resolves.toMatchObject({
|
await expect(listPlatformUnifiedOrders({ page: 1, size: 20 })).resolves.toMatchObject({
|
||||||
records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3, locationCodes: 'A0-01, B2-03', actualGrossWeightKg: 12.5, actualVolumeCbm: 0.45, totalPalletCount: 3, totalNetWeightKg: 11.2, chargeableVolumeCbm: 0.5, totalQuantity: 48, skuNos: ['SKU-1'], requestedPickupAt: '2026-08-18T09:30:00', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: '90001', childCount: 2 } }],
|
records: [{ orderId: '90001', tenantId: '208', inboundId: '77', inboundVersion: 3, channelCode: 'NY_EXX', locationCodes: 'A0-01, B2-03', actualGrossWeightKg: 12.5, actualVolumeCbm: 0.45, totalPalletCount: 3, totalNetWeightKg: 11.2, chargeableVolumeCbm: 0.5, totalQuantity: 48, skuNos: ['SKU-1'], requestedPickupAt: '2026-08-18T09:30:00', customerRemark: '客户备注', latestInternalNote: '最新内部备注', relation: { role: 'PARENT', rootOrderId: '90001', childCount: 2 } }],
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export interface UnifiedOrderResource {
|
|||||||
internalOrderStatus: string | null
|
internalOrderStatus: string | null
|
||||||
customerId: number | string | null
|
customerId: number | string | null
|
||||||
customerName: string | null
|
customerName: string | null
|
||||||
|
channelCode: string | null
|
||||||
channelName: string | null
|
channelName: string | null
|
||||||
receivingWarehouseId: number | string | null
|
receivingWarehouseId: number | string | null
|
||||||
receivingWarehouseName: string | null
|
receivingWarehouseName: string | null
|
||||||
@@ -328,6 +329,11 @@ function id(value: number | string | null | undefined) {
|
|||||||
return value === null || value === undefined || value === '' ? null : String(value)
|
return value === null || value === undefined || value === '' ? null : String(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function optionalText(value: string | null | undefined) {
|
||||||
|
const normalized = value?.trim()
|
||||||
|
return normalized || null
|
||||||
|
}
|
||||||
|
|
||||||
function queryParams(query: UnifiedOrderQuery) {
|
function queryParams(query: UnifiedOrderQuery) {
|
||||||
return Object.fromEntries(Object.entries(query).filter(([, value]) => value !== undefined && value !== ''))
|
return Object.fromEntries(Object.entries(query).filter(([, value]) => value !== undefined && value !== ''))
|
||||||
}
|
}
|
||||||
@@ -338,6 +344,7 @@ function normalizeRow(raw: UnifiedOrderResource): UnifiedOrderResource {
|
|||||||
orderId: String(raw.orderId),
|
orderId: String(raw.orderId),
|
||||||
tenantId: String(raw.tenantId),
|
tenantId: String(raw.tenantId),
|
||||||
customerId: id(raw.customerId),
|
customerId: id(raw.customerId),
|
||||||
|
channelCode: optionalText(raw.channelCode),
|
||||||
receivingWarehouseId: id(raw.receivingWarehouseId),
|
receivingWarehouseId: id(raw.receivingWarehouseId),
|
||||||
destinationWarehouseId: id(raw.destinationWarehouseId),
|
destinationWarehouseId: id(raw.destinationWarehouseId),
|
||||||
inboundId: id(raw.inboundId),
|
inboundId: id(raw.inboundId),
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ vi.mock('@/modules/unified-orders/api/coLoadGroups', () => ({
|
|||||||
const rows = ['5001', '5002'].map((orderId, index) => ({
|
const rows = ['5001', '5002'].map((orderId, index) => ({
|
||||||
orderId, tenantId: `600${index}`, orderNo: `ORD-${index + 1}`, mainStatus: 'WAIT_SPLIT' as const, nextAction: null,
|
orderId, tenantId: `600${index}`, orderNo: `ORD-${index + 1}`, mainStatus: 'WAIT_SPLIT' as const, nextAction: null,
|
||||||
relation: null, internalOrderStatus: 'WAIT_SPLIT', customerId: `600${index}`, customerName: `客户${index + 1}`,
|
relation: null, internalOrderStatus: 'WAIT_SPLIT', customerId: `600${index}`, customerName: `客户${index + 1}`,
|
||||||
channelName: 'NY-EXX', receivingWarehouseId: '8', receivingWarehouseName: '义乌仓', destinationWarehouseId: '9', destinationWarehouseName: 'ONT8',
|
channelCode: 'NY_EXX', channelName: '纽约渠道', receivingWarehouseId: '8', receivingWarehouseName: '义乌仓', destinationWarehouseId: '9', destinationWarehouseName: 'ONT8',
|
||||||
inboundId: `700${index}`, inboundVersion: 1, inboundCode: `IN-${index + 1}`, expectedShipDate: null, totalPackageCount: 5,
|
inboundId: `700${index}`, inboundVersion: 1, inboundCode: `IN-${index + 1}`, expectedShipDate: null, totalPackageCount: 5,
|
||||||
locationCodes: null, actualGrossWeightKg: 10, actualVolumeCbm: 1, measurement: null, preload: null, loading: null, delivery: null, transport: null,
|
locationCodes: null, actualGrossWeightKg: 10, actualVolumeCbm: 1, measurement: null, preload: null, loading: null, delivery: null, transport: null,
|
||||||
createdAt: null, updatedAt: null, expectedArrivalAt: null, feeReviewStatus: null, requestedPickupAt: null, totalPalletCount: null,
|
createdAt: null, updatedAt: null, expectedArrivalAt: null, feeReviewStatus: null, requestedPickupAt: null, totalPalletCount: null,
|
||||||
@@ -68,12 +68,14 @@ describe('UnifiedOrdersView co-load group behavior', () => {
|
|||||||
expect(wrapper.text()).toContain('设为同柜不可拆组(2)')
|
expect(wrapper.text()).toContain('设为同柜不可拆组(2)')
|
||||||
expect(wrapper.text()).toContain('成员路线不一致')
|
expect(wrapper.text()).toContain('成员路线不一致')
|
||||||
expect(wrapper.text()).toContain('A0409 request-coload trace-coload')
|
expect(wrapper.text()).toContain('A0409 request-coload trace-coload')
|
||||||
|
expect(wrapper.text()).not.toContain('5001')
|
||||||
|
expect(wrapper.text()).not.toContain('5002')
|
||||||
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(1)
|
expect(listPlatformUnifiedOrders).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
['平台收货仓不同', [{ ...rows[0], receivingWarehouseId: '8' }, { ...rows[1], receivingWarehouseId: '9' }], '所选订单的平台收货仓不同,不能设为同柜组'],
|
['平台收货仓不同', [{ ...rows[0], receivingWarehouseId: '8', receivingWarehouseName: '义乌仓' }, { ...rows[1], receivingWarehouseId: '9', receivingWarehouseName: '东莞仓' }], '所选订单的平台收货仓不同:IN-1(仓库:义乌仓;渠道:纽约渠道)、IN-2(仓库:东莞仓;渠道:纽约渠道),不能设为同柜组'],
|
||||||
['渠道不同', [{ ...rows[0], channelName: 'NY-EXX' }, { ...rows[1], channelName: 'LA-SEA' }], '所选订单的渠道不同,不能设为同柜组'],
|
['渠道不同', [{ ...rows[0], channelCode: 'NY_EXX', channelName: '纽约渠道' }, { ...rows[1], channelCode: 'LA_SEA', channelName: '洛杉矶渠道' }], '所选订单的渠道不同:IN-1(仓库:义乌仓;渠道:纽约渠道)、IN-2(仓库:义乌仓;渠道:洛杉矶渠道),不能设为同柜组'],
|
||||||
['已有同柜组', [rows[0], { ...rows[1], coLoadGroup: { status: 'ACTIVE', memberCount: 2, memberInboundCodes: ['IN-2', 'IN-3'] } }], '所选订单中已有同柜组,请先解除原同柜组'],
|
['已有同柜组', [rows[0], { ...rows[1], coLoadGroup: { status: 'ACTIVE', memberCount: 2, memberInboundCodes: ['IN-2', 'IN-3'] } }], '所选订单中已有同柜组,请先解除原同柜组'],
|
||||||
])('blocks create immediately when %s', async (_case, selectedRows, reason) => {
|
])('blocks create immediately when %s', async (_case, selectedRows, reason) => {
|
||||||
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: selectedRows, page: 1, size: 20, total: 2, pages: 1 })
|
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: selectedRows, page: 1, size: 20, total: 2, pages: 1 })
|
||||||
@@ -87,6 +89,39 @@ describe('UnifiedOrdersView co-load group behavior', () => {
|
|||||||
expect(createPlatformCoLoadGroup).not.toHaveBeenCalled()
|
expect(createPlatformCoLoadGroup).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['平台收货仓', [{ ...rows[0], receivingWarehouseId: null, receivingWarehouseName: null }, rows[1]], '以下入仓码缺少平台收货仓:IN-1(仓库:未配置仓库;渠道:纽约渠道),请刷新后重试'],
|
||||||
|
['渠道', [{ ...rows[0], channelCode: null, channelName: null }, rows[1]], '以下入仓码缺少渠道:IN-1(仓库:义乌仓;渠道:未配置渠道),请刷新后重试'],
|
||||||
|
])('lists the affected inbound code and display names when %s is missing', async (_case, selectedRows, reason) => {
|
||||||
|
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: selectedRows, page: 1, size: 20, total: 2, pages: 1 })
|
||||||
|
const wrapper = mountView()
|
||||||
|
await flushPromises()
|
||||||
|
await wrapper.get('[data-testid="select"]').trigger('click')
|
||||||
|
expect(wrapper.text()).toContain(reason)
|
||||||
|
expect(createPlatformCoLoadGroup).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('allows different display names for the same channel code', async () => {
|
||||||
|
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: [rows[0], { ...rows[1], channelName: '纽约渠道(新名称)' }], page: 1, size: 20, total: 2, pages: 1 })
|
||||||
|
vi.mocked(createPlatformCoLoadGroup).mockResolvedValue({ groupId: '9001', groupNo: 'CLG-1', status: 'ACTIVE', warehouseId: '8', channelCode: 'NY_EXX', lockedPlanId: null, version: 1, members: [], fullPackageCount: 10, createdAt: '', updatedAt: '' })
|
||||||
|
const wrapper = mountView()
|
||||||
|
await flushPromises()
|
||||||
|
await wrapper.get('[data-testid="select"]').trigger('click')
|
||||||
|
await wrapper.findAll('button').find((button) => button.text().includes('设为同柜不可拆组'))!.trigger('click')
|
||||||
|
await wrapper.get('[data-testid="dialog-confirm"]').trigger('click')
|
||||||
|
await flushPromises()
|
||||||
|
expect(createPlatformCoLoadGroup).toHaveBeenCalledWith(['5001', '5002'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('blocks identical display names when channel codes differ', async () => {
|
||||||
|
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: [rows[0], { ...rows[1], channelCode: 'LA_SEA', channelName: '纽约渠道' }], page: 1, size: 20, total: 2, pages: 1 })
|
||||||
|
const wrapper = mountView()
|
||||||
|
await flushPromises()
|
||||||
|
await wrapper.get('[data-testid="select"]').trigger('click')
|
||||||
|
expect(wrapper.text()).toContain('所选订单的渠道不同:IN-1(仓库:义乌仓;渠道:纽约渠道)、IN-2(仓库:义乌仓;渠道:纽约渠道),不能设为同柜组')
|
||||||
|
expect(createPlatformCoLoadGroup).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
it('uses inbound codes as the only row identifier and shows an explicit pending state', async () => {
|
it('uses inbound codes as the only row identifier and shows an explicit pending state', async () => {
|
||||||
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: [rows[0], { ...rows[1], inboundCode: null }], page: 1, size: 20, total: 2, pages: 1 })
|
vi.mocked(listPlatformUnifiedOrders).mockResolvedValue({ records: [rows[0], { ...rows[1], inboundCode: null }], page: 1, size: 20, total: 2, pages: 1 })
|
||||||
const wrapper = mountView()
|
const wrapper = mountView()
|
||||||
|
|||||||
@@ -175,8 +175,10 @@ describe('UnifiedOrdersView contract', () => {
|
|||||||
expect(createSource).not.toContain('relation')
|
expect(createSource).not.toContain('relation')
|
||||||
expect(createSource).toContain('createPlatformCoLoadGroup(selectedOrders.value.map((row) => String(row.orderId)))')
|
expect(createSource).toContain('createPlatformCoLoadGroup(selectedOrders.value.map((row) => String(row.orderId)))')
|
||||||
expect(source).toContain(':disabled="loading || !canCreateCoLoadGroup"')
|
expect(source).toContain(':disabled="loading || !canCreateCoLoadGroup"')
|
||||||
expect(source).toContain('所选订单的平台收货仓不同,不能设为同柜组')
|
expect(source).toContain('所选订单的平台收货仓不同:')
|
||||||
expect(source).toContain('所选订单的渠道不同,不能设为同柜组')
|
expect(source).toContain('所选订单的渠道不同:')
|
||||||
|
expect(source).toContain('row.channelCode!.trim()')
|
||||||
|
expect(source).not.toContain('String(row.channelName || \'\').trim()')
|
||||||
expect(source).toContain('所选订单中已有同柜组,请先解除原同柜组')
|
expect(source).toContain('所选订单中已有同柜组,请先解除原同柜组')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -815,15 +815,28 @@ function handleOrderSelectionChange(rows: UnifiedOrderResource[]) {
|
|||||||
selectedApprovalOrders.value = rows.filter(canApprove)
|
selectedApprovalOrders.value = rows.filter(canApprove)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function coLoadSelectionMemberLabel(row: UnifiedOrderResource) {
|
||||||
|
const inboundCode = row.inboundCode?.trim() || '待生成入仓码'
|
||||||
|
const warehouseName = row.receivingWarehouseName?.trim() || '未配置仓库'
|
||||||
|
const channelName = row.channelName?.trim() || '未配置渠道'
|
||||||
|
return `${inboundCode}(仓库:${warehouseName};渠道:${channelName})`
|
||||||
|
}
|
||||||
|
|
||||||
|
function coLoadSelectionMembersLabel(rows: UnifiedOrderResource[]) {
|
||||||
|
return rows.map(coLoadSelectionMemberLabel).join('、')
|
||||||
|
}
|
||||||
|
|
||||||
const coLoadSelectionProblem = computed(() => {
|
const coLoadSelectionProblem = computed(() => {
|
||||||
if (selectedOrders.value.length < 2) return null
|
if (selectedOrders.value.length < 2) return null
|
||||||
if (selectedOrders.value.some((row) => row.coLoadGroup)) return '所选订单中已有同柜组,请先解除原同柜组'
|
if (selectedOrders.value.some((row) => row.coLoadGroup)) return '所选订单中已有同柜组,请先解除原同柜组'
|
||||||
const warehouseIds = selectedOrders.value.map((row) => String(row.receivingWarehouseId || '').trim())
|
const missingWarehouseRows = selectedOrders.value.filter((row) => !String(row.receivingWarehouseId || '').trim())
|
||||||
if (warehouseIds.some((value) => !value)) return '所选订单缺少平台收货仓,请刷新后重试'
|
if (missingWarehouseRows.length) return `以下入仓码缺少平台收货仓:${coLoadSelectionMembersLabel(missingWarehouseRows)},请刷新后重试`
|
||||||
if (new Set(warehouseIds).size > 1) return '所选订单的平台收货仓不同,不能设为同柜组'
|
const warehouseIds = selectedOrders.value.map((row) => String(row.receivingWarehouseId).trim())
|
||||||
const channels = selectedOrders.value.map((row) => String(row.channelName || '').trim())
|
if (new Set(warehouseIds).size > 1) return `所选订单的平台收货仓不同:${coLoadSelectionMembersLabel(selectedOrders.value)},不能设为同柜组`
|
||||||
if (channels.some((value) => !value)) return '所选订单缺少渠道,请刷新后重试'
|
const missingChannelRows = selectedOrders.value.filter((row) => !row.channelCode?.trim())
|
||||||
if (new Set(channels).size > 1) return '所选订单的渠道不同,不能设为同柜组'
|
if (missingChannelRows.length) return `以下入仓码缺少渠道:${coLoadSelectionMembersLabel(missingChannelRows)},请刷新后重试`
|
||||||
|
const channelCodes = selectedOrders.value.map((row) => row.channelCode!.trim())
|
||||||
|
if (new Set(channelCodes).size > 1) return `所选订单的渠道不同:${coLoadSelectionMembersLabel(selectedOrders.value)},不能设为同柜组`
|
||||||
return null
|
return null
|
||||||
})
|
})
|
||||||
const canCreateCoLoadGroup = computed(() => selectedOrders.value.length >= 2 && !coLoadSelectionProblem.value)
|
const canCreateCoLoadGroup = computed(() => selectedOrders.value.length >= 2 && !coLoadSelectionProblem.value)
|
||||||
|
|||||||
Reference in New Issue
Block a user