Unify dispatch operations with matched published transfer fixture
This commit is contained in:
@@ -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({})
|
||||
@@ -108,43 +107,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,9 +180,9 @@ 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('/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))
|
||||
@@ -211,3 +211,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)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user