338 lines
27 KiB
JavaScript
338 lines
27 KiB
JavaScript
import test from 'node:test'
|
|
import assert from 'node:assert/strict'
|
|
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 = '', countPatch = {}) {
|
|
const copy = value => structuredClone(value), calls = [], customers = [{ id: '100', username: 'existing-a', password: 'offline-a' }, { id: '101', username: 'existing-b', password: 'offline-b' }], 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,
|
|
vehiclePlate: 'TEST', driverName: 'TEST', driverPhone: '19900000000' }, dispatches: [] }
|
|
const origin = { warehouse: { id: '10', tenantId: '0', warehouseType: 'OVERSEAS', status: 'ACTIVE', warehouseCode: 'ORIGIN', warehouseName: 'Origin' },
|
|
capabilities: [{ capabilityCode: 'RECEIVING', enabled: fault !== 'disabled receiving' }] }
|
|
const destination = { warehouse: { id: '20', tenantId: '0', warehouseType: 'FBA', status: 'ACTIVE', warehouseCode: 'FBA', warehouseName: 'Destination' },
|
|
addresses: [{ addressLine1: 'TEST street', countryCode: 'US' }] }
|
|
const response = data => ({ http: 200, body: { code: '200', data: copy(data) }, requestId: 'offline-request', traceId: 'offline-trace' })
|
|
const buckets = { availablePackages: '1', holdPackages: '0', allocatedPackages: '0', pickedPackages: '0', receivingPackages: '0' }
|
|
const stock = transfer => {
|
|
const n = transfers.indexOf(transfer), scope = { customerId: transfer.customerId, overseasWarehouseId: '10', inboundCode: transfer.transferNo }
|
|
const position = { ...scope, ...buckets, id: `P${n}`, balanceId: `B${n}`, palletId: `PAL${n}`, palletNo: `PALLET${n}` }
|
|
if (fault === 'foreign position') position.customerId = 'OTHER'
|
|
return { balance: { ...scope, ...buckets, id: `B${n}`, inTransitPackages: '0', procurementPackages: '0' }, positions: [position],
|
|
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' }] }
|
|
}
|
|
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:${transfer.destinationWarehouseCode}` } }))
|
|
const parentFixture = { marker: 'PARENT', tenantId: 'PARENT-ID', password: 'parent-secret' }
|
|
const context = { marker: 'RUN-CROSS', token: 'admin-secret', options: { customerName: 'Parent customer', fixedCustomerId: '100', merchantUsername: 'existing-a', merchantPassword: 'offline-a', fixedSecondaryCustomer: { customerId: '101', username: 'existing-b', password: 'offline-b' }, fixedFleetResources: [{ vehicleId: '11', driverId: '12' }] }, context: { merchantQualificationFixture: parentFixture },
|
|
createdResources: { merchantQualificationFixture: { marker: 'PARENT' } },
|
|
assert(_module, label, _response, passed) { assert.ok(passed, label) },
|
|
apiSuccess(_module, label, result) { assert.equal(result.http, 200, label); assert.equal(result.body.code, '200', label); return result.body.data },
|
|
async findAvailablePhone() { return `1990000000${customers.length}` },
|
|
async requestWithBody(method, raw, form) {
|
|
calls.push({ method, raw, multipart: true }); assert.ok(form instanceof FormData)
|
|
assert.match(raw, /^\/api\/platform\/v1\/files\/qualifications\/\d+\/required-package\?/)
|
|
return response({})
|
|
},
|
|
async requestMultipart(method, raw, file, token = this.token) {
|
|
calls.push({ method, raw, token, multipart: true }); assert.ok(file.content.length > 0)
|
|
if (raw.includes('/license?')) return response({ id: 'DRIVER', recordVersion: '1', licenseFileId: 'LICENSE' })
|
|
const evidenceKind = raw.match(/^\/api\/platform\/v1\/last-leg\/dispatches\/DS-ONE\/evidence\/(VEHICLE|DRIVER_PHOTO|DRIVER)$/)?.[1]
|
|
if (evidenceKind) {
|
|
assert.equal(token, 'admin-secret')
|
|
assert.equal(file.type, 'image/png')
|
|
return response({ fileId: { VEHICLE: '901', DRIVER_PHOTO: '902', DRIVER: '903' }[evidenceKind] })
|
|
}
|
|
assert.match(raw, /\/warehouse-transfers\/IN[01]\/measurement-photos\/files$/); assert.equal(token, options.pdaToken)
|
|
return response({ fileId: 'PHOTO', sizeBytes: file.content.length })
|
|
},
|
|
async request(method, raw, body, token = this.token) {
|
|
calls.push({ method, raw, body: copy(body), token })
|
|
const url = new URL(raw, 'https://offline.invalid'), path = url.pathname
|
|
if (path.startsWith('/api/platform/')) assert.equal(token, 'admin-secret')
|
|
if (path.startsWith('/api/pda/')) assert.equal(token, options.pdaToken)
|
|
if (path.endsWith('/warehouse-admin/10')) return response(origin)
|
|
if (path.endsWith('/warehouse-admin/20')) return response(destination)
|
|
if (path.endsWith('/warehouse-admin/21')) return response({ ...destination,
|
|
warehouse: { ...destination.warehouse, id: '21', warehouseCode: 'FBA2', warehouseName: 'Second destination' } })
|
|
if (path.endsWith('/dimension-initialization')) return response({ status: 'READY' })
|
|
if (path === '/api/auth/login') {
|
|
assert.equal(token, '')
|
|
const customer = customers.find(row => row.username === body.loginId); assert.equal(body.password, customer.password)
|
|
return response({ accessToken: `merchant-${customer.id}-secret`, refreshToken: `refresh-${customer.id}-secret` })
|
|
}
|
|
if (path === '/api/auth/logout') {
|
|
assert.equal(body.refreshToken, token.replace('merchant-', 'refresh-'))
|
|
if (fault === 'logout failure' && token.includes('100')) throw new Error('offline logout failure')
|
|
return response(null)
|
|
}
|
|
const merchant = customers.find(row => token === `merchant-${row.id}-secret`)
|
|
if (path === '/api/merchant/v1/iam/tenant/profile') return response({ id: fault === 'foreign login' ? 'OTHER' : fault === 'reused customer' ? '100' : merchant.id, status: 'ACTIVE', tenantName: 'Existing fixture customer' })
|
|
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.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', packageCount: 1, pallets: [], destinationWarehouseCode: body.destinationWarehouseCode,
|
|
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/')) {
|
|
const transfer = transfers.find(row => path.includes(`/${row.transferNo}`)), n = transfers.indexOf(transfer)
|
|
assert.ok(transfer)
|
|
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')) {
|
|
// 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('/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)
|
|
}
|
|
if (path.includes('/inventory/')) {
|
|
const transfer = transfers.find(row => row.customerId === url.searchParams.get('customerId')), facts = stock(transfer)
|
|
if (path.endsWith('/positions')) return response(facts.positions)
|
|
if (/\/balances\/B[01]$/.test(path)) return response(facts.balance)
|
|
// Match InventoryLedgerView: the public DTO has neither balanceId nor positionId.
|
|
if (path.endsWith('/ledger')) return response({ records: facts.ledger.map(({ balanceId, positionId, ...view }) => view), total: fault === 'truncated ledger' ? 2 : 1 })
|
|
assert.ok(path.endsWith('/balances')); return response({ records: [facts.balance], total: 1 })
|
|
}
|
|
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({ complete: true, candidates: [{ customerId: fault === 'foreign candidate' ? 'OTHER' : transfer.customerId,
|
|
warehouseId: '10', inboundCode: transfer.transferNo, palletNo: `PALLET${n}`, availablePackages: 1, destination: { key: `FBA:${transfer.destinationWarehouseCode}` },
|
|
sourceVersion: `SOURCE${n}`, blockReasons: [], selection: { sourceType: 'INBOUND_PLAN', planPalletId: `PLAN${n}`, sourceVersion: `SOURCE${n}` } }] })
|
|
}
|
|
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' }, ...[...new Set(transfers.map(row => row.destinationWarehouseCode))]
|
|
.map(code => ({ name: code, destinationKey: `FBA:${code}` }))] })
|
|
dispatches.push(native); return response(native)
|
|
}
|
|
if (path.endsWith('/master-data/vehicles/11')) return response({ id: '11', plateNumber: 'TEST', status: 'IN_SERVICE', region: 'OVERSEAS', ownership: 'OWNED' })
|
|
if (path.endsWith('/fleet/drivers/12')) return response({ id: '12', active: true, reviewStatus: 'APPROVED', details: { ownership: 'OWNED' }, qualification: { canDispatch: true, reason: 'ELIGIBLE' } })
|
|
if (raw.startsWith('/api/platform/v1/last-leg/fleet/trips?')) return response({ records: trip.trip.vehicleId ? [trip.trip] : [], total: trip.trip.vehicleId ? 1 : 0, size: 200, page: 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)
|
|
assert.deepEqual(body.vehicle.evidenceFileIds, ['901'])
|
|
assert.deepEqual(body.driver.evidence.photoFileIds, ['902'])
|
|
assert.deepEqual(body.driver.evidence.fileIds, ['903'])
|
|
Object.assign(trip.trip, { vehicleId: body.vehicle.vehicleId, driverId: body.driver.driverId })
|
|
native.vehicleInfo = { vehicle: { mode: 'MASTER', vehicleId: '11', plate: trip.trip.vehiclePlate }, driver: { driverId: '12' } }
|
|
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' }
|
|
}
|
|
const detail = copy(native)
|
|
if (fault === 'foreign delivery') detail.documents.deliveries = [{ customerId: 'OTHER' }]
|
|
return response(detail)
|
|
}
|
|
throw new Error(`Unexpected offline request ${method} ${path}`)
|
|
},
|
|
}
|
|
options.readInventory = async scope => {
|
|
assert.equal(scope.marker, context.marker); assert.equal(scope.warehouseId, options.warehouseId)
|
|
const facts = stock(transfers.find(row => row.customerId === scope.customerId))
|
|
const raw = { scope: { ...scope, coverage: 'CUSTOMER_WAREHOUSE_COMPLETE' }, balances: [facts.balance], positions: facts.positions, ledger: facts.ledger }
|
|
// The physical position table links to its inbound through balance_id; only the public joined DTO has inboundCode.
|
|
for (const position of raw.positions) delete position.inboundCode
|
|
if (fault === 'wrong raw scope') raw.scope.marker = 'OLD'
|
|
if (fault === 'wrong raw ledger position') raw.ledger[0].positionId = 'OTHER'
|
|
if (fault === 'wrong raw ledger balance') raw.ledger[0].balanceId = 'OTHER'
|
|
return copy(raw)
|
|
}
|
|
return { context, options, calls, customers, transfers, trip, parentFixture }
|
|
}
|
|
|
|
test('default preparation is network-free and exposes caller-owned lifecycle', async () => {
|
|
assert.equal(await createFleetCrossCustomerFixture(), preparationContract)
|
|
assert.equal(preparationContract.stage, fixtureContract.stage)
|
|
})
|
|
|
|
test('real helpers build independent current-run customers, receipts and one complete shared trip without picking', async () => {
|
|
const m = model(), oldEnvironment = { username: process.env.MERCHANT_TEST_USERNAME, password: process.env.MERCHANT_TEST_PASSWORD }
|
|
const fixture = await createFleetCrossCustomerFixture(m.context, m.options)
|
|
assert.equal(fixture.stage, fixtureContract.stage)
|
|
assert.equal(fixture.members.length, 2); assert.notEqual(fixture.members[0].customerId, fixture.members[1].customerId)
|
|
assert.ok(fixture.members.every(row => row.receiptVerified && row.marker === m.context.marker && row.positionId && row.balanceId))
|
|
assert.equal(m.context.context.merchantQualificationFixture, m.parentFixture)
|
|
assert.deepEqual(m.context.createdResources.merchantQualificationFixture, { marker: 'PARENT' })
|
|
assert.equal(m.context.options.customerName, 'Parent customer')
|
|
assert.deepEqual({ username: process.env.MERCHANT_TEST_USERNAME, password: process.env.MERCHANT_TEST_PASSWORD }, oldEnvironment)
|
|
assert.equal(m.calls.some(row => row.raw === '/api/platform/v1/iam/tenants' || row.raw === '/api/auth/register'), false)
|
|
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, 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 history of [false, true]) test(`GRAY preparation reuses two customers and preserves the parent slot: history=${history}`, async () => {
|
|
const m = model()
|
|
Object.assign(m.context.options, { environment: 'gray', fixedCustomerId: 'TEN100', merchantUsername: 'existing-a', merchantPassword: 'offline-a',
|
|
fixedSecondaryCustomer: { customerId: 'TEN101', username: 'existing-b', password: 'offline-b' }, fixedFleetResources: [] })
|
|
if (history) {
|
|
const read = m.options.readInventory
|
|
m.options.readInventory = async scope => {
|
|
const raw = await read(scope), balance = structuredClone(raw.balances[0]), position = structuredClone(raw.positions[0]), ledger = structuredClone(raw.ledger[0])
|
|
Object.assign(balance, { id: 'HIST-B', inboundCode: 'OLD-RECEIPT' })
|
|
Object.assign(position, { id: 'HIST-P', balanceId: 'HIST-B', palletId: 'HIST-PALLET', palletNo: 'HIST-PALLET' })
|
|
Object.assign(ledger, { id: 'HIST-L', balanceId: 'HIST-B', positionId: 'HIST-P', inboundCode: 'OLD-RECEIPT', businessOrderNo: 'OLD-RECEIPT' })
|
|
raw.balances.push(balance); raw.positions.push(position); raw.ledger.push(ledger)
|
|
return raw
|
|
}
|
|
}
|
|
await assert.rejects(createFleetCrossCustomerFixture(m.context, m.options), /固定车辆司机组合/)
|
|
const proof = m.context.createdResources.fleetCrossCustomerFixture
|
|
assert.deepEqual(proof.members.map(row => row.customerId), ['100', '101'])
|
|
assert.ok(proof.members.every(row => row.qualification.reused === true && row.receiptVerified === true))
|
|
assert.equal(m.customers.length, 2)
|
|
assert.equal(m.calls.some(row => row.raw === '/api/platform/v1/iam/tenants' || row.raw === '/api/auth/register'), false)
|
|
assert.equal(m.context.context.merchantQualificationFixture, m.parentFixture)
|
|
assert.deepEqual(proof.sessionCleanup.map(row => row.status), ['LOGGED_OUT', 'LOGGED_OUT'])
|
|
if (history) assert.ok(proof.members.every(row => row.receipt.raw.balances.length === 2))
|
|
})
|
|
|
|
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))
|
|
const proof = m.context.createdResources.fleetCrossCustomerFixture
|
|
assert.equal(proof.status, 'FAILED')
|
|
const logins = m.calls.filter(row => row.raw === '/api/auth/login'), logouts = m.calls.filter(row => row.raw === '/api/auth/logout')
|
|
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)
|
|
})
|
|
}
|
|
|
|
test('missing explicit warehouse or caller PDA fails before reading or writing APIs', async () => {
|
|
for (const field of ['warehouseId', 'destinationWarehouseId', 'pdaToken', 'readInventory']) {
|
|
const m = model(); delete m.options[field]
|
|
await assert.rejects(createFleetCrossCustomerFixture(m.context, m.options), /Missing/)
|
|
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('explicit per-member destinations are read before writes and preserved in both receipts and route', async () => {
|
|
const m = model(); m.options.destinationWarehouseIds = ['20', '21']
|
|
const fixture = await createFleetCrossCustomerFixture(m.context, m.options)
|
|
assert.deepEqual(fixture.members.map(row => row.destination.key), ['FBA:FBA', 'FBA:FBA2'])
|
|
assert.deepEqual(m.trip.trip.stops.slice(1).map(row => row.destinationKey), ['FBA:FBA', 'FBA:FBA2'])
|
|
const secondDestination = m.calls.findIndex(row => row.raw.includes('/warehouse-admin/21'))
|
|
const firstWrite = m.calls.findIndex(row => row.method !== 'GET')
|
|
assert.ok(secondDestination >= 0 && secondDestination < firstWrite)
|
|
assert.deepEqual(m.calls.filter(row => row.method === 'POST' && row.raw === '/api/merchant/v1/last-leg/warehouse-transfers')
|
|
.map(row => row.body.destinationWarehouseId), ['20', '21'])
|
|
})
|
|
|
|
test('malformed member destination scope fails without network', async () => {
|
|
for (const destinations of [[], ['20'], ['20', '21', '22'], ['21', '20'], ['20', '']]) {
|
|
const m = model(); m.options.destinationWarehouseIds = destinations
|
|
await assert.rejects(createFleetCrossCustomerFixture(m.context, m.options), /Two explicit/)
|
|
assert.equal(m.calls.length, 0)
|
|
}
|
|
})
|
|
|
|
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)
|
|
}
|
|
})
|