169 lines
10 KiB
JavaScript
169 lines
10 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'/)
|
|
})
|
|
|
|
test('dispatch requires bill of lading number and active BILL_OF_LADING attachment', () => {
|
|
const source = readFileSync(new URL('truck-appointments.mjs', import.meta.url), 'utf8')
|
|
assert.match(source, /attachmentType=BILL_OF_LADING/)
|
|
assert.match(source, /billOfLadingNo:/)
|
|
assert.match(source, /DISPATCH 缺提单号与文件明确拒绝/)
|
|
assert.match(source, /派车前保留有效 BILL_OF_LADING 提单附件/)
|
|
const openapi = readFileSync(new URL('../../firstleg/src/main/resources/openapi/truck-appointments-v1.yaml', import.meta.url), 'utf8')
|
|
assert.match(openapi, /DISPATCH requires a nonblank billOfLadingNo/)
|
|
assert.match(openapi, /BILL_OF_LADING attachment/)
|
|
})
|