feat(fleet): add internal capacity pool and warehouse fulfillment bridge

This commit is contained in:
2026-10-07 16:46:56 +08:00
parent 564cfa107c
commit 001219460f
57 changed files with 3820 additions and 38 deletions
+2
View File
@@ -1,5 +1,7 @@
# 后端脚本管理与全量 HTTP 验收索引
`fleet-capacity-pool-test-env.mjs` 是内部拼车需求池 TEST 专项入口,默认 `--plan` 零网络;实际执行须显式 `--execute --environment TEST --run-id <新批次> --warehouse-id <实际海外仓ID> --destination-warehouse-id <目的仓ID>`,凭据由命令前的 `node --env-file=<明确TEST配置>` 提供,配置同时包含 Jenkins 只读取证凭据与 `FBA_CORE_JENKINS_AGENT`。创建隔离客户、销售、调度、预订及商业订单,验证容量争抢、销售隔离、命令重放、两类本土订单实际到仓/PDA收货/共享车次/出库签收和费用唯一;不部署、不写业务SQL。证据保存在 `reports/fleet-cross-<新批次>/`,包含 `pool-scope.json` 和写请求journal;未知结果停止,保留原样本与请求,不覆盖批次、不自动重造数据。结束退出独立会话并沿fixture清理其自有PDA访问,业务样本保留;页面及未覆盖矩阵单独报告,不将专项通过当成全量验收。离线保护由 `fleet-capacity-pool-test-env.test.mjs` 验证。
`inbound-plan-resource-fault-executor.mjs` 是单柜入仓计划资源确认故障的 TEST 环境执行入口,内部使用 `inbound-plan-resource-fault-runtime.py`。默认 `plan --plan <单柜计划.json>` 零网络;实际动作必须读取明确的TEST/Jenkins外部配置并传 `--confirm-test-runtime yes`。`preflight --routing <私有路由.json>` 只读校验Lastleg与Warehouse Overseas镜像/指纹、实际网络和Nacos common/lastleg配置摘要;`prepare`另要求 `--expected-image <已冻结镜像ID>` 及预检摘要。`activate`只临时改Lastleg的 `baiyue.warehouse.base-url`,配合 `mode --mode CONFIRM_ONCE --ttl 60` 精确注入一次confirm失败;业务转换和关闭重开页面由调用者独立操作,工具没有造数、SQL或业务写接口。`status/health/records`用于观测,结果未知先 `status/reconcile`,禁止因观察超时重复创建容器;`restore/verify-restored/finish`须按原镜像、完整配置/挂载/网络、Nacos及上游健康恢复后才停自有代理、清私有凭据。服务变更先记录意图,受限远端恢复状态目录为 `/tmp/chongchong-inbound-resource-<marker>`,由总控持独占Lastleg故障窗口;调用结果和业务证据另存本次独立 `reports/` 目录,失败保留、不覆盖,不与部署或其他共享业务写入并行。`inbound-plan-resource-fault-executor.test.mjs`、`inbound-plan-resource-fault-runtime.test.mjs` 为离线边界及恢复测试,不证明真实故障已跑通。
F33 分阶段助手的组合换标顺序为 `unit → unit-carton → bind`:只有完整逐箱作业事实后才可绑定实物托盘,不用默认数量补齐缺失事实。已明确拒绝的绑定仅允许当前已冻结原票、原批次、原命令摘要的 `F33_BIND_A0409_AFTER_UNIT` 恢复,必须同时核对不可变失败journal、完整回滚证明和新作业事实/原资源版本,恢复写入独立journal,不覆盖原失败。未知结果、其他拒绝和其他阶段仍禁止自动重试;响应message脱敏留存。该受限恢复只用于原TEST验收,不放宽生产接口或变更库存规则。
+12
View File
@@ -1,5 +1,17 @@
# Admin 测试环境 HTTP 模块
`fleet-capacity-pool-schema-contract.test.mjs` 是离线数据库合同:验证发布与持久命令唯一约束、唯一商业履约来源、可空实际数量、服务与统一基线一致、非破坏性结构补齐及独立销售/调度权限;不访问环境、不证明真实履约已通过。
## 当前新增脚本的执行边界
- 人工状态矩阵的 `FEE_CONFIRMING` 使用独立新订单,在测量提交成功后暂停审核,经正式 `fees/calculate` 生成费用;回查同一费用版本、入库客户身份、测量仍待审核及入仓码精确筛选结果。该状态是必验项,不再列入不可造数缺口;不发送费用、不推进该样本、不借历史数据。入口仍为 `--modules manual-state-matrix`,对应 `order-inbound-status-fixtures-contract.test.mjs` 和 `manual-state-matrix-contract.test.mjs`。
- `WAIT_PRICING` 另建保留原始 `APPROVED` 的订单,先形成未发布的 `FEE_CONFIRMING`,再通过客户实时渠道目录与报价选择同仓、同目的仓、同税制的不同渠道,以正式申报更新触发费用失效;回查 `WAIT_PRICING/UNPRICED`、新渠道及精确筛选。缺少可计价替换渠道、客户漂移或状态未收敛均失败;不写业务SQL,退出专用目录查询会话。该状态同样为必验项,原先借历史 `WAIT_SPLIT` 样本的兜底已移除。
- `grouped-dispatch-acceptance.mjs` 由默认 `phase4-overseas-dispatch` 在主链预排前调用。使用本轮独立未拣托盘,通过 `/drafts/pre-arrange` 验证错组拒绝、原草稿不变、每组独立排车、同命令重放及数量/来源;成功响应明确的本轮未拣明细通过正式 API 移除并释放锁,保留空对照单据。写请求超时或 5xx 保留资源,不自动重试或清理未知结果。对应 `grouped-dispatch-acceptance-contract.test.mjs`。
- `fleet-trip-finance-acceptance.mjs` 由默认 `phase5-overseas-fulfillment` 结案后调用。只读本轮实际发车车次及已冻结派送来源,核对 AVAILABLE/BILLED/SETTLED 来源、原币应收、明确归属实收与单据,未分配实收保持未知(null 或 NON_NULL 响应省略);前后车次、资源、审核不得变化。失败保留 CHECKING 证据,不创建账单、收款或财务分配。对应 `fleet-trip-finance-acceptance-contract.test.mjs` 的部分/全额付款和受限权限反例不代替真实样本;默认链没有独立实付样本,不能据此宣称付款生命周期或真实权限边界已经验收。
两者都是内部模块,没有独立 CLI;沿 Admin 原入口的 TEST 配置、统一会话、JSON 报告与权限执行。依赖缺失、版本未部署或前置链失败时明确失败,不借历史样本或生成模拟成功。源码准备与实际执行分开记录;离线合同不能代替真实 HTTP。开始验收后,先跑新增合同及脚本文档门禁,再跑相应依赖链和全量;当前执行结果仅维护在测试环境部署主契约及其原始报告中。
`fleet-handover-acceptance.mjs` 是车辆专项实际DEPART后的内部交接助手,由`../fleet-test-env-acceptance.mjs`调用;仅接受本轮已验证车次/派送及自有PDA会话,核对原DEPART/费用不变、真实剩余货物和接替资源、同键重放及旧资源分别释放。调用方必须注入发送前持久化检查点,落盘失败不发送HTTP;仅保存脱敏原命令及附件摘要。签收前导出本轮`handover-fixture.json`供页面/运营统计检查,成本验证与账号清理由同一入口编排;失败保留证据,不借旧车次或自动续跑。对应`fleet-handover-acceptance-contract.test.mjs`为离线合同。
`fleet-quotes-operations.mjs` 的`runQuoteSession`使用独立TEST登录及本轮客户准备,`run`只验证报价增改/状态/历史及原子CSV;`verifyOperations`另需当前轮DEPART的独立托位/货量/原币成本期望,报价通过不代表运营统计通过。后者默认只读,实测更正必须显式传入写入开关和独立幂等命名空间;不推算财务实付。由专项编排器提供持久化检查点和独占报告目录,原请求未知时不自动重造样本,结束退出自身会话。对应`fleet-quotes-operations-contract.test.mjs`不访问真实环境。
@@ -0,0 +1,57 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { readInitialization, table, seedRows } from '../sql-baseline-contract-helpers.mjs'
const overseas = readInitialization('OVERSEAS')
const cn = readInitialization()
const read = relative => readFileSync(new URL(`../../${relative}`, import.meta.url), 'utf8')
test('pool has a unique publication per trip and full durable command identity, no second capacity balance', () => {
const offer = table(overseas, 'baiyue_lastleg', 'll_fleet_capacity_offer')
assert.match(offer, /UNIQUE KEY\s+\w+\(trip_id\)/i)
assert.match(offer, /UNIQUE KEY\s+\w+\(offer_no\)/i)
assert.match(offer, /published_fingerprint CHAR\(64\) NULL/i)
assert.doesNotMatch(offer, /remaining_pallet|available_pallet|reserved_pallet/i)
const command = table(overseas, 'baiyue_lastleg', 'll_fleet_capacity_offer_command')
assert.match(command, /UNIQUE KEY\s+\w+\(trip_id,operator_id,action,scope_no,idempotency_key\)/i)
assert.match(command, /request_hash CHAR\(64\) NOT NULL/i)
assert.match(command, /result_json JSON NOT NULL/i)
})
test('one booking maps to exactly one commercial order and one actual incoming source', () => {
const link = table(overseas, 'baiyue_lastleg', 'll_fleet_pool_order_link')
for (const columns of ['booking_id', 'booking_no', 'business_type,business_order_id', 'incoming_no', 'lastleg_order_id']) {
assert.match(link, new RegExp(`UNIQUE KEY\\s+\\w+\\(${columns}\\)`, 'i'))
}
assert.match(link, /incoming_no VARCHAR\(64\) NULL/i)
assert.match(link, /lastleg_order_id BIGINT NULL/i)
assert.match(link, /actual_pallet_count INT NULL/i)
assert.match(link, /actual_pallet_spaces INT NULL/i)
assert.doesNotMatch(link, /(?:agreed_amount|receivable_amount|paid_amount)\s+DECIMAL/i)
const booking = table(overseas, 'baiyue_lastleg', 'll_fleet_external_booking')
assert.match(booking, /sales_owner_user_id VARCHAR\(64\) NULL/i)
assert.match(booking, /expected_pallet_count INT NULL/i)
})
test('module and unified fresh baselines contain the exact same new table definitions', () => {
const module = read('lastleg-service/src/main/resources/db/lastleg.sql')
for (const name of ['ll_fleet_capacity_offer', 'll_fleet_capacity_offer_command', 'll_fleet_pool_order_link']) {
const definition = table(overseas, 'baiyue_lastleg', name)
assert.ok(module.includes(definition), `${name}: module and unified contract drift`)
}
const sql = read('deploy/sql/migrations/overseas/baiyue_lastleg/V10__fleet_capacity_pool.sql')
assert.doesNotMatch(sql.replace(/^--.*$/gm, ''), /\b(?:DROP|TRUNCATE|DELETE|UPDATE|REPLACE)\b/i)
assert.match(sql, /ADD COLUMN expected_pallet_count INT NULL/i)
assert.match(sql, /ADD CONSTRAINT fk_fleet_booking_offer/i)
})
test('pool permissions are independent of trip details and default only to the platform administrator', () => {
const permissions = seedRows(cn, 'iam_permission').filter(row => String(row.permission_code).startsWith('platform:fleet:capacity-offer:'))
assert.equal(permissions.length, 5)
assert.deepEqual(permissions.map(row => row.permission_code.split(':').at(-1)).sort(), ['inquiry-cancel','inquiry-create','inquiry-edit','list','manage'])
const ids = new Set(permissions.map(row => String(row.id)))
const grants = seedRows(cn, 'iam_role_permission').filter(row => ids.has(String(row.permission_id)))
assert.equal(grants.length, 5)
assert.ok(grants.every(row => String(row.role_id) === '4000001' && String(row.tenant_id) === '0'))
})
@@ -9,7 +9,7 @@ const domesticSchemas = ['baiyue_auth', 'baiyue_iam', 'baiyue_order', 'baiyue_wa
const overseasSchemas = ['baiyue_lastleg', 'baiyue_lastlag_warehouse']
test('两个入口分别只操作八个主业务库与两个仓储业务库,包括限定名引用', () => {
for (const [sql, allowed, other, count] of [[cn, domesticSchemas, overseasSchemas, 226], [overseas, overseasSchemas, domesticSchemas, 154]]) {
for (const [sql, allowed, other, count] of [[cn, domesticSchemas, overseasSchemas, 226], [overseas, overseasSchemas, domesticSchemas, 157]]) {
assert.deepEqual([...sql.matchAll(/^CREATE DATABASE IF NOT EXISTS (\w+)/gm)].map(m => m[1]), allowed)
assert.deepEqual([...new Set([...sql.matchAll(/^USE (\w+);/gm)].map(m => m[1]))], allowed)
const body = sql.replace(/^--.*$/gm, '')
@@ -39,7 +39,7 @@ test('纯初始化禁止迁移、数据回填、清理、重复建表和提前
assert.doesNotMatch(body.replace(/^CREATE TRIGGER [\s\S]*?\$\$/gm, ''), /^\s*(?:UPDATE|DELETE FROM|REPLACE INTO)\b/gmi)
assert.doesNotMatch(body, /CREATE TABLE IF NOT EXISTS/i)
assert.equal((body.match(/AS initialization_status/g) ?? []).length, 1)
assert.match(body.trim(), /SELECT 'PASS' AS initialization_status, '(CN|OVERSEAS)' AS region, (226|154) AS initialized_tables;$/)
assert.match(body.trim(), /SELECT 'PASS' AS initialization_status, '(CN|OVERSEAS)' AS region, (226|157) AS initialized_tables;$/)
}
})
@@ -121,7 +121,7 @@ test('平台管理员由外部bcrypt引导,强制首登改密且不创建客
})
test('必要系统种子完整保留,但不写入示例资源与业务事实', () => {
for (const [name, count] of Object.entries({ iam_role: 8, iam_permission: 412, iam_role_permission: 593,
for (const [name, count] of Object.entries({ iam_role: 8, iam_permission: 417, iam_role_permission: 598,
iam_data_scope: 7, wh_warehouse: 318, wh_warehouse_address: 318, wh_service_capability: 4,
fl_fee_item: 112, fl_price_channel_catalog: 15, fl_price_origin_alias: 7, fl_price_warehouse_mapping: 4,
file_export_task_guard: 1, fin_subject: 1 })) assert.equal(seedRows(cn, name).length, count, name)
+30 -2
View File
@@ -628,7 +628,7 @@ foreach ($route in @(
Assert-Matches $clientOverseasRoutes $routePattern "Client 正式路由 $route"
}
# 四端统一 Gateway,保留服务与会话均来自 TEST 配置。
# 四端统一 Gateway;Compose默认TEST,生产只走Jenkins固定环境及部署白名单。
$unifiedGateway = Read-RequiredText (Join-Path $backendRoot 'gateway/src/main/resources/application.yml')
foreach ($gatewayPath in @('/api/merchant/v1/last-leg/**', '/api/platform/v1/last-leg/**', '/api/pda/v1/last-leg/**', '/api/platform/v1/warehouse-admin/**', '/api/platform/v1/overseas-warehouse-resources/**')) {
Assert-Contains $unifiedGateway $gatewayPath '统一 Gateway 路由'
@@ -638,9 +638,37 @@ Assert-Contains $unifiedGateway 'uri: lb://warehouse-overseas-service' '海外
$serviceMap = Read-RequiredText (Join-Path $backendRoot 'deploy/service-map.yml')
$testCompose = Read-RequiredText (Join-Path $backendRoot 'deploy/docker-compose.yml')
$serviceJenkinsfile = Read-RequiredText (Join-Path $backendRoot 'Jenkinsfile.service')
$dispatchJenkinsfile = Read-RequiredText (Join-Path $backendRoot 'Jenkinsfile')
$builtServiceDeploy = Read-RequiredText (Join-Path $backendRoot 'deploy/scripts/deploy-built-service.sh')
$batchServiceDeploy = Read-RequiredText (Join-Path $backendRoot 'deploy/scripts/deploy-services.sh')
Assert-Contains $testCompose 'AUTH_SESSION_VALIDATION_URL: http://auth:9100/internal/auth/accounts/sessions/validate' '统一权威会话校验'
Assert-Contains $testCompose 'REDIS_TOKEN_KEY_PREFIX' '统一会话前缀'
Assert-Contains $testCompose 'SPRING_PROFILES_ACTIVE: test' '唯一 TEST profile'
Assert-Matches $testCompose '(?m)^ SPRING_PROFILES_ACTIVE: \$\{DEPLOY_ENV:-test\}\r?$' 'Compose业务profile默认TEST'
Assert-Matches $testCompose '(?m)^ SPRING_PROFILES_ACTIVE: \$\{DEPLOY_ENV:-test\},cn\r?$' '统一Gateway默认TEST及cn路由profile'
Assert-Contains $testCompose 'name: firstleg-${DEPLOY_ENV:-test}' 'Compose项目默认TEST并按环境隔离'
Assert-Contains $builtServiceDeploy 'case "$DEPLOY_ENV" in test|production) ;; *)' '单服务部署环境白名单'
Assert-Contains $builtServiceDeploy 'Only test and production are supported' '非法部署环境拒绝'
Assert-Contains $batchServiceDeploy '[ "$1" = "test" ] || exit 2' '批量部署入口仍限TEST'
Assert-Matches $dispatchJenkinsfile '(?s)case ''backend-java'':\s*return ''test''.*?case ''backend-java-prod'':\s*return ''production''' '总控Job固定TEST及生产映射'
Assert-Contains $dispatchJenkinsfile 'if (!resolvedEnvironment || !resolvedJobPrefix)' '未知总控Job拒绝'
Assert-Contains $dispatchJenkinsfile 'params.ENVIRONMENT != resolvedEnvironment' '总控参数不得切换固定环境'
Assert-Contains $dispatchJenkinsfile "return [fixedEnvironmentForJob(jobBaseName) ?: 'test']" '总控环境选项默认TEST'
Assert-Contains $serviceJenkinsfile "if (jobBaseName?.startsWith('baiyue-firstleg-prod-')) return 'production'" '生产单服务Job固定映射'
Assert-Contains $serviceJenkinsfile "!jobBaseName.startsWith('baiyue-firstleg-dev-')" '单服务Job不接受开发环境前缀'
Assert-Contains $serviceJenkinsfile 'if (!resolvedEnvironment)' '未知单服务Job拒绝'
Assert-Contains $serviceJenkinsfile 'params.ENVIRONMENT != resolvedEnvironment' '单服务参数不得切换固定环境'
Assert-Contains $serviceJenkinsfile "return [fixedEnvironmentForServiceJob(jobBaseName) ?: 'test']" '单服务环境选项默认TEST'
foreach ($mapping in @(
@{ Text = $dispatchJenkinsfile; Function = 'fixedEnvironmentForJob'; Scope = '总控环境映射' },
@{ Text = $serviceJenkinsfile; Function = 'fixedEnvironmentForServiceJob'; Scope = '单服务环境映射' }
)) {
$bodyMatch = [regex]::Match($mapping.Text, '(?ms)^def ' + $mapping.Function + '\([^\r\n]*\)\s*\{(.*?)^\}')
Assert-Matches $bodyMatch.Value 'return ''test''' ($mapping.Scope + '默认TEST能力')
Assert-Matches $bodyMatch.Value 'return null' ($mapping.Scope + '未知Job无环境')
foreach ($returned in [regex]::Matches($bodyMatch.Value, 'return ''([^'']+)''')) {
Assert-Matches $returned.Groups[1].Value '^(test|production)$' ($mapping.Scope + '仅允许TEST及生产')
}
}
foreach ($retired in @('gateway-overseas:', 'event-bridge-service:')) {
Assert-NotContains $serviceMap $retired '部署映射'
Assert-NotContains $testCompose $retired 'TEST Compose'
+448
View File
@@ -0,0 +1,448 @@
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 }
}
@@ -0,0 +1,97 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { parsePoolArguments, poolPlan, runPoolAcceptance, assertPoolRemaining, assertSingleCommercial,
shrinkingVehicleRequest, expiryWaitMs, POOL_COVERAGE, POOL_REMAINING } from './fleet-capacity-pool-test-env.mjs'
test('default and explicit plans make no network requests or invoke the mutating runner', async () => {
const original = globalThis.fetch
globalThis.fetch = async () => { throw Error('unexpected network') }
try {
for (const args of [[], ['--plan']]) {
const config = parsePoolArguments(args)
const plan = await runPoolAcceptance(config, { runner: async () => { throw Error('unexpected runner') } })
assert.equal(plan.execute, false)
assert.equal(plan.environment, 'TEST')
assert.equal(plan.runId, 'plan')
assert.match(plan.boundary, /no network or filesystem writes/)
}
} finally { globalThis.fetch = original }
})
test('execution needs explicit TEST, fresh run ID and current warehouse identifiers', () => {
for (const args of [
['--execute'], ['--execute', '--run-id', 'fresh', '--warehouse-id', '10', '--destination-warehouse-id', '11'],
['--execute', '--environment', 'gray', '--run-id', 'fresh', '--warehouse-id', '10', '--destination-warehouse-id', '11'],
['--execute', '--environment', 'TEST', '--run-id', '../reuse', '--warehouse-id', '10', '--destination-warehouse-id', '11'],
['--execute', '--plan'], ['--resume', 'old-report'],
]) assert.throws(() => parsePoolArguments(args))
const parsed = parsePoolArguments(['--execute', '--environment', 'TEST', '--run-id', 'pool-r1', '--warehouse-id', '10', '--destination-warehouse-id', '11'])
assert.equal(parsed.execute, true)
assert.equal(parsed.runId, 'pool-r1')
})
test('plan distinguishes implemented HTTP scope from remaining whole-feature acceptance', () => {
const plan = poolPlan(parsePoolArguments([]))
assert.deepEqual(plan.coveredByThisEntry, POOL_COVERAGE)
assert.deepEqual(plan.remainingSeparateAcceptance, POOL_REMAINING)
assert.ok(plan.coveredByThisEntry.includes('both-business-types-real-pda-receipt'))
assert.ok(plan.remainingSeparateAcceptance.includes('full-UI-1920x1080-and-network-request-budget'))
assert.ok(plan.coveredByThisEntry.includes('weight-and-volume-independent-capacity-rejections'))
assert.ok(plan.coveredByThisEntry.includes('paused-create-edit-confirm-rejected'))
assert.ok(plan.coveredByThisEntry.includes('expired-cutoff-create-edit-confirm-rejected'))
assert.ok(plan.coveredByThisEntry.includes('assigned-vehicle-disable-and-shrink-rejected'))
assert.ok(plan.remainingSeparateAcceptance.includes('out-of-band-assigned-vehicle-drift-not-constructible-through-supported-api'))
assert.match(plan.boundary, /unknown outcome/)
assert.match(plan.boundary, /No automatic rerun or resume/)
})
test('remaining capacity uses unique current destination and rejects unknown, other-stop or duplicate rows', () => {
const offer = { capacityKnown: true, destinations: [{ destinationKey: 'FBA:A', remaining: { palletSpaces: 4 } },
{ destinationKey: 'FBA:B', remaining: { palletSpaces: 30 } }] }
assert.doesNotThrow(() => assertPoolRemaining(offer, 'FBA:A', 4))
for (const change of [o => { o.capacityKnown = false }, o => { o.destinations[0].remaining = null },
o => { o.destinations[0].destinationKey = 'FBA:C' }, o => { o.destinations.push(structuredClone(o.destinations[0])) },
o => { o.destinations[0].remaining.palletSpaces = 5 }]) {
const altered = structuredClone(offer); change(altered)
assert.throws(() => assertPoolRemaining(altered, 'FBA:A', 4), /EXACT_SEGMENT/)
}
})
test('successful confirmation replay must preserve commercial, booking, business type and customer identity', () => {
const before = { bookingNo: 'FB-1', businessOrderNo: 'LB-1', businessType: 'LOCAL_LOOSEBOARD', customerId: '9' }
assert.doesNotThrow(() => assertSingleCommercial(before, { ...before }))
for (const field of Object.keys(before)) assert.throws(() => assertSingleCommercial(before, { ...before, [field]: 'OTHER' }), /IDENTITY_CHANGED/)
assert.throws(() => assertSingleCommercial({ ...before, businessOrderNo: null }, { ...before }), /IDENTITY_CHANGED/)
})
test('default executor rejects non-TEST process configuration before any fixture writes', async () => {
const config = parsePoolArguments(['--execute', '--environment', 'TEST', '--run-id', 'pool-r1', '--warehouse-id', '10', '--destination-warehouse-id', '11'])
await assert.rejects(runPoolAcceptance(config, { env: { CHONGCHONG_ENV: 'prod' } }), /Explicit TEST configuration/)
})
test('vehicle shrink command preserves the authoritative full request and only changes capacity by one', () => {
const vehicle = { id: '7', recordVersion: 11, plateNumber: 'TEST-7', model: 'Box Truck', fuelConsumptionPer100Km: 12.5,
mileageKm: 100, driverOrDispatchCompany: 'TEST', phone: null, region: 'OVERSEAS', ownership: 'OWNED', remark: 'own fixture',
maxPalletSpaces: 30, maxLoadKg: 18000, maxVolumeCbm: 90, status: 'IN_SERVICE' }
const request = shrinkingVehicleRequest(vehicle)
assert.equal(request.maxPalletSpaces, 29)
for (const field of Object.keys(request).filter(name => name !== 'maxPalletSpaces')) assert.equal(request[field], vehicle[field])
assert.equal('status' in request, false)
assert.equal('id' in request, false)
assert.equal(vehicle.maxPalletSpaces, 30)
})
test('vehicle matrix refuses incomplete snapshots instead of inventing capacity for a write', () => {
for (const input of [null, {}, { id: '7', recordVersion: 1, maxPalletSpaces: 30 },
{ id: '7', recordVersion: 1, maxPalletSpaces: 1, maxLoadKg: 18000, maxVolumeCbm: 90 }]) {
assert.throws(() => shrinkingVehicleRequest(input), /SNAPSHOT_REQUIRED/)
}
})
test('cutoff wait is bounded and follows the returned instant without negative or unbounded sleeps', () => {
const now = Date.parse('2030-01-01T08:00:00Z')
assert.equal(expiryWaitMs('2030-01-01T08:00:08Z', now), 8300)
assert.equal(expiryWaitMs('2030-01-01T07:59:00Z', now), 0)
assert.throws(() => expiryWaitMs('invalid', now), /INVALID_SERVER_CUTOFF/)
assert.throws(() => expiryWaitMs('2030-01-01T08:10:00Z', now), /BOUNDED_WINDOW/)
})