fix(fleet): preserve unknown commercial facts for pooled freight

This commit is contained in:
2026-10-07 19:38:56 +08:00
parent cfdedd8861
commit c216f393a7
14 changed files with 123 additions and 13 deletions
+2
View File
@@ -1,5 +1,7 @@
# 单测试环境数据库空库初始化
`migrations/overseas/baiyue_lastleg/V12__preserve_unknown_pool_order_fields.sql` 仅放开散板打盘需求及两类本土订单柜型三列为空、默认空,供未提供这些事实的拼车商业订单使用;普通订单输入校验仍由原服务端请求合同执行。对应源及统一空库基线同步最终定义,不回填已生成订单或把托盘数量解释成打盘服务需求。
内部拼车需求池的当前结构在 Lastleg 源基线和海外统一空库基线同步维护,独立五项权限在 IAM 源种子和国内统一基线维护。已存在基础表的指定环境结构补齐使用 `migrations/overseas/baiyue_lastleg/V10__fleet_capacity_pool.sql` 与 `migrations/cn/baiyue_iam/V4__fleet_capacity_offer_permissions.sql`;执行前确认三张新表、六个新增字段均未存在,并单独核对目标环境。随后 `V11__align_capacity_pool_collation.sql` 将预订与商业桥的客户关联字段显式统一为 `utf8mb4_unicode_ci`,与原订单域一致,避免依赖MySQL实例默认排序规则导致JOIN失败。它们不回填、删除或重置业务数据,不能重放整个空库基线。候选文件存在不表示已经执行。
## 唯一入口与区域边界
@@ -0,0 +1,10 @@
-- Region: OVERSEAS
-- Schema: baiyue_lastleg
-- Purpose: 拼车商业单未提供的柜型和打盘需求保持未知
-- Preconditions: 既有两类商业订单表存在,普通订单仍由服务端执行原字段校验
-- Postconditions: 三列允许NULL且默认NULL;不回填或覆盖既有订单事实
ALTER TABLE ll_local_looseboard_order
MODIFY COLUMN pallet_required VARCHAR(8) NULL DEFAULT NULL COMMENT '是否需要打盘:YES或NO;拼车来源未提供时为空',
MODIFY COLUMN container_type VARCHAR(32) NULL DEFAULT NULL COMMENT '柜型;拼车来源未提供时为空,不推断为整柜';
ALTER TABLE ll_us_truck_order
MODIFY COLUMN container_type VARCHAR(32) NULL DEFAULT NULL COMMENT '柜型;拼车来源未提供时为空,不推断为整柜';
@@ -1341,13 +1341,13 @@ CREATE TABLE ll_local_looseboard_order (
lastleg_order_id bigint DEFAULT NULL COMMENT '关联尾程主订单ID',
order_no varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '本土散板订单号',
transport_mode varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '运输方式:拼车或优先拼载',
pallet_required varchar(8) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '是否需要打盘:YES或NO',
pallet_required varchar(8) COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '是否需要打盘:YES或NO;拼车来源未提供时为空',
transport_type varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '运输类型',
origin varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '起运地',
origin_address varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '起运详细地址',
destination varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '目的地',
destination_address varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '目的详细地址、码头或仓号',
container_type varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '40HQ' COMMENT '平台统一柜型:40GP、40HQ、45HQ',
container_type varchar(32) COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '柜型;拼车来源未提供时为空,不推断为整柜',
package_count int NOT NULL DEFAULT '0' COMMENT '货物件数',
total_weight_kg decimal(18,3) NOT NULL DEFAULT '0.000' COMMENT '总重量(千克)',
total_volume_cbm decimal(18,6) NOT NULL DEFAULT '0.000000' COMMENT '总体积(立方米)',
@@ -2522,7 +2522,7 @@ CREATE TABLE ll_us_truck_order (
origin_address varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '起运地址',
destination varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '目的地',
destination_address varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '目的地址',
container_type varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '40HQ' COMMENT '平台统一柜型:40GP、40HQ、45HQ',
container_type varchar(32) COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '柜型;拼车来源未提供时为空,不推断为整柜',
pallet_count int NOT NULL DEFAULT '1' COMMENT '客户申报托盘数',
package_count int DEFAULT NULL COMMENT '可选箱数,零或空值均有效',
received_pallet_count int NOT NULL DEFAULT '0' COMMENT 'PDA实收托盘数',
@@ -55,7 +55,9 @@ public class FleetPoolOrderBridge {
String transport=in.destinationKey().startsWith("FBA:")?"WAREHOUSE_TO_WAREHOUSE":"WAREHOUSE_TO_DOOR";
if("LOCAL_LOOSEBOARD".equals(in.businessType())) {
var order=new LocalLooseboardOrder();order.setId(row.getBusinessOrderId());order.setCustomerId(in.customerId());order.setOrderNo(row.getBusinessOrderNo());
order.setTransportMode("SHARED_TRUCK");order.setPalletRequired("YES");order.setTransportType(transport);
order.setTransportMode("SHARED_TRUCK");order.setTransportType(transport);
// A pallet count/space is not a palletizing service request or a container specification.
order.setPalletRequired(null);order.setContainerType(null);
order.setOrigin(in.warehouseName());order.setDestination(in.destinationLabel());order.setPackageCount(in.packageCount());
order.setTotalWeightKg(in.weightKg());order.setTotalVolumeCbm(in.volumeCbm());order.setCurrency(in.currency());
order.setContactName(in.contactName());order.setContactInfo(in.contactPhone());order.setRequestedDate(in.plannedDepartureAt().toLocalDate());
@@ -65,6 +67,7 @@ public class FleetPoolOrderBridge {
} else {
var order=new UsTruckOrder();order.setId(row.getBusinessOrderId());order.setCustomerId(in.customerId());order.setOrderNo(row.getBusinessOrderNo());
order.setTransportType(transport);order.setOrigin(in.warehouseName());order.setDestination(in.destinationLabel());
order.setContainerType(null); // Pool inquiry does not collect a container specification.
order.setPalletCount(in.expectedPalletCount());order.setPackageCount(in.packageCount());order.setReceivedPalletCount(0);order.setAllocatedPalletCount(0);
order.setTotalWeightKg(in.weightKg());order.setTotalVolumeCbm(in.volumeCbm());order.setCurrency(in.currency());
order.setContactName(in.contactName());order.setContactInfo(in.contactPhone());order.setRequestedDate(in.plannedDepartureAt().toLocalDate());
@@ -8,6 +8,7 @@ import java.util.List;
public final class LocalLooseboardResponses {
private LocalLooseboardResponses() { }
/** Pool orders retain null containerType/palletRequired when the inquiry did not specify these facts. */
public record Summary(
Long id, String customerId, String orderNo, String transportMode, String palletRequired,
String transportType, String origin, String destination, String containerType, int packageCount,
@@ -8,6 +8,7 @@ import java.util.List;
public final class UsTruckOrderResponses {
private UsTruckOrderResponses() {}
/** Pool orders retain null containerType; forecast physical pallets do not imply a container type. */
public record Summary(
Long id, String customerId, String orderNo, String transportType,
String origin, String destination, String containerType, int palletCount, int packageCount,
@@ -2661,7 +2661,7 @@ CREATE TABLE IF NOT EXISTS ll_us_truck_order (
origin_address VARCHAR(500) NULL COMMENT '起运地址',
destination VARCHAR(255) NOT NULL COMMENT '目的地',
destination_address VARCHAR(500) NULL COMMENT '目的地址',
container_type VARCHAR(32) NOT NULL DEFAULT '40HQ' COMMENT '平台统一柜型:40GP、40HQ、45HQ',
container_type VARCHAR(32) NULL DEFAULT NULL COMMENT '柜型;拼车来源未提供时为空,不推断为整柜',
pallet_count INT NOT NULL DEFAULT 1 COMMENT '客户申报托盘数',
package_count INT NULL COMMENT '可选箱数,零或空值均有效',
received_pallet_count INT NOT NULL DEFAULT 0 COMMENT 'PDA实收托盘数',
@@ -2736,10 +2736,10 @@ CREATE TABLE IF NOT EXISTS ll_local_looseboard_order (
id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID',
lastleg_order_id BIGINT NULL COMMENT '关联尾程主订单ID',
order_no VARCHAR(64) NOT NULL COMMENT '本土散板订单号', transport_mode VARCHAR(32) NOT NULL COMMENT '运输方式:拼车或优先拼载',
pallet_required VARCHAR(8) NOT NULL COMMENT '是否需要打盘:YES或NO', transport_type VARCHAR(32) NOT NULL COMMENT '运输类型',
pallet_required VARCHAR(8) NULL DEFAULT NULL COMMENT '是否需要打盘:YES或NO;拼车来源未提供时为空', transport_type VARCHAR(32) NOT NULL COMMENT '运输类型',
origin VARCHAR(255) NOT NULL COMMENT '起运地', origin_address VARCHAR(500) NULL COMMENT '起运详细地址',
destination VARCHAR(255) NOT NULL COMMENT '目的地', destination_address VARCHAR(500) NULL COMMENT '目的详细地址、码头或仓号',
container_type VARCHAR(32) NOT NULL DEFAULT '40HQ' COMMENT '平台统一柜型:40GP、40HQ、45HQ', package_count INT NOT NULL DEFAULT 0 COMMENT '货物件数',
container_type VARCHAR(32) NULL DEFAULT NULL COMMENT '柜型;拼车来源未提供时为空,不推断为整柜', package_count INT NOT NULL DEFAULT 0 COMMENT '货物件数',
total_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '总重量(千克)',
total_volume_cbm DECIMAL(18,6) NOT NULL DEFAULT 0 COMMENT '总体积(立方米)',
budget_amount DECIMAL(18,2) NULL COMMENT '预算总价', currency CHAR(3) NOT NULL DEFAULT 'USD' COMMENT '预算币种',
@@ -3362,7 +3362,7 @@ components:
transportType: { type: string, nullable: true }
origin: { type: string }
destination: { type: string }
containerType: { type: string, enum: [40GP, 40HQ, 45HQ], description: 平台统一柜型 }
containerType: { type: string, nullable: true, enum: [40GP, 40HQ, 45HQ], description: 已知时为平台统一柜型;池询价未提供时未知,不默认40HQ }
palletCount: { type: integer }
packageCount: { type: integer }
receivedPalletCount: { type: integer }
@@ -3411,17 +3411,17 @@ components:
LooseboardSummary:
type: object
description: 散板商业主单摘要;池来源附带同客户fleetArrangement,独立订单该字段为null。池来源不创建第二张占位尾程来源。
required: [id, customerId, orderNo, transportMode, palletRequired, transportType, origin, destination, packageCount, totalWeightKg, totalVolumeCbm, status, version, createdAt, updatedAt]
required: [id, customerId, orderNo, transportMode, transportType, origin, destination, packageCount, totalWeightKg, totalVolumeCbm, status, version, createdAt, updatedAt]
properties:
id: { type: string, pattern: '^[0-9]+$' }
customerId: { type: string }
orderNo: { type: string }
transportMode: { type: string, enum: [SHARED_TRUCK, PRIORITY_CONSOLIDATION] }
palletRequired: { type: string, enum: [YES, NO] }
palletRequired: { type: string, nullable: true, enum: [YES, NO], description: 是否委托打盘服务;池询价未提供时未知,不从实托数或托位推断YES }
transportType: { type: string }
origin: { type: string }
destination: { type: string }
containerType: { type: string, enum: [40GP, 40HQ, 45HQ], description: 平台统一柜型 }
containerType: { type: string, nullable: true, enum: [40GP, 40HQ, 45HQ], description: 已知时为平台统一柜型;池询价未提供时未知,不默认40HQ }
packageCount: { type: integer }
totalWeightKg: { type: number }
totalVolumeCbm: { type: number }
@@ -74,6 +74,27 @@ class FleetPoolOrderBridgeTest {
assertThat(truckOrder.getBudgetAmount()).isNull();assertThat(truckOrder.getStatus()).isEqualTo("PENDING_DISPATCH");
verifyNoInteractions(loose);
}
@Test void looseboardInquiryDoesNotInventContainerOrPalletizingServiceFromForecastPallets(){
var source=input("LOCAL_LOOSEBOARD");service.confirm(source,"op");
assertThat(looseOrder.getContainerType()).isNull();assertThat(looseOrder.getPalletRequired()).isNull();
assertThat(looseOrder.getTransportMode()).isEqualTo("SHARED_TRUCK");
assertThat(looseOrder.getTransportType()).isEqualTo("WAREHOUSE_TO_WAREHOUSE");
assertThat(looseOrder.getOrigin()).isEqualTo(source.warehouseName());assertThat(looseOrder.getDestination()).isEqualTo(source.destinationLabel());
assertThat(looseOrder.getPackageCount()).isEqualTo(source.packageCount());
assertThat(looseOrder.getTotalWeightKg()).isEqualByComparingTo(source.weightKg());assertThat(looseOrder.getTotalVolumeCbm()).isEqualByComparingTo(source.volumeCbm());
assertThat(looseOrder.getOriginAddress()).isNull();assertThat(looseOrder.getDestinationAddress()).isNull();assertThat(looseOrder.getBudgetAmount()).isNull();
incoming();service.receiving("9","WT-7","pda");service.acceptedReceipt(7,receipt(),3,"op");
assertThat(looseOrder.getPalletRequired()).isNull();assertThat(looseOrder.getContainerType()).isNull();
}
@Test void truckInquiryPreservesUnknownContainerWhileZeroActualCountsRemainRealUntilReceipt(){
var source=input("US_TRUCK");service.confirm(source,"op");
assertThat(truckOrder.getContainerType()).isNull();assertThat(truckOrder.getPalletCount()).isEqualTo(source.expectedPalletCount());
assertThat(truckOrder.getReceivedPalletCount()).isZero();assertThat(truckOrder.getAllocatedPalletCount()).isZero();
assertThat(truckOrder.getPackageCount()).isEqualTo(source.packageCount());assertThat(truckOrder.getTransportType()).isEqualTo("WAREHOUSE_TO_WAREHOUSE");
assertThat(truckOrder.getOriginAddress()).isNull();assertThat(truckOrder.getDestinationAddress()).isNull();assertThat(truckOrder.getBudgetAmount()).isNull();
incoming();service.receiving("9","WT-7","pda");service.acceptedReceipt(7,receipt(),3,"op");
assertThat(truckOrder.getReceivedPalletCount()).isEqualTo(2);assertThat(truckOrder.getContainerType()).isNull();
}
@Test void incomingMustMatchCustomerWarehouseAndDestinationAndNeverBindSecondTailOrder(){
service.confirm(input("LOCAL_LOOSEBOARD"),"op");
assertThatThrownBy(()->service.bindIncoming("FB-7","another","WT-7",77,"92","FBA:ONT8","op")).hasMessageContaining("客户");
@@ -43,4 +43,20 @@ class LocalLooseboardSchemaContractTest {
int end = sql.indexOf(";", start);
return start < 0 || end < 0 ? "" : sql.substring(start, end);
}
@Test
void poolUnknownContainerAndPalletizingSurviveOmittedNullInsertColumnsInBothBaselines() throws Exception {
String baseline;
try (var input = getClass().getResourceAsStream("/db/lastleg.sql")) {
baseline = new String(input.readAllBytes(), StandardCharsets.UTF_8);
}
String unified = Files.readString(Path.of("..", "deploy", "sql", "海外区域统一增量-MySQL8.sql"), StandardCharsets.UTF_8);
for (String sql : java.util.List.of(baseline, unified)) {
String block = tableBlock(sql.replace("CREATE TABLE IF NOT EXISTS", "CREATE TABLE"), "ll_local_looseboard_order").toLowerCase();
assertTrue(java.util.regex.Pattern.compile("(?:^|,)\\s*container_type\\s+varchar\\(32\\)\\s+(?:collate\\s+\\w+\\s+)?(?:null\\s+)?default\\s+null\\b").matcher(block).find(),
"Unknown container must remain null when MyBatis omits its insert column");
assertTrue(java.util.regex.Pattern.compile("(?:^|,)\\s*pallet_required\\s+varchar\\(8\\)\\s+(?:collate\\s+\\w+\\s+)?(?:null\\s+)?default\\s+null\\b").matcher(block).find(),
"Forecast pallets must not invent YES or NO palletizing service");
}
}
}
@@ -48,4 +48,19 @@ class UsTruckOrderSchemaContractTest {
int end = sql.indexOf(';', start);
return start < 0 || end < 0 ? "" : sql.substring(start, end);
}
@Test
void poolUnknownContainerSurvivesOmittedNullInsertColumnInBothBaselines() throws Exception {
String baseline;
try (var input = getClass().getResourceAsStream("/db/lastleg.sql")) {
baseline = new String(input.readAllBytes(), StandardCharsets.UTF_8);
}
String unified = Files.readString(Path.of("..", "deploy", "sql", "海外区域统一增量-MySQL8.sql"), StandardCharsets.UTF_8);
for (String sql : java.util.List.of(baseline, unified)) {
String normalized = sql.toLowerCase().replace("create table if not exists", "create table");
String block = tableBlock(normalized, "ll_us_truck_order");
assertTrue(java.util.regex.Pattern.compile("(?:^|,)\\s*container_type\\s+varchar\\(32\\)\\s+(?:collate\\s+\\w+\\s+)?(?:null\\s+)?default\\s+null\\b").matcher(block).find(),
"Unknown container must remain null when MyBatis omits its insert column");
}
}
}
@@ -55,6 +55,18 @@ test('customer identity joins use the same explicit collation as existing fulfil
assert.doesNotMatch(sql.replace(/^--.*$/gm, ''), /\b(?:DROP|TRUNCATE|DELETE|UPDATE|REPLACE)\b/i)
})
test('commercial schemas do not invent container or palletizing facts for pool orders', () => {
for (const name of ['ll_local_looseboard_order', 'll_us_truck_order']) {
const definition = table(overseas, 'baiyue_lastleg', name)
assert.match(definition, /container_type\s+varchar\(32\)(?:\s+COLLATE\s+\w+)?\s+(?:NULL\s+)?DEFAULT NULL/i)
assert.doesNotMatch(definition, /container_type[^\n]*DEFAULT '40HQ'/i)
}
assert.match(table(overseas, 'baiyue_lastleg', 'll_local_looseboard_order'), /pallet_required\s+varchar\(8\)(?:\s+COLLATE\s+\w+)?\s+(?:NULL\s+)?DEFAULT NULL/i)
const sql = read('deploy/sql/migrations/overseas/baiyue_lastleg/V12__preserve_unknown_pool_order_fields.sql')
assert.equal((sql.match(/MODIFY COLUMN/g) || []).length, 3)
assert.doesNotMatch(sql.replace(/^--.*$/gm, ''), /\b(?:DROP|TRUNCATE|DELETE|UPDATE|REPLACE)\b/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)
+13 -1
View File
@@ -34,6 +34,7 @@ export const POOL_COVERAGE = Object.freeze([
'receipt-overcapacity-rejected-with-source-unchanged', 'open-offer-bind-replan-requires-explicit-resume',
'bound-cargo-reservation-removed-without-double-count', 'other-sales-write-and-sensitive-trip-denied',
'other-customer-commercial-read-denied-with-source-unchanged',
'pool-unspecified-container-and-palletizing-remain-unknown',
])
export const POOL_REMAINING = Object.freeze([
'out-of-band-assigned-vehicle-drift-not-constructible-through-supported-api',
@@ -62,6 +63,14 @@ export function assertSingleCommercial(before, after) {
&& before.customerId === after.customerId, 'COMMERCIAL_REPLAY_IDENTITY_CHANGED')
}
export function assertPoolUnknownCommercialFacts(businessType, summary) {
requireThat(['LOCAL_LOOSEBOARD', 'US_TRUCK'].includes(businessType) && summary && typeof summary === 'object',
'POOL_COMMERCIAL_SUMMARY_REQUIRED')
requireThat(summary.containerType == null, 'POOL_CONTAINER_TYPE_MUST_REMAIN_UNKNOWN')
if (businessType === 'LOCAL_LOOSEBOARD') requireThat(summary.palletRequired == null, 'POOL_PALLETIZING_SERVICE_MUST_REMAIN_UNKNOWN')
return true
}
export function poolInquiryWrite(inquiry, idempotencyKey) {
return { tripRecordVersion: inquiry.tripRecordVersion, offerRecordVersion: inquiry.offerRecordVersion,
recordVersion: inquiry.recordVersion, customerId: inquiry.customerId, businessType: inquiry.businessType,
@@ -397,6 +406,7 @@ async function poolScenario(context, fixture, { execute = false, readInventory,
const poolMembers = []
for (const [index, order] of proof.orders.entries()) {
const source = await request('GET', businessPath(order))
check('New pool commercial order retains unknown container and palletizing facts', assertPoolUnknownCommercialFacts(order.businessType, source.summary))
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?')
@@ -459,6 +469,7 @@ async function poolScenario(context, fixture, { execute = false, readInventory,
proof.covered.push('shared-real-departure-and-signature')
for (const [index, order] of proof.orders.entries()) {
const commercial = await request('GET', businessPath(order))
check('Completed commercial order retains unknown container and palletizing facts', assertPoolUnknownCommercialFacts(order.businessType, commercial.summary))
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}`)
@@ -467,9 +478,10 @@ async function poolScenario(context, fixture, { execute = false, readInventory,
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 completed order retains unknown container and palletizing facts', assertPoolUnknownCommercialFacts(order.businessType, own.summary))
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.covered.push('business-completion-and-fee-uniqueness', 'pool-unspecified-container-and-palletizing-remain-unknown')
proof.poolMembers = poolMembers; proof.status = 'VERIFIED'
return proof
} catch (error) { proof.status = 'FAILED'; throw error }
+18 -1
View File
@@ -2,7 +2,8 @@ import test from 'node:test'
import assert from 'node:assert/strict'
import { parsePoolArguments, poolPlan, runPoolAcceptance, assertPoolRemaining, assertSingleCommercial,
shrinkingVehicleRequest, expiryWaitMs, poolLocalDateTime, poolPickScope, pickPoolDispatch, POOL_COVERAGE, POOL_REMAINING,
poolInquiryWrite, poolReadIsDenied, assertPoolBoundCapacity, verifyPoolConfirmedCancellation, verifyPoolBindingRevalidation } from './fleet-capacity-pool-test-env.mjs'
poolInquiryWrite, poolReadIsDenied, assertPoolBoundCapacity, verifyPoolConfirmedCancellation, verifyPoolBindingRevalidation,
assertPoolUnknownCommercialFacts } 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
@@ -167,6 +168,22 @@ test('foreign read rejection requires actual access/not-found code and no leaked
{ http: 200, body: { code: 'A0409' } }, { http: 403, body: { data: { orderNo: 'PRIVATE' } } }]) assert.equal(poolReadIsDenied(response), false)
})
test('pool commercial facts accept nullish Jackson fields but reject invented container or palletizing answers', () => {
for (const type of ['LOCAL_LOOSEBOARD', 'US_TRUCK']) {
for (const summary of [{}, { containerType: null, palletRequired: null }, { containerType: undefined, palletRequired: undefined }]) {
assert.doesNotThrow(() => assertPoolUnknownCommercialFacts(type, summary))
}
for (const containerType of ['40HQ', '40GP', '45HQ', '', 'UNKNOWN', false, 0]) {
assert.throws(() => assertPoolUnknownCommercialFacts(type, { containerType }), /CONTAINER_TYPE_MUST_REMAIN_UNKNOWN/)
}
assert.throws(() => assertPoolUnknownCommercialFacts(type, null), /SUMMARY_REQUIRED/)
}
for (const palletRequired of ['YES', 'NO', '', 'UNKNOWN', false, 0]) {
assert.throws(() => assertPoolUnknownCommercialFacts('LOCAL_LOOSEBOARD', { palletRequired }), /PALLETIZING_SERVICE_MUST_REMAIN_UNKNOWN/)
}
assert.throws(() => assertPoolUnknownCommercialFacts('OTHER', {}), /SUMMARY_REQUIRED/)
})
test('owner rejection sends a valid current edit rather than malformed input', () => {
const source = { tripRecordVersion: 5, offerRecordVersion: 7, recordVersion: 9, customerId: '42', businessType: 'US_TRUCK',
deliveryMode: 'SELF_DELIVERY', estimatedArrivalAt: '2030-01-01T00:00:00Z', expectedPalletCount: 1,