From 80e83a66689c2531068f5652dbe3b3eff6191c87 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Fri, 11 Sep 2026 15:38:28 +0800 Subject: [PATCH] feat: model mixed-SKU dropship cartons --- src/modules/first-leg/api/firstLeg.ts | 1 + src/modules/first-leg/store/firstLegOrders.ts | 3 +- src/modules/first-leg/types/firstLegOrder.ts | 1 + .../first-leg/utils/firstLegOrder.test.mjs | 25 +++++++++++++- src/modules/first-leg/utils/firstLegOrder.ts | 33 +++++++++++++++++-- .../views/FirstLegOrdersView.test.mjs | 2 ++ .../first-leg/views/FirstLegOrdersView.vue | 11 ++++--- 7 files changed, 67 insertions(+), 9 deletions(-) diff --git a/src/modules/first-leg/api/firstLeg.ts b/src/modules/first-leg/api/firstLeg.ts index 5273066..964be03 100644 --- a/src/modules/first-leg/api/firstLeg.ts +++ b/src/modules/first-leg/api/firstLeg.ts @@ -113,6 +113,7 @@ export interface FirstLegOrderCargoPayload { shipmentId?: string referenceId?: string productImageFileId?: number | string + packingGroupNo?: string } export interface ProductImageUploadResult { diff --git a/src/modules/first-leg/store/firstLegOrders.ts b/src/modules/first-leg/store/firstLegOrders.ts index 5fc9e54..75703f4 100644 --- a/src/modules/first-leg/store/firstLegOrders.ts +++ b/src/modules/first-leg/store/firstLegOrders.ts @@ -90,6 +90,7 @@ function savePayload(draft: FirstLegOrderDraft, warehouse: ClientWarehouse, cust customsHsCode: item.customsHsCode || undefined, packageType: item.packageType, packageCount: item.packageCount, + packingGroupNo: draft.fulfillmentType === 'ONE_CLICK_DROPSHIP' ? item.packingGroupNo.trim() || undefined : undefined, grossWeightKg: item.grossWeightKg, volumeCbm: item.volumeCbm, lengthCm: item.lengthCm || undefined, @@ -296,7 +297,7 @@ function mapOrder(order: MerchantFirstLegOrder, warehouses: PlatformReceivingWar productImageName: '', productImagePreviewUrl: '', productImageUploading: false, productImageError: '', customsHsCode: item.customsHsCode ?? '', lengthCm: item.lengthCm ?? 0, widthCm: item.widthCm ?? 0, heightCm: item.heightCm ?? 0, unitsPerPackage: item.unitsPerPackage ?? 1, - pieces: item.totalQuantity ?? item.packageCount, packageCount: item.packageCount, grossWeightKg: item.grossWeightKg, + pieces: item.totalQuantity ?? item.packageCount, packageCount: item.packageCount, packingGroupNo: item.packingGroupNo ?? '', grossWeightKg: item.grossWeightKg, volumeCbm: item.volumeCbm, remark: item.cargoRemark ?? '', packageType: item.packageType, hsCode: item.hsCode ?? '', declaredValue: item.declaredValue ?? 0, currency: item.currency ?? 'USD', dangerousGoods: item.dangerousGoods ?? false, diff --git a/src/modules/first-leg/types/firstLegOrder.ts b/src/modules/first-leg/types/firstLegOrder.ts index 18b24e6..d663a29 100644 --- a/src/modules/first-leg/types/firstLegOrder.ts +++ b/src/modules/first-leg/types/firstLegOrder.ts @@ -26,6 +26,7 @@ export interface FirstLegCargoItem { unitsPerPackage: number pieces: number packageCount: number + packingGroupNo: string grossWeightKg: number volumeCbm: number netWeightKg: number diff --git a/src/modules/first-leg/utils/firstLegOrder.test.mjs b/src/modules/first-leg/utils/firstLegOrder.test.mjs index 65a4c79..d7d6a93 100644 --- a/src/modules/first-leg/utils/firstLegOrder.test.mjs +++ b/src/modules/first-leg/utils/firstLegOrder.test.mjs @@ -54,7 +54,7 @@ function validDraft() { destinationType: 'FBA', amazonWarehouseCode: 'ONT8', shipmentId: 'FBA-001', - items: [{ ...createEmptyCargoItem('cargo-1'), sku: 'SKU-001', upc: '012345678905', cargoNameCn: '家居用品', pieces: 100, grossWeightKg: 1200, volumeCbm: 3.5, shipmentId: 'FBA-001', referenceId: 'REF-001' }], + items: [{ ...createEmptyCargoItem('cargo-1'), sku: 'SKU-001', upc: '012345678905', cargoNameCn: '家居用品', unitsPerPackage: 100, pieces: 100, grossWeightKg: 1200, volumeCbm: 3.5, shipmentId: 'FBA-001', referenceId: 'REF-001' }], } } @@ -94,6 +94,17 @@ test('validates the confirmed minimum submission fields', () => { assert.ok(errors.includes('第 1 条货物毛重必须大于 0')) }) +test('rejects a total quantity that does not equal units per package multiplied by package count', () => { + const draft = validDraft() + draft.items[0].unitsPerPackage = 24 + draft.items[0].packageCount = 5 + draft.items[0].pieces = 119 + + assert.ok(validateFirstLegOrderDraft(draft).includes('第 1 条货物总数量应等于每箱数量 × 箱数(120)')) + draft.items[0].pieces = 120 + assert.ok(!validateFirstLegOrderDraft(draft).some((message) => message.includes('总数量应等于'))) +}) + test('only requires sender for pickup and receiver for a private destination', () => { const publicDraft = validDraft() publicDraft.shipperPartySnapshot = null @@ -169,3 +180,15 @@ test('summarizes cargo values from line items', () => { volumeCbm: 4.75, }) }) + +test('counts one physical carton series for SKU lines in the same mixed-carton group', () => { + const first = { ...validDraft().items[0], packingGroupNo: 'A', packageCount: 10, pieces: 20, unitsPerPackage: 2 } + const second = { ...first, id: 'cargo-2', sku: 'SKU-002', pieces: 30, unitsPerPackage: 3 } + + assert.equal(summarizeFirstLegCargo([first, second]).packageCount, 10) + const draft = { ...validDraft(), fulfillmentType: 'ONE_CLICK_DROPSHIP', items: [first, second] } + assert.ok(!validateFirstLegOrderDraft(draft).some((message) => message.includes('混箱组合'))) + + second.packageCount = 9 + assert.ok(validateFirstLegOrderDraft(draft).includes('混箱组合 A 的箱数、包装类型和箱规必须一致')) +}) diff --git a/src/modules/first-leg/utils/firstLegOrder.ts b/src/modules/first-leg/utils/firstLegOrder.ts index 30709d4..708ce27 100644 --- a/src/modules/first-leg/utils/firstLegOrder.ts +++ b/src/modules/first-leg/utils/firstLegOrder.ts @@ -65,6 +65,7 @@ export function createEmptyCargoItem(id = `cargo-${Date.now()}`): FirstLegCargoI unitsPerPackage: 1, pieces: 1, packageCount: 1, + packingGroupNo: '', grossWeightKg: 0, volumeCbm: 0, netWeightKg: 0, @@ -130,22 +131,48 @@ export function validateFirstLegOrderDraft(draft: FirstLegOrderDraft) { if (!Number.isInteger(item.pieces) || item.pieces <= 0) errors.push(`${line}件数必须为正整数`) if (!Number.isInteger(item.packageCount) || item.packageCount <= 0) errors.push(`${line}箱数必须为正整数`) if (!Number.isInteger(item.unitsPerPackage) || item.unitsPerPackage <= 0) errors.push(`${line}每箱数量必须为正整数`) + if (Number.isInteger(item.unitsPerPackage) && item.unitsPerPackage > 0 + && Number.isInteger(item.packageCount) && item.packageCount > 0 + && Number.isInteger(item.pieces) && item.pieces > 0 + && item.pieces !== item.unitsPerPackage * item.packageCount) { + errors.push(`${line}总数量应等于每箱数量 × 箱数(${item.unitsPerPackage * item.packageCount})`) + } if (item.grossWeightKg <= 0) errors.push(`${line}毛重必须大于 0`) if (item.volumeCbm <= 0) errors.push(`${line}体积必须大于 0`) if (item.woodenProduct && draft.customsInspection !== true) errors.push(`${line}为木制品,“是否商检”必须选择“是”`) }) + if (draft.fulfillmentType === 'ONE_CLICK_DROPSHIP') { + const groups = new Map() + draft.items.forEach((item) => { + const group = item.packingGroupNo.trim() + if (!group) return + const first = groups.get(group) + if (!first) groups.set(group, item) + else if (first.packageCount !== item.packageCount || first.packageType !== item.packageType + || first.lengthCm !== item.lengthCm || first.widthCm !== item.widthCm || first.heightCm !== item.heightCm) { + errors.push(`混箱组合 ${group} 的箱数、包装类型和箱规必须一致`) + } + }) + } + return errors } export function summarizeFirstLegCargo(items: FirstLegCargoItem[]) { + const countedGroups = new Set() return items.reduce( - (summary, item) => ({ + (summary, item) => { + const group = item.packingGroupNo.trim() + const packageCount = group && countedGroups.has(group) ? 0 : item.packageCount + if (group) countedGroups.add(group) + return ({ pieces: summary.pieces + item.pieces, - packageCount: summary.packageCount + item.packageCount, + packageCount: summary.packageCount + packageCount, grossWeightKg: summary.grossWeightKg + item.grossWeightKg, volumeCbm: summary.volumeCbm + item.volumeCbm, - }), + }) + }, { pieces: 0, packageCount: 0, grossWeightKg: 0, volumeCbm: 0 }, ) } diff --git a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs index 5653e00..93336b5 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.test.mjs +++ b/src/modules/first-leg/views/FirstLegOrdersView.test.mjs @@ -430,6 +430,8 @@ test('shows field-level validation and concise guidance for required order input assert.match(source, /prop="consigneePartySnapshot\.address" required/) assert.match(source, /:prop="`items\.\$\{index\}\.cargoNameCn`" required/) assert.match(source, /:prop="`items\.\$\{index\}\.grossWeightKg`" required/) + assert.match(source, /