优化
This commit is contained in:
@@ -7,6 +7,12 @@ import { run } from './finance-reconciliation-full.mjs'
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const source = fs.readFileSync(path.join(here, 'finance-reconciliation-full.mjs'), 'utf8')
|
||||
|
||||
test('Finance pool keeps formal MANUAL_COST and maps container cost by stable container identity', () => {
|
||||
assert.doesNotMatch(source, /sourceType \?\? ''\) !== 'MANUAL_COST'/)
|
||||
assert.match(source, /\[raw\.containerBookingId, raw\.loadingPlanId\]\.map\(id\)\.includes\(id\(item\?\.containerId\)\)/)
|
||||
assert.match(source, /!isUnboundContainerCost\(source\.raw\) \|\| hasContainerAllocations/)
|
||||
})
|
||||
const financePermissions = [
|
||||
'platform:finance:customer-account:list', 'platform:finance:customer-statement:list',
|
||||
'platform:finance:reconciliation:create', 'platform:finance:reconciliation:review',
|
||||
|
||||
@@ -176,7 +176,6 @@ async function normalizeFixture(context) {
|
||||
`${BASE}/fee-pool?customerId=${encodeURIComponent(customerId)}&direction=ALL`)
|
||||
const poolData = poolResponse?.body?.data
|
||||
pool = rows(poolData).concat(rows(poolData?.items))
|
||||
.filter((item) => String(item?.sourceType ?? '') !== 'MANUAL_COST')
|
||||
const visible = new Set(pool.map((item) => id(item?.sourceId)))
|
||||
if ([...expectedSourceIds].every((source) => visible.has(source))) break
|
||||
await new Promise((resolve) => setTimeout(resolve, 500))
|
||||
@@ -185,6 +184,8 @@ async function normalizeFixture(context) {
|
||||
const attemptedSources = root.feeSources.map((raw) => {
|
||||
const order = orders.get(id(raw.orderId))
|
||||
const financeSource = poolBySource.get(sourceId(raw))
|
||||
|| pool.find((item) => String(item?.direction || '').toUpperCase() === 'PAYABLE'
|
||||
&& [raw.containerBookingId, raw.loadingPlanId].map(id).includes(id(item?.containerId)))
|
||||
return {
|
||||
raw, feeSourceId: financePoolId(financeSource), sourceFeeId: sourceId(raw), customerId: id(raw.customerId ?? customerId),
|
||||
direction: String(raw.direction ?? ''), currency: String(raw.currency ?? raw.currencyCode ?? '').toUpperCase(),
|
||||
@@ -199,9 +200,11 @@ async function normalizeFixture(context) {
|
||||
|| (isUnboundContainerCost(source.raw) && !hasContainerAllocations))
|
||||
return {
|
||||
root, customerId,
|
||||
sources: attemptedSources.filter((source) => source.feeSourceId && !isUnboundContainerCost(source.raw)),
|
||||
sources: attemptedSources.filter((source) => source.feeSourceId
|
||||
&& (!isUnboundContainerCost(source.raw) || hasContainerAllocations)),
|
||||
uncreatedSources,
|
||||
sourceMappings: attemptedSources.filter((source) => source.feeSourceId && !isUnboundContainerCost(source.raw))
|
||||
sourceMappings: attemptedSources.filter((source) => source.feeSourceId
|
||||
&& (!isUnboundContainerCost(source.raw) || hasContainerAllocations))
|
||||
.map((source) => ({ sourceFeeId: source.sourceFeeId, feeSourceId: source.feeSourceId,
|
||||
sourceType: source.raw?.sourceType ?? null, direction: source.direction })),
|
||||
}
|
||||
|
||||
@@ -19,7 +19,9 @@ test('each remainder uses a simulation linked to a confirmed then released booki
|
||||
const releaseStart = source.indexOf('const releaseResponse')
|
||||
const releaseBlock = source.slice(releaseStart, source.indexOf('bookingDetailResponse = await', releaseStart))
|
||||
for (const field of ['billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt',
|
||||
'estimatedArrivalAt', 'expectedVersion']) assert.match(releaseBlock, new RegExp(`${field}:`))
|
||||
'estimatedArrivalAt']) assert.match(releaseBlock, new RegExp(`${field}: persistedTransport\\.${field}`))
|
||||
assert.match(releaseBlock, /expectedVersion: Number\(bookingDetail\.oceanBooking\.version\)/)
|
||||
assert.doesNotMatch(releaseBlock, /BL-\$\{|SPLIT-VESSEL|SV-\$\{|2026-/)
|
||||
assert.match(releaseBlock, /'Idempotency-Key': `\$\{operationBase\}-booking-release`\.slice\(0, 128\)/)
|
||||
assert.match(source, /oceanBooking\?\.status === 'RELEASED' && releasedAllocationMatches/)
|
||||
assert.match(source, /id\(item\?\.containerBookingId\) === containerId/)
|
||||
@@ -28,6 +30,15 @@ test('each remainder uses a simulation linked to a confirmed then released booki
|
||||
assert.doesNotMatch(source, /candidates\?warehouseId=/)
|
||||
})
|
||||
|
||||
test('release stops unless creation response and detail expose the same complete transport snapshot', () => {
|
||||
assert.match(source, /const BOOKING_TRANSPORT_FIELDS = \[/)
|
||||
assert.match(source, /const createdTransport = bookingTransportSnapshot\(bookingCreated\?\.oceanBooking\)/)
|
||||
assert.match(source, /const persistedTransport = bookingTransportSnapshot\(bookingDetail\?\.oceanBooking\)/)
|
||||
assert.match(source, /completeBookingTransport\(createdTransport\)/)
|
||||
assert.match(source, /sameBookingTransport\(createdTransport, persistedTransport\)/)
|
||||
assert.match(source, /if \(!transportFrozen\) return null/)
|
||||
})
|
||||
|
||||
test('PDA opens only after a real container number and uses stable plan identity', () => {
|
||||
const pushIndex = source.indexOf("`${SMART}/plans/${planId}/push-preload`")
|
||||
const transportIndex = source.indexOf("`${CONTAINERS}/${encodeURIComponent(containerId)}/transport`")
|
||||
@@ -71,6 +82,7 @@ test('booking freezes the source customer channel and warehouse scope', () => {
|
||||
assert.match(source, /channelCode: source\.channelCode/)
|
||||
assert.match(source, /warehouseId: source\.warehouseId/)
|
||||
assert.match(source, /containerType === '40HQ'/)
|
||||
assert.match(source, /containerType: '40HQ', capacityCbm: 68, maxWeightKg: 28000/)
|
||||
})
|
||||
|
||||
test('booking fixture never invents supplier names without formal IDs', () => {
|
||||
|
||||
@@ -25,6 +25,23 @@ function hasTrace(response) {
|
||||
&& typeof response?.traceId === 'string' && response.traceId.length > 0
|
||||
}
|
||||
|
||||
const BOOKING_TRANSPORT_FIELDS = [
|
||||
'billOfLadingNo', 'vesselName', 'voyageNo', 'estimatedSailingAt', 'estimatedArrivalAt',
|
||||
]
|
||||
|
||||
function bookingTransportSnapshot(value) {
|
||||
return Object.fromEntries(BOOKING_TRANSPORT_FIELDS.map((field) => [field, value?.[field] ?? null]))
|
||||
}
|
||||
|
||||
function completeBookingTransport(snapshot) {
|
||||
return BOOKING_TRANSPORT_FIELDS.every((field) => typeof snapshot?.[field] === 'string'
|
||||
&& snapshot[field].trim().length > 0)
|
||||
}
|
||||
|
||||
function sameBookingTransport(left, right) {
|
||||
return BOOKING_TRANSPORT_FIELDS.every((field) => left?.[field] === right?.[field])
|
||||
}
|
||||
|
||||
async function request(context, method, path, body, headers = {}, token = context.token) {
|
||||
try {
|
||||
const response = await fetch(`${context.options.gateway}${path}`, {
|
||||
@@ -131,7 +148,7 @@ async function loadOneFragment(context, state) {
|
||||
const { tenantId, rootOrderId, sourceOrderId, sourceOrderNo, warehouseId, sourcePackageCount, sequence } = state
|
||||
const operationBase = `${context.marker}-chain-${sequence}`.slice(0, 100)
|
||||
const simulationResponse = await request(context, 'POST', CONTAINERS, {
|
||||
channelCode: state.channelCode, warehouseId, containerType: '40HQ', capacityCbm: 100, maxWeightKg: 100000,
|
||||
channelCode: state.channelCode, warehouseId, containerType: '40HQ', capacityCbm: 68, maxWeightKg: 28000,
|
||||
})
|
||||
const simulation = context.apiSuccess(MODULE, `为 ${sourceOrderNo} 新增内部模拟柜`, simulationResponse, '', OPTIONS)
|
||||
const containerId = id(simulation?.id)
|
||||
@@ -221,12 +238,24 @@ async function loadOneFragment(context, state) {
|
||||
`oceanBookingId=${id(bookingDetail?.oceanBooking?.id) || 'missing'}, containerBookingId=${containerId}, allocations=${bookingDetail?.containerAllocations?.length ?? 'n/a'}`, OPTIONS)
|
||||
if (!allocationMatches || id(bookingDetail?.oceanBooking?.id) !== oceanBookingId) return null
|
||||
|
||||
const createdTransport = bookingTransportSnapshot(bookingCreated?.oceanBooking)
|
||||
const persistedTransport = bookingTransportSnapshot(bookingDetail?.oceanBooking)
|
||||
const transportFrozen = completeBookingTransport(createdTransport)
|
||||
&& completeBookingTransport(persistedTransport)
|
||||
&& sameBookingTransport(createdTransport, persistedTransport)
|
||||
context.assert(MODULE, `${sourceOrderNo} 创建响应与详情五项运输字段冻结一致`, bookingDetailResponse,
|
||||
transportFrozen,
|
||||
BOOKING_TRANSPORT_FIELDS.map((field) => `${field}=${persistedTransport[field] || 'missing'}`).join(', '), OPTIONS)
|
||||
if (!transportFrozen) return null
|
||||
|
||||
if (bookingDetail.oceanBooking.status === 'CONFIRMED') {
|
||||
const releaseResponse = await request(context, 'POST', `${bookingPath}/release`, {
|
||||
expectedVersion: Number(bookingDetail.oceanBooking.version),
|
||||
billOfLadingNo: `BL-${operationBase}`.slice(0, 128),
|
||||
vesselName: `SPLIT-VESSEL-${sequence}`.slice(0, 128), voyageNo: `SV-${sequence}`.slice(0, 64),
|
||||
estimatedSailingAt: '2026-09-16T08:00:00', estimatedArrivalAt: '2026-10-16T08:00:00',
|
||||
billOfLadingNo: persistedTransport.billOfLadingNo,
|
||||
vesselName: persistedTransport.vesselName,
|
||||
voyageNo: persistedTransport.voyageNo,
|
||||
estimatedSailingAt: persistedTransport.estimatedSailingAt,
|
||||
estimatedArrivalAt: persistedTransport.estimatedArrivalAt,
|
||||
reason: context.marker,
|
||||
}, { 'Idempotency-Key': `${operationBase}-booking-release`.slice(0, 128) })
|
||||
context.apiSuccess(MODULE, `放舱 ${sourceOrderNo} 的真实订舱并满足 PDA 门禁`, releaseResponse, '', OPTIONS)
|
||||
|
||||
Reference in New Issue
Block a user