This commit is contained in:
2026-08-08 12:01:45 +08:00
parent 1a2f598c9a
commit a35cd868b9
56 changed files with 7178 additions and 735 deletions
+344 -16
View File
@@ -60,6 +60,7 @@ const report = {
terminalInboundId: null,
terminalOrderId: null,
terminalInboundCode: null,
terminalMeasurementId: null,
pendingOrderId: null,
foreignOrderId: null,
measurementImageFileIds: [],
@@ -74,6 +75,8 @@ let terminalFixture
let pendingFixture
let foreignFixture
let platformTerminalDetail
let platformMeasurement
let dimensionRules = []
let merchantBilling
try {
@@ -190,6 +193,148 @@ async function fixtureDiscoveryModule() {
}))
platformTerminalDetail = detail.json.data
const dimensionConfig = await apiStep('phase2-measurement-fixture',
'load four customer dimension weighting rules', {
method: 'GET',
path: `/api/platform/v1/first-leg/customer-dimension-config?tenantId=${report.resources.tenantId}`,
token: platformToken,
}, ({ json }) => {
requireSuccess(json)
const rules = arrayData(json.data?.rules)
assertEqual(rules.length, 4, 'dimension rule count')
assertEqual(new Set(rules.map((rule) => rule.boxType)).size, 4, 'dimension box type count')
for (const rule of rules) assertDimensionRule(rule)
}, ({ json }) => ({
tenantId: json.data?.tenantId,
rules: arrayData(json.data?.rules).map((rule) => ({
boxType: rule.boxType,
inner: dimensions(rule, 'inner'),
edgeAdditionCm: rule.edgeAdditionCm,
outer: dimensions(rule, 'outer'),
volumeCbm: rule.volumeCbm,
version: rule.version,
})),
}))
dimensionRules = dimensionConfig.json.data.rules
const measurement = await apiStep('phase2-measurement-fixture',
'load approved terminal measurement detail', {
method: 'GET',
path: `/api/platform/v1/first-leg/inbounds/${report.resources.terminalInboundId}`
+ `/measurement?tenantId=${report.resources.tenantId}`,
token: platformToken,
}, ({ json }) => {
requireSuccess(json)
assertEqual(String(json.data?.inboundId), report.resources.terminalInboundId, 'measurement inbound id')
assertEqual(json.data?.processStatus, 'WAIT_SPLIT', 'measurement process status')
assertEqual(json.data?.measureReviewStatus, 'APPROVED', 'measurement review status')
assertEqual(json.data?.measurementStatus, 'APPROVED', 'measurement batch status')
assertPresent(json.data?.measurementId, 'measurement id')
assertTrue(arrayData(json.data?.items).length > 0, 'measurement items must not be empty')
assertTrue(arrayData(json.data?.weightResults).length > 0, 'weight results must not be empty')
}, ({ json }) => ({
inboundId: json.data?.inboundId,
measurementId: json.data?.measurementId,
measurementStatus: json.data?.measurementStatus,
actualPackageCount: json.data?.actualPackageCount,
actualGrossWeightKg: json.data?.actualGrossWeightKg,
actualVolumeCbm: json.data?.actualVolumeCbm,
volumeWeightKg: json.data?.volumeWeightKg,
itemCount: arrayData(json.data?.items).length,
weightResultCount: arrayData(json.data?.weightResults).length,
}))
platformMeasurement = measurement.json.data
report.resources.terminalMeasurementId = String(platformMeasurement.measurementId)
await localStep('phase2-measurement-fixture', 'carton measurements contain complete traceable data', () => {
for (const item of platformMeasurement.items) {
assertStringId(item.id, 'measurement item id')
assertStringId(item.inboundDetailId, 'measurement inbound detail id')
assertPresent(item.itemNo, 'measurement item number')
assertPresent(item.boxCode, 'measurement box code')
assertPresent(item.machineNo, 'measurement machine number')
assertPresent(item.deviceMeasuredAt, 'device measured time')
assertStringId(item.intelligentImageFileId, 'measurement image file id')
assertTrue(Number(item.packageCount) > 0, 'measurement package count must be positive')
assertEqual(Number(item.confirmedPackageCount), Number(item.packageCount),
'confirmed package count')
for (const field of ['lengthCm', 'widthCm', 'heightCm', 'unitVolumeCbm', 'totalVolumeCbm']) {
assertTrue(Number(item[field]) > 0, `${field} must be positive`)
}
}
}, { itemCount: platformMeasurement.items.length })
await localStep('phase2-measurement-fixture', 'carton volume and package aggregates are consistent', () => {
const packageCount = sum(platformMeasurement.items, 'packageCount')
const volumeCbm = sum(platformMeasurement.items, 'totalVolumeCbm')
const volumeWeightKg = sum(platformMeasurement.items, 'volumeWeightKg')
assertNumberEqual(packageCount, platformMeasurement.actualPackageCount, 'measurement package aggregate')
assertApproxEqual(volumeCbm, platformMeasurement.actualVolumeCbm, 0.000001,
'measurement volume aggregate')
assertApproxEqual(volumeWeightKg, platformMeasurement.volumeWeightKg, 0.001,
'measurement volume weight aggregate')
}, {
actualPackageCount: platformMeasurement.actualPackageCount,
actualVolumeCbm: platformMeasurement.actualVolumeCbm,
volumeWeightKg: platformMeasurement.volumeWeightKg,
})
await localStep('phase2-measurement-fixture', 'confirmed weight results form an independent weight line', () => {
for (const weight of platformMeasurement.weightResults) {
assertStringId(weight.id, 'weight result id')
assertStringId(weight.fileId, 'weight image file id')
assertEqual(weight.recognitionStatus, 'CONFIRMED', 'weight recognition status')
assertPresent(weight.confirmedAt, 'weight confirmation time')
assertTrue(Number(weight.finalWeightKg) > 0, 'final weight must be positive')
}
assertApproxEqual(sum(platformMeasurement.weightResults, 'finalWeightKg'),
platformMeasurement.actualGrossWeightKg, 0.001, 'measurement gross weight aggregate')
}, {
actualGrossWeightKg: platformMeasurement.actualGrossWeightKg,
weightResultCount: platformMeasurement.weightResults.length,
})
await softLocalStep('phase2-measurement-fixture',
'carton billing volume uses current customer dimension weighting', () => {
for (const item of platformMeasurement.items) {
const expected = resolveWeightedDimensions(item, dimensionRules)
assertApproxEqual(item.unitVolumeCbm, expected.unitVolumeCbm, 0.000001,
`${item.itemNo} weighted unit volume`)
}
}, {
measurementId: platformMeasurement.measurementId,
items: platformMeasurement.items.map((item) => ({
itemNo: item.itemNo,
measured: dimensions(item),
storedOuter: dimensions(item, 'outer'),
unitVolumeCbm: item.unitVolumeCbm,
expected: resolveWeightedDimensions(item, dimensionRules),
})),
})
await apiStep('phase2-client-security', 'client cannot read platform measurement workspace directly', {
method: 'GET',
path: `/api/platform/v1/first-leg/inbounds/${report.resources.terminalInboundId}`
+ `/measurement?tenantId=${report.resources.tenantId}`,
token: clientToken,
}, ({ status, json }) => {
assertEqual(status, 403, 'HTTP status')
assertTrue(json?.code == null || String(json.code) === 'A0403',
`expected absent/A0403 business code, actual=${json?.code}`)
}, ({ json }) => ({ rejectionCode: json?.code }))
const platformImageFileId = platformMeasurement.items[0]?.intelligentImageFileId
await apiStep('phase2-client-security', 'client cannot bypass billing publication to read platform image', {
method: 'GET',
path: `/api/platform/v1/files/measurements/${report.resources.tenantId}`
+ `/${report.resources.terminalInboundId}/${platformImageFileId}`,
token: clientToken,
}, ({ status, json }) => {
assertEqual(status, 403, 'HTTP status')
assertTrue(json?.code == null || String(json.code) === 'A0403',
`expected absent/A0403 business code, actual=${json?.code}`)
}, ({ json }) => ({ fileId: platformImageFileId, rejectionCode: json?.code }))
const allTerminal = await apiStep('phase2-fixture', 'discover foreign terminal fixture for isolation', {
method: 'GET',
path: '/api/platform/inbounds?page=1&size=200',
@@ -254,15 +399,23 @@ async function terminalBillingModule() {
rejectionCode: json?.code,
}))
if (!response) {
recordSkipped('phase2-client-billing', 'terminal billing aggregate contract',
'Terminal billing endpoint did not return a visible aggregate')
recordSkipped('phase2-client-images', 'published measurement and weight image access',
'Terminal billing endpoint did not return image references')
const reason = 'Terminal billing endpoint did not return a visible aggregate'
for (const name of [
'terminal billing aggregate contract',
'published measurement summary matches approved platform snapshot',
'published carton measurements match platform snapshot',
'published carton volumes follow current weighting rules',
'published weight results match confirmed platform weights',
'weight and volume billing lines are independently consistent',
'published fee detail matches confirmed fee snapshot',
'client aggregate excludes platform internal collections',
]) recordSkipped('phase2-client-billing', name, reason)
recordSkipped('phase2-client-images', 'published measurement and weight image access', reason)
return
}
merchantBilling = response.json.data
await localStep('phase2-client-billing', 'terminal billing aggregate contract', () => {
await softLocalStep('phase2-client-billing', 'terminal billing aggregate contract', () => {
const summary = merchantBilling.inbound.summary
const fee = merchantBilling.latestFee
assertStringId(summary.id, 'inbound summary id')
@@ -291,7 +444,7 @@ async function terminalBillingModule() {
currency: merchantBilling.latestFee.currency,
})
await localStep('phase2-client-billing', 'client aggregate excludes platform internal collections', () => {
await softLocalStep('phase2-client-billing', 'client aggregate excludes platform internal collections', () => {
assertEqual(arrayData(merchantBilling.inbound.internalNotes).length, 0, 'merchant internal note count')
assertEqual(arrayData(merchantBilling.inbound.statusHistory).length, 0, 'merchant status history count')
for (const file of arrayData(merchantBilling.inbound.files)) {
@@ -306,22 +459,123 @@ async function terminalBillingModule() {
visibleFileCount: arrayData(merchantBilling.inbound.files).length,
})
await localStep('phase2-client-billing', 'measurement and billing basis are internally consistent', () => {
await softLocalStep('phase2-client-billing', 'published measurement summary matches approved platform snapshot', () => {
const summary = merchantBilling.measurementSummary
assertPresent(summary?.measurementId, 'measurement id')
assertEqual(String(summary.measurementId), String(platformMeasurement.measurementId), 'measurement id')
assertEqual(summary.measurementStatus, platformMeasurement.measurementStatus, 'measurement status')
assertNumberEqual(summary.actualPackageCount, platformMeasurement.actualPackageCount,
'measurement package count')
assertApproxEqual(summary.actualGrossWeightKg, platformMeasurement.actualGrossWeightKg, 0.001,
'measurement gross weight')
assertApproxEqual(summary.actualVolumeCbm, platformMeasurement.actualVolumeCbm, 0.000001,
'measurement volume')
assertApproxEqual(summary.volumeWeightKg, platformMeasurement.volumeWeightKg, 0.001,
'measurement volume weight')
}, {
measurementId: merchantBilling.measurementSummary?.measurementId,
measurementStatus: merchantBilling.measurementSummary?.measurementStatus,
actualPackageCount: merchantBilling.measurementSummary?.actualPackageCount,
actualGrossWeightKg: merchantBilling.measurementSummary?.actualGrossWeightKg,
actualVolumeCbm: merchantBilling.measurementSummary?.actualVolumeCbm,
})
await softLocalStep('phase2-client-billing', 'published carton measurements match platform snapshot', () => {
const publishedItems = arrayData(merchantBilling.measurementItems)
assertEqual(publishedItems.length, platformMeasurement.items.length, 'published measurement item count')
const platformById = new Map(platformMeasurement.items.map((item) => [String(item.id), item]))
for (const item of publishedItems) {
assertStringId(item.id, 'published measurement item id')
const expected = platformById.get(String(item.id))
assertPresent(expected, `platform measurement item ${item.id}`)
for (const field of ['packageCount', 'lengthCm', 'widthCm', 'heightCm', 'unitWeightKg',
'totalWeightKg', 'unitVolumeCbm', 'totalVolumeCbm', 'volumeWeightKg']) {
assertApproxEqual(item[field], expected[field], 0.000001, `${item.itemNo} ${field}`)
}
}
}, { measurementItemCount: arrayData(merchantBilling.measurementItems).length })
await softLocalStep('phase2-client-billing', 'published carton volumes follow current weighting rules', () => {
for (const item of arrayData(merchantBilling.measurementItems)) {
const expected = resolveWeightedDimensions(item, dimensionRules)
assertApproxEqual(item.unitVolumeCbm, expected.unitVolumeCbm, 0.000001,
`${item.itemNo} published weighted unit volume`)
}
}, {
items: arrayData(merchantBilling.measurementItems).map((item) => ({
itemNo: item.itemNo,
measured: dimensions(item),
unitVolumeCbm: item.unitVolumeCbm,
expected: resolveWeightedDimensions(item, dimensionRules),
})),
})
await softLocalStep('phase2-client-billing', 'published weight results match confirmed platform weights', () => {
const publishedWeights = arrayData(merchantBilling.weightResults)
assertEqual(publishedWeights.length, platformMeasurement.weightResults.length,
'published weight result count')
const platformById = new Map(platformMeasurement.weightResults.map((item) => [String(item.id), item]))
for (const weight of publishedWeights) {
assertStringId(weight.id, 'published weight result id')
const expected = platformById.get(String(weight.id))
assertPresent(expected, `platform weight result ${weight.id}`)
assertEqual(String(weight.fileId), String(expected.fileId), 'weight image file id')
assertEqual(weight.recognitionStatus, 'CONFIRMED', 'published weight status')
assertApproxEqual(weight.finalWeightKg, expected.finalWeightKg, 0.001, 'published final weight')
}
assertApproxEqual(sum(publishedWeights, 'finalWeightKg'),
merchantBilling.measurementSummary.actualGrossWeightKg, 0.001, 'published weight aggregate')
}, { weightResultCount: arrayData(merchantBilling.weightResults).length })
await softLocalStep('phase2-client-billing',
'weight and volume billing lines are independently consistent', () => {
const summary = merchantBilling.measurementSummary
const basis = merchantBilling.billingBasis
const fee = merchantBilling.latestFee
assertNumberEqual(basis.actualWeightKg, summary.actualGrossWeightKg, 'billing actual weight')
assertNumberEqual(basis.actualVolumeCbm, summary.actualVolumeCbm, 'billing actual volume')
assertNumberEqual(basis.actualWeightKg, fee.actualWeightKg, 'fee actual weight')
assertNumberEqual(basis.actualVolumeCbm, fee.actualVolumeCbm, 'fee actual volume')
assertNumberEqual(basis.convertedQuantity, fee.convertedQuantity, 'billing converted quantity')
assertNumberEqual(basis.chargeableQuantity, fee.chargeableQuantity, 'billing chargeable quantity')
assertTrue(['ACTUAL_WEIGHT', 'VOLUME_WEIGHT', 'EQUAL', 'ACTUAL_VOLUME',
'WEIGHT_CONVERTED_VOLUME'].includes(basis.chargeableBasis), 'chargeable basis must be supported')
}, {
actualWeightKg: merchantBilling.billingBasis?.actualWeightKg,
actualVolumeCbm: merchantBilling.billingBasis?.actualVolumeCbm,
convertedQuantity: merchantBilling.billingBasis?.convertedQuantity,
chargeableQuantity: merchantBilling.billingBasis?.chargeableQuantity,
chargeableBasis: merchantBilling.billingBasis?.chargeableBasis,
})
await softLocalStep('phase2-client-billing', 'published fee detail matches confirmed fee snapshot', () => {
const detail = merchantBilling.publishedFeeDetail
const fee = merchantBilling.latestFee
assertPresent(detail?.id, 'published fee detail id')
assertEqual(String(detail.inboundId), report.resources.terminalInboundId, 'published fee inbound id')
assertEqual(String(detail.sourceFeeId), String(fee.feeId), 'published source fee id')
assertEqual(Number(detail.sourceFeeVersion), Number(fee.feeVersion), 'published source fee version')
assertEqual(String(detail.customerId), report.resources.tenantId, 'published fee customer id')
assertEqual(detail.publicationStatus, 'PUBLISHED', 'published fee publication status')
assertPresent(detail.publishedAt, 'published fee time')
assertMoneyEqual(detail.amount, fee.totalAmount, 'published fee amount')
assertNumberEqual(detail.chargeableQuantity, fee.chargeableQuantity, 'published chargeable quantity')
assertEqual(detail.chargeUnit, fee.chargeUnit, 'published charge unit')
}, {
publishedFeeDetailId: merchantBilling.publishedFeeDetail?.id,
sourceFeeId: merchantBilling.publishedFeeDetail?.sourceFeeId,
publicationStatus: merchantBilling.publishedFeeDetail?.publicationStatus,
amount: merchantBilling.publishedFeeDetail?.amount,
})
await softLocalStep('phase2-client-billing', 'measurement and billing basis are internally consistent', () => {
const basis = merchantBilling.billingBasis
const fee = merchantBilling.latestFee
assertPresent(summary?.measurementId, 'measurement id')
assertEqual(summary.measurementStatus, 'SUBMITTED', 'measurement status')
assertTrue(Number(summary.actualPackageCount) > 0, 'actual package count must be positive')
assertTrue(Number(summary.actualGrossWeightKg) > 0, 'measurement gross weight must be positive')
assertTrue(Number(summary.actualVolumeCbm) > 0, 'measurement volume must be positive')
assertNumberEqual(basis.actualWeightKg, fee.actualWeightKg, 'billing actual weight')
assertNumberEqual(basis.actualVolumeCbm, fee.actualVolumeCbm, 'billing actual volume')
assertNumberEqual(basis.convertedQuantity, fee.convertedQuantity, 'billing converted quantity')
assertNumberEqual(basis.chargeableQuantity, fee.chargeableQuantity, 'billing chargeable quantity')
}, {
measurementId: merchantBilling.measurementSummary?.measurementId,
measurementStatus: merchantBilling.measurementSummary?.measurementStatus,
measurementItemCount: arrayData(merchantBilling.measurementItems).length,
weightResultCount: arrayData(merchantBilling.weightResults).length,
chargeableBasis: merchantBilling.billingBasis?.chargeableBasis,
@@ -365,7 +619,7 @@ async function terminalBillingModule() {
async function unifiedOrderModule() {
const orderNo = terminalFixture.orderNo
await apiStep('phase2-unified-orders', 'unified order list exposes phase-two terminal state', {
await softApiStep('phase2-unified-orders', 'unified order list exposes phase-two terminal state', {
method: 'GET',
path: `/api/merchant/v1/orders?page=1&size=50&orderNo=${encodeURIComponent(orderNo)}`,
token: clientToken,
@@ -387,7 +641,7 @@ async function unifiedOrderModule() {
actualGrossWeightKg: row?.actualGrossWeightKg, actualVolumeCbm: row?.actualVolumeCbm }
})
await apiStep('phase2-unified-orders', 'unified order detail exposes phase-two summary and timeline', {
await softApiStep('phase2-unified-orders', 'unified order detail exposes phase-two summary and timeline', {
method: 'GET', path: `/api/merchant/v1/orders/${report.resources.terminalOrderId}`, token: clientToken,
}, ({ json }) => {
requireSuccess(json)
@@ -469,6 +723,10 @@ async function localStep(module, name, validate, evidence = null) {
}
}
async function softLocalStep(module, name, validate, evidence = null) {
try { await localStep(module, name, validate, evidence) } catch { }
}
async function httpRequest({ method = 'GET', path, token, headers = {}, body, responseType = 'json' }) {
const requestHeaders = { Accept: 'application/json', 'X-Request-Id': `client-phase2-${randomUUID()}`, ...headers }
if (token) requestHeaders.Authorization = `Bearer ${token}`
@@ -532,6 +790,67 @@ function assertTerminalFixture(row) {
assertEqual(row.feeStatus, 'CONFIRMED', 'fee status')
}
function assertDimensionRule(rule) {
assertPresent(rule?.boxType, 'dimension box type')
assertTrue(Number(rule.version) >= 0, `${rule.boxType} version must not be negative`)
for (const field of ['innerLengthCm', 'innerWidthCm', 'innerHeightCm', 'edgeAdditionCm',
'outerLengthCm', 'outerWidthCm', 'outerHeightCm', 'volumeCbm']) {
assertTrue(Number(rule[field]) > 0, `${rule.boxType} ${field} must be positive`)
}
const edgePair = Number(rule.edgeAdditionCm) * 2
assertApproxEqual(rule.outerLengthCm, Number(rule.innerLengthCm) + edgePair, 0.000001,
`${rule.boxType} outer length`)
assertApproxEqual(rule.outerWidthCm, Number(rule.innerWidthCm) + edgePair, 0.000001,
`${rule.boxType} outer width`)
assertApproxEqual(rule.outerHeightCm, Number(rule.innerHeightCm) + edgePair, 0.000001,
`${rule.boxType} outer height`)
assertApproxEqual(rule.volumeCbm,
Number(rule.outerLengthCm) * Number(rule.outerWidthCm) * Number(rule.outerHeightCm) / 1_000_000,
0.0005, `${rule.boxType} configured volume`)
}
function resolveWeightedDimensions(item, rules) {
const measured = [Number(item.lengthCm), Number(item.widthCm), Number(item.heightCm)]
.sort((left, right) => right - left)
const candidates = [...arrayData(rules)].sort((left, right) =>
Number(left.innerLengthCm) * Number(left.innerWidthCm) * Number(left.innerHeightCm)
- Number(right.innerLengthCm) * Number(right.innerWidthCm) * Number(right.innerHeightCm))
const rule = candidates.find((candidate) => {
const inner = [Number(candidate.innerLengthCm), Number(candidate.innerWidthCm),
Number(candidate.innerHeightCm)].sort((left, right) => right - left)
return measured.every((value, index) => value <= inner[index])
})
if (!rule) {
const rawVolume = measured.reduce((total, value) => total * value, 1) / 1_000_000
return { boxType: null, ruleVersion: null, outer: dimensions(item),
unitVolumeCbm: round(rawVolume, 3) }
}
const edgePair = Number(rule.edgeAdditionCm) * 2
const outer = [Number(rule.innerLengthCm) + edgePair, Number(rule.innerWidthCm) + edgePair,
Number(rule.innerHeightCm) + edgePair]
return {
boxType: rule.boxType,
ruleVersion: rule.version,
outer,
unitVolumeCbm: round(outer.reduce((total, value) => total * value, 1) / 1_000_000, 3),
}
}
function dimensions(value, prefix = '') {
const field = (name) => `${prefix}${prefix ? name[0].toUpperCase() + name.slice(1) : name}`
return [value?.[field('lengthCm')] ?? null, value?.[field('widthCm')] ?? null,
value?.[field('heightCm')] ?? null]
}
function sum(values, field) {
return arrayData(values).reduce((total, value) => total + Number(value?.[field] ?? 0), 0)
}
function round(value, scale) {
const factor = 10 ** scale
return Math.round((Number(value) + Number.EPSILON) * factor) / factor
}
function statusCounts(values) {
const counts = {}
for (const row of arrayData(values)) {
@@ -551,6 +870,15 @@ function assertNumberEqual(actual, expected, label) {
assertEqual(Number(actual), Number(expected), label)
}
function assertApproxEqual(actual, expected, tolerance, label) {
const actualNumber = Number(actual)
const expectedNumber = Number(expected)
if (!Number.isFinite(actualNumber) || !Number.isFinite(expectedNumber)
|| Math.abs(actualNumber - expectedNumber) > tolerance) {
throw new Error(`${label}: expected=${format(expected)} actual=${format(actual)}`)
}
}
function assertMoneyEqual(actual, expected, label) {
assertEqual(Math.round(Number(actual) * 100), Math.round(Number(expected) * 100), label)
}