2026-08-27 16:17:11 +08:00
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
import { readFileSync } from 'node:fs'
|
|
|
|
|
import test from 'node:test'
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
IMMUTABLE_TAILLEG_EVENT_FIXTURE,
|
|
|
|
|
TAILLEG_ORDER_NUMBER,
|
|
|
|
|
validContainerOrderGroup,
|
|
|
|
|
validEventReplay,
|
|
|
|
|
validOwnerSnapshots,
|
|
|
|
|
} from './tailleg-owner-order-number-contract.mjs'
|
|
|
|
|
|
|
|
|
|
const read = (relativeUrl) => readFileSync(new URL(relativeUrl, import.meta.url), 'utf8')
|
|
|
|
|
const eventV2 = read('../../common-event-contract/src/main/java/com/baiyue/common/event/FirstLegLoadingPlanCompletedV2.java')
|
2026-09-01 11:06:26 +08:00
|
|
|
const eventV2Tests = read('../../common-event-contract/src/test/java/com/baiyue/common/event/FirstLegLoadingPlanCompletedV2Test.java')
|
2026-08-27 16:17:11 +08:00
|
|
|
const consumer = read('../../lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java')
|
|
|
|
|
const orderEntity = read('../../lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrder.java')
|
2026-09-01 11:06:26 +08:00
|
|
|
const orderSummary = read('../../lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderSummary.java')
|
2026-08-27 16:17:11 +08:00
|
|
|
const orderService = read('../../lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java')
|
|
|
|
|
const generator = read('../../lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderNoGenerator.java')
|
|
|
|
|
const generatorTests = read('../../lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderNoGeneratorTest.java')
|
|
|
|
|
const consumerTests = read('../../lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java')
|
|
|
|
|
|
|
|
|
|
test('new 15-order event requires one non-empty trusted owner snapshot', () => {
|
|
|
|
|
const orders = Array.from({ length: 15 }, (_, index) => ({
|
|
|
|
|
sourceInboundId: `inbound-${index + 1}`, customerId: 'customer-1', customerName: '验收货主',
|
|
|
|
|
}))
|
|
|
|
|
assert.equal(validOwnerSnapshots(orders, 15), true)
|
|
|
|
|
assert.equal(validOwnerSnapshots(orders.map((order, index) => index === 7
|
|
|
|
|
? { ...order, customerName: '' } : order), 15), false)
|
|
|
|
|
assert.equal(validOwnerSnapshots(orders.map((order, index) => index === 7
|
|
|
|
|
? { ...order, customerName: '另一货主' } : order), 15), false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('the processed historical event is immutable and cannot be reused for acceptance', () => {
|
|
|
|
|
assert.deepEqual(IMMUTABLE_TAILLEG_EVENT_FIXTURE, {
|
|
|
|
|
eventId: 'b569c7f6-3493-4a0f-a944-dd42698f183b',
|
|
|
|
|
status: 'PROCESSED', payloadMutable: false, reusableForAcceptance: false,
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-01 11:06:26 +08:00
|
|
|
test('V2 event contract freezes customerId and customerName and rejects a blank or mixed owner', () => {
|
2026-08-27 16:17:11 +08:00
|
|
|
assert.match(eventV2, /List<Item> items,\s*String customerId,\s*String customerName,\s*String orderType/)
|
2026-09-01 11:06:26 +08:00
|
|
|
assert.match(eventV2, /customerId\s*=\s*customerId\.trim\(\)/)
|
|
|
|
|
assert.match(eventV2, /customerName\s*=\s*customerName\.trim\(\)/)
|
|
|
|
|
assert.match(eventV2, /orders must share one immutable customerId\/customerName snapshot/)
|
|
|
|
|
assert.match(eventV2Tests, /rejectsMissingImmutableCustomerName/)
|
|
|
|
|
assert.match(eventV2Tests, /freezesTrimmedOwnerSnapshotAndRejectsMixedOwners/)
|
2026-08-27 16:17:11 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('Lastleg rejects a missing owner snapshot, persists it, and never calls IAM to repair it', () => {
|
2026-09-01 11:06:26 +08:00
|
|
|
assert.match(consumer, /OwnerSnapshot owner\s*=\s*requireOwnerSnapshot\(payload\.orders\(\),\s*envelope\.tenantId\(\)\)/)
|
|
|
|
|
assert.match(consumer, /requiredOwnerValue\(orders\.get\(0\)\.customerName\(\),\s*"customerName"\)/)
|
|
|
|
|
assert.match(consumer, /value\s*==\s*null\s*\|\|\s*value\.isBlank\(\)/)
|
2026-08-27 16:17:11 +08:00
|
|
|
assert.match(consumer, /order\.setCustomerName\(customerName\)/)
|
|
|
|
|
assert.match(orderEntity, /private String customerName;/)
|
|
|
|
|
assert.doesNotMatch(consumer, /(?:Iam|IAM|\/iam|CustomerClient|customer.*client)/)
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-01 11:06:26 +08:00
|
|
|
test('Lastleg query DTO uses the persisted frozen owner name', () => {
|
|
|
|
|
assert.match(orderSummary, /String customerName/)
|
2026-08-27 16:17:11 +08:00
|
|
|
assert.match(orderService, /o\.getCustomerName\(\)/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('new tailleg number is Shanghai daily LMO-yyMMdd-#### with concurrency and cross-day tests', () => {
|
|
|
|
|
assert.equal(TAILLEG_ORDER_NUMBER.pattern.test('LMO-260827-0001'), true)
|
|
|
|
|
assert.equal(TAILLEG_ORDER_NUMBER.pattern.test('LMO2087379674555461633'), false)
|
|
|
|
|
assert.equal(TAILLEG_ORDER_NUMBER.timeZone, 'Asia/Shanghai')
|
|
|
|
|
assert.match(generator, /Asia\/Shanghai/)
|
|
|
|
|
assert.match(generator, /yyMMdd/)
|
|
|
|
|
assert.match(generator, /LMO-/)
|
|
|
|
|
assert.match(generator, /%04d|padStart\(4|String\.format/)
|
|
|
|
|
assert.match(generatorTests, /(?:concurrent|并发)/i)
|
|
|
|
|
assert.match(generatorTests, /(?:cross.?day|next.?day|跨天)/i)
|
2026-09-01 11:06:26 +08:00
|
|
|
assert.match(generatorTests, /LMO-260827-0001/)
|
|
|
|
|
assert.match(generatorTests, /LMO-260828-0001/)
|
2026-08-27 16:17:11 +08:00
|
|
|
})
|
|
|
|
|
|
2026-09-04 17:17:08 +08:00
|
|
|
test('one container with 15 inbound rows shares one stable container order number', () => {
|
2026-08-27 16:17:11 +08:00
|
|
|
const rows = Array.from({ length: 15 }, (_, index) => ({
|
|
|
|
|
containerNo: 'CONT-15', orderNo: 'LMO-260827-0042', sourceInboundId: `inbound-${index + 1}`,
|
|
|
|
|
}))
|
|
|
|
|
assert.equal(validContainerOrderGroup(rows), true)
|
|
|
|
|
assert.equal(validContainerOrderGroup(rows.map((row, index) => index === 14
|
|
|
|
|
? { ...row, orderNo: 'LMO-260827-0043' } : row)), false)
|
2026-09-04 17:17:08 +08:00
|
|
|
assert.match(consumer, /String orderNo\s*=\s*orderNoGenerator\.forContainerNo\(payload\.containerNo\(\)\);[\s\S]*for \(int index/)
|
2026-09-01 11:06:26 +08:00
|
|
|
assert.match(consumer, /createOrder\(envelope, payload, source, orderNo, rawSource, containerNo/)
|
2026-09-04 17:17:08 +08:00
|
|
|
assert.match(consumerTests, /oneContainerWith15InboundRowsSharesOneStableContainerOrderNumber/)
|
2026-08-27 16:17:11 +08:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('same event replay adds nothing and preserves the allocated number', () => {
|
|
|
|
|
const before = { orderCount: 15, orderNo: 'LMO-260827-0042', detailCount: 30, historyCount: 15 }
|
|
|
|
|
assert.equal(validEventReplay(before, { ...before }), true)
|
|
|
|
|
assert.equal(validEventReplay(before, { ...before, orderCount: 30 }), false)
|
|
|
|
|
assert.match(consumer, /if\s*\(existing\s*!=\s*null\)[\s\S]{0,400}return countOrders/)
|
2026-09-01 11:06:26 +08:00
|
|
|
assert.match(consumerTests, /replayPreservesExistingLmoNumberAndAddsNothing/)
|
|
|
|
|
assert.match(consumerTests, /verify\(sequences,never\(\)\)\.nextValue/)
|
2026-08-27 16:17:11 +08:00
|
|
|
})
|