323 lines
20 KiB
JavaScript
323 lines
20 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import test from 'node:test'
|
|
import { completeDropshipPalletization, verifyMeasurementSubmitReplay,
|
|
verifyMeasurementApprovalReplay } from './inbound-measurement.mjs'
|
|
|
|
const source = readFileSync(new URL('inbound-measurement.mjs', import.meta.url), 'utf8')
|
|
const read = (relativeUrl) => readFileSync(new URL(relativeUrl, import.meta.url), 'utf8')
|
|
const labelTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/InboundCartonLabelServiceTest.java')
|
|
const measurementTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/InboundMeasurementServiceTest.java')
|
|
const cartonTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/CartonMeasurementServiceTest.java')
|
|
const reviewTests = read('../../firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementReviewServiceTest.java')
|
|
|
|
test('measurement acceptance scans the stable box code frozen by first print', () => {
|
|
assert.match(source, /const boxCode = `\$\{inboundCode\}#\$\{originalPackageCount\}-1`/)
|
|
assert.doesNotMatch(source, /#V\d+#/)
|
|
})
|
|
|
|
test('reprint and Mojia scan share the same frozen box code variable', () => {
|
|
assert.match(source, /重复整单打印复用已冻结箱码映射[\s\S]*boxCode=\$\{boxCode\}/)
|
|
assert.match(source, /const firstScanResponse = await multipartRequest[\s\S]*boxCode, \.\.\.firstDimensions/)
|
|
})
|
|
|
|
test('measurement submission confirms weight evidence before submit', () => {
|
|
assert.match(source, /files\/measurements\/\$\{tenantId\}\/\$\{inboundId\}/)
|
|
assert.match(source, /\/weight-results\?\$\{query\(tenantId\)\}/)
|
|
assert.match(source, /\/weight-results\/\$\{created\.id\}\/confirm\?\$\{query\(tenantId\)\}/)
|
|
assert.match(source, /createConfirmedWeightEvidence[\s\S]*\/measurement\/submit/)
|
|
})
|
|
|
|
test('measurement fixture derives pallet resources from confirmed image count', () => {
|
|
assert.match(source, /waitSplitVariant\?\.palletCount \|\| 1/)
|
|
assert.match(source, /palletIds: warehouseResourcesReady\.palletIds/)
|
|
assert.match(source, /confirmedRows\.length === palletIds\.length/)
|
|
assert.doesNotMatch(source, /Math\.ceil\(Number\(totalVolumeCbm\)/)
|
|
assert.doesNotMatch(source, /Math\.ceil\(totalVolume \/ DEFAULT_PALLET_CAPACITY_CBM\)/)
|
|
})
|
|
|
|
test('volume ceiling removes binary floating point noise before scaling', () => {
|
|
assert.match(source, /Number\.EPSILON \* Math\.max\(1, Math\.abs\(value\)\) \* 4/)
|
|
})
|
|
|
|
test('measurement draft preserves server-calculated dimensions and the frozen coefficient mode', () => {
|
|
assert.match(source, /measuredItem\?\.calculatedOuterLengthCm \?\? measuredItem\?\.outerLengthCm/)
|
|
assert.match(source, /outerLengthCm: item\.outerLengthCm \?\? null/)
|
|
assert.match(source, /outerLengthCm: measuredItem\.outerLengthCm \?\? null/)
|
|
assert.match(source, /startsWith\('CUSTOMER_DIMENSION_'\)/)
|
|
assert.match(source, /\? 'ADD' : item\.coefficientMode \?\? null/)
|
|
assert.match(cartonTests, /getCalculationRuleVersion\(\)\)\s*\.isEqualTo\("ACTUAL_DIMENSION_V1"\)/)
|
|
})
|
|
|
|
test('demo package freezes repeated label printing and Mojia dimension replacement', () => {
|
|
assert.match(labelTests, /repeatedAndPartialPrintsReadFrozenMappingWithoutReallocation/)
|
|
assert.match(labelTests, /changedCargoKeepsTheFirstFrozenBoxCodes/)
|
|
assert.match(cartonTests, /repeatedScanReplacesDeviceValuesButPreservesManualCountAndOldImageIsDeleted/)
|
|
assert.match(source, /增量 fixture 形成动态真实独立货物测量明细/)
|
|
})
|
|
|
|
test('demo package covers OCR failure, manual confirmation, aggregation and stale versions', () => {
|
|
assert.match(source, /OCR无有效重量时转人工确认/)
|
|
assert.match(source, /称重旧版本确认被拒绝且不覆盖已确认结果/)
|
|
assert.match(source, /图片张数决定托数且净重汇总已落地/)
|
|
assert.match(measurementTests, /ocrFailureCreatesManualWeightThatCanBeAdjustedAndConfirmed/)
|
|
assert.match(measurementTests, /confirmingFifthPalletSeparatelyAggregatesGrossAndCargoNetWeight/)
|
|
assert.match(measurementTests, /confirmingWithStaleVersionDoesNotTreatRequestAsIdempotent/)
|
|
})
|
|
|
|
test('demo package keeps pallet pages bounded and historical snapshots read-only', () => {
|
|
assert.match(source, /weight-pallet-options[^\n]+page=1&size=20&keyword=/)
|
|
assert.match(source, /records\.length <= 20/)
|
|
assert.match(measurementTests, /detailUsesStoredPalletSnapshotAfterCandidateBecomesUnavailable/)
|
|
})
|
|
|
|
test('opt-in HTTP acceptance verifies whole-ticket slot identity and delete-to-empty semantics', () => {
|
|
assert.match(source, /verifyPalletLocationGuide/)
|
|
assert.match(source, /expectedWeightResultIds\.every/)
|
|
assert.match(source, /asId\(row\.reservationId\) === asId\(slot\.reservationId\)/)
|
|
assert.match(source, /逐张确认后回查整票库位块/)
|
|
assert.match(source, /扩容只追加且既有槽身份不漂移/)
|
|
assert.match(source, /verified\.slots\.length >= previous\.slots\.length && priorSlotsStable/)
|
|
assert.match(source, /\['RESERVED_EMPTY', 'BOUND', 'TRANSFERRED', 'RELEASED'\]/)
|
|
assert.match(source, /deleteSucceeded/)
|
|
assert.match(source, /retainedSlot\?\.slotStatus === 'RESERVED_EMPTY'/)
|
|
assert.match(source, /recreateConfirmedWeightEvidence/)
|
|
assert.match(source, /reboundSlot\?\.slotStatus === 'BOUND'/)
|
|
assert.match(source, /replacementWeightResultId/)
|
|
assert.match(source, /不批量停用普通库位制造容量不足/)
|
|
})
|
|
|
|
test('approval freezes measurement and requires quantity weight and volume conservation', () => {
|
|
assert.match(source, /审核及合托完成后箱码尺寸与图片快照不可再覆盖/)
|
|
assert.match(measurementTests, /approvedMeasurementCannotBeResubmittedOrMutateFrozenTotals/)
|
|
assert.match(reviewTests, /shouldApproveMultipleCargoLinesOnlyWhenAllFrozenTotalsMatch/)
|
|
assert.match(reviewTests, /shouldRejectApprovalWhenVolumeSnapshotDoesNotConserve/)
|
|
assert.match(source, /actualPackageCount[^\n]+originalPackageCount/)
|
|
assert.match(source, /actualGrossWeightKg[^\n]+weightEvidence\.grossWeightKg/)
|
|
assert.match(source, /actualNetWeightKg[^\n]+weightEvidence\.netWeightKg/)
|
|
assert.match(source, /actualVolumeCbm[^\n]+totalVolume/)
|
|
assert.match(source, /volumeWeightKg[^\n]+volumeWeight/)
|
|
})
|
|
|
|
test('dropship measurement completes the formal palletization workflow before smart loading', () => {
|
|
assert.match(source, /pda-dropship\/tasks\/pending/)
|
|
assert.match(source, /pda-dropship\/tasks\/\$\{taskId\}\/claim/)
|
|
assert.match(source, /cartons\/resolve/)
|
|
assert.match(source, /cartons\/count/)
|
|
assert.match(source, /pallets\/bind-pending/)
|
|
assert.match(source, /预生成托盘绑定重放不新增托盘且计数稳定/)
|
|
assert.match(source, /预生成托盘覆盖测量冻结的全部箱数/)
|
|
assert.doesNotMatch(source, /palletHash|palletDate/)
|
|
assert.match(source, /pallets\/\$\{asId\(pallet.palletId\)\}\/seal/)
|
|
assert.match(source, /重复清点操作按operationId幂等返回/)
|
|
assert.doesNotMatch(source, /pallets\/open/)
|
|
assert.doesNotMatch(source, /pallets\/\$\{palletId\}\/scan/)
|
|
assert.match(source, /dropship \? \['WAIT_PALLETIZE'\] : \['WAIT_SPLIT'\]/)
|
|
assert.match(source, /一件代发合托完成后进入智能分柜门禁/)
|
|
})
|
|
|
|
test('confirmed package count reconciles an uncertain transport result before retrying', () => {
|
|
assert.match(source, /async function updateConfirmedPackageCount/)
|
|
assert.match(source, /recoveredFromTransportFailure: true/)
|
|
assert.match(source, /Number\(detail\?\.version\) > Number\(body\.measurementVersion\)/)
|
|
assert.match(source, /await updateConfirmedPackageCount\(context, measurementPath/)
|
|
})
|
|
|
|
async function exerciseSystemPalletScenario(fault = '') {
|
|
const task = { taskId: '81', inboundId: '82', warehouseId: '83', tenantId: '85', generatedPalletCount: 1, palletSetupReady: true, taskStatus: 'CLAIMED',
|
|
totalCartons: 2, countedCartons: 0, boundCartons: 0, unboundCartons: 0, pallets: [{ palletId: '84', palletNo: 'DP-260909-0001', cartonCount: 0, status: 'OPEN', version: 1 }] }
|
|
const seenCounts = new Map()
|
|
const seenCreates = new Set()
|
|
const checks = []
|
|
const response = (data, code = '200') => ({ http: 200, body: { code, data: structuredClone(data) } })
|
|
const context = {
|
|
marker: 'fixture-system-pallet',
|
|
apiSuccess(_module, name, result) {
|
|
assert.equal(result.body.code, '200', name)
|
|
return result.body.data
|
|
},
|
|
assert(_module, name, _response, condition) { checks.push({ name, passed: Boolean(condition) }) },
|
|
async request(method, path, body) {
|
|
if (path.includes('/pending?')) return response([task])
|
|
if (path.endsWith('/claim')) return response({ ...task, pallets: [] })
|
|
if (path === '/api/platform/v1/first-leg/pda-dropship/inbound-orders/82/pallets?accountId=85') return response(task.pallets)
|
|
if (method === 'GET' && path.includes('/tasks/81?')) return response(task)
|
|
if (path.includes('/cartons/resolve?')) return response({ boxCode: new URL(path, 'http://fixture').searchParams.get('boxCode'), canCount: true, skuLines: [{ inboundDetailId: '91', skuCode: 'SKU-1', expectedQuantity: 10 }] })
|
|
if (path.endsWith('/cartons/count')) {
|
|
if (seenCounts.has(body.operationId)) return seenCounts.get(body.operationId)
|
|
task.countedCartons += 1
|
|
const targetPallet = task.pallets.find(pallet => pallet.palletNo === body.palletNo)
|
|
if (body.palletNo) { task.boundCartons += 1; targetPallet.cartonCount += 1; targetPallet.version += 1 }
|
|
else task.unboundCartons += 1
|
|
const result = response({ ...task, carton: { boxCode: body.boxCode, skuLines: [{ inboundDetailId: '91', skuCode: 'SKU-1', expectedQuantity: 10, actualQuantity: 10, discrepancyStatus: 'MATCH' }] }, pallet: body.palletNo ? targetPallet : null })
|
|
seenCounts.set(body.operationId, result)
|
|
return result
|
|
}
|
|
if (path.endsWith('/bind-pending')) {
|
|
assert.deepEqual(Object.keys(body).sort(), ['operationId', 'palletNo', 'warehouseId'])
|
|
if (seenCreates.has(body.operationId)) {
|
|
if (fault === 'duplicate-replay') task.pallets.push({ ...task.pallets[0], palletId: '999', palletNo: 'DP-260909-0999' })
|
|
return response(task)
|
|
}
|
|
seenCreates.add(body.operationId)
|
|
task.pallets[0].cartonCount += task.unboundCartons
|
|
task.boundCartons += task.unboundCartons
|
|
task.unboundCartons = 0
|
|
if (fault === 'empty-pallet') task.pallets.push({ palletId: '998', palletNo: 'DP-260909-0998', cartonCount: 0, status: 'SEALED', version: 1 })
|
|
return response(task)
|
|
}
|
|
if (path.endsWith('/84/seal')) {
|
|
task.taskStatus = 'COMPLETED'
|
|
task.pallets.forEach(pallet => { pallet.status = 'SEALED'; pallet.version += 1 })
|
|
return response(task)
|
|
}
|
|
throw new Error(`Unexpected scenario request: ${method} ${path}`)
|
|
},
|
|
}
|
|
const sealed = await completeDropshipPalletization(context, { inboundId: '82', inboundCode: 'CODE', warehouseId: '83', measurement: { actualPackageCount: 2, expectedPalletCount: 1 } })
|
|
assert.deepEqual(sealed.sealedCartonMarks, ['CODE#2-1', 'CODE#2-2'])
|
|
if (!fault) assert.deepEqual(sealed.sealedSourcePalletNos, ['DP-260909-0001'])
|
|
return checks
|
|
}
|
|
|
|
test('pregenerated pallet scenario covers multi-SKU count, bind replay and sealing', async () => {
|
|
const checks = await exerciseSystemPalletScenario()
|
|
for (const name of ['审核通过时已按预计托数预生成全部DP托盘码', '重复清点没有重复累计箱数', '待组托箱绑定到OPEN托盘且不再游离', '预生成托盘绑定重放不新增托盘且计数稳定', '预生成托盘覆盖测量冻结的全部箱数', '全部箱数封托后入仓单允许进入WAIT_SPLIT']) assert.ok(checks.some(check => check.name === name), name)
|
|
assert.deepEqual(checks.filter(check => !check.passed), [])
|
|
})
|
|
|
|
test('pregenerated pallet regression detects duplicate replay and orphan empty pallets', async () => {
|
|
const replayChecks = await exerciseSystemPalletScenario('duplicate-replay')
|
|
assert.ok(replayChecks.some(check => check.name.includes('预生成托盘绑定重放不新增托盘') && !check.passed))
|
|
const emptyChecks = await exerciseSystemPalletScenario('empty-pallet')
|
|
assert.ok(emptyChecks.some(check => check.name.includes('预生成托盘覆盖测量冻结的全部箱数') && !check.passed))
|
|
})
|
|
|
|
function measurementReplayContext(approved = false, mutate = () => {}, submitCode = 'A0409') {
|
|
const input = { inboundId: '9007199254740993101', tenantId: '9007199254740993102',
|
|
measurementId: '9007199254740993103' }
|
|
const state = {
|
|
measurement: { inboundId: input.inboundId, measurementId: input.measurementId, inboundCode: 'REPLAY-IN',
|
|
measurementStatus: approved ? 'APPROVED' : 'SUBMITTED', measureReviewStatus: approved ? 'APPROVED' : 'PENDING',
|
|
processStatus: approved ? 'WAIT_SPLIT' : 'MEASURE_PENDING', version: 3, inboundVersion: 4,
|
|
actualPackageCount: 4, actualGrossWeightKg: 100, actualVolumeCbm: 1,
|
|
items: [{ id: '9007199254740993104', packageCount: 4, boxCode: 'REPLAY-IN#4-1' }],
|
|
weightResults: [{ id: '9007199254740993105', fileId: '9007199254740993115', grossWeightKg: 100 }],
|
|
palletLocationGuide: { blockId: '9007199254740993106', slots: [{ reservationId: '9007199254740993107' }] } },
|
|
inbound: { summary: { id: input.inboundId, tenantId: input.tenantId, version: 4 },
|
|
statusHistory: [{ id: '9007199254740993108', action: approved ? 'MEASURE_APPROVE' : 'MEASURE_SUBMIT' }] },
|
|
reviews: [{ id: '9007199254740993109', measurementId: input.measurementId, reviewResult: 'APPROVED' }],
|
|
fee: { feeId: '9007199254740993110', feeVersion: 1, inboundId: input.inboundId,
|
|
measurementId: input.measurementId, feeStatus: 'CALCULATED', totalAmount: 77 },
|
|
details: [{ id: '9007199254740993111', sourceFeeId: '9007199254740993110', sourceFeeVersion: 1,
|
|
inboundId: input.inboundId, amount: 77 }],
|
|
}
|
|
const calls = []
|
|
const context = { marker: 'MEASUREMENT-REPLAY', token: 'memory-only-test-token',
|
|
request: async (method, requestPath, body, token, headers) => {
|
|
calls.push({ method, requestPath, body: structuredClone(body), token, headers })
|
|
if (method === 'POST') {
|
|
mutate(state)
|
|
if (requestPath.includes('/measurement/submit?')) return {
|
|
http: 200, body: { code: submitCode }, requestId: 'submit-repeat', traceId: 'trace-submit',
|
|
}
|
|
assert.match(requestPath, /\/measurements\/approve\?/)
|
|
return { http: 200, body: { code: '200', data: {
|
|
initialPricingSucceeded: true, initialFee: structuredClone(state.fee),
|
|
} }, requestId: 'approve-repeat', traceId: 'trace-approve' }
|
|
}
|
|
let data
|
|
if (requestPath.includes('/measurement?')) data = state.measurement
|
|
else if (requestPath.startsWith(`/api/platform/inbounds/${input.inboundId}?`)) data = state.inbound
|
|
else if (requestPath.includes('/measurement-review-history?')) data = {
|
|
records: state.reviews, total: state.reviews.length, page: 1, size: 200,
|
|
}
|
|
else if (requestPath.includes('/fees/latest?')) data = state.fee
|
|
else if (requestPath.includes('/fee-details?')) {
|
|
assert.equal(new URL(requestPath, 'https://example.invalid').searchParams.get('includeHistory'), 'true')
|
|
data = state.details
|
|
} else throw new Error(`unexpected ${method} ${requestPath}`)
|
|
return { http: 200, body: { code: '200', data: structuredClone(data) }, requestId: 'read', traceId: 'trace' }
|
|
},
|
|
apiSuccess: (_module, name, response) => {
|
|
assert.equal(response.http, 200, name); assert.equal(response.body.code, '200', name); return response.body.data
|
|
},
|
|
assert: (_module, name, _response, passed) => assert.ok(passed, name),
|
|
}
|
|
return { input, state, context, calls }
|
|
}
|
|
|
|
test('submitted measurement repeats the original payload, requires conflict and preserves all readable facts', async () => {
|
|
const fixture = measurementReplayContext()
|
|
const payload = { measurementId: fixture.input.measurementId, version: 2 }
|
|
const evidence = await verifyMeasurementSubmitReplay(fixture.context, fixture.input, payload)
|
|
assert.equal(evidence.rejected, true); assert.equal(evidence.unchanged, true)
|
|
assert.equal(evidence.beforeDigest, evidence.afterDigest)
|
|
assert.equal(evidence.historyCount, 1)
|
|
assert.deepEqual(fixture.calls.find(call => call.method === 'POST').body, payload)
|
|
assert.equal(fixture.calls.find(call => call.method === 'POST').headers, undefined)
|
|
assert.equal(Object.hasOwn(evidence, 'weightResults'), false)
|
|
})
|
|
|
|
for (const [name, mutate] of [
|
|
['new batch', state => { state.measurement.measurementId = '9007199254740993199' }],
|
|
['batch version', state => { state.measurement.version += 1 }],
|
|
['extra item', state => state.measurement.items.push({ id: '9007199254740993198', packageCount: 1 })],
|
|
['changed weight', state => { state.measurement.weightResults[0].grossWeightKg += 1 }],
|
|
['extra inbound history', state => state.inbound.statusHistory.push({ id: '9007199254740993197' })],
|
|
]) {
|
|
test(`duplicate measurement submission detects ${name}`, async () => {
|
|
const fixture = measurementReplayContext(false, mutate)
|
|
await assert.rejects(verifyMeasurementSubmitReplay(fixture.context, fixture.input,
|
|
{ measurementId: fixture.input.measurementId, version: 2 }), /零漂移|原入库原批次/)
|
|
})
|
|
}
|
|
|
|
test('duplicate measurement submission must not accept a successful mutation response', async () => {
|
|
const fixture = measurementReplayContext(false, () => {}, '200')
|
|
await assert.rejects(verifyMeasurementSubmitReplay(fixture.context, fixture.input,
|
|
{ measurementId: fixture.input.measurementId, version: 2 }), /再次提交明确A0409/)
|
|
})
|
|
|
|
test('approval replay uses the original key and old expectedVersion and compares all fee versions and histories', async () => {
|
|
const fixture = measurementReplayContext(true)
|
|
const payload = { measurementId: fixture.input.measurementId, expectedVersion: 3, reason: null }
|
|
const evidence = await verifyMeasurementApprovalReplay(fixture.context, fixture.input, payload, 'original-review-key')
|
|
const post = fixture.calls.find(call => call.method === 'POST')
|
|
assert.deepEqual(post.body, payload)
|
|
assert.equal(post.headers['Idempotency-Key'], 'original-review-key')
|
|
assert.equal(evidence.unchanged, true)
|
|
assert.equal(evidence.beforeDigest, evidence.afterDigest)
|
|
assert.equal(evidence.feeId, fixture.state.fee.feeId)
|
|
assert.equal(evidence.feeVersion, 1)
|
|
assert.equal(evidence.reviewIds.length, 1)
|
|
assert.equal(evidence.feeDetailIds.length, 1)
|
|
assert.equal(JSON.stringify(evidence).includes(fixture.context.token), false)
|
|
})
|
|
|
|
for (const [name, mutate] of [
|
|
['different fee ID', state => { state.fee.feeId = '9007199254740993191' }],
|
|
['extra fee version', state => { state.fee.feeVersion += 1 }],
|
|
['extra fee detail', state => state.details.push({ ...state.details[0], id: '9007199254740993192' })],
|
|
['changed historical fee detail', state => { state.details[0].amount += 1 }],
|
|
['duplicate review record', state => state.reviews.push({ ...state.reviews[0], id: '9007199254740993193' })],
|
|
['extra inbound history', state => state.inbound.statusHistory.push({ id: '9007199254740993194' })],
|
|
]) {
|
|
test(`approval replay detects ${name} despite successful response`, async () => {
|
|
const fixture = measurementReplayContext(true, mutate)
|
|
await assert.rejects(verifyMeasurementApprovalReplay(fixture.context, fixture.input,
|
|
{ measurementId: fixture.input.measurementId, expectedVersion: 3, reason: null }, 'original-review-key'),
|
|
/零新增|唯一审核及已生成初算费用事实有效/)
|
|
})
|
|
}
|
|
|
|
test('review history uses the real reviewResult field and rejects a fabricated result alias', async () => {
|
|
const fixture = measurementReplayContext(true)
|
|
fixture.state.reviews[0] = { id: '9007199254740993109', measurementId: fixture.input.measurementId, result: 'APPROVED' }
|
|
await assert.rejects(verifyMeasurementApprovalReplay(fixture.context, fixture.input,
|
|
{ measurementId: fixture.input.measurementId, expectedVersion: 3, reason: null }, 'original-review-key'),
|
|
/唯一审核及已生成初算费用事实有效/)
|
|
assert.equal(fixture.calls.some(call => call.method === 'POST'), false)
|
|
assert.match(source, /item\.reviewResult === 'APPROVED'/)
|
|
})
|