Compare commits

...
4 changed files with 28 additions and 12 deletions
@@ -5106,4 +5106,4 @@ CREATE TABLE ll_storage_rent_preparation (
KEY idx_storage_rent_release (release_status,updated_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='仓租扫码资金预占与结算快照';
SELECT 'PASS' AS initialization_status, 'OVERSEAS' AS region, 238 AS initialized_tables;
SELECT 'PASS' AS initialization_status, 'OVERSEAS' AS region, 239 AS initialized_tables;
@@ -215,7 +215,7 @@ class DispatchMergedRelabelPersistenceTest {
assertThat(f.count("ll_dispatch_order")).isZero();assertThat(f.count("ll_dispatch_pallet_lock")).isZero();
}
/** 只隔离测量和车队外部目录;来源提供者、候选、库存与所有排车Mapper均为实际实现。 */
/** 隔离测量、车队及仓租门禁;来源提供者、候选、库存与所有排车Mapper均为实际实现。 */
private static final class Merged {
final DispatchAggregatePersistenceTest.Fixture f=new DispatchAggregatePersistenceTest.Fixture();
final DispatchService rules=new DispatchService();final DispatchPickingService picking;final DispatchPdaQueryService pda;
@@ -261,6 +261,7 @@ class DispatchMergedRelabelPersistenceTest {
ReflectionTestUtils.setField(attribution,"attributions",f.session.getMapper(DispatchPdaPickOrderMapper.class));ReflectionTestUtils.setField(attribution,"orders",f.session.getMapper(LastlegOrderMapper.class));ReflectionTestUtils.setField(attribution,"relabelSources",origins);
picking=new DispatchPickingService(f.service,f.repository,sourceRepository,new DispatchPickingRepository(f.session.getMapper(DispatchPdaTaskMapper.class),f.session.getMapper(DispatchPdaPickMapper.class),f.session.getMapper(InventoryPositionMapper.class),f.session.getMapper(DispatchItemMapper.class)),attribution,inventory,mock(ReturnRelabelDispatchStock.class),new DispatchCommandLedger(new DispatchCommandRepository(f.session.getMapper(DispatchCommandMapper.class)),new DispatchCommandCodec(f.json)),new DispatchCommandHasher(f.json),f.json);
ReflectionTestUtils.setField(picking,"sourceRules",rules);ReflectionTestUtils.setField(picking,"relabelSources",snapshots);
ReflectionTestUtils.setField(picking,"storageRent",mock(com.baiyue.lastleg.storagerent.service.StorageRentService.class));
pda=new DispatchPdaQueryService(new DispatchPdaReadRepository(f.session.getMapper(DispatchWholePdaReadMapper.class)),f.service);ReflectionTestUtils.setField(pda,"sourceOrigins",origins);
balance(601,"101",5);position(101,601,"101",crossCustomer?2:5);
if(crossCustomer){f.db.update("UPDATE ll_inventory_balance SET hold_packages=2 WHERE id=601");balance(602,"102",3);position(102,602,"102",3);}
@@ -279,6 +279,28 @@ test('task page permits explicit platform scope zero and positive customer scope
}
})
test('船节点分页接受已规范化的非 ISO 柜号,仍拒绝空值和未规范化输入', async () => {
for (const trackingNo of ['JZXH10231043', 'TEST-GRAY-CAB-NOISO', 'ZCSU7881971']) {
assert.equal(await taskPageStructure({ trackingNo }), true, trackingNo)
}
for (const trackingNo of ['', ' ', '\t', 'jzxh10231043', ' JZXH10231043', 'JZXH 10231043', null, undefined, 123]) {
assert.equal(await taskPageStructure({ trackingNo }), false, String(trackingNo))
}
})
test('非 ISO 柜号沿本轮归属核验完成船节点详情及人工节点链', async () => {
const fixture = markerOwnedContext()
fixture.task.trackingNo = 'TEST-GRAY-CAB-NOISO'
fixture.context.createdResources.phase3CarrierScheduleQuery.trackingNo = fixture.task.trackingNo
await runUntilPickupReady(fixture.context)
assert.equal(fixture.task.latestActualNode, 'PICKUP_READY')
assert.ok(fixture.assertions.every((check) => check.passed))
assert.ok(fixture.context.calls.filter((call) => call.method === 'POST')
.every((call) => call.path === `${BASE}/${QUERY_ID}/manual-nodes`))
})
test('task page still rejects negative, missing, numeric and noncanonical customer scopes', async () => {
for (const customerId of ['-1', '-9007199254740993002', '', '00', '01', '+0', '0.0', 'customer', null, undefined, 0, 7]) {
assert.equal(await taskPageStructure({ customerId }), false, `customerId=${String(customerId)}`)
@@ -10,7 +10,6 @@ export const STANDARD_NODES = Object.freeze([
])
export const EXCEPTION_CODES = Object.freeze(['CUSTOMS_HOLD', 'TERMINAL_HOLD', 'DOCUMENT_HOLD', 'UNKNOWN_HOLD'])
export const NODE_SEQUENCES = Object.freeze(Object.fromEntries(STANDARD_NODES.map((node, index) => [node, (index + 1) * 10])))
const ISO_CONTAINER_PATTERN = /^[A-Z]{3}[UJZ][0-9]{7}$/
function id(value) { return value == null ? '' : String(value) }
function number(value) { return Number.isFinite(Number(value)) }
@@ -20,16 +19,10 @@ function validLongId(value) { return typeof value === 'string' && /^[1-9]\d{15,}
function validNumericId(value) { return typeof value === 'string' && /^\d+$/.test(value) && value !== '0' }
function validCustomerScope(value) { return typeof value === 'string' && /^(?:0|[1-9]\d*)$/.test(value) }
function validOperatorId(value) { return value === '0' || validNumericId(value) }
// 柜号沿现行合同仅要求非空且已规范化,不能再以旧 ISO 格式或校验位拒绝合法业务事实。
function validContainerNo(value) {
if (typeof value !== 'string' || !ISO_CONTAINER_PATTERN.test(value)) return false
const letterValues = [10, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 23, 24,
25, 26, 27, 28, 29, 30, 31, 32, 34, 35, 36, 37, 38]
let sum = 0
for (let index = 0; index < 10; index += 1) {
const code = value.charCodeAt(index)
sum += (code >= 48 && code <= 57 ? code - 48 : letterValues[code - 65]) * (2 ** index)
}
return Number(value[10]) === (sum % 11) % 10
return typeof value === 'string' && value.length > 0
&& value === value.replace(/\s/g, '').toUpperCase()
}
function codeOf(response) { return response?.body?.code == null ? 'missing' : String(response.body.code) }
function rejected(response) { return response?.http === 200 && ['A0409', '409'].includes(codeOf(response)) }