import test from 'node:test' import assert from 'node:assert/strict' import { createFleetCrossCustomerFixture, preparationContract } 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 = '') { 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, 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' }] } } 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 } }) 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' } }, 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' }) 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 === '/api/platform/v1/iam/tenants') { const n = customers.length, id = fault === 'reused customer' ? '100' : String(100 + n) const customer = { id, username: `user${n}`, password: `fresh-secret-${n}`, marker: body.internalRemark } customers.push(customer) return response({ tenant: { id }, administratorUsername: customer.username, temporaryPassword: customer.password }) } if (path.endsWith('/submit-review')) 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({}) if (path.endsWith('/qualifications')) return response(['BUSINESS_LICENSE', 'LEGAL_PERSON_ID'].map(qualificationType => ({ qualificationType, status: 'APPROVED' }))) 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' : 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) const transferNo = `IN${transfers.length}` const transfer = { transferNo, inboundCode: transferNo, customerId: merchant.id, operatingWarehouseId: body.operatingWarehouseId, status: 'DRAFT', pallets: [] } 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')) transfer.pallets = [{ id: `DRAFT${n}`, status: 'DRAFT', cartonCount: body.cartonCount }] 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('/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([{ 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 }]) } 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.endsWith('/master-data/vehicles')) 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.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 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}`) }, } 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.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.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']) { 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) 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) } })