Files
baiyue-firstleg/scripts/fleet-capacity-pool-test-env.mjs
T

449 lines
40 KiB
JavaScript

import { mkdirSync, openSync, writeFileSync, fsyncSync, closeSync, readFileSync } from 'node:fs'
import { resolve, dirname, join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { createHash, randomUUID } from 'node:crypto'
import { AcceptanceRun } from './admin-test-env-acceptance.mjs'
import { parseCrossCustomerArguments, runCrossCustomer } from './fleet-cross-customer-test-env.mjs'
import { guardFleetWrites, redactFleetWrite, findFleetActorPhone } from './fleet-test-env-acceptance.mjs'
import { controlledPalletMeasurements } from './admin-test-env-modules/fleet-trip-fulfillment.mjs'
import { pickAll, saveReceiptAndComplete } from './admin-test-env-modules/phase5-overseas-fulfillment.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
const API = '/api/platform/v1/last-leg', POOL = `${API}/fleet/capacity-offers`, IAM = '/api/platform/v1/iam'
const MODULE = 'fleet-capacity-pool', PREFIX = 'platform:fleet:capacity-offer:'
const id = value => String(value ?? ''), requireThat = (ok, reason) => { if (!ok) throw Error(reason) }
const success = response => response?.http === 200 && id(response.body?.code) === '200'
const denied = response => [403, 409].includes(response?.http) || response?.http === 200 && ['A0403', 'A0409'].includes(id(response.body?.code))
const encode = encodeURIComponent
const localTime = ms => new Date(Date.now() + ms).toISOString().slice(0, 19)
const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH1kAAAAASUVORK5CYII=', 'base64')
export const POOL_COVERAGE = Object.freeze([
'same-segment-plan-18-of-30-reserve-8-leaves-4', 'two-sales-owner-isolation', 'two-dispatchers-last-four-race',
'unknown-load-stays-null', 'close-and-successful-confirmation-replay', 'one-commercial-order-and-one-incoming',
'both-business-types-real-pda-receipt', 'shared-real-departure-and-signature', 'business-completion-and-fee-uniqueness',
'merchant-cannot-read-pool', 'old-manual-order-endpoints-rejected',
'assigned-vehicle-disable-and-shrink-rejected', 'paused-create-edit-confirm-rejected',
'expired-cutoff-create-edit-confirm-rejected', 'weight-and-volume-independent-capacity-rejections',
])
export const POOL_REMAINING = Object.freeze([
'out-of-band-assigned-vehicle-drift-not-constructible-through-supported-api',
'full-UI-1920x1080-and-network-request-budget',
])
export function parsePoolArguments(args) { return parseCrossCustomerArguments(args) }
export function poolPlan(config) {
return { environment: 'TEST', execute: Boolean(config.execute), runId: config.runId,
output: `reports/fleet-cross-${config.runId}`, coveredByThisEntry: POOL_COVERAGE, remainingSeparateAcceptance: POOL_REMAINING,
sample: 'Two fresh qualified customers and owned one-box pallets, one 30-space trip, two pool business orders, one real incoming/pallet each. Planning uses 18 spaces; actual physical load is separately confirmed.',
prerequisites: ['explicit TEST private env with CHONGCHONG_ENV=test and CHONGCHONG_ADMIN_*', 'current Warehouse/FBA IDs',
'TEST read-only inventory evidence credentials', 'current capacity-offer permission seeds and backend deployed'],
boundary: 'Default plan performs no network or filesystem writes. Execute retains every sample and original command, blocks all further writes on an unknown outcome, never deploys or writes business SQL. No automatic rerun or resume.' }
}
/** Strict quantities are based on the selected unloading segment, never a global displayed total. */
export function assertPoolRemaining(offer, destinationKey, expected) {
const destination = offer?.destinations?.filter(row => row.destinationKey === destinationKey)
requireThat(offer?.capacityKnown === true && destination?.length === 1
&& Number(destination[0].remaining?.palletSpaces) === expected, `EXPECTED_EXACT_SEGMENT_REMAINING_${expected}`)
}
export function assertSingleCommercial(before, after) {
requireThat(before?.bookingNo && before.businessOrderNo && before.businessOrderNo === after?.businessOrderNo
&& before.bookingNo === after.bookingNo && before.businessType === after.businessType
&& before.customerId === after.customerId, 'COMMERCIAL_REPLAY_IDENTITY_CHANGED')
}
export function shrinkingVehicleRequest(vehicle) {
requireThat(vehicle?.id && Number.isInteger(Number(vehicle.recordVersion)) && Number(vehicle.maxPalletSpaces) > 1
&& Number(vehicle.maxLoadKg) > 0 && Number(vehicle.maxVolumeCbm) > 0, 'COMPLETE_OWNED_VEHICLE_SNAPSHOT_REQUIRED')
return { recordVersion: vehicle.recordVersion, plateNumber: vehicle.plateNumber, model: vehicle.model,
fuelConsumptionPer100Km: vehicle.fuelConsumptionPer100Km, mileageKm: vehicle.mileageKm,
driverOrDispatchCompany: vehicle.driverOrDispatchCompany ?? null, phone: vehicle.phone ?? null,
region: vehicle.region, ownership: vehicle.ownership, remark: vehicle.remark ?? null,
maxPalletSpaces: Number(vehicle.maxPalletSpaces) - 1, maxLoadKg: vehicle.maxLoadKg, maxVolumeCbm: vehicle.maxVolumeCbm }
}
export function expiryWaitMs(cutoffAt, now = Date.now()) {
const cutoff = Date.parse(cutoffAt)
requireThat(Number.isFinite(cutoff) && Number.isFinite(now), 'INVALID_SERVER_CUTOFF')
const delay = Math.max(0, cutoff + 300 - now)
requireThat(delay <= 15000, 'CUTOFF_SCENARIO_WAIT_EXCEEDS_BOUNDED_WINDOW')
return delay
}
const capacitySnapshot = offer => JSON.stringify({ capacityKnown: offer.capacityKnown, destinations: offer.destinations, segments: offer.segments })
const inquiryFact = row => JSON.stringify({ bookingNo: row.bookingNo, customerId: row.customerId, businessType: row.businessType,
recordVersion: row.recordVersion, status: row.status, cargo: row.cargo, expectedPalletCount: row.expectedPalletCount,
businessOrderNo: row.businessOrderNo ?? null, incomingNo: row.incomingNo ?? null, dispatchNo: row.dispatchNo ?? null })
/** Current supported vehicle API prevents this state from being created on an assigned trip.
* Verify the real rejection; never write SQL or claim an impossible live-drift fixture succeeded. */
async function verifyAssignedVehicleGuards(context, fixture, tripProof, { request, reject, check, offerDetail, proof }) {
requireThat(context.createdResources.fleetTrips.includes(tripProof) && tripProof.tripNo === fixture.tripNo && tripProof.vehicleId,
'ONLY_CURRENT_FIXTURE_VEHICLE_MAY_BE_TESTED')
const path = `${API}/master-data/vehicles/${encode(tripProof.vehicleId)}`
const before = await request('GET', path), offerBefore = (await offerDetail()).offer
check('Vehicle guard targets the original fixture assigned vehicle', id(before.id) === id(tripProof.vehicleId) && before.status === 'IN_SERVICE')
const disabled = await reject('Assigned vehicle cannot be disabled through master-data API', 'PATCH', `${path}/status`,
{ recordVersion: before.recordVersion, status: 'INACTIVE' })
check('Disable rejected specifically because the vehicle is assigned', /正在车次|改派|取消安排/.test(id(disabled.body?.message)))
const shrunk = await reject('Assigned vehicle cannot shrink capacity through master-data API', 'PUT', path, shrinkingVehicleRequest(before))
check('Shrink rejected specifically because the vehicle is assigned', /正在车次|改派|取消安排/.test(id(shrunk.body?.message)))
const after = await request('GET', path), offerAfter = (await offerDetail()).offer
check('Rejected vehicle commands leave master version status and all capacities unchanged', JSON.stringify(after) === JSON.stringify(before))
check('Rejected vehicle commands do not alter pool capacity or acceptance eligibility', capacitySnapshot(offerBefore) === capacitySnapshot(offerAfter)
&& offerAfter.canConfirm === offerBefore.canConfirm)
proof.vehicleGuards = { status: 'VERIFIED_REJECTION', vehicleId: id(before.id), originalVersion: before.recordVersion,
liveDrift: 'NOT_CONSTRUCTIBLE_THROUGH_SUPPORTED_API', explanation: 'VehicleApplicationService requires unassigned before status/capacity changes; no database bypass used.' }
proof.covered.push('assigned-vehicle-disable-and-shrink-rejected')
}
async function verifyPauseAndCutoff({ offerNo, bookingNo, salesToken, dispatcherToken, request, reject, check, key, proof,
wait = ms => new Promise(resolve => setTimeout(resolve, ms)) }) {
const detail = async () => (await request('GET', `${POOL}/${offerNo}`, undefined, dispatcherToken)).offer
const latest = () => request('GET', `${POOL}/inquiries/${bookingNo}`, undefined, dispatcherToken)
const control = async (action, suffix) => {
const offer = await detail()
return (await request('POST', `${POOL}/${offerNo}/${action}`, { tripRecordVersion: offer.tripRecordVersion,
offerRecordVersion: offer.recordVersion, idempotencyKey: key(suffix), reason: 'TEST暂停与截止门禁' }, dispatcherToken)).offer
}
const editCutoff = async (cutoffAt, suffix) => {
const offer = await detail()
return (await request('PUT', `${POOL}/${offerNo}`, { tripNo: offer.tripNo, tripRecordVersion: offer.tripRecordVersion,
offerRecordVersion: offer.recordVersion, cutoffAt, contactName: offer.contactName, contactPhone: offer.contactPhone,
remark: offer.remark, idempotencyKey: key(suffix) }, dispatcherToken)).offer
}
const refusedCommands = async (label, reason) => {
const current = await latest(), before = inquiryFact(current), beforeOffer = await detail()
const body = { tripRecordVersion: current.tripRecordVersion, offerRecordVersion: current.offerRecordVersion,
recordVersion: current.recordVersion, customerId: current.customerId, businessType: current.businessType,
deliveryMode: current.deliveryMode, estimatedArrivalAt: current.estimatedArrivalAt, expectedPalletCount: current.expectedPalletCount,
cargo: current.cargo, quoteRef: null, idempotencyKey: key(`${label}-EDIT`) }
for (const [method, path, command, token] of [
['POST', `${POOL}/${offerNo}/inquiries`, { ...body, recordVersion: null, idempotencyKey: key(`${label}-CREATE`) }, salesToken],
['PUT', `${POOL}/inquiries/${bookingNo}`, body, salesToken],
['POST', `${POOL}/inquiries/${bookingNo}/confirm`, { tripRecordVersion: current.tripRecordVersion,
offerRecordVersion: current.offerRecordVersion, recordVersion: current.recordVersion, idempotencyKey: key(`${label}-CONFIRM`) }, dispatcherToken],
]) {
const response = await reject(`${label} ${method} cannot mutate business`, method, path, command, token)
check(`${label} rejection comes from expected offer gate`, reason.test(id(response.body?.message)))
}
check(`${label} preserves original draft and does not create a commercial order`, before === inquiryFact(await latest()))
check(`${label} preserves actual reservations`, capacitySnapshot(beforeOffer) === capacitySnapshot(await detail()))
}
const original = await detail()
const paused = await control('pause', 'PAUSE-MATRIX')
check('Pause blocks both create and confirm affordances', paused.status === 'PAUSED' && !paused.canCreateInquiry && !paused.canConfirm)
await refusedCommands('PAUSED', /暂停|开放|接单|发布/)
const resumed = await control('resume', 'RESUME-MATRIX')
check('Explicit resume restores the same capacity', resumed.status === 'OPEN' && resumed.canCreateInquiry && resumed.canConfirm
&& capacitySnapshot(original) === capacitySnapshot(resumed))
proof.covered.push('paused-create-edit-confirm-rejected')
const expiring = await editCutoff(new Date(Date.now() + 8000).toISOString(), 'SHORT-CUTOFF')
const delay = expiryWaitMs(expiring.cutoffAt)
await wait(delay)
const expired = await detail()
check('Server marks cutoff expiry without mutating the stored OPEN status', expired.status === 'OPEN' && !expired.canCreateInquiry
&& !expired.canConfirm && /截止|发车时间/.test(id(expired.blockReason)))
await refusedCommands('EXPIRED', /截止|发车时间/)
const restoreAt = Math.min(Date.now() + 1800000, Date.parse(expired.plannedDepartureAt) - 60000)
requireThat(restoreAt > Date.now() + 10000, 'TRIP_NO_LONGER_HAS_TIME_TO_RESTORE_CUTOFF')
await editCutoff(new Date(restoreAt).toISOString(), 'RESTORE-CUTOFF')
const restored = await control('resume', 'RESTORE-AFTER-CUTOFF')
check('Deadline scenario ends with explicit valid acceptance restored', restored.canCreateInquiry && restored.canConfirm)
proof.cutoffMatrix = { status: 'VERIFIED', cutoffAt: expiring.cutoffAt, waitedMs: delay, restoredCutoffAt: restored.cutoffAt }
proof.covered.push('expired-cutoff-create-edit-confirm-rejected')
}
async function poolScenario(context, fixture, { execute = false, readInventory, credentials }) {
if (!execute) return poolPlan({ execute: false, runId: 'plan' })
requireThat(fixture?.marker === context.marker && fixture.members?.length === 2 && fixture.pdaToken
&& typeof readInventory === 'function', 'CURRENT_PHYSICAL_FIXTURE_REQUIRED')
const proof = context.createdResources.fleetCapacityPool = { status: 'CHECKING', marker: context.marker,
tripNo: fixture.tripNo, covered: [], remaining: [...POOL_REMAINING], actors: [], orders: [], snapshots: {} }
const tripProof = context.createdResources.fleetTrips.find(row => row.tripNo === fixture.tripNo)
const tripPath = `${API}/fleet/trips/${encode(fixture.tripNo)}`
const key = label => createHash('sha256').update(`${context.marker}:POOL:${label}`).digest('hex')
const check = (label, ok) => { context.assert(MODULE, label, null, Boolean(ok), '', { scenario: MODULE, deploymentBlocker: true }); requireThat(ok, label) }
const raw = (method, path, body, token = context.token) => context.request(method, path, body, token)
const request = async (method, path, body, token = context.token) => {
const response = await raw(method, path, body, token)
check(`${method} ${path}`, success(response) && response.requestId && response.traceId)
return response.body.data
}
const reject = async (label, method, path, body, token) => {
const response = await raw(method, path, body, token)
check(label, denied(response) && response.requestId && response.traceId)
return response
}
const permissions = await request('GET', `${IAM}/permissions`)
const actorSessions = [], merchantSessions = []
const businessPath = order => `${API}/${order.businessType === 'US_TRUCK' ? 'us-truck-orders' : 'local-looseboard-orders'}/${encode(order.businessOrderNo)}?customerId=${encode(order.customerId)}`
const createActor = async (kind, index, codes) => {
const suffix = createHash('sha256').update(`${context.marker}:${kind}:${index}`).digest('hex').slice(0, 16)
const found = codes.map(code => permissions.filter(p => p.permissionCode === code && p.status === 'ACTIVE'))
check('Exact pool permissions exist', found.every(rows => rows.length === 1 && rows[0].id))
const role = await request('POST', `${IAM}/roles`, { roleCode: `POOL_${kind}_${suffix}`.toUpperCase(), roleName: `${context.marker}-${kind}-${index}`, description: context.marker })
const actor = { kind, index, roleId: id(role.id) }; proof.actors.push(actor)
await request('PUT', `${IAM}/roles/permissions`, { roleId: actor.roleId, permissionIds: found.map(rows => id(rows[0].id)) })
const username = `pool_${kind}_${suffix}`, password = `Pool${randomUUID().replaceAll('-', '')}A9`
const user = await request('POST', `${IAM}/users`, { username, displayName: `${context.marker}-同名测试人员`,
phone: await findFleetActorPhone(context, suffix), email: `${username}@example.com`, tenantId: '0', roleIds: [actor.roleId], password, status: 'ACTIVE' })
actor.membershipId = id(user.membershipId)
const login = await request('POST', '/api/auth/login', { loginId: username, password }, '')
actorSessions.push({ accessToken: login.accessToken, refreshToken: login.refreshToken })
const effective = await request('GET', `${IAM}/authorization/permissions`, undefined, login.accessToken)
check('Temporary actor has exact permissions', Array.isArray(effective) && JSON.stringify([...effective].sort()) === JSON.stringify([...codes].sort()))
return login.accessToken
}
try {
const sales = [], dispatchers = []
for (let n = 0; n < 2; n++) sales.push(await createActor('sales', n, ['list', 'inquiry-create', 'inquiry-edit', 'inquiry-cancel'].map(s => PREFIX + s)))
for (let n = 0; n < 2; n++) dispatchers.push(await createActor('dispatch', n, [PREFIX + 'manage']))
for (const member of fixture.members) {
const credential = credentials?.get(member.customerId)
check('Original fixture customer credentials available only in memory', Boolean(credential))
const login = await request('POST', '/api/auth/login', { loginId: credential.username, password: credential.password }, '')
merchantSessions.push({ customerId: member.customerId, accessToken: login.accessToken, refreshToken: login.refreshToken })
await reject('Merchant token cannot read internal capacity pool', 'GET', `${POOL}?page=1&size=20`, undefined, login.accessToken)
}
proof.covered.push('merchant-cannot-read-pool')
let detail = await request('GET', tripPath), plan = await request('GET', `${tripPath}/capacity-plan`)
check('One actual thirty-space trip and two original real pallets', Number(detail.trip.maxPalletSpaces) === 30 && plan.cargo.length === 2)
const planned = controlledPalletMeasurements(plan.cargo, detail.trip.stops).map(p => ({ ...p, palletSpaces: 9 }))
await request('POST', `${tripPath}/capacity-plan`, { recordVersion: plan.recordVersion, manifestHash: plan.manifestHash, pallets: planned, reason: 'TEST容量计划18托位;实际装载另行实测确认' })
detail = await request('GET', tripPath)
const offerDraft = await request('POST', POOL, { tripNo: fixture.tripNo, tripRecordVersion: detail.trip.recordVersion,
cutoffAt: new Date(Date.now() + 1800000).toISOString(), contactName: 'TEST调度', contactPhone: '19900000000', remark: context.marker, idempotencyKey: key('OFFER') }, dispatchers[0])
let offer = (await request('POST', `${POOL}/${offerDraft.offer.offerNo}/publish`, { tripRecordVersion: offerDraft.offer.tripRecordVersion,
offerRecordVersion: offerDraft.offer.recordVersion, idempotencyKey: key('PUBLISH') }, dispatchers[0])).offer
proof.offerNo = offer.offerNo
const destinationKey = plan.cargo[0].destinationKey
check('Owned physical sources share exact destination', plan.cargo.every(row => row.destinationKey === destinationKey))
assertPoolRemaining(offer, destinationKey, 12)
const offerDetail = async token => request('GET', `${POOL}/${proof.offerNo}`, undefined, token)
const createInquiry = async (index, type, spaces, label, incomplete = false, cargoOverrides = {}) => {
offer = (await offerDetail(sales[index])).offer
const body = { tripRecordVersion: offer.tripRecordVersion, offerRecordVersion: offer.recordVersion,
customerId: fixture.members[index].customerId, businessType: type, deliveryMode: 'SELF_DELIVERY', expectedPalletCount: incomplete ? null : 1,
estimatedArrivalAt: new Date(Date.now() + 900000).toISOString(), idempotencyKey: key(label),
cargo: { destinationKey, cargoName: `${context.marker}-${label}`, packageCount: incomplete ? null : 1, palletSpaces: spaces,
weightKg: 100, volumeCbm: 1, priceBasis: 'PALLET', unitPrice: 30, currency: 'CNY', contactName: 'TEST货主', contactPhone: '19900000000', remark: context.marker, ...cargoOverrides } }
return (await request('POST', `${POOL}/${proof.offerNo}/inquiries`, body, sales[index])).inquiry
}
const incomplete = await createInquiry(0, 'US_TRUCK', null, 'UNKNOWN', true)
check('Unknown box/pallet counts are null instead of fabricated one', incomplete.cargo.packageCount == null && incomplete.expectedPalletCount == null)
proof.covered.push('unknown-load-stays-null')
const first = await createInquiry(0, 'LOCAL_LOOSEBOARD', 8, 'LOOSE')
await reject('Other sales cannot read this inquiry', 'GET', `${POOL}/inquiries/${first.bookingNo}`, undefined, sales[1])
const second = await createInquiry(1, 'US_TRUCK', 4, 'TRUCK-A')
check('Same display name does not collapse distinct sales identities', first.salesOwnerUserId && second.salesOwnerUserId && first.salesOwnerUserId !== second.salesOwnerUserId)
proof.covered.push('two-sales-owner-isolation')
const latest = no => request('GET', `${POOL}/inquiries/${no}`, undefined, dispatchers[0])
const command = (inquiry, label) => ({ tripRecordVersion: inquiry.tripRecordVersion, offerRecordVersion: inquiry.offerRecordVersion,
recordVersion: inquiry.recordVersion, idempotencyKey: key(label) })
await verifyAssignedVehicleGuards(context, fixture, tripProof, { request, reject, check, offerDetail: () => offerDetail(dispatchers[0]), proof })
await verifyPauseAndCutoff({ offerNo: proof.offerNo, bookingNo: first.bookingNo, salesToken: sales[0],
dispatcherToken: dispatchers[0], request, reject, check, key, proof })
proof.dimensionRejections = []
for (const dimension of ['weightKg', 'volumeCbm']) {
const available = (await offerDetail(dispatchers[0])).offer.destinations.find(row => row.destinationKey === destinationKey)?.remaining
check('Independent dimension test has ordinary spare pallet weight and volume capacity', Number(available?.palletSpaces) >= 1
&& Number(available?.weightKg) > 100 && Number(available?.volumeCbm) > 1)
const inquiry = await createInquiry(1, 'US_TRUCK', 1, `OVER-${dimension}`, false, { [dimension]: Number(available[dimension]) + 1 })
const before = await latest(inquiry.bookingNo), capacityBefore = (await offerDetail(dispatchers[0])).offer
const response = await reject(`${dimension} overrun cannot be hidden by spare pallet spaces`, 'POST', `${POOL}/inquiries/${inquiry.bookingNo}/confirm`,
command(before, `REJECT-${dimension}`), dispatchers[1])
check('Independent load rejection is an actual capacity-limit conflict', /超过.*限制|容量|载重|容积/.test(id(response.body?.message)))
const after = await latest(inquiry.bookingNo)
check('Dimension refusal leaves draft source and all prior reservations unchanged', inquiryFact(before) === inquiryFact(after)
&& after.status === 'DRAFT' && !after.businessOrderNo && capacitySnapshot(capacityBefore) === capacitySnapshot((await offerDetail(dispatchers[0])).offer))
proof.dimensionRejections.push({ dimension, bookingNo: inquiry.bookingNo, submitted: inquiry.cargo[dimension], available: available[dimension], status: 'REJECTED_UNCHANGED' })
}
proof.covered.push('weight-and-volume-independent-capacity-rejections')
let current = await latest(first.bookingNo), firstCommand = command(current, 'CONFIRM-LOOSE')
await reject('Sales cannot confirm capacity', 'POST', `${POOL}/inquiries/${first.bookingNo}/confirm`, firstCommand, sales[0])
const firstConfirmed = await request('POST', `${POOL}/inquiries/${first.bookingNo}/confirm`, firstCommand, dispatchers[0])
assertPoolRemaining(firstConfirmed.offer, destinationKey, 4)
proof.covered.push('same-segment-plan-18-of-30-reserve-8-leaves-4')
const oversized = await createInquiry(1, 'US_TRUCK', 5, 'OVER-CAPACITY')
await reject('Five spaces cannot fit last four', 'POST', `${POOL}/inquiries/${oversized.bookingNo}/confirm`, command(await latest(oversized.bookingNo), 'REJECT-FIVE'), dispatchers[1])
const rival = await createInquiry(0, 'US_TRUCK', 4, 'TRUCK-B')
const contenders = [await latest(second.bookingNo), await latest(rival.bookingNo)]
const race = await Promise.all(contenders.map((row, index) => raw('POST', `${POOL}/inquiries/${row.bookingNo}/confirm`, command(row, `RACE-${index}`), dispatchers[index])))
check('Two dispatcher race confirms exactly one remaining reservation', race.filter(success).length === 1 && race.filter(denied).length === 1)
const winner = race.find(success).body.data.inquiry
offer = (await offerDetail(dispatchers[0])).offer; assertPoolRemaining(offer, destinationKey, 0)
proof.orders = [firstConfirmed.inquiry, winner]
proof.covered.push('two-dispatchers-last-four-race')
const closed = await request('POST', `${POOL}/${proof.offerNo}/close`, { tripRecordVersion: offer.tripRecordVersion,
offerRecordVersion: offer.recordVersion, idempotencyKey: key('CLOSE'), reason: context.marker }, dispatchers[0])
check('Offer closed without cancelling commitments', closed.offer.status === 'CLOSED')
const replay = await request('POST', `${POOL}/inquiries/${first.bookingNo}/confirm`, firstCommand, dispatchers[0])
assertSingleCommercial(firstConfirmed.inquiry, replay.inquiry)
check('Successful confirmation still replays after close', replay.replayed === true)
const history = await request('GET', `${POOL}/inquiries?page=1&size=100&scope=MINE`, undefined, sales[0])
check('Sales retains its own history after close', history.records.some(row => row.bookingNo === first.bookingNo)
&& history.records.every(row => row.salesOwnerUserId === first.salesOwnerUserId))
proof.covered.push('close-and-successful-confirmation-replay')
const poolMembers = []
for (const [index, order] of proof.orders.entries()) {
const source = await request('GET', businessPath(order))
check('Pool commercial order has source arrangement and waits for real inbound', source.summary.status === 'WAITING_INBOUND'
&& source.summary.fleetArrangement?.bookingNo === order.bookingNo && !source.summary.fleetArrangement.incomingNo)
const oldStatusPath = businessPath(order).replace('?', '/status?')
await reject('Old commercial status mutation cannot bypass pool workflow', 'PATCH', oldStatusPath,
{ version: source.summary.version, status: 'CANCELLED', idempotencyKey: key(`OLD-CANCEL-${index}`), remark: context.marker })
poolMembers.push(await receiveAndBindPoolOrder(context, fixture, order, index, { request, check, key, readInventory }))
}
proof.covered.push('old-manual-order-endpoints-rejected', 'one-commercial-order-and-one-incoming', 'both-business-types-real-pda-receipt')
detail = await request('GET', tripPath)
for (const binding of detail.dispatches) detail = await request('POST', `${tripPath}/dispatches/${encode(binding.dispatchNo)}/review`, { recordVersion: detail.trip.recordVersion, decision: 'APPROVED' })
const members = [...fixture.members, ...poolMembers]
for (const member of members) {
await pickAll(context, fixture.pdaToken, member.dispatchNo)
const task = await request('GET', `/api/pda/v1/last-leg/dispatches/${encode(member.dispatchNo)}`, undefined, fixture.pdaToken)
check('Every owned and external pallet was actually picked', task.items.every(row => Number(row.pickedPackages) === Number(row.scheduledPackages)))
}
const loading = await request('GET', `${tripPath}/loading`)
check('All four original physical pallets present for actual loading', loading.cargo.length === 4 && !loading.blockReason)
const loaded = await request('POST', `${tripPath}/loading`, { recordVersion: loading.recordVersion, manifestHash: loading.manifestHash,
pallets: controlledPalletMeasurements(loading.cargo, detail.trip.stops), reason: 'TEST受控实测每实托1托位;不同于之前18托位接单计划' })
detail = await request('POST', `${tripPath}/loading-review`, { recordVersion: loaded.recordVersion, decision: 'APPROVED' })
const departBody = { recordVersion: detail.trip.recordVersion, idempotencyKey: key('DEPART'), remark: context.marker }
const departPath = `/api/pda/v1/last-leg/dispatches/${encode(fixture.members[0].dispatchNo)}/depart`
const departed = await request('POST', departPath, departBody, fixture.pdaToken)
check('One whole-trip actual departure includes exactly four dispatches', departed.status === 'IN_TRANSIT' && Number(departed.shippedPackages) === 4
&& departed.dispatches.length === 4 && members.every(member => departed.dispatches.some(row => row.dispatchNo === member.dispatchNo)))
check('Departure original-key replay is explicit', (await request('POST', departPath, departBody, fixture.pdaToken)).replayed === true)
for (const [index, member] of members.entries()) {
const path = `${API}/delivery-orders/${encode(member.deliveryNo)}?customerId=${encode(member.customerId)}`
const currentDelivery = await request('GET', path)
await request('PUT', path.replace('?', '/status?'), { status: 'DELIVERING', expectedVersion: currentDelivery.order.version, remark: context.marker })
const signed = await saveReceiptAndComplete(context, { deliveryNo: member.deliveryNo }, member.customerId, index)
check('Receipt actually completes this delivery', signed?.order?.status === 'DELIVERED')
}
proof.covered.push('shared-real-departure-and-signature')
for (const [index, order] of proof.orders.entries()) {
const commercial = await request('GET', businessPath(order))
check('Each business type completes from actual signature', commercial.summary.status === 'COMPLETED'
&& commercial.summary.fleetArrangement?.fulfillmentStage === 'COMPLETED' && Number(commercial.summary.fleetArrangement.signedPackages) === 1)
const member = poolMembers[index], actual = await request('GET', `${API}/delivery-orders/${member.deliveryNo}?customerId=${order.customerId}`)
const fees = actual.fees.filter(row => row.feeType === 'DELIVERY_FEE' && row.feeDirection === 'RECEIVABLE')
check('One agreed receivable remains, no order/receipt duplicate fee', fees.length === 1 && Number(fees[0].amount) === 30 && fees[0].currency === 'CNY')
const merchant = merchantSessions.find(session => session.customerId === order.customerId)
const merchantPath = `/api/merchant/v1/last-leg/${order.businessType === 'US_TRUCK' ? 'us-truck-orders' : 'local-looseboard-orders'}/${encode(order.businessOrderNo)}`
const own = await request('GET', merchantPath, undefined, merchant.accessToken)
check('Client sees only own complete commercial arrangement', own.summary.orderNo === order.businessOrderNo && own.summary.fleetArrangement?.businessOrderNo === order.businessOrderNo)
}
proof.covered.push('business-completion-and-fee-uniqueness')
proof.poolMembers = poolMembers; proof.status = 'VERIFIED'
return proof
} catch (error) { proof.status = 'FAILED'; throw error }
finally {
for (const actor of proof.actors) {
if (actor.membershipId) await request('PUT', `${IAM}/users/${actor.membershipId}/status`, { status: 'DISABLED' })
if (actor.roleId) await request('PUT', `${IAM}/roles/${actor.roleId}/status`, { status: 'DISABLED' })
}
// The outer runner tracks and logs out all successful login sessions, including these.
actorSessions.length = 0; merchantSessions.length = 0
}
}
async function receiveAndBindPoolOrder(context, fixture, order, index, { request, check, key, readInventory }) {
const tripPath = `${API}/fleet/trips/${encode(fixture.tripNo)}`, bookingPath = `${tripPath}/bookings/${encode(order.bookingNo)}`
let bookingList = await request('GET', `${tripPath}/bookings`), booking = bookingList.records.find(row => row.bookingNo === order.bookingNo)
check('Pool commercial association survived into existing booking workflow', booking?.status === 'CONFIRMED')
const body = { tripRecordVersion: bookingList.tripRecordVersion, recordVersion: booking.recordVersion, estimatedArrivalAt: new Date(Date.now() + 300000).toISOString(),
deliveryInstructions: `${context.marker} TEST客户自送`, idempotencyKey: key(`INCOMING-${index}`) }
const created = await request('POST', `${bookingPath}/incoming`, body), incomingNo = created.booking.incomingNo
const repeated = await request('POST', `${bookingPath}/incoming`, body)
check('One incoming document on exact replay', incomingNo && repeated.replayed === true && repeated.booking.incomingNo === incomingNo)
const pdaPath = `/api/pda/v1/last-leg/warehouse-transfers/${encode(incomingNo)}`
const measured = await request('POST', `${pdaPath}/pallets/drafts`, { idempotencyKey: key(`MEASURE-${index}`), cartonCount: 1,
lengthCm: 100, widthCm: 100, heightCm: 100, volumeCbm: 1, grossWeightKg: 100 }, fixture.pdaToken)
check('One actual measured draft only', measured.pallets.length === 1 && measured.pallets[0].status === 'DRAFT')
const palletPath = `${pdaPath}/pallets/${measured.pallets[0].id}`
const generated = await request('POST', `${palletPath}/generate`, { idempotencyKey: key(`GENERATE-${index}`) }, fixture.pdaToken)
const pallet = generated.pallets[0]
await request('POST', `${palletPath}/bind`, { palletCode: pallet.palletNo, idempotencyKey: key(`BIND-PALLET-${index}`) }, fixture.pdaToken)
const uploaded = await context.requestMultipart('POST', `${pdaPath}/measurement-photos/files`, { name: `${context.marker}-pool-${index}-TEST.png`, type: 'image/png', content: PNG }, fixture.pdaToken)
check('Real TEST measurement file exists', success(uploaded) && uploaded.body.data?.fileId)
const received = await request('POST', `${pdaPath}/palletize`, { idempotencyKey: key(`PALLETIZE-${index}`), photoFileIds: [id(uploaded.body.data.fileId)] }, fixture.pdaToken)
check('Actual PDA receipt completed one original pallet', received.status === 'WAIT_CUSTOMER_CONFIRMATION' && received.pallets.length === 1 && received.pallets[0].status === 'COMPLETED')
const evidence = await request('GET', `${bookingPath}/incoming`)
await request('POST', `${bookingPath}/receipt-confirmation`, { tripRecordVersion: evidence.tripRecordVersion, recordVersion: evidence.recordVersion,
receiptHash: evidence.incoming.receiptHash, pallets: [{ warehousePalletId: id(pallet.warehousePalletId), palletSpaces: 1 }], unitPrice: 30, idempotencyKey: key(`RECEIPT-${index}`) })
const inventory = await readInventory({ marker: context.marker, customerId: order.customerId, warehouseId: fixture.warehouseId })
check('Immutable actual incoming STOCK_IN is unique', inventory.scope?.coverage === 'CUSTOMER_WAREHOUSE_COMPLETE'
&& inventory.ledger.filter(row => row.businessOrderNo === incomingNo && row.businessType === 'STOCK_IN' && Number(row.deltaPackages) === 1).length === 1)
const dispatchBase = `${API}/dispatches`, candidates = await request('GET', `${dispatchBase}/candidates?${new URLSearchParams({ sourceStage: 'RECEIVED', orderNo: incomingNo })}`)
check('Received candidate exactly belongs to this commercial incoming', candidates.length === 1 && candidates[0].inboundCode === incomingNo
&& id(candidates[0].customerId) === order.customerId && id(candidates[0].warehousePalletId) === id(pallet.warehousePalletId))
const draftToken = key(`DRAFT-${index}`), container = await request('POST', `${dispatchBase}/drafts/containers`, { draftToken, capacity: 30 })
await request('POST', `${dispatchBase}/draft-locks/lock`, { planPalletId: id(candidates[0].planPalletId), draftToken, draftContainerId: id(container.id) })
const dispatch = await request('POST', `${dispatchBase}/pre-arrange`, { planPalletIds: [id(candidates[0].planPalletId)], draftToken })
bookingList = await request('GET', `${tripPath}/bookings`); booking = bookingList.records.find(row => row.bookingNo === order.bookingNo)
await request('POST', `${bookingPath}/dispatch`, { tripRecordVersion: bookingList.tripRecordVersion, recordVersion: booking.recordVersion,
dispatchNo: dispatch.dispatchNo, idempotencyKey: key(`BIND-DISPATCH-${index}`) })
let plan = await request('GET', `${tripPath}/capacity-plan`), detail = await request('GET', tripPath)
// Reconfirm actual current cargo after binding; remaining unbound bookings stay separately reserved.
await request('POST', `${tripPath}/capacity-plan`, { recordVersion: plan.recordVersion, manifestHash: plan.manifestHash,
pallets: controlledPalletMeasurements(plan.cargo, detail.trip.stops), reason: `${context.marker} 实收后重新核对,不重复扣外接预留` })
const path = `${dispatchBase}/${encode(dispatch.dispatchNo)}`, freight = await request('GET', `${path}/external-freight`)
check('The one pool commercial source owns the one agreed freight', freight.requiresDeliveryFee === false && freight.quotes.length === 1
&& freight.quotes[0].bookingNo === order.bookingNo && Number(freight.quotes[0].amount) === 30)
detail = await request('GET', tripPath)
const transfer = { idempotencyKey: key(`TRANSFER-${index}`), plannedDeliveryAt: localTime(21600000), deliveryMethod: 'TRUCK_PALLET', pickupMethod: 'WAREHOUSE_PICKUP',
loadingType: 'PALLET', vehicleType: 'TRUCK', vehicleSize: '30托', vehiclePlate: detail.trip.vehiclePlate, driverName: detail.trip.driverName,
driverPhone: detail.trip.driverPhone, appointmentAccount: 'SELF', externalFreightHash: freight.snapshotHash, fees: [], remark: context.marker }
const converted = await request('POST', `${path}/transfer-delivery`, transfer)
check('Pool source produces exactly one outbound and delivery', converted.outboundNo && converted.deliveries.length === 1)
const replay = await request('POST', `${path}/transfer-delivery`, transfer)
check('Transfer replay reuses outbound and delivery', replay.replayed === true && replay.outboundNo === converted.outboundNo && replay.deliveries[0].deliveryNo === converted.deliveries[0].deliveryNo)
return { customerId: order.customerId, bookingNo: order.bookingNo, businessOrderNo: order.businessOrderNo, incomingNo, dispatchNo: dispatch.dispatchNo,
outboundNo: converted.outboundNo, deliveryNo: converted.deliveries[0].deliveryNo, palletId: id(pallet.warehousePalletId), palletNo: pallet.palletNo }
}
export async function runPoolAcceptance(config, { env = process.env, outputRoot = ROOT, runner = runCrossCustomer } = {}) {
if (!config.execute) return poolPlan(config)
const credentials = new Map(); let restoreWrites
try {
const result = await runner(config, { env, outputRoot,
contextFactory: options => {
const context = new AcceptanceRun(options), original = context.request
context.request = async function (...args) {
const response = await original.apply(this, args)
if (args[0] === 'POST' && args[1] === `${IAM}/tenants` && success(response)) {
const data = response.body.data, customer = data.tenant || data.customer || data
credentials.set(id(customer.id || data.tenantId), { username: data.administratorUsername, password: data.temporaryPassword })
}
return response
}
const directory = join(outputRoot, 'reports', `fleet-cross-${config.runId}`, 'pool-write-journal')
mkdirSync(directory)
restoreWrites = guardFleetWrites(context, async entry => {
const file = openSync(join(directory, `${entry.sequence}.json`), 'w')
try { writeFileSync(file, JSON.stringify(redactFleetWrite(entry), null, 2) + '\n'); fsyncSync(file) } finally { closeSync(file) }
}, { guardFetch: true })
return context
},
verify: (context, fixture, options) => poolScenario(context, fixture, { ...options, credentials }) })
result.scope = 'FLEET_CAPACITY_POOL_HTTP'; result.fullAcceptance = false
result.remainingSeparateAcceptance = [...POOL_REMAINING]
const output = join(outputRoot, 'reports', `fleet-cross-${config.runId}`)
const sources = ['scripts/fleet-capacity-pool-test-env.mjs'].map(path => ({ path, sha256: createHash('sha256').update(readFileSync(join(ROOT, path))).digest('hex') }))
writeFileSync(join(output, 'pool-scope.json'), JSON.stringify({ plan: poolPlan(config), sources, success: result.success,
fullAcceptance: false, covered: result.createdResources?.fleetCapacityPool?.covered || [], remaining: POOL_REMAINING }, null, 2) + '\n', { flag: 'wx' })
return result
} finally { restoreWrites?.(); credentials.clear() }
}
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
try {
const config = parsePoolArguments(process.argv.slice(2)), result = await runPoolAcceptance(config)
console.log(JSON.stringify(config.execute ? { success: result.success, fullAcceptance: result.fullAcceptance, error: result.error,
summary: result.summary, covered: result.createdResources?.fleetCapacityPool?.covered, remaining: result.remainingSeparateAcceptance,
output: poolPlan(config).output } : result, null, 2))
if (config.execute && !result.success) process.exitCode = 1
} catch (error) { console.error(error.message); process.exitCode = 1 }
}