2026-08-13 17:14:46 +08:00
|
|
|
import assert from 'node:assert/strict'
|
|
|
|
|
import { readFileSync } from 'node:fs'
|
|
|
|
|
import test from 'node:test'
|
|
|
|
|
|
2026-09-01 16:36:41 +08:00
|
|
|
import { activeCustomerIdentity, inspectOceanBookingIdentity, minimalPdfBytes,
|
|
|
|
|
platformWarehouseIdentity, truckAppointmentPayload } from './truck-appointments.mjs'
|
2026-09-01 14:44:30 +08:00
|
|
|
|
2026-09-01 16:36:41 +08:00
|
|
|
const source = readFileSync(new URL('truck-appointments.mjs', import.meta.url), 'utf8')
|
|
|
|
|
|
2026-09-20 19:45:24 +08:00
|
|
|
test('self-contained truck booking bill has its own full run namespace', () => {
|
|
|
|
|
assert.match(source, /billOfLadingNo: `BL-TRUCK-\$\{context.marker\}`/)
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-01 16:36:41 +08:00
|
|
|
test('PDF fixtures contain actual PDF magic bytes', () => {
|
|
|
|
|
const content = Buffer.from(minimalPdfBytes('TRUCK')).toString('ascii')
|
2026-09-01 14:44:30 +08:00
|
|
|
assert.equal(content.slice(0, 5), '%PDF-')
|
|
|
|
|
assert.match(content, /%%EOF\n$/)
|
|
|
|
|
})
|
2026-09-01 10:21:37 +08:00
|
|
|
|
2026-09-01 16:36:41 +08:00
|
|
|
test('platform customer and warehouse helpers remain bounded', () => {
|
2026-09-01 10:21:37 +08:00
|
|
|
assert.deepEqual(activeCustomerIdentity({ id: '21', tenantName: '现有客户' }),
|
|
|
|
|
{ customerId: '21', customerName: '现有客户' })
|
|
|
|
|
assert.equal(activeCustomerIdentity({ id: '23' }), null)
|
2026-09-01 16:36:41 +08:00
|
|
|
assert.deepEqual(platformWarehouseIdentity({ warehouseId: '41' }), { warehouseId: '41', platformTenantId: '0' })
|
2026-09-01 10:21:37 +08:00
|
|
|
})
|
|
|
|
|
|
2026-09-01 16:36:41 +08:00
|
|
|
test('booking identity contains no customer or container snapshot', () => {
|
|
|
|
|
const result = inspectOceanBookingIdentity('RUN', { id: '91', marker: 'RUN', warehouseId: '41' })
|
2026-09-01 09:51:59 +08:00
|
|
|
assert.deepEqual(result.identity, { oceanBookingId: '91', warehouseId: '41' })
|
2026-09-01 16:36:41 +08:00
|
|
|
for (const field of ['customerId', 'containerBookingId', 'loadingPlanId', 'containerNo', 'cabinetNo']) {
|
|
|
|
|
assert.equal(Object.hasOwn(result.identity, field), false)
|
|
|
|
|
}
|
2026-08-13 17:14:46 +08:00
|
|
|
})
|
|
|
|
|
|
2026-09-01 16:36:41 +08:00
|
|
|
test('save payload is strictly booking level', () => {
|
2026-09-01 09:51:59 +08:00
|
|
|
const context = { createdResources: { truckSupplier: { supplierId: '501', supplierName: '正式拖车商' } } }
|
|
|
|
|
const payload = truckAppointmentPayload(context, '2', {
|
2026-09-01 16:36:41 +08:00
|
|
|
oceanBookingId: '91', warehouseId: '41', billOfLadingNo: 'BL-1',
|
2026-08-13 17:14:46 +08:00
|
|
|
})
|
2026-09-01 16:36:41 +08:00
|
|
|
assert.equal(payload.oceanBookingId, '91')
|
|
|
|
|
assert.equal(payload.carrierId, '501')
|
|
|
|
|
for (const field of ['customerId', 'customerName', 'containerBookingId', 'loadingPlanId', 'containerNo', 'cabinetNo', 'containers']) {
|
|
|
|
|
assert.equal(Object.hasOwn(payload, field), false, field)
|
|
|
|
|
}
|
2026-08-13 17:14:46 +08:00
|
|
|
})
|
|
|
|
|
|
2026-09-01 16:36:41 +08:00
|
|
|
test('fixture creates and releases booking without creating or linking a container', () => {
|
|
|
|
|
for (const label of ['自包含创建 DRAFT 订舱', 'DRAFT订舱不进入拖车候选',
|
|
|
|
|
'上传当前有效 BILL_OF_LADING 文件', '放舱自包含订舱', 'RELEASED且资料有效的订舱进入候选']) {
|
|
|
|
|
assert.match(source, new RegExp(label))
|
|
|
|
|
}
|
|
|
|
|
assert.doesNotMatch(source, /\/api\/platform\/v1\/containers|ocean-booking-link|isoContainerNo|创建自包含模拟柜/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('HTTP lifecycle covers uniqueness idempotency attachments and every action', () => {
|
|
|
|
|
for (const label of ['创建后订舱立即退出候选', '同订舱第二张非终态拖车单冲突',
|
2026-09-04 15:29:43 +08:00
|
|
|
'重复创建未改变版本和状态', '上传并关联DISPATCH_ORDER拖车附件', '查询拖车附件列表',
|
|
|
|
|
'删除首次上传的拖车附件引用', '重新引用平台归属拖车附件',
|
|
|
|
|
'删除重新引用的拖车附件', '恢复一份DISPATCH_ORDER拖车附件供后续状态流转',
|
2026-09-14 20:03:22 +08:00
|
|
|
'编辑订舱级拖车为RMB应付费用', '整单推进SUBMIT', 'SUBMIT同键同载荷重放',
|
2026-09-01 16:36:41 +08:00
|
|
|
'SUBMIT重放版本零漂移', 'SUBMIT同键异载荷A0409',
|
2026-09-14 20:03:22 +08:00
|
|
|
'禁止手工离仓或完成拖车',
|
2026-09-01 16:36:41 +08:00
|
|
|
'CANCELLED后订舱恢复候选', 'CANCELLED后同订舱可再次新建',
|
|
|
|
|
'EXCEPTION后订舱恢复候选', 'EXCEPTION后同订舱可再次新建']) {
|
|
|
|
|
assert.match(source, new RegExp(label), label)
|
|
|
|
|
}
|
|
|
|
|
assert.match(source, /\['DISPATCH', '派车'\], \['PICKUP', '提柜'\], \['ARRIVE', '到仓'\]/)
|
2026-09-14 20:03:22 +08:00
|
|
|
assert.match(source, /\['LOAD', '装货'\]/)
|
2026-09-01 16:36:41 +08:00
|
|
|
assert.match(source, /context\.apiSuccess\(MODULE, `整单推进\$\{name\}`/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('negative matrix covers booking warehouse bill carrier and currency', () => {
|
|
|
|
|
for (const label of ['错误订舱拒绝创建', '错误仓库拒绝创建', '错误提单拒绝创建',
|
|
|
|
|
'错误承运商拒绝创建', '非法拖车币种拒绝创建']) assert.match(source, new RegExp(label))
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('backend and OpenAPI expose no container write or response protocol', () => {
|
2026-09-01 09:51:59 +08:00
|
|
|
const paths = [
|
|
|
|
|
'../../firstleg/src/main/java/com/baiyue/firstleg/dto/TruckAppointmentDtos.java',
|
|
|
|
|
'../../firstleg/src/main/java/com/baiyue/firstleg/mapper/TruckAppointmentMapper.java',
|
|
|
|
|
'../../firstleg/src/main/java/com/baiyue/firstleg/service/TruckAppointmentService.java',
|
|
|
|
|
'../../firstleg/src/main/resources/openapi/truck-appointments-v1.yaml',
|
2026-08-13 17:14:46 +08:00
|
|
|
]
|
2026-09-01 16:36:41 +08:00
|
|
|
const dto = readFileSync(new URL(paths[0], import.meta.url), 'utf8')
|
2026-09-01 09:51:59 +08:00
|
|
|
assert.match(dto, /@NotNull @Positive Long oceanBookingId/)
|
2026-09-01 16:36:41 +08:00
|
|
|
assert.doesNotMatch(dto, /SaveRequest\([^)]*(containerBookingId|loadingPlanId|containerNo|cabinetNo)/s)
|
|
|
|
|
const openApi = readFileSync(new URL(paths[3], import.meta.url), 'utf8')
|
|
|
|
|
for (const field of ['containerBookingId:', 'loadingPlanId:', 'containerNo:', 'cabinetNo:', 'containers:']) {
|
|
|
|
|
assert.doesNotMatch(openApi, new RegExp(field))
|
2026-09-01 09:51:59 +08:00
|
|
|
}
|
2026-08-18 09:09:28 +08:00
|
|
|
})
|
|
|
|
|
|
2026-09-01 16:36:41 +08:00
|
|
|
test('schema owns one active appointment per ocean booking', () => {
|
|
|
|
|
const sql = readFileSync(new URL('../../firstleg/src/main/resources/db/phase3-truck-appointment-schema.sql', import.meta.url), 'utf8')
|
|
|
|
|
assert.match(sql, /UNIQUE KEY uk_truck_active_ocean_booking\(active_ocean_booking_id\)/)
|
|
|
|
|
assert.match(sql, /DROP INDEX uk_truck_active_container/)
|
|
|
|
|
assert.match(sql, /visibility VARCHAR\(24\) NOT NULL DEFAULT 'PLATFORM'/)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('admin entry keeps truck module independent of loading fixtures', () => {
|
2026-09-01 10:16:07 +08:00
|
|
|
const entry = readFileSync(new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8')
|
|
|
|
|
assert.match(entry, /\['truck-appointments', \['merchant-qualification-fixture'\]\]/)
|
|
|
|
|
assert.doesNotMatch(entry, /\['truck-appointments', \[[^\]]*phase3-loading/)
|
2026-08-13 17:14:46 +08:00
|
|
|
})
|