import type { BillingConditionValue, BillingRuleConditions, BillingParameterSnapshot, BillingPriceRule, BillingQuote, BillingSelectedParameterTier, BillingUnit, GenerationCapability, GenerationProvider } from "@/lib/types"; export const BILLING_CURRENCY = "CNY" as const; export const BILLING_UNIT_OPTIONS: Array<{ value: BillingUnit; label: string }> = [ { value: "request", label: "按次" }, { value: "image", label: "按张" }, { value: "video_second", label: "按秒" } ]; export function calculateBillingAmountFen(standardUnitPriceFen: number, quantity: number, markupMultiplier: number): number { if (!Number.isFinite(standardUnitPriceFen) || standardUnitPriceFen < 0) throw new Error("标准单价必须是非负金额。"); if (!Number.isFinite(quantity) || quantity <= 0) throw new Error("计费数量必须大于 0。"); if (!Number.isFinite(markupMultiplier) || markupMultiplier < 1) throw new Error("上浮倍率不能低于 1.00。"); return Math.max(0, Math.ceil(standardUnitPriceFen * quantity * markupMultiplier)); } export function quoteFromPriceRule(input: { rule: BillingPriceRule; provider: GenerationProvider; capability: GenerationCapability; reqKey: string; quantity: number; parameters?: BillingParameterSnapshot; conditions?: BillingRuleConditions; quantitySource?: BillingPriceRule["quantitySource"]; }): BillingQuote { const { rule } = input; if (rule.provider !== input.provider || rule.capability !== input.capability) { throw new Error("计费规则与生成服务不匹配。"); } const parameterPricing = resolveBillingParameterPricing(rule, input.parameters); if (!parameterPricing) throw new Error("当前生成参数没有对应的平台标准价格。"); const standardUnitPriceFen = parameterPricing.standardUnitPriceFen; const markupMultiplier = parameterPricing.markupMultiplier; return { priceRuleId: rule.id, provider: input.provider, capability: input.capability, reqKey: input.reqKey, variantKey: rule.variantKey, unit: rule.unit, quantity: input.quantity, standardUnitPriceFen, markupMultiplier, amountFen: calculateBillingAmountFen(standardUnitPriceFen, input.quantity, markupMultiplier), currency: BILLING_CURRENCY, conditions: input.conditions ?? rule.conditions, quantitySource: input.quantitySource ?? rule.quantitySource, parameters: input.parameters, baseStandardUnitPriceFen: rule.parameterDimensions?.length ? rule.standardUnitPriceFen : undefined, parameterTiers: parameterPricing.parameterTiers.length ? parameterPricing.parameterTiers : undefined, source: rule.source }; } export type BillingParameterPricing = { standardUnitPriceFen: number; markupMultiplier: number; parameterTiers: BillingSelectedParameterTier[]; }; export function resolveBillingParameterPricing(rule: BillingPriceRule, parameters: BillingParameterSnapshot = {}): BillingParameterPricing | null { const dimensions = rule.parameterDimensions?.filter((dimension) => dimension.tiers.length) || []; if (!dimensions.length) { return { standardUnitPriceFen: rule.standardUnitPriceFen, markupMultiplier: rule.markupMultiplier, parameterTiers: [] }; } let standardFactor = 1; let markupMultiplier = rule.parameterDimensions?.length ? 1 : rule.markupMultiplier; const parameterTiers: BillingSelectedParameterTier[] = []; for (const dimension of dimensions) { const actualValue = parameters[dimension.key] ?? dimension.defaultValue ?? dimension.baselineValue; const tier = dimension.tiers.find((candidate) => candidate.enabled && billingConditionMatchesValue(candidate.match ?? candidate.value, actualValue)); if (!tier) return null; const factor = Number(tier.standardFactor); const tierMarkup = Number(tier.markupMultiplier); if (!Number.isFinite(factor) || factor <= 0 || !Number.isFinite(tierMarkup) || tierMarkup < 1) return null; standardFactor *= factor; // A combination has one customer-facing uplift. The highest selected // tier multiplier wins so editing one expensive tier never lowers the // configured uplift of another selected tier. markupMultiplier = Math.max(markupMultiplier, tierMarkup); parameterTiers.push({ ...tier, dimensionKey: dimension.key, dimensionLabel: dimension.label, actualValue, standardUnitPriceFen: Math.ceil(rule.standardUnitPriceFen * factor) }); } return { standardUnitPriceFen: Math.ceil(rule.standardUnitPriceFen * standardFactor), markupMultiplier, parameterTiers }; } export function billingConditionMatchesValue(condition: BillingConditionValue, actual: string | number | boolean): boolean { if (typeof condition === "object" && condition !== null && !Array.isArray(condition)) { if (Array.isArray(condition.values) && !condition.values.some((value) => billingScalarEquals(value, actual))) return false; const numericActual = Number(actual); if (condition.min !== undefined && (!Number.isFinite(numericActual) || numericActual < condition.min)) return false; if (condition.max !== undefined && (!Number.isFinite(numericActual) || numericActual > condition.max)) return false; return true; } return billingScalarEquals(condition as string | number | boolean, actual); } function billingScalarEquals(left: string | number | boolean, right: string | number | boolean): boolean { if (typeof left === "number" || typeof right === "number") return Number(left) === Number(right); if (typeof left === "boolean" || typeof right === "boolean") return Boolean(left) === Boolean(right); return String(left).trim().toLowerCase() === String(right).trim().toLowerCase(); } export function formatBillingAmount(fen: number, currency = BILLING_CURRENCY): string { const amount = Math.max(0, Number(fen || 0)) / 100; return new Intl.NumberFormat("zh-CN", { style: "currency", currency, minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(amount); } export function billingUnitLabel(unit: BillingUnit): string { return BILLING_UNIT_OPTIONS.find((item) => item.value === unit)?.label || unit; } export function billingQuantityLabel(quantity: number, unit: BillingUnit): string { const value = Number.isInteger(quantity) ? String(quantity) : quantity.toFixed(2).replace(/0+$/, "").replace(/\.$/, ""); return `${value}${unit === "video_second" ? " 秒" : unit === "image" ? " 张" : " 次"}`; }