You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

160 lines
4.3 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/**
* axel/quote 响应契约解析阶段0 录证专用,与 RPA normalize 解耦)
*
* 真实结构2026-06 录证):
* body.data.rates.standard.{ lowest, fastest, bestValue }
* body.data.rates.guaranteed.{ lowest, fastest, bestValue? }
* body.data.availableRates.{ rate_<id>: RateObject }
*/
export type AxelRateTier = {
id?: string;
price?: number;
finalPrice?: number;
baseQuotePrice?: number;
days?: number;
serviceLevel?: string;
serviceType?: string;
quoteType?: string;
serviceCommitment?: string;
serviceCommitmentDescription?: string;
};
export type AxelQuoteContract = {
sourcePath: string;
dataRates: {
standard: Record<string, AxelRateTier>;
guaranteed: Record<string, AxelRateTier>;
} | null;
availableRateCount: number;
/** 四档映射standard/guaranteed × lowest/fastest */
fourTierCoverage: {
standardLowest: boolean;
standardFastest: boolean;
guaranteedLowest: boolean;
guaranteedFastest: boolean;
};
pricedLeafCount: number;
};
function readData(body: unknown): Record<string, unknown> | null {
if (body == null || typeof body !== "object" || Array.isArray(body)) {
return null;
}
const data = (body as Record<string, unknown>).data;
if (data == null || typeof data !== "object" || Array.isArray(data)) {
return null;
}
return data as Record<string, unknown>;
}
function asTierMap(value: unknown): Record<string, AxelRateTier> | null {
if (value == null || typeof value !== "object" || Array.isArray(value)) {
return null;
}
return value as Record<string, AxelRateTier>;
}
function hasPositivePrice(tier: AxelRateTier): boolean {
for (const key of ["price", "finalPrice", "baseQuotePrice"] as const) {
const val = tier[key];
if (typeof val === "number" && val > 0) {
return true;
}
}
return false;
}
function countPricedLeavesInAvailableRates(
availableRates: unknown,
): number {
if (
availableRates == null ||
typeof availableRates !== "object" ||
Array.isArray(availableRates)
) {
return 0;
}
return Object.values(availableRates as Record<string, AxelRateTier>).filter(
(r) => hasPositivePrice(r),
).length;
}
function pickTier(
branch: Record<string, AxelRateTier> | null | undefined,
name: string,
): AxelRateTier | null {
if (!branch) {
return null;
}
const tier = branch[name];
return tier && hasPositivePrice(tier) ? tier : null;
}
/** 从 axel/quote body 提取契约摘要 */
export function extractAxelQuoteContract(body: unknown): AxelQuoteContract {
const data = readData(body);
const dataRatesRaw = data?.rates;
let dataRates: AxelQuoteContract["dataRates"] = null;
if (dataRatesRaw && typeof dataRatesRaw === "object" && !Array.isArray(dataRatesRaw)) {
const dr = dataRatesRaw as Record<string, unknown>;
dataRates = {
standard: asTierMap(dr.standard) ?? {},
guaranteed: asTierMap(dr.guaranteed) ?? {},
};
}
const availableRateCount = countPricedLeavesInAvailableRates(
data?.availableRates,
);
const std = dataRates?.standard;
const guar = dataRates?.guaranteed;
const fourTierCoverage = {
standardLowest: pickTier(std, "lowest") != null,
standardFastest: pickTier(std, "fastest") != null,
guaranteedLowest:
pickTier(guar, "lowest") != null || pickTier(guar, "bestValue") != null,
guaranteedFastest:
pickTier(guar, "fastest") != null || pickTier(guar, "bestValue") != null,
};
let pricedLeafCount = 0;
if (std) {
pricedLeafCount += Object.values(std).filter(hasPositivePrice).length;
}
if (guar) {
pricedLeafCount += Object.values(guar).filter(hasPositivePrice).length;
}
return {
sourcePath: "data.rates",
dataRates,
availableRateCount,
fourTierCoverage,
pricedLeafCount,
};
}
/** 阶段0 A3 验收:四档齐全或 data.rates 定价叶子 ≥ 4 */
export function countAxelQuoteTiers(body: unknown): {
pricedLeafCount: number;
fourTierOk: boolean;
availableRateCount: number;
} {
const c = extractAxelQuoteContract(body);
const fourTierOk =
c.fourTierCoverage.standardLowest &&
c.fourTierCoverage.standardFastest &&
c.fourTierCoverage.guaranteedLowest &&
c.fourTierCoverage.guaranteedFastest;
return {
pricedLeafCount: Math.max(c.pricedLeafCount, c.availableRateCount),
fourTierOk,
availableRateCount: c.availableRateCount,
};
}