2026-08-14 19:59:26 +08:00
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
const adminPage = readFileSync ( new URL ( '../../../apps/admin-web/src/modules/ocean-bookings/pages/OceanBookingDetailView.vue' , import . meta . url ), 'utf8' )
const adminTest = readFileSync ( new URL ( '../../../apps/admin-web/src/modules/ocean-bookings/pages/OceanBookingDetailView.test.ts' , import . meta . url ), 'utf8' )
const adminTypes = readFileSync ( new URL ( '../../../apps/admin-web/src/modules/ocean-bookings/types.ts' , import . meta . url ), 'utf8' )
const prototype = readFileSync ( new URL ( '../../../docs/悦/货代SAAS平台后台/prototype-src/modules/06-containers.html' , import . meta . url ), 'utf8' )
const dto = readFileSync ( new URL ( '../../firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/dto/OceanBookingDtos.java' , import . meta . url ), 'utf8' )
const openapi = readFileSync ( new URL ( '../../firstleg/src/main/resources/openapi/ocean-bookings-v1.yaml' , import . meta . url ), 'utf8' )
const schema = readFileSync ( new URL ( '../../firstleg/src/main/resources/db/phase3-ocean-booking-schema.sql' , import . meta . url ), 'utf8' )
const unifiedSql = readFileSync ( new URL ( '../../deploy/sql/头程数据库统一增量-MySQL8.sql' , import . meta . url ), 'utf8' )
const fixture = readFileSync ( new URL ( './ocean-booking-fixture.mjs' , import . meta . url ), 'utf8' )
const bookingModule = readFileSync ( new URL ( './ocean-bookings.mjs' , import . meta . url ), 'utf8' )
const containerLoading = readFileSync ( new URL ( './phase3-container-loading.mjs' , import . meta . url ), 'utf8' )
const releaseFields = [ 'billOfLadingNo' , 'vesselName' , 'voyageNo' , 'estimatedSailingAt' , 'estimatedArrivalAt' ]
test ( 'prototype evidence keeps service term freight term and optional customer fields' , () => {
assert . match ( prototype , /主要运输条款 Service Term \*<\/label><select><option>CY-CY<\/option>/ )
assert . match ( prototype , /运费条款 Freight Term \*<\/label><select><option>NINGBO<\/option>/ )
assert . match ( prototype , /<label>客户\/公司名称<\/label><input id="booking-customer"/ )
})
test ( 'Admin keeps source-backed free text terms without invented option dictionaries' , () => {
assert . match ( adminPage , /v-model="form\.data\.serviceTerm"[^>]*maxlength="32"/ )
assert . match ( adminPage , /v-model="form\.data\.freightTerm"[^>]*maxlength="32"/ )
assert . doesNotMatch ( adminPage , /serviceTermOptions|freightTermOptions/ )
for ( const invented of [ 'CY-DOOR' , 'DOOR-CY' , 'DOOR-DOOR' , 'PREPAID' , 'COLLECT' ]) {
assert . doesNotMatch ( adminPage , new RegExp ( invented ), ` ${ invented } must not be an invented Admin option` )
}
assert . match ( adminTest , /not\.toContain\('serviceTermOptions'\)/ )
assert . match ( adminTest , /not\.toContain\('freightTermOptions'\)/ )
})
test ( 'service and freight terms are required across Java OpenAPI and SQL' , () => {
assert . match ( dto , /@NotBlank @Size\(max = 32\) String serviceTerm/ )
assert . match ( dto , /@NotBlank @Size\(max = 32\) String freightTerm/ )
const required = openapi . match ( /DraftData:\s*\n\s*type: object\s*\n\s*required: \[([^\]]*)\]/ ) ? .[ 1 ] || ''
assert . match ( required , /serviceTerm/ )
assert . match ( required , /freightTerm/ )
assert . match ( openapi , /serviceTerm: \{type: string, maxLength: 32, description: Required free-text Service Term snapshot/ )
assert . match ( openapi , /freightTerm: \{type: string, maxLength: 32, description: Required free-text Freight Term snapshot/ )
assert . doesNotMatch ( openapi , /(?:serviceTerm|freightTerm): \{[^\n]*nullable: true/ )
for ( const sql of [ schema , unifiedSql ]) {
assert . match ( sql , /service_term VARCHAR\(32\) NOT NULL/ )
assert . match ( sql , /freight_term VARCHAR\(32\) NOT NULL/ )
}
})
test ( 'customer is optional across Admin DraftData DTO and OpenAPI' , () => {
assert . match ( adminPage , /label="客户(可选)"[\s\S]*?filterable clearable/ )
assert . doesNotMatch ( adminPage , /customerId:\s*\[\{\s*required:\s*true/ )
assert . match ( adminTypes , /customerId:\s*string \| null;\s*customerName:\s*string \| null/ )
assert . match ( dto , /record DraftData\(@Positive Long customerId,\s*@Size\(max = 128\) String customerName/ )
assert . doesNotMatch ( dto , /record DraftData\(@NotNull @Positive Long customerId/ )
const required = openapi . match ( /DraftData:\s*\n\s*type: object\s*\n\s*required: \[([^\]]*)\]/ ) ? .[ 1 ] || ''
assert . ok ( required , 'BLOCKED_CONTRACT: DraftData.required is missing from OpenAPI' )
assert . doesNotMatch ( required , /customerId|customerName/ )
assert . match ( openapi , /customerId: \{[^\n]*nullable: true/ )
assert . match ( openapi , /customerName: \{[^\n]*nullable: true/ )
})
2026-08-15 09:35:52 +08:00
test ( 'offline-confirmed registration requires transport values while release keeps the frozen five-field snapshot' , () => {
2026-08-14 19:59:26 +08:00
const draftType = adminTypes . slice ( adminTypes . indexOf ( 'export interface OceanBookingDraftData' ), adminTypes . indexOf ( 'export interface OceanBookingResource' ))
for ( const field of releaseFields ) {
2026-08-15 09:35:52 +08:00
assert . match ( adminPage , new RegExp ( `v-model="form\\.data\\. ${ field } "` ),
` ${ field } must be entered when registering an offline-confirmed booking` )
assert . match ( draftType , new RegExp ( ` ${ field } \\??:` ), ` ${ field } must belong to OceanBookingDraftData` )
2026-08-14 19:59:26 +08:00
}
2026-08-15 09:35:52 +08:00
assert . match ( adminTest , /线下订舱资料已登记并确认/ )
assert . match ( adminTest , /登记后直接确认/ )
2026-08-14 19:59:26 +08:00
const saveRegion = adminPage . slice ( adminPage . indexOf ( 'async function save' ), adminPage . indexOf ( 'function requireReason' ))
assert . doesNotMatch ( saveRegion , /data:\s*form\.data/ )
2026-08-15 09:35:52 +08:00
const required = openapi . match ( /DraftData:\s*\n\s*type: object\s*\n\s*required: \[([^\]]*)\]/ ) ? .[ 1 ] || ''
for ( const field of releaseFields ) assert . match ( required , new RegExp ( field ))
assert . match ( dto , /@NotBlank @Size\(max = 128\) String billOfLadingNo/ )
assert . match ( dto , /@NotBlank @Size\(max = 128\) String vesselName/ )
assert . match ( dto , /@NotBlank @Size\(max = 64\) String voyageNo/ )
assert . match ( dto , /@NotNull LocalDateTime estimatedSailingAt/ )
assert . match ( dto , /@NotNull LocalDateTime estimatedArrivalAt/ )
2026-08-14 19:59:26 +08:00
const releaseCall = adminPage . slice ( adminPage . indexOf ( 'releaseOceanBooking(id.value' ), adminPage . indexOf ( 'releaseDialogOpen.value = false' ))
for ( const field of releaseFields ) assert . match ( releaseCall , new RegExp ( field ))
})
test ( 'HTTP fixtures cover unassigned CONFIRMED persistence and assigned customer isolation' , () => {
const unassignedFlow = bookingModule . slice ( bookingModule . indexOf ( 'async function createConfirmedUnassignedBooking' ), bookingModule . indexOf ( 'async function releaseLoadingBooking' ))
assert . match ( fixture , /oceanBookingDraftWithoutCustomer/ )
assert . match ( fixture , /delete draft\.customerId[\s\S]*delete draft\.customerName/ )
assert . match ( fixture , /serviceTerm: 'CY-CY', freightTerm: 'NINGBO'/ )
assert . doesNotMatch ( fixture , /serviceTerm: 'CY\/CY'|freightTerm: 'PREPAID'/ )
assert . match ( bookingModule , /createdResources\.oceanBookingUnassignedCustomer/ )
2026-08-17 13:47:20 +08:00
assert . match ( bookingModule , /未指定客户订舱保持空客户且原子生成一条真实柜/ )
2026-08-14 19:59:26 +08:00
assert . match ( bookingModule , /按真实订舱号查询未指定客户订舱/ )
assert . match ( bookingModule , /keyword=\$\{encodeURIComponent\(bookingNo\)\}/ )
assert . doesNotMatch ( unassignedFlow , /keyword=\$\{encodeURIComponent\(context\.marker\)\}/ )
assert . doesNotMatch ( bookingModule , /keyword=\$\{encodeURIComponent\(context\.marker\)\}/ )
assert . match ( bookingModule , /指定客户订舱持久化本轮客户范围快照/ )
assert . match ( bookingModule , /phase3Loading[\s\S]*phase3ContainerLoading/ )
assert . match ( containerLoading , /source\?\.marker !== context\.marker/ )
assert . match ( containerLoading , /id\(candidate\?\.accountId\) === tenantId/ )
assert . match ( containerLoading , /id\(item\.accountId\) === tenantId/ )
})
2026-08-15 10:35:54 +08:00
test ( 'independent ocean booking release reuses the server-frozen transport snapshot' , () => {
2026-08-15 11:05:58 +08:00
assert . match ( bookingModule , /estimatedSailingAt: detail\.oceanBooking\.estimatedSailingAt/ )
assert . match ( bookingModule , /estimatedArrivalAt: detail\.oceanBooking\.estimatedArrivalAt/ )
2026-08-15 10:35:54 +08:00
assert . match ( bookingModule , /const releaseSnapshotResponse = await request\(context, 'GET', path\)/ )
assert . match ( bookingModule , /const frozenTransport = Object\.fromEntries\(transportFields\.map/ )
2026-08-15 11:05:58 +08:00
assert . match ( bookingModule , /const frozenTransportMatches = frozenTransportValid/ )
assert . match ( bookingModule , /if \(!frozenTransportMatches\) return/ )
2026-08-15 10:35:54 +08:00
assert . match ( bookingModule , /expectedVersion: version, \.\.\.frozenTransport, reason: context\.marker/ )
assert . doesNotMatch ( bookingModule , /expectedVersion: version, billOfLadingNo: `BL-\$\{suffix\}`/ )
assert . doesNotMatch ( bookingModule , /estimatedSailingAt: '2026-09-01T08:00:00'/ )
})