2026-08-14 21:00:20 +08:00
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
import { readFileSync } from 'node:fs'
|
|
|
|
|
import test from 'node:test'
|
|
|
|
|
|
|
|
|
|
const read = (path) => readFileSync(new URL(path, import.meta.url), 'utf8')
|
|
|
|
|
const controller = read('../../firstleg/src/main/java/com/baiyue/firstleg/controller/PlatformContainerController.java')
|
|
|
|
|
const containerDtos = read('../../firstleg/src/main/java/com/baiyue/firstleg/dto/ContainerDtos.java')
|
|
|
|
|
const containerService = read('../../firstleg/src/main/java/com/baiyue/firstleg/service/ContainerService.java')
|
|
|
|
|
const oceanService = read('../../firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/service/OceanBookingService.java')
|
|
|
|
|
const oceanMapper = read('../../firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/mapper/OceanBookingMapper.java')
|
|
|
|
|
const firstlegOpenApi = read('../../firstleg/src/main/resources/openapi/firstleg-v1.yaml')
|
|
|
|
|
const oceanOpenApi = read('../../firstleg/src/main/resources/openapi/ocean-bookings-v1.yaml')
|
|
|
|
|
const loadingFixture = read('phase3-container-loading.mjs')
|
|
|
|
|
const fullEntry = read('../admin-test-env-acceptance.mjs')
|
|
|
|
|
const matrix = read('../../../docs/三期/货柜管理全链验收矩阵-2026-08-13.md')
|
|
|
|
|
|
|
|
|
|
test('operational ocean booking list excludes unconfirmed data', () => {
|
|
|
|
|
assert.match(oceanService, /OPERATIONAL_LIST_STATUSES\s*=\s*Set\.of\([\s\S]*CONFIRMED[\s\S]*CHANGE_PENDING[\s\S]*RELEASED[\s\S]*CANCELLED/)
|
|
|
|
|
assert.match(oceanMapper, /status IN \('CONFIRMED','CHANGE_PENDING','RELEASED','CANCELLED'\)/)
|
|
|
|
|
assert.match(oceanOpenApi, /enum: \[CONFIRMED, CHANGE_PENDING, RELEASED, CANCELLED\]/)
|
2026-08-15 09:47:17 +08:00
|
|
|
assert.match(oceanOpenApi, /DRAFT is internal to the create transaction and legacy SUBMITTED\/FAILED values are invalid/)
|
2026-08-14 21:00:20 +08:00
|
|
|
assert.doesNotMatch(oceanOpenApi.match(/name: status[^\n]*/)?.[0] || '', /DRAFT|SUBMITTED/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('simulation creation uses server-owned identity and formal master data', () => {
|
|
|
|
|
assert.match(controller, /@PostMapping[\s\S]*SimulationCreateRequest request[\s\S]*createSimulation/)
|
|
|
|
|
assert.match(containerDtos, /SimulationCreateRequest\(@NotBlank[\s\S]*String channelCode[\s\S]*Long warehouseId[\s\S]*String containerType[\s\S]*BigDecimal capacityCbm[\s\S]*BigDecimal maxWeightKg/)
|
|
|
|
|
assert.match(containerService, /requireDomesticTransitWarehouse\(request\.warehouseId\(\)\)/)
|
|
|
|
|
assert.match(containerService, /activeVersionId\(\)[\s\S]*priceBooks\.channels\(versionId, warehouse\.id\(\)\)/)
|
|
|
|
|
assert.match(containerService, /setCabinetNo\("CAB-" \+ value\.getId\(\)\)/)
|
|
|
|
|
assert.match(firstlegOpenApi, /服务端生成稳定 containerBookingId\/cabinetNo,不要求真实柜号或订舱/)
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-17 13:24:48 +08:00
|
|
|
test('link consumes one active real booking container through the frozen request', () => {
|
2026-08-14 21:00:20 +08:00
|
|
|
assert.match(controller, /@PostMapping\("\/\{id\}\/ocean-booking-link"\)/)
|
|
|
|
|
assert.match(controller, /@DeleteMapping\("\/\{id\}\/ocean-booking-link"\)/)
|
2026-08-17 13:24:48 +08:00
|
|
|
assert.match(containerDtos, /OceanBookingLinkRequest\(@NotNull @Positive Long realContainerBookingId,[\s\S]*expectedVersion/)
|
2026-08-14 21:00:20 +08:00
|
|
|
assert.match(containerService, /requireFirstLinkMutable\(container\)/)
|
2026-08-17 13:24:48 +08:00
|
|
|
assert.match(containerService, /repository\.findForUpdate\(request\.realContainerBookingId\(\)\)/)
|
|
|
|
|
assert.match(containerService, /"OCEAN_BOOKING"\.equals\(source\.getSourceType\(\)\)/)
|
|
|
|
|
assert.match(containerService, /"ACTIVE"\.equals\(source\.getRecordStatus\(\)\)/)
|
|
|
|
|
assert.match(containerService, /"CONFIRMED"\.equals\(booking\.getStatus\(\)\)[\s\S]*"RELEASED"\.equals\(booking\.getStatus\(\)\)/)
|
2026-08-14 21:00:20 +08:00
|
|
|
assert.match(containerService, /hasPickTask[\s\S]*hasEvidence[\s\S]*hasDocuments/)
|
2026-08-17 13:24:48 +08:00
|
|
|
assert.match(firstlegOpenApi, /realContainerBookingId/)
|
|
|
|
|
assert.match(firstlegOpenApi, /原真实柜置 MERGED 且不再出现在正常货柜列表/)
|
2026-08-14 21:00:20 +08:00
|
|
|
})
|
|
|
|
|
|
2026-08-17 13:24:48 +08:00
|
|
|
test('real booking merge locks scope and changes all identities atomically', () => {
|
2026-08-14 21:00:20 +08:00
|
|
|
for (const field of ['warehouseId', 'channelCode', 'containerType', 'capacityCbm', 'maxWeightKg']) {
|
|
|
|
|
assert.match(containerService, new RegExp(`container\\.get${field[0].toUpperCase()}${field.slice(1)}\\(\\)`), field)
|
|
|
|
|
}
|
|
|
|
|
assert.match(containerService, /hasMismatchedCustomerItems/)
|
2026-08-17 13:24:48 +08:00
|
|
|
assert.match(containerService, /consumeRealSource\(source\.getId\(\), id/)
|
|
|
|
|
assert.match(containerService, /mergeRealSource\(id, source\.getId\(\), source\.getContainerNo\(\)/)
|
|
|
|
|
assert.match(containerService, /reassignAllocation\(source\.getId\(\), id\)/)
|
2026-08-14 21:00:20 +08:00
|
|
|
assert.doesNotMatch(containerService.match(/linkOceanBooking[\s\S]*?return required\(id\);/)?.[0] || '', /container\.setId\(|container\.setCabinetNo\(/)
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-17 13:24:48 +08:00
|
|
|
test('booking creation atomically registers one real operational container per input row', () => {
|
|
|
|
|
assert.match(oceanOpenApi, /containers: \{type: array, minItems: 1, maxItems: 20/)
|
|
|
|
|
assert.match(oceanOpenApi, /required: \[containerNo, containerType, containerKind, capacityCbm, maxWeightKg\]/)
|
|
|
|
|
assert.match(oceanService, /request\.data\(\)\.containers\(\)\.forEach/)
|
|
|
|
|
assert.match(oceanService, /同一订舱内集装箱号不能重复/)
|
|
|
|
|
assert.match(oceanService, /createRealContainers\(value, requirements, request\.data\(\)\.containers\(\)/)
|
|
|
|
|
assert.match(oceanService, /setSourceType\("OCEAN_BOOKING"\)/)
|
|
|
|
|
assert.match(oceanService, /setRecordStatus\("ACTIVE"\)/)
|
|
|
|
|
assert.match(oceanService, /setQuantity\(1\)/)
|
|
|
|
|
})
|
|
|
|
|
|
2026-08-14 21:00:20 +08:00
|
|
|
test('HTTP fixture follows simulation arrange link PDA sequence without auto-container fallback', () => {
|
|
|
|
|
const ordered = [
|
|
|
|
|
'新增内部模拟柜',
|
|
|
|
|
'人工分配本轮候选货物',
|
|
|
|
|
'首次关联真实已确认订舱柜',
|
|
|
|
|
'查询本轮柜次 PDA 拣货任务',
|
|
|
|
|
].map((label) => loadingFixture.indexOf(label))
|
|
|
|
|
assert.ok(ordered.every((value) => value >= 0), `missing sequence label: ${ordered.join(',')}`)
|
|
|
|
|
assert.ok(ordered.every((value, index) => index === 0 || value > ordered[index - 1]), `invalid sequence: ${ordered.join(',')}`)
|
|
|
|
|
assert.match(loadingFixture, /ocean-booking-link/)
|
|
|
|
|
assert.match(loadingFixture, /关联前后内部柜次、计划和排货明细身份稳定/)
|
2026-08-17 13:24:48 +08:00
|
|
|
assert.match(loadingFixture, /已消费 MERGED 真实柜拒绝再次关联/)
|
2026-08-14 21:00:20 +08:00
|
|
|
assert.match(loadingFixture, /已有排货后拒绝解绑或换绑/)
|
2026-08-17 13:24:48 +08:00
|
|
|
assert.match(loadingFixture, /合并后列表仅保留模拟柜原地转成的一条 ACTIVE 操作身份/)
|
|
|
|
|
assert.match(loadingFixture, /订舱真实柜 allocation 已原子改指稳定模拟柜身份/)
|
2026-08-14 21:00:20 +08:00
|
|
|
assert.doesNotMatch(loadingFixture, /确认订舱并自动生成|确认响应包含自动生成/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('default full acceptance preserves the linked simulation chain before downstream completion', () => {
|
|
|
|
|
const loading = fullEntry.indexOf("'phase3-container-loading'")
|
|
|
|
|
const pda = fullEntry.indexOf("'phase3-pda-convergence'")
|
|
|
|
|
const finalize = fullEntry.indexOf("'phase3-loading-finalize'")
|
|
|
|
|
const outbound = fullEntry.indexOf("'phase3-outbound-dispatch'")
|
|
|
|
|
assert.ok(loading >= 0 && pda > loading && finalize > pda && outbound > finalize)
|
|
|
|
|
assert.match(matrix, /内部模拟柜关联真实订舱增量/)
|
|
|
|
|
assert.doesNotMatch(matrix, /CONFIRMED` 自动建柜[\s\S]*既有冻结/)
|
|
|
|
|
})
|