This commit is contained in:
2026-08-04 21:01:34 +08:00
parent 161bf580c7
commit 2313fd238c
12 changed files with 716 additions and 24 deletions
@@ -0,0 +1,29 @@
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import test from 'node:test'
const source = readFileSync(new URL('./inboundBilling.ts', import.meta.url), 'utf8')
const typesSource = readFileSync(new URL('../types/inboundBilling.ts', import.meta.url), 'utf8')
test('queries the merchant order inbound-billing endpoint without tenant parameters', () => {
assert.match(source, /url: `\/merchant\/v1\/first-leg\/orders\/\$\{orderId\}\/inbound-billing`/)
assert.match(source, /method: 'GET'/)
assert.doesNotMatch(source, /tenantId|method: '(?:POST|PUT|PATCH|DELETE)'/)
})
test('sanitizes the platform-shaped response into a merchant whitelist', () => {
assert.match(source, /function sanitizeInboundBilling/)
assert.match(source, /summary: sanitizeSummary\(response\.inbound\.summary\)/)
assert.match(source, /palletLocations: response\.inbound\.palletLocations\.map\(sanitizePalletLocation\)/)
assert.match(source, /latestFee: sanitizeFee\(response\.latestFee\)/)
assert.doesNotMatch(source, /\.\.\.response|\.\.\.summary|\.\.\.fee/)
assert.doesNotMatch(source, /internalNotes|statusHistory|files/)
})
test('keeps all backend Long identifiers as strings', () => {
for (const field of ['id', 'orderId', 'warehouseId', 'palletId', 'locationId', 'feeId', 'inboundId', 'chargeRuleId', 'carrierRateId', 'customerId', 'carrierId']) {
assert.match(typesSource, new RegExp(`${field}: string`))
}
assert.doesNotMatch(typesSource, /(?:id|Id): number/)
assert.doesNotMatch(typesSource, /internalNotes|statusHistory|files/)
})
+121
View File
@@ -0,0 +1,121 @@
import { apiRequest } from '@/api/http'
import type {
MerchantInboundBilling,
MerchantInboundFeeSnapshot,
MerchantInboundResult,
MerchantInboundSummary,
MerchantPalletLocation,
} from '@/modules/first-leg/types/inboundBilling'
interface MerchantInboundBillingWireResponse {
inbound: MerchantInboundResult
latestFee: MerchantInboundFeeSnapshot
}
function sanitizeSummary(summary: MerchantInboundSummary): MerchantInboundSummary {
return {
id: summary.id,
orderId: summary.orderId,
orderNo: summary.orderNo,
inboundCode: summary.inboundCode,
processStatus: summary.processStatus,
feeStatus: summary.feeStatus,
notificationStatus: summary.notificationStatus,
warehouseName: summary.warehouseName,
deliveryWarehouseName: summary.deliveryWarehouseName,
destinationCode: summary.destinationCode,
destinationName: summary.destinationName,
totalPackageCount: summary.totalPackageCount,
actualGrossWeightKg: summary.actualGrossWeightKg,
actualNetWeightKg: summary.actualNetWeightKg,
actualVolumeCbm: summary.actualVolumeCbm,
volumeWeightKg: summary.volumeWeightKg,
chargeableWeightKg: summary.chargeableWeightKg,
chargeableVolumeCbm: summary.chargeableVolumeCbm,
palletCount: summary.palletCount,
locationCodes: summary.locationCodes,
measureCompletedAt: summary.measureCompletedAt,
inboundedAt: summary.inboundedAt,
}
}
function sanitizePalletLocation(item: MerchantPalletLocation): MerchantPalletLocation {
return {
allocationNo: item.allocationNo,
warehouseId: item.warehouseId,
palletId: item.palletId,
palletCode: item.palletCode,
palletCapacityCbm: item.palletCapacityCbm,
allocatedVolumeCbm: item.allocatedVolumeCbm,
allocatedPackageCount: item.allocatedPackageCount,
locationId: item.locationId,
locationCode: item.locationCode,
allocationStatus: item.allocationStatus,
}
}
function sanitizeFee(fee: MerchantInboundFeeSnapshot): MerchantInboundFeeSnapshot {
return {
feeId: fee.feeId,
feeNo: fee.feeNo,
feeVersion: fee.feeVersion,
feeStatus: fee.feeStatus,
inboundId: fee.inboundId,
inboundCode: fee.inboundCode,
chargeRuleId: fee.chargeRuleId,
ruleCode: fee.ruleCode,
chargeRuleVersion: fee.chargeRuleVersion,
carrierRateId: fee.carrierRateId,
rateCode: fee.rateCode,
carrierRateVersion: fee.carrierRateVersion,
customerId: fee.customerId,
customerName: fee.customerName,
originWarehouseId: fee.originWarehouseId,
originWarehouseName: fee.originWarehouseName,
businessType: fee.businessType,
channelCode: fee.channelCode,
carrierId: fee.carrierId,
carrierName: fee.carrierName,
destinationCode: fee.destinationCode,
taxMode: fee.taxMode,
chargeMode: fee.chargeMode,
conversionFactor: fee.conversionFactor,
roundingScale: fee.roundingScale,
roundingMode: fee.roundingMode,
actualWeightKg: fee.actualWeightKg,
actualVolumeCbm: fee.actualVolumeCbm,
convertedQuantity: fee.convertedQuantity,
chargeableQuantity: fee.chargeableQuantity,
chargeUnit: fee.chargeUnit,
unitPrice: fee.unitPrice,
subtotalAmount: fee.subtotalAmount,
additionalAmount: fee.additionalAmount,
totalAmount: fee.totalAmount,
currency: fee.currency,
rateEffectiveFrom: fee.rateEffectiveFrom,
rateEffectiveTo: fee.rateEffectiveTo,
calculatedAt: fee.calculatedAt,
confirmedAt: fee.confirmedAt,
sentAt: fee.sentAt,
nextProcessStatus: fee.nextProcessStatus,
}
}
function sanitizeInboundBilling(response: MerchantInboundBillingWireResponse): MerchantInboundBilling {
return {
inbound: {
summary: sanitizeSummary(response.inbound.summary),
publicRemark: response.inbound.publicRemark,
palletLocations: response.inbound.palletLocations.map(sanitizePalletLocation),
},
latestFee: sanitizeFee(response.latestFee),
}
}
export async function getMerchantInboundBilling(orderId: string): Promise<MerchantInboundBilling> {
const response = await apiRequest<MerchantInboundBillingWireResponse>({
method: 'GET',
url: `/merchant/v1/first-leg/orders/${orderId}/inbound-billing`,
})
return sanitizeInboundBilling(response)
}