133 lines
10 KiB
JavaScript
133 lines
10 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import test from 'node:test'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
import {
|
|
containerFieldGaps,
|
|
customsCaseFieldGaps,
|
|
loadingListFieldGaps,
|
|
oceanBookingFieldGaps,
|
|
outboundAggregateFieldGaps,
|
|
portScheduleFieldGaps,
|
|
preloadFieldGaps,
|
|
truckAppointmentFieldGaps,
|
|
} from './fulfillment-list-field-contract.mjs'
|
|
|
|
const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
|
|
const workspace = path.resolve(repo, '..')
|
|
const read = (...parts) => fs.readFileSync(path.join(...parts), 'utf8')
|
|
|
|
const ids = {
|
|
id: '2089000000000000001', plan: '2089000000000000002', container: '2089000000000000003',
|
|
order: '2089000000000000004', inbound: '2089000000000000005', task: '2089000000000000006',
|
|
}
|
|
const at = '2026-08-17T08:00:00'
|
|
|
|
test('原型锁定货柜、预装、装柜、订舱与拖车关键列表字段', () => {
|
|
const source = read(workspace, 'docs', '悦', '货代SAAS平台后台', 'prototype-src', 'modules', '06-containers.html')
|
|
for (const label of ['集装箱号', '装柜清单号', '货柜状态', '柜号', '预装单号', '客户', '仓库',
|
|
'船名航次', '箱型', '总毛重', '总体积', '订舱代理', '派车单号', '运输公司', '预计装箱日期']) {
|
|
assert.match(source, new RegExp(label))
|
|
}
|
|
})
|
|
|
|
test('八类 Admin 列表均以左固定操作列开头', () => {
|
|
const sources = [
|
|
['container-loading/pages/ContainersView.vue', 1],
|
|
['loading-lists/components/LoadingListsView.vue', 2],
|
|
['ocean-bookings/pages/OceanBookingsView.vue', 1],
|
|
['port-schedules/pages/PortSchedulesView.vue', 1],
|
|
['trucking/pages/TruckAppointmentsView.vue', 1],
|
|
['customs/pages/CustomsCasesView.vue', 1],
|
|
['outbound-dispatch/pages/OutboundDispatchesView.vue', 1],
|
|
]
|
|
for (const [relative, count] of sources) {
|
|
const source = read(workspace, 'apps', 'admin-web', 'src', 'modules', relative)
|
|
assert.equal([...source.matchAll(/\{ key: 'actions', label: '操作',[^\n}]*fixed: 'left'|\{ key: 'actions', label: '操作', fixed: 'left'/g)].length, count,
|
|
`${relative} operation column count`)
|
|
}
|
|
})
|
|
|
|
test('Admin 当前列表逐字段列与验收矩阵一致', () => {
|
|
const expectations = {
|
|
'container-loading/pages/ContainersView.vue': ['cabinetNo', 'oceanBooking', 'containerNo', 'planStatus', 'warehouseName', 'terminalTruckingCompany', 'billOfLadingNo', 'containerType', 'shippingCompanyName', 'estimatedSailingAt', 'actualPortArrivalAt'],
|
|
'loading-lists/components/LoadingListsView.vue': ['preloadNo', 'customers', 'skuCount', 'loadingListNo', 'totalPackageCount', 'totalGrossWeightKg', 'totalNetWeightKg', 'totalVolumeCbm', 'totalQuantity', 'createdByName'],
|
|
'ocean-bookings/pages/OceanBookingsView.vue': ['bookingNo', 'customerName', 'billOfLadingNo', 'carrierName', 'bookingAgent', 'vesselName', 'pol', 'pod', 'etd', 'eta', 'updatedAt'],
|
|
'port-schedules/pages/PortSchedulesView.vue': ['schedule.scheduleNo', 'schedule.sourceName', 'schedule.vesselName', 'schedule.departurePortName', 'schedule.destinationPortName', 'schedule.etdAt', 'schedule.etaAt', 'schedule.observedAt', 'expired'],
|
|
'trucking/pages/TruckAppointmentsView.vue': ['appointmentNo', 'containerNo', 'company', 'driver', 'vehicleNo', 'store', 'payableAmount', 'receivableAmount', 'plannedLoadingAt', 'attachmentCount'],
|
|
'customs/pages/CustomsCasesView.vue': ['customsNo', 'orderLink', 'inboundLink', 'checklistCode', 'checklistVersionNo', 'status', 'updatedAt'],
|
|
'outbound-dispatch/pages/OutboundDispatchesView.vue': ['inboundCode', 'customerName', 'containerNo', 'oceanBookingStatus', 'truckAppointmentStatus', 'pickTaskStatus', 'dispatchStatus', 'gate'],
|
|
}
|
|
for (const [relative, fields] of Object.entries(expectations)) {
|
|
const source = read(workspace, 'apps', 'admin-web', 'src', 'modules', relative)
|
|
for (const field of fields) assert.match(source, new RegExp(field), `${relative}: ${field}`)
|
|
}
|
|
})
|
|
|
|
test('正式 DTO/OpenAPI 暴露矩阵使用的稳定字段', () => {
|
|
const sources = [
|
|
['firstleg/src/main/java/com/baiyue/firstleg/entity/ContainerBookingEntity.java', ['cabinetNo', 'containerNo', 'channelCode', 'warehouseName', 'capacityCbm', 'maxWeightKg', 'planStatus']],
|
|
['firstleg/src/main/java/com/baiyue/firstleg/dto/LoadingListDtos.java', ['record PreloadSummary', 'record ConfirmedSummary', 'totalGrossWeightKg', 'totalVolumeCbm', 'customers']],
|
|
['firstleg/src/main/java/com/baiyue/firstleg/oceanbooking/dto/OceanBookingDtos.java', ['record Detail', 'record RealContainer', 'bookingAgentName', 'estimatedSailingAt', 'estimatedArrivalAt']],
|
|
['firstleg/src/main/java/com/baiyue/firstleg/portschedule/dto/PortScheduleDtos.java', ['record ScheduleView', 'sourceName', 'etdAt', 'etaAt', 'expiresAt']],
|
|
['firstleg/src/main/java/com/baiyue/firstleg/entity/TruckAppointmentEntity.java', ['appointmentNo', 'carrierName', 'plannedLoadingAt', 'containerNo']],
|
|
['firstleg/src/main/java/com/baiyue/firstleg/customs/entity/CustomsCaseEntity.java', ['customsNo', 'checklistCode', 'checklistVersionNo', 'updatedAt']],
|
|
['firstleg/src/main/java/com/baiyue/firstleg/outbound/dispatch/dto/OutboundDispatchDtos.java', ['record Aggregate', 'customerName', 'pickTaskStatus', 'dispatchStatus', 'GateResult']],
|
|
]
|
|
for (const [relative, fields] of sources) {
|
|
const source = read(repo, relative)
|
|
for (const field of fields) assert.match(source, new RegExp(field), `${relative}: ${field}`)
|
|
}
|
|
})
|
|
|
|
test('字段校验器接受正式完整投影并精确暴露缺字段', () => {
|
|
const samples = [
|
|
[containerFieldGaps, { id: ids.container, cabinetNo: 'CAB-1', containerNo: 'MSCU1234567', warehouseId: ids.id, warehouseName: '宁波仓', channelCode: 'EXX', containerType: '40HC', capacityCbm: 76, maxWeightKg: 28000, planStatus: 'EDITING' }, { requireContainerNo: true }, 'warehouseName'],
|
|
[preloadFieldGaps, { planId: ids.plan, containerBookingId: ids.container, preloadNo: 'PL-1', cabinetNo: 'CAB-1', customers: [{ accountId: ids.id, customerName: '测试客户' }], warehouseName: '宁波仓', containerType: '40HC', status: 'EDITING', skuCount: 1, totalPackageCount: 1, totalVolumeCbm: 1, totalGrossWeightKg: 10, createdAt: at }, undefined, 'customers'],
|
|
[loadingListFieldGaps, { planId: ids.plan, containerBookingId: ids.container, loadingListNo: 'LL-1', containerNo: 'MSCU1234567', cabinetNo: 'CAB-1', warehouseName: '宁波仓', status: 'COMPLETED', totalPackageCount: 1, totalGrossWeightKg: 10, totalNetWeightKg: 9, totalVolumeCbm: 1, totalQuantity: 2, createdAt: at }, undefined, 'containerNo'],
|
|
[oceanBookingFieldGaps, { id: ids.id, bookingNo: 'BK-1', status: 'RELEASED', vesselName: 'VESSEL', voyageNo: 'V1', estimatedSailingAt: at, estimatedArrivalAt: at, customerId: ids.id, customerName: '测试客户', shippingCompanyId: null, shippingCompanyName: null, bookingAgentId: ids.id, bookingAgentName: '代理' }, undefined, 'voyageNo'],
|
|
[portScheduleFieldGaps, { portSchedule: { schedule: { id: ids.id, scheduleNo: 'PS-1', sourceName: '船公司', vesselName: 'VESSEL', voyageNo: 'V1', departurePortCode: 'CNNGB', destinationPortCode: 'USLAX', etdAt: at, etaAt: at, observedAt: at, updatedAt: at, expiresAt: at }, expired: false } }, undefined, 'sourceName'],
|
|
[truckAppointmentFieldGaps, { id: ids.id, appointmentNo: 'TA-1', oceanBookingId: ids.id, containerBookingId: ids.container, loadingPlanId: ids.plan, warehouseId: ids.id, warehouseName: '宁波仓', carrierId: ids.id, carrierName: '拖车商', plannedLoadingAt: at, status: 'DRAFT', attachmentCount: 0 }, undefined, 'appointmentNo'],
|
|
[customsCaseFieldGaps, { customsCase: { id: ids.id, customsNo: 'CUS-1', orderId: ids.order, inboundId: ids.inbound, containerBookingId: ids.container, checklistVersionId: ids.id, checklistCode: 'CHECK', checklistVersionNo: 'V1', status: 'DOCUMENTS_PENDING', updatedAt: at } }, undefined, 'checklistCode'],
|
|
[outboundAggregateFieldGaps, { containerBookingId: ids.container, containerNo: 'MSCU1234567', loadingPlanId: ids.plan, loadingPlanStatus: 'COMPLETED', oceanBookingId: ids.id, oceanBookingStatus: 'RELEASED', dispatchStatus: 'READY_FOR_DISPATCH', version: 1, inbounds: [{ orderId: ids.order, inboundId: ids.inbound, inboundCode: 'IN-1', tenantId: ids.id, inboundStatus: 'LOADED', pickTaskId: ids.task, pickTaskStatus: 'COMPLETED' }], gate: { inbounds: [{ inboundId: ids.inbound, inboundCode: 'IN-1', gate: { items: [] } }] } }, undefined, 'containerNo'],
|
|
]
|
|
for (const [validator, sample, options, missing] of samples) {
|
|
assert.deepEqual(validator(sample, options), [])
|
|
const invalid = structuredClone(sample)
|
|
const target = invalid.portSchedule?.schedule ?? invalid.customsCase ?? invalid
|
|
target[missing] = ''
|
|
assert.ok(validator(invalid, options).includes(missing))
|
|
}
|
|
})
|
|
|
|
test('字段断言接入正式生产模块和默认全量入口', () => {
|
|
const consumers = {
|
|
'phase3-container-loading.mjs': ['containerFieldGaps', 'preloadFieldGaps', 'loadingListFieldGaps'],
|
|
'ocean-bookings.mjs': ['oceanBookingFieldGaps'],
|
|
'phase3-port-schedules.mjs': ['portScheduleFieldGaps'],
|
|
'truck-appointments.mjs': ['truckAppointmentFieldGaps'],
|
|
'phase3-customs.mjs': ['customsCaseFieldGaps'],
|
|
'phase3-outbound-dispatch.mjs': ['outboundAggregateFieldGaps'],
|
|
}
|
|
for (const [file, tokens] of Object.entries(consumers)) {
|
|
const source = read(repo, 'scripts', 'admin-test-env-modules', file)
|
|
for (const token of tokens) assert.match(source, new RegExp(token), `${file}: ${token}`)
|
|
}
|
|
const entry = read(repo, 'scripts', 'admin-test-env-acceptance.mjs')
|
|
for (const module of Object.keys(consumers).map((file) => file.replace(/\.mjs$/, ''))) {
|
|
assert.match(entry, new RegExp(`['"]${module}['"]`), `default entry: ${module}`)
|
|
}
|
|
})
|
|
|
|
test('矩阵登记 LANDED 与 UNKNOWN 且禁止 fixture 伪造未知字段', () => {
|
|
const matrix = read(workspace, 'docs', '三期', '货柜履约原型字段验收矩阵-2026-08-17.md')
|
|
assert.match(matrix, /LANDED/)
|
|
assert.match(matrix, /UNKNOWN/)
|
|
assert.match(matrix, /不得由 fixture 伪造|不生成假值/)
|
|
for (const label of ['货柜', '预装清单', '装柜清单', '订舱', '船期', '拖车', '报关协同', '国内出仓']) {
|
|
assert.match(matrix, new RegExp(label))
|
|
}
|
|
})
|