This commit is contained in:
2026-10-20 10:35:52 +08:00
parent ce189f062f
commit c13175bf77
202 changed files with 38397 additions and 7138 deletions
@@ -1,10 +1,10 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { createFleetCrossCustomerFixture, preparationContract } from './fleet-cross-customer-fixture.mjs'
import { createFleetCrossCustomerFixture, preparationContract, safeFixtureCount } from './fleet-cross-customer-fixture.mjs'
import { fixtureContract } from './fleet-cross-customer-acceptance.mjs'
// Strict offline HTTP model executes the real qualification and prepareFleetTrip helpers too.
function model(fault = '') {
function model(fault = '', countPatch = {}) {
const copy = value => structuredClone(value), calls = [], customers = [], transfers = [], dispatches = []
const options = { execute: true, warehouseId: '10', destinationWarehouseId: '20', pdaToken: 'caller-pda-secret' }
const trip = { trip: { tripNo: 'TRIP', recordVersion: '0', warehouseId: '10', status: 'PLANNED', maxPalletSpaces: 30,
@@ -23,11 +23,10 @@ function model(fault = '') {
ledger: [{ ...scope, id: `L${n}`, balanceId: `B${n}`, positionId: `P${n}`, palletNo: `PALLET${n}`, businessType: 'STOCK_IN', businessOrderNo: transfer.transferNo,
beforePackages: '0', deltaPackages: '1', afterPackages: '1' }] }
}
const cargo = () => trip.dispatches.map(binding => {
const d = dispatches.find(row => row.dispatchNo === binding.dispatchNo), n = dispatches.indexOf(d)
return { dispatchNo: d.dispatchNo, customerId: binding.customerId, warehousePalletId: `PAL${n}`, palletNo: `PALLET${n}`,
destinationKey: 'FBA:FBA', destinationLabel: 'FBA', packages: 1 }
})
let native
const cargo = () => transfers.map((transfer, n) => ({ dispatchItemId: `ITEM${n}`, customerId: transfer.customerId,
warehousePalletId: `PAL${n}`, palletNo: `PALLET${n}`, scheduledPackages: 1, pickedPackages: 0,
destination: { key: 'FBA:FBA' } }))
const parentFixture = { marker: 'PARENT', tenantId: 'PARENT-ID', password: 'parent-secret' }
const context = { marker: 'RUN-CROSS', token: 'admin-secret', options: { customerName: 'Parent customer' }, context: { merchantQualificationFixture: parentFixture },
createdResources: { merchantQualificationFixture: { marker: 'PARENT' } },
@@ -58,7 +57,7 @@ function model(fault = '') {
customers.push(customer)
return response({ tenant: { id }, administratorUsername: customer.username, temporaryPassword: customer.password })
}
if (path.endsWith('/submit-review')) return response({ status: 'PENDING' })
if (path.endsWith('/submit-review') && path.includes('/iam/')) return response({ status: 'PENDING' })
if (path === '/api/platform/v1/iam/reviews') return response(customers.map(row => ({ id: `REV${row.id}`, tenantId: row.id })))
if (path.endsWith('/dimension-initialization')) return response({ status: 'READY' })
if (path.endsWith('/approve')) return response({})
@@ -77,10 +76,14 @@ function model(fault = '') {
if (path === '/api/merchant/v1/iam/tenant/profile') return response({ id: fault === 'foreign login' ? 'OTHER' : merchant.id })
if (path === '/api/merchant/v1/last-leg/warehouse-transfers') {
assert.ok(merchant); assert.equal(body.inboundCode, undefined); assert.deepEqual(body.sourceLastlegOrderIds, [])
assert.equal(body.needRelabel, false); assert.equal(body.items.length, 1); assert.equal(body.items[0].packageCount, 1)
assert.equal(body.needRelabel, false); assert.equal(body.items.length, 1); assert.equal(body.items[0].packageCount, 1); assert.equal(body.items[0].quantity, 1)
const transferNo = `IN${transfers.length}`
const itemId = `FORECAST-ITEM${transfers.length}`
const transfer = { transferNo, inboundCode: transferNo, customerId: merchant.id, operatingWarehouseId: body.operatingWarehouseId,
status: 'DRAFT', pallets: [] }
status: 'DRAFT', packageCount: 1, pallets: [],
items: [{ ...copy(body.items[0]), id: itemId, ...(fault === 'missing declared quantity' ? { quantity: null } : {}) }],
cartons: fault === 'missing forecast carton' ? [] : [{ cartonMark: `BX-FORECAST${transfers.length}`, status: 'FORECAST',
transferItemId: fault === 'foreign forecast item' ? 'OTHER-ITEM' : itemId }] }
transfers.push(transfer); return response(transfer)
}
if (path.includes('/warehouse-transfers/')) {
@@ -89,10 +92,39 @@ function model(fault = '') {
if (path.startsWith('/api/merchant/')) assert.equal(transfer.customerId, merchant.id)
if (path.endsWith('/submit')) transfer.status = 'PENDING_REVIEW'
else if (path.endsWith('/review')) { assert.equal(url.searchParams.get('customerId'), transfer.customerId); assert.equal(body.decision, 'APPROVE'); transfer.status = 'WAIT_PDA_RECEIVING' }
else if (path.endsWith('/pallets/drafts')) transfer.pallets = [{ id: `DRAFT${n}`, status: 'DRAFT', cartonCount: body.cartonCount }]
else if (path.endsWith('/pallets/drafts')) {
// The actual PalletDraft DTO accepts identity only; never derive measurement from ignored request fields.
assert.deepEqual(Object.keys(body), ['idempotencyKey'])
transfer.pallets = [{ id: `DRAFT${n}`, status: 'DRAFT', cartonCount: 0, lengthCm: 0, widthCm: 0, heightCm: 0, volumeCbm: 0, grossWeightKg: 0 }]
}
else if (path.endsWith('/generate')) Object.assign(transfer.pallets[0], { status: 'GENERATED', warehousePalletId: `PAL${n}`, palletNo: `PALLET${n}` })
else if (path.endsWith('/bind')) { assert.equal(body.palletCode, transfer.pallets[0].palletNo); transfer.pallets[0].status = 'BOUND' }
else if (path.endsWith('/palletize')) { transfer.status = 'WAIT_CUSTOMER_CONFIRMATION'; transfer.pallets[0].status = 'COMPLETED' }
else if (path.endsWith('/cartons/count')) {
assert.equal(method, 'POST'); assert.equal(transfer.pallets[0].status, 'BOUND')
assert.equal(transfer.pallets[0].cartonCount, 0); assert.equal(body.forecastPackageCount, transfer.packageCount)
assert.equal(body.boxes.length, 1)
const box = body.boxes[0], item = transfer.items[0], carton = transfer.cartons[0], pallet = transfer.pallets[0]
assert.equal(box.cartonMark, carton.cartonMark); assert.equal(box.transferItemId, item.id)
assert.equal(box.transferPalletId, pallet.id); assert.notEqual(box.transferPalletId, pallet.warehousePalletId)
assert.deepEqual([box.length, box.width, box.height, box.weight], [100, 100, 100, 100])
assert.deepEqual(box.items, [{ skuCode: item.skuCode, quantity: item.quantity }])
Object.assign(carton, { transferPalletId: pallet.id, status: 'COUNTED', countedAt: '2026-10-20T09:00:00',
length: box.length, width: box.width, height: box.height, weight: box.weight, quantity: 1,
items: [{ transferItemId: item.id, skuCode: item.skuCode, quantity: 1 }] })
Object.assign(pallet, { cartonCount: 1, volumeCbm: 1, grossWeightKg: 100 })
if (fault === 'uncounted carton') { carton.status = 'FORECAST'; carton.countedAt = null }
if (fault === 'wrong counted pallet') carton.transferPalletId = 'OTHER-PALLET'
if (fault === 'wrong counted sku') carton.items[0].skuCode = 'OTHER-SKU'
if (fault === 'missing carton measurement') carton.weight = null
if (fault === 'zero counted pallet') pallet.cartonCount = 0
if (fault === 'wrong measured pallet weight') pallet.grossWeightKg = 0
}
else if (path.endsWith('/palletize')) {
assert.equal(transfer.cartons.length, 1); assert.equal(transfer.cartons[0].status, 'COUNTED'); assert.ok(transfer.cartons[0].countedAt)
assert.equal(transfer.pallets[0].cartonCount, 1); assert.equal(transfer.pallets[0].grossWeightKg, 100)
assert.equal(transfer.pallets[0].volumeCbm, 1); assert.ok(body.photoFileIds.length)
transfer.status = 'WAIT_CUSTOMER_CONFIRMATION'; transfer.pallets[0].status = 'COMPLETED'
}
else if (path.endsWith('/confirm-measurement')) transfer.status = 'READY_FOR_DISPATCH'
else assert.equal(method, 'GET')
return response(transfer)
@@ -108,43 +140,44 @@ function model(fault = '') {
if (path.endsWith('/dispatches/candidates')) {
const n = transfers.findIndex(row => row.transferNo === url.searchParams.get('orderNo')), transfer = transfers[n]
assert.equal(transfer.status, 'READY_FOR_DISPATCH')
return response([{ customerId: fault === 'foreign candidate' ? 'OTHER' : transfer.customerId, overseasWarehouseId: '10', inboundCode: transfer.transferNo,
warehousePalletId: `PAL${n}`, palletNo: `PALLET${n}`, planPalletId: `PLAN${n}`, sourceStage: 'RECEIVED', packageCount: 1 }])
return response({ complete: true, candidates: [{ customerId: fault === 'foreign candidate' ? 'OTHER' : transfer.customerId,
warehouseId: '10', inboundCode: transfer.transferNo, palletNo: `PALLET${n}`, availablePackages: 1, destination: { key: 'FBA:FBA' },
sourceVersion: `SOURCE${n}`, blockReasons: [], selection: { sourceType: 'INBOUND_PLAN', planPalletId: `PLAN${n}`, sourceVersion: `SOURCE${n}` } }] })
}
if (path.endsWith('/drafts/containers')) return response({ id: `CONTAINER${dispatches.length}` })
if (path.endsWith('/draft-locks/lock')) return response(null)
if (path.endsWith('/pre-arrange')) {
const n = Number(body.planPalletIds[0].slice(-1))
const dispatch = { dispatchNo: `DS${n}`, overseasWarehouseId: '10', status: 'PENDING_DEPARTURE', items: [{ warehousePalletId: `PAL${n}` }],
packageCount: 1, pickedPackageCount: 0, shippedPackageCount: 0 }
dispatches.push(dispatch); return response(dispatch)
if (path === '/api/platform/v1/last-leg/dispatches' && method === 'POST') {
assert.equal(body.cargo.length, 2); assert.equal(body.warehouseId, '10')
assert.deepEqual(body.cargo.map(row => row.planPalletId), ['PLAN0', 'PLAN1'])
assert.ok(body.cargo.every(row => !Object.hasOwn(row, 'customerId')))
native = { dispatchNo: 'DS-ONE', status: 'ACTIVE', stage: 'BEFORE_LOADING', reviewState: 'DRAFT', recordVersion: 0,
counts: { itemCount: 2, packageCount: 2, pickedPackageCount: 0, shippedPackageCount: 0, ...countPatch }, reviews: {},
documents: { outbounds: [], deliveries: [] }, dispatchInfo: {} }
trip.dispatches = [{ dispatchNo: native.dispatchNo }]
Object.assign(trip.trip, { tripPurpose: 'DELIVERY', stops: [{ name: 'Origin' }, { name: 'FBA', destinationKey: 'FBA:FBA' }] })
dispatches.push(native); return response(native)
}
if (path.endsWith('/master-data/vehicles')) return response({ id: 'VEHICLE' })
if (path.endsWith('/master-data/vehicles')) { trip.trip.vehiclePlate = body.plateNumber; return response({ id: 'VEHICLE' }) }
if (path.endsWith('/fleet/drivers')) return response({ id: 'DRIVER', recordVersion: '0' })
if (path.endsWith('/fleet/drivers/DRIVER/review')) return response({ reviewStatus: 'APPROVED' })
if (path.endsWith('/fleet/trips')) { Object.assign(trip.trip, body); return response(trip) }
if (path.includes('/fleet/trips/TRIP')) {
if (path.endsWith('/loading')) return response({ recordVersion: trip.trip.recordVersion, cargo: fault === 'missing second cargo' ? cargo().slice(0, 1) : cargo() })
if (method !== 'GET') {
assert.equal(body.recordVersion, trip.trip.recordVersion)
trip.trip.recordVersion = String(Number(trip.trip.recordVersion) + 1)
if (path === '/api/platform/v1/last-leg/fleet/trips') return response({ records: [trip.trip], total: 1, size: 200, page: 1 })
if (path === '/api/platform/v1/last-leg/fleet/trips/TRIP') return response(trip)
if (path === '/api/platform/v1/last-leg/dispatches/DS-ONE/items') return response({ dispatchNo: native.dispatchNo, recordVersion: native.recordVersion,
items: fault === 'missing second cargo' ? cargo().slice(0,1) : cargo() })
if (path.startsWith('/api/platform/v1/last-leg/dispatches/DS-ONE')) {
if (method === 'PUT') {
assert.equal(body.recordVersion, native.recordVersion)
Object.assign(trip.trip, { vehicleId: body.vehicle.vehicleId, driverId: body.driver.driverId })
native.vehicleInfo = { vehicle: { mode: 'MASTER', vehicleId: 'VEHICLE', plate: trip.trip.vehiclePlate }, driver: { driverId: 'DRIVER' } }
native.recordVersion++
} else if (method === 'POST') {
assert.equal(body.recordVersion, native.recordVersion); native.recordVersion++
if (path.endsWith('/submit-review')) native.reviewState = 'DISPATCH_PENDING'
if (path.endsWith('/dispatch-review')) { native.reviewState = 'LOADING_PENDING'; native.reviews.dispatchReview = { status: 'APPROVED' } }
if (path.endsWith('/loading-review')) { native.reviewState = 'APPROVED'; native.reviews.loadingReview = { status: 'APPROVED' }; native.stage = 'LOADING' }
}
if (path.endsWith('/assignment')) Object.assign(trip.trip, { vehicleId: body.vehicleId, driverId: body.driverId })
else if (path.endsWith('/dispatches/bind')) trip.dispatches.push({ dispatchNo: body.dispatchNo, customerId: transfers[Number(body.dispatchNo.slice(-1))].customerId, review: { status: 'PENDING' } })
else if (path.endsWith('/plan')) trip.trip.stops = body.stops
else if (path.endsWith('/review')) trip.dispatches.find(row => path.includes(`/${row.dispatchNo}/`)).review.status = 'APPROVED'
return response(trip)
const detail = copy(native)
if (fault === 'foreign delivery') detail.documents.deliveries = [{ customerId: 'OTHER' }]
return response(detail)
}
const n = Number(path.match(/(?:DS|OUT|DL)([01])/)[1]), dispatch = dispatches[n]
const order = { customerId: transfers[n].customerId, deliveryNo: `DL${n}`, outboundNo: `OUT${n}`, dispatchNo: dispatch.dispatchNo, status: 'WAIT_DELIVERY' }
if (path.endsWith('/external-freight')) return response({ requiresDeliveryFee: true, quotes: [] })
if (path.endsWith('/transfer-delivery')) {
assert.equal(body.fees.length, 1); assert.equal(body.fees[0].amount, 30)
return response({ dispatchNo: dispatch.dispatchNo, outboundNo: `OUT${n}`, deliveries: [order], packageCount: 1 })
}
if (path.includes('/delivery-orders/')) { assert.equal(url.searchParams.get('customerId'), transfers[n].customerId); return response({ order: fault === 'foreign delivery' ? { ...order, customerId: 'OTHER' } : order }) }
if (path.includes('/outbound-orders/')) return response({ outbound: { outboundNo: `OUT${n}`, dispatchNo: dispatch.dispatchNo, overseasWarehouseId: '10', status: 'WAIT_OUTBOUND' }, deliveries: [order] })
if (path.endsWith(`/dispatches/${dispatch.dispatchNo}`)) return response(dispatch)
throw new Error(`Unexpected offline request ${method} ${path}`)
},
}
@@ -180,15 +213,28 @@ test('real helpers build independent current-run customers, receipts and one com
assert.notEqual(m.customers[0].marker, m.customers[1].marker)
const proof = m.context.createdResources.fleetCrossCustomerFixture
assert.equal(proof.status, 'READY'); assert.deepEqual(proof.sessionCleanup.map(row => row.status), ['LOGGED_OUT', 'LOGGED_OUT'])
assert.equal(m.trip.dispatches.length, 2); assert.deepEqual(m.trip.trip.stops.map(row => row.destinationKey).filter(Boolean), ['FBA:FBA'])
assert.equal(m.calls.filter(row => row.raw.endsWith('/plan')).length, 2)
assert.equal(m.calls.filter(row => row.raw.endsWith('/transfer-delivery')).length, 2)
assert.equal(m.trip.dispatches.length, 1); assert.equal(new Set(fixture.members.map(row => row.dispatchNo)).size, 1); assert.deepEqual(m.trip.trip.stops.map(row => row.destinationKey).filter(Boolean), ['FBA:FBA'])
assert.equal(m.calls.filter(row => row.method === 'POST' && row.raw === '/api/platform/v1/last-leg/dispatches').length, 1)
assert.equal(m.calls.filter(row => row.raw.endsWith('/cartons/count')).length, 2)
for (const member of fixture.members) {
const prefix = `/api/pda/v1/last-leg/warehouse-transfers/${member.incomingNo}`
const draft = m.calls.findIndex(row => row.raw === `${prefix}/pallets/drafts`)
const bind = m.calls.findIndex(row => row.raw.startsWith(`${prefix}/pallets/`) && row.raw.endsWith('/bind'))
const count = m.calls.findIndex(row => row.raw === `${prefix}/cartons/count`)
const photo = m.calls.findIndex(row => row.raw === `${prefix}/measurement-photos/files`)
const receipt = m.calls.findIndex(row => row.raw === `${prefix}/palletize`)
assert.ok(draft < bind && bind < count && count < photo && photo < receipt)
assert.equal(member.measurementReceipt.countedCarton.cartonMark, m.calls[count].body.boxes[0].cartonMark)
assert.equal(member.measurementReceipt.syntheticTestMaterials, true)
assert.equal(member.measurementReceipt.finalizedPallet.cartonCount, 1)
}
assert.equal(m.calls.filter(row => row.raw.endsWith('/transfer-delivery')).length, 0)
assert.ok(!m.calls.some(row => /\/pick$|\/depart$|\/bookings|\/capacity-plan/.test(row.raw)))
const persisted = JSON.stringify(m.context.createdResources)
for (const secret of ['caller-pda-secret', 'admin-secret', 'fresh-secret-', 'merchant-100-secret', 'refresh-100-secret']) assert.ok(!persisted.includes(secret))
})
for (const fault of ['disabled receiving', 'reused customer', 'foreign login', 'foreign position', 'truncated ledger', 'wrong raw scope', 'wrong raw ledger position', 'wrong raw ledger balance', 'foreign candidate', 'missing second cargo', 'foreign delivery', 'logout failure']) {
for (const fault of ['disabled receiving', 'reused customer', 'foreign login', 'missing forecast carton', 'missing declared quantity', 'foreign forecast item', 'uncounted carton', 'wrong counted pallet', 'wrong counted sku', 'missing carton measurement', 'zero counted pallet', 'wrong measured pallet weight', 'foreign position', 'truncated ledger', 'wrong raw scope', 'wrong raw ledger position', 'wrong raw ledger balance', 'foreign candidate', 'missing second cargo', 'foreign delivery', 'logout failure']) {
test(`fail closed, retain partial resources and clean owned sessions: ${fault}`, async () => {
const m = model(fault)
await assert.rejects(createFleetCrossCustomerFixture(m.context, m.options))
@@ -198,6 +244,8 @@ for (const fault of ['disabled receiving', 'reused customer', 'foreign login', '
assert.equal(logouts.length, logins.length)
assert.ok(!m.calls.some(row => row.method === 'DELETE' || row.raw.includes('/users/')))
if (fault === 'disabled receiving') assert.equal(m.calls.filter(row => row.method !== 'GET').length, 0)
if (['missing forecast carton', 'missing declared quantity', 'foreign forecast item', 'uncounted carton', 'wrong counted pallet', 'wrong counted sku', 'missing carton measurement', 'zero counted pallet', 'wrong measured pallet weight'].includes(fault))
assert.equal(m.calls.some(row => row.raw.endsWith('/palletize') || row.raw === '/api/platform/v1/last-leg/dispatches'), false)
const previousCount = m.calls.length
await assert.rejects(createFleetCrossCustomerFixture(m.context, m.options), /already started/)
assert.equal(m.calls.length, previousCount)
@@ -211,3 +259,31 @@ test('missing explicit warehouse or caller PDA fails before reading or writing A
assert.equal(m.calls.length, 0)
}
})
test('native Jackson Long count strings build the same complete one-main fixture', async () => {
const m = model('', { packageCount: '2', pickedPackageCount: '0', shippedPackageCount: '0' })
const fixture = await createFleetCrossCustomerFixture(m.context, m.options)
assert.equal(fixture.dispatchNo, 'DS-ONE')
assert.equal(fixture.tripNo, 'TRIP')
assert.equal(fixture.members.length, 2)
assert.ok(fixture.members.every(member => member.dispatchNo === fixture.dispatchNo))
})
test('fixture count parser accepts only explicit non-negative safe integers and rejects coercion', () => {
for (const [value, expected] of [[0, 0], ['0', 0], [2, 2], ['2', 2], [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER], [String(Number.MAX_SAFE_INTEGER), Number.MAX_SAFE_INTEGER]]) {
assert.equal(safeFixtureCount(value), expected)
}
for (const value of [null, undefined, true, false, '', ' ', ' 2 ', '02', '+2', '-0', '-1', -1, 1.5, '1.5', '1e2', '2E0', NaN, Infinity, {}, [], Number.MAX_SAFE_INTEGER + 1, '9007199254740992']) {
assert.throws(() => safeFixtureCount(value), /非负安全整数/)
}
})
test('invalid created main counts fail before resource preparation or picking', async () => {
for (const field of ['packageCount', 'pickedPackageCount', 'shippedPackageCount']) {
const m = model('', { [field]: field === 'packageCount' ? '2e0' : false })
await assert.rejects(createFleetCrossCustomerFixture(m.context, m.options), /非负安全整数/)
assert.equal(m.calls.some(row => row.raw.endsWith('/master-data/vehicles')), false)
assert.equal(m.calls.some(row => row.raw.endsWith('/pick')), false)
}
})