Files
baiyue-firstleg/scripts/admin-test-env-modules/truck-appointments-contract.test.mjs
T
2026-08-18 09:09:28 +08:00

158 lines
9.7 KiB
JavaScript

import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
import { inspectOceanBookingIdentity, selectIndependentTruckCustomer, truckAppointmentPayload } from './truck-appointments.mjs'
test('truck appointment accepts only the current marker ocean booking identity', () => {
const current = inspectOceanBookingIdentity('RUN-2', {
id: '91', marker: 'RUN-2', customerId: '21', warehouseId: '41', customerName: '本轮客户',
})
assert.deepEqual(current.identity, {
oceanBookingId: '91', customerId: '21', warehouseId: '41', customerName: '本轮客户',
})
const stale = inspectOceanBookingIdentity('RUN-2', {
id: '90', marker: 'RUN-1', customerId: '20', warehouseId: '40', customerName: '历史客户',
})
assert.equal(stale.identity, null)
assert.match(stale.reason, /marker 不属于本轮/)
})
test('truck appointment fills missing identity only from the current booking detail', () => {
const partial = { id: '91', marker: 'RUN-2', customerId: '21' }
const beforeDetail = inspectOceanBookingIdentity('RUN-2', partial)
assert.equal(beforeDetail.identity, null)
assert.deepEqual(beforeDetail.missingFields, ['warehouseId', 'customerName'])
const resolved = inspectOceanBookingIdentity('RUN-2', partial, { oceanBooking: {
id: 91, customerId: 21, warehouseId: 41, customerName: '本轮客户',
} })
assert.deepEqual(resolved.identity, {
oceanBookingId: '91', customerId: '21', warehouseId: '41', customerName: '本轮客户',
})
})
test('truck appointment rejects mismatched booking detail instead of mixing identities', () => {
const wrongResource = inspectOceanBookingIdentity('RUN-2', { id: '91', marker: 'RUN-2' }, {
oceanBooking: { id: '92', customerId: '21', warehouseId: '41', customerName: '其他订舱' },
})
assert.equal(wrongResource.identity, null)
assert.match(wrongResource.reason, /资源归属不一致/)
const conflictingCustomer = inspectOceanBookingIdentity('RUN-2', {
id: '91', marker: 'RUN-2', customerId: '21',
}, { oceanBooking: { id: '91', customerId: '22', warehouseId: '41', customerName: '本轮客户' } })
assert.equal(conflictingCustomer.identity, null)
assert.match(conflictingCustomer.reason, /customerId/)
})
test('create edit and stale replay payloads keep the independent customer and empty links stable', () => {
const context = { marker: 'RUN-2', createdResources: {
truckSupplier: { supplierId: '501', supplierName: '正式拖车商' },
} }
const identity = { customerId: '21', customerName: '本轮客户' }
const payloads = [
truckAppointmentPayload(context, '2', identity),
truckAppointmentPayload(context, '2', identity, 3),
truckAppointmentPayload(context, '2', identity, 0),
]
for (const payload of payloads) {
assert.deepEqual({
oceanBookingId: payload.oceanBookingId, containerBookingId: payload.containerBookingId,
loadingPlanId: payload.loadingPlanId, customerId: payload.customerId,
customerName: payload.customerName, carrierId: payload.carrierId,
}, { oceanBookingId: null, containerBookingId: null, loadingPlanId: null,
customerId: '21', customerName: '本轮客户', carrierId: '501' })
}
assert.deepEqual(payloads.map((payload) => payload.expectedVersion), [0, 3, 0])
})
test('truck payload requires a verified carrier ID/name pair', () => {
const identity = { customerId: '21', customerName: '本轮客户' }
const missing = truckAppointmentPayload({ marker: 'RUN-2', createdResources: {} }, '2', identity)
assert.equal(missing.carrierId, null)
assert.equal(missing.carrierName, null)
const valid = truckAppointmentPayload({ marker: 'RUN-2', createdResources: {
truckSupplier: { supplierId: '501', supplierName: '正式拖车商' },
} }, '2', identity)
assert.deepEqual({ carrierId: valid.carrierId, carrierName: valid.carrierName }, { carrierId: '501', carrierName: '正式拖车商' })
const incomplete = truckAppointmentPayload({ marker: 'RUN-2', createdResources: {
truckSupplier: { supplierId: '501', supplierName: null },
} }, '2', identity)
assert.equal(incomplete.carrierId, '501')
assert.equal(incomplete.carrierName, null)
})
test('independent customer comes only from the current marker fixture', () => {
const current = selectIndependentTruckCustomer({ marker: 'RUN-2', createdResources: {
orders: { marker: 'RUN-2', tenantId: '21', customerName: '本轮客户' },
} })
assert.deepEqual(current, { customerId: '21', customerName: '本轮客户' })
const stale = selectIndependentTruckCustomer({ marker: 'RUN-2', createdResources: {
orders: { marker: 'RUN-1', tenantId: '20', customerName: '历史客户' },
} })
assert.equal(stale, null)
})
test('truck appointment source has no booking prerequisite historical list or hardcoded identity fallback', () => {
const source = readFileSync(new URL('truck-appointments.mjs', import.meta.url), 'utf8')
assert.doesNotMatch(source, /customerId:\s*['"]10001['"]|warehouseId:\s*['"]193000000000000001['"]/)
assert.doesNotMatch(source, /import\(new URL\('\.\/ocean-bookings\.mjs'/)
assert.match(source, /创建无订舱柜次计划关联的独立拖车预约/)
assert.match(source, /缺少同 marker 的 customerId;未借用历史客户或订舱/)
})
test('backend freezes independent creation association validation and PDA failure isolation', () => {
const dto = readFileSync(new URL('../../firstleg/src/main/java/com/baiyue/firstleg/dto/TruckAppointmentDtos.java', import.meta.url), 'utf8')
assert.match(dto, /@Positive Long oceanBookingId,@Positive Long containerBookingId,@Positive Long loadingPlanId/)
assert.match(dto, /@NotNull @Positive Long customerId/)
assert.match(dto, /@NotNull @Positive Long carrierId/)
assert.match(dto, /@NotNull LocalDateTime plannedLoadingAt/)
assert.doesNotMatch(dto, /@NotNull @Positive Long oceanBookingId|@NotNull @Positive Long containerBookingId/)
const openapi = readFileSync(new URL('../../firstleg/src/main/resources/openapi/truck-appointments-v1.yaml', import.meta.url), 'utf8')
assert.match(openapi, /required: \[customerId, carrierId, plannedLoadingAt, expectedVersion\]/)
assert.match(openapi, /Ocean booking, container booking and loading plan are optional links/)
assert.match(openapi, /absence, duplicates or concurrent truck edits never roll back loading completion/)
const service = readFileSync(new URL('../../firstleg/src/main/java/com/baiyue/firstleg/service/TruckAppointmentService.java', import.meta.url), 'utf8')
for (const token of ['customers.active(r.customerId(),r.customerName())', 'suppliers.trucking(r.carrierId(),r.carrierName())',
'customerId与订舱客户不一致', 'customerId与柜次客户不一致', 'containerBookingId未分配至该订舱',
'loadingPlanId与柜次当前装柜计划不一致', 'Propagation.REQUIRES_NEW', 'active.isEmpty()',
'active.size()!=1', 'repo.completeLoadingFromPda']) assert.match(service, new RegExp(token.replace(/[().]/g, '\\$&')))
const loading = readFileSync(new URL('../../firstleg/src/main/java/com/baiyue/firstleg/service/LoadingPickService.java', import.meta.url), 'utf8')
assert.match(loading, /completeTruckAfterCommit/)
assert.match(loading, /afterCommit\(\) \{ completion\.run\(\); \}/)
assert.match(loading, /catch \(RuntimeException exception\)/)
assert.ok(loading.indexOf('loadingListService.publishCompletedEvent') < loading.indexOf('completeTruckAfterCommit'),
'PDA task/plan/inbound completion must precede best-effort truck synchronization')
const truckTests = readFileSync(new URL('../../firstleg/src/test/java/com/baiyue/firstleg/service/TruckAppointmentServiceTest.java', import.meta.url), 'utf8')
for (const scenario of ['pdaCompletionDoesNotRequireTruckAppointment',
'pdaCompletionLocksAndCompletesTheOnlyActiveTruckWithHistory',
'pdaCompletionRejectsMultipleActiveTruckAppointmentsWithoutMutation',
'pdaCompletionRejectsConcurrentTruckModificationWithoutHistory']) assert.match(truckTests, new RegExp(scenario))
const loadingTests = readFileSync(new URL('../../firstleg/src/test/java/com/baiyue/firstleg/service/LoadingPickServiceTest.java', import.meta.url), 'utf8')
assert.match(loadingTests, /conflictingTruckAppointmentsDoNotBlockPdaCompletion/)
})
test('Admin requires customer carrier and planned time but keeps booking and container optional', () => {
const source = readFileSync(new URL('../../../apps/admin-web/src/modules/trucking/components/TruckAppointmentForm.vue', import.meta.url), 'utf8')
assert.match(source, /customerId: \[\{ required: true/)
assert.match(source, /carrierId: \[\{ required: true/)
assert.match(source, /plannedLoadingAt: \[\{ required: true/)
assert.match(source, /label="关联柜次(可选)"/)
assert.match(source, /label="关联订舱(可选)"/)
assert.doesNotMatch(source, /containerBookingId: \[\{ required: true|oceanBookingId: \[\{ required: true/)
})
test('default HTTP entry carries independent truck positives and association negatives after current-marker producers', () => {
const source = readFileSync(new URL('truck-appointments.mjs', import.meta.url), 'utf8')
for (const label of ['缺少 customerId 拒绝创建独立拖车', '缺少 ACTIVE TRUCKING carrier 拒绝创建独立拖车',
'缺少 plannedLoadingAt 拒绝创建独立拖车', 'loadingPlanId 与关联柜次当前计划不一致时零写入拒绝',
'warehouseId 与可选关联柜次不一致时零写入拒绝', '订舱与柜次无 allocation 关系时零写入拒绝',
'创建无订舱柜次计划关联的独立拖车预约']) assert.match(source, new RegExp(label))
const entry = readFileSync(new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8')
assert.match(entry, /'orders'[\s\S]*'phase3-loading'[\s\S]*'ocean-bookings'[\s\S]*'truck-appointments'/)
})