75 lines
4.7 KiB
JavaScript
75 lines
4.7 KiB
JavaScript
// Shared declaration / measurement arithmetic. Pure functions; no HTTP or master-data writes.
|
||||
|
|
export const CARTON_PROFILE = Object.freeze({ lengthCm: 40, widthCm: 30, heightCm: 30, weightKg: 7.5 })
|
|||
|
|
|
|||
|
|
export function cartonTotals(packageCount) {
|
|||
|
|
if (!Number.isInteger(packageCount) || packageCount < 1 || packageCount > 2) throw new Error('生产冒烟仅允许 1~2 箱')
|
|||
|
|
return { packageCount, lengthCm: 40, widthCm: 30, heightCm: 30,
|
|||
|
|
grossWeightKg: packageCount * 7.5, volumeCbm: packageCount * 36 / 1000 }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function normalizeOrderInput(input) {
|
|||
|
|
if (!Array.isArray(input?.payload?.cargoes) || input.payload.cargoes.length !== 1) throw new Error('生产冒烟仅支持一个 SKU')
|
|||
|
|
const cargo = input.payload.cargoes[0]
|
|||
|
|
const totals = cartonTotals(cargo.packageCount)
|
|||
|
|
if (cargo.netWeightKg != null && (!Number.isFinite(Number(cargo.netWeightKg))
|
|||
|
|
|| Number(cargo.netWeightKg) <= 0 || Number(cargo.netWeightKg) > totals.grossWeightKg)) {
|
|||
|
|
throw new Error('申报商品净重无效或超过含箱货重;不能据此推算商品净重')
|
|||
|
|
}
|
|||
|
|
return { ...input, payload: { ...input.payload, cargoes: [{ ...cargo, ...totals }] } }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function scaled(value, places, label, allowZero = false) {
|
|||
|
|
const text = String(value)
|
|||
|
|
if (!/^\d+(?:\.\d+)?$/.test(text)) throw new Error(`${label} 无效`)
|
|||
|
|
const [whole, fraction = ''] = text.split('.')
|
|||
|
|
if (fraction.slice(places).replace(/0/g, '')) throw new Error(`${label} 超出允许精度`)
|
|||
|
|
const result = BigInt(whole) * 10n ** BigInt(places) + BigInt(fraction.slice(0, places).padEnd(places, '0'))
|
|||
|
|
if ((!allowZero && result === 0n) || result > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error(`${label} 超出安全范围`)
|
|||
|
|
return result
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function measurementPlan({ packageCount, warehouseId, pallets, allowedCodes }) {
|
|||
|
|
const totals = cartonTotals(packageCount)
|
|||
|
|
if (!Array.isArray(allowedCodes) || !allowedCodes.length || new Set(allowedCodes).size !== allowedCodes.length
|
|||
|
|
|| allowedCodes.some(code => typeof code !== 'string' || !code.trim())) throw new Error('需要明确允许复用的既有托盘编码')
|
|||
|
|
if (typeof warehouseId !== 'string' || !/^[1-9]\d*$/.test(warehouseId)) throw new Error('仓库 ID 无效')
|
|||
|
|
const candidates = pallets.filter(p => p.warehouseId === warehouseId && allowedCodes.includes(p.palletCode))
|
|||
|
|
// Resolve every allowed code exactly; do not silently choose another warehouse or create a replacement.
|
|||
|
|
for (const code of allowedCodes) {
|
|||
|
|
if (candidates.filter(p => p.palletCode === code).length !== 1) throw new Error('允许的托盘缺失或编码重复')
|
|||
|
|
}
|
|||
|
|
const pallet = candidates.find(p => p.palletCode === allowedCodes[0])
|
|||
|
|
if (typeof pallet.id !== 'string' || !/^[1-9]\d*$/.test(pallet.id)) throw new Error('托盘 ID 无效')
|
|||
|
|
const capacity = scaled(pallet.capacityCbm, 6, '托容量')
|
|||
|
|
const tare = scaled(pallet.weightKg, 3, '托盘皮重', true)
|
|||
|
|
const cartonVolume = 36000n // cubic centimetres, i.e. 0.036 m³
|
|||
|
|
const totalVolume = cartonVolume * BigInt(packageCount)
|
|||
|
|
const count = Number((totalVolume + capacity - 1n) / capacity)
|
|||
|
|
const cartonsPerPallet = Number(capacity / cartonVolume)
|
|||
|
|
if (!cartonsPerPallet || count > packageCount || cartonsPerPallet * count < packageCount) {
|
|||
|
|
throw new Error('按体积取整无法容纳完整箱,需人工确认托盘容量或装托规则')
|
|||
|
|
}
|
|||
|
|
let remaining = packageCount
|
|||
|
|
const weights = Array.from({ length: count }, (_, index) => {
|
|||
|
|
const cartons = Math.min(cartonsPerPallet, remaining - (count - index - 1))
|
|||
|
|
remaining -= cartons
|
|||
|
|
const netGrams = BigInt(cartons) * 7500n
|
|||
|
|
if (netGrams + tare > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error('连托毛重超出安全范围')
|
|||
|
|
return { sequence: index + 1, palletId: pallet.id, palletCode: pallet.palletCode,
|
|||
|
|
packageCount: cartons, volumeCbm: cartons * 36 / 1000,
|
|||
|
|
palletTareWeightKg: Number(tare) / 1000, netWeightKg: Number(netGrams) / 1000,
|
|||
|
|
grossWeightKg: Number(netGrams + tare) / 1000 }
|
|||
|
|
})
|
|||
|
|
return { status: 'MEASUREMENT_PLAN_ONLY', fullFlowVerified: false, ...totals,
|
|||
|
|
cargoWeightKg: totals.grossWeightKg, palletCount: count, capacityCbm: Number(capacity) / 1_000_000,
|
|||
|
|
weights, weightGrossTotalKg: weights.reduce((sum, w) => sum + Math.round(w.grossWeightKg * 1000), 0) / 1000,
|
|||
|
|
locationPolicy: 'SERVER_ALLOCATES_EXISTING_OR_A0', measurementWritten: false }
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
export function assertConfirmedWeight(actual, expected) {
|
|||
|
|
if (actual?.recognitionStatus !== 'CONFIRMED' || actual.palletId !== expected.palletId) throw new Error('称重确认身份或状态不符')
|
|||
|
|
for (const key of ['grossWeightKg', 'netWeightKg', 'palletTareWeightKg']) {
|
|||
|
|
if (scaled(actual[key], 3, key, true) !== scaled(expected[key], 3, key, true)) throw new Error(`称重回查 ${key} 不守恒`)
|
|||
|
|
}
|
|||
|
|
}
|