|
|
import { roundHalfUp } from "@/lib/math";
|
|
|
import { prisma } from "@/lib/prisma";
|
|
|
|
|
|
export type MarkupType = "percent" | "fixed";
|
|
|
|
|
|
export interface MarkupRule {
|
|
|
type: MarkupType;
|
|
|
percent: number;
|
|
|
fixedAmount: number;
|
|
|
}
|
|
|
|
|
|
export const DEFAULT_MARKUP_RULE: MarkupRule = {
|
|
|
type: "percent",
|
|
|
percent: 0,
|
|
|
fixedAmount: 0,
|
|
|
};
|
|
|
|
|
|
export interface MarkupResult {
|
|
|
markupAmount: number;
|
|
|
finalTotal: number;
|
|
|
}
|
|
|
|
|
|
/** 加价公式:百分比仅 raw_freight 参与;固定金额为每档固定叠加(GD-6 扩展) */
|
|
|
export function applyMarkup(
|
|
|
rawFreight: number,
|
|
|
rawTotal: number,
|
|
|
rule: MarkupRule | number,
|
|
|
): MarkupResult {
|
|
|
const normalized =
|
|
|
typeof rule === "number"
|
|
|
? { type: "percent" as const, percent: rule, fixedAmount: 0 }
|
|
|
: rule;
|
|
|
|
|
|
const markupAmount =
|
|
|
normalized.type === "fixed"
|
|
|
? roundHalfUp(normalized.fixedAmount, 2)
|
|
|
: roundHalfUp(rawFreight * (normalized.percent / 100), 2);
|
|
|
const finalTotal = roundHalfUp(rawTotal + markupAmount, 2);
|
|
|
return { markupAmount, finalTotal };
|
|
|
}
|
|
|
|
|
|
/** 读取客户加价规则;未配置默认 0%(BR-6.3) */
|
|
|
export async function getMarkupRule(customerId: string): Promise<MarkupRule> {
|
|
|
const config = await prisma.markupConfig.findFirst({
|
|
|
where: { customerId, isDeleted: false },
|
|
|
select: {
|
|
|
markupType: true,
|
|
|
markupPercent: true,
|
|
|
markupFixedAmount: true,
|
|
|
},
|
|
|
});
|
|
|
if (!config) {
|
|
|
return DEFAULT_MARKUP_RULE;
|
|
|
}
|
|
|
if (config.markupType === "fixed") {
|
|
|
return {
|
|
|
type: "fixed",
|
|
|
percent: 0,
|
|
|
fixedAmount: Number(config.markupFixedAmount ?? 0),
|
|
|
};
|
|
|
}
|
|
|
return {
|
|
|
type: "percent",
|
|
|
percent: Number(config.markupPercent),
|
|
|
fixedAmount: 0,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
/** @deprecated 请使用 getMarkupRule;保留兼容旧调用 */
|
|
|
export async function getMarkupPercent(customerId: string): Promise<number> {
|
|
|
const rule = await getMarkupRule(customerId);
|
|
|
return rule.type === "percent" ? rule.percent : 0;
|
|
|
}
|
|
|
|
|
|
export interface RawQuoteTier {
|
|
|
service_level: string;
|
|
|
rate_option: string;
|
|
|
carrier: string;
|
|
|
transit_days: string;
|
|
|
transit_description: string;
|
|
|
raw_freight: number;
|
|
|
surcharges: number;
|
|
|
raw_total: number;
|
|
|
}
|
|
|
|
|
|
export interface MarkedUpQuoteTier extends RawQuoteTier {
|
|
|
markup_percent: number;
|
|
|
markup_amount: number;
|
|
|
final_total: number;
|
|
|
breakdown: Array<{ item: string; amount: number }>;
|
|
|
}
|
|
|
|
|
|
function markupBreakdownLabel(rule: MarkupRule): string {
|
|
|
if (rule.type === "fixed") {
|
|
|
return `运营加价(固定 ${rule.fixedAmount.toFixed(2)} USD)`;
|
|
|
}
|
|
|
return `运营加价(${rule.percent.toFixed(1)}%)`;
|
|
|
}
|
|
|
|
|
|
/** 对单档报价应用加价并生成 breakdown */
|
|
|
export function applyMarkupToTier(
|
|
|
tier: RawQuoteTier,
|
|
|
rule: MarkupRule | number,
|
|
|
): MarkedUpQuoteTier {
|
|
|
const normalized =
|
|
|
typeof rule === "number"
|
|
|
? { type: "percent" as const, percent: rule, fixedAmount: 0 }
|
|
|
: rule;
|
|
|
const { markupAmount, finalTotal } = applyMarkup(
|
|
|
tier.raw_freight,
|
|
|
tier.raw_total,
|
|
|
normalized,
|
|
|
);
|
|
|
return {
|
|
|
...tier,
|
|
|
markup_percent: normalized.type === "percent" ? normalized.percent : 0,
|
|
|
markup_amount: markupAmount,
|
|
|
final_total: finalTotal,
|
|
|
breakdown: [
|
|
|
{ item: "运费", amount: tier.raw_freight },
|
|
|
{ item: "附加费", amount: tier.surcharges },
|
|
|
{
|
|
|
item: markupBreakdownLabel(normalized),
|
|
|
amount: markupAmount,
|
|
|
},
|
|
|
],
|
|
|
};
|
|
|
}
|
|
|
|
|
|
/** 对多档原始报价批量加价 */
|
|
|
export function applyMarkupToQuotes(
|
|
|
quotes: RawQuoteTier[],
|
|
|
rule: MarkupRule | number,
|
|
|
): MarkedUpQuoteTier[] {
|
|
|
return quotes.map((tier) => applyMarkupToTier(tier, rule));
|
|
|
}
|