|
|
import { Prisma } from "@prisma/client";
|
|
|
import { PRICE_DEVIATION_THRESHOLD } from "@/lib/constants/rpa";
|
|
|
import { prisma } from "@/lib/prisma";
|
|
|
import { writeAlert } from "@/modules/alert/service";
|
|
|
import type { RawQuoteTier } from "@/modules/pricing/engine";
|
|
|
import { getStandardLowestTotal } from "@/modules/rpa/quote-mapper";
|
|
|
|
|
|
/** 偏差检测 + quote_cache_meta 更新(task-051 最小实现,供 RPA 成功路径调用) */
|
|
|
export async function detectDeviation(
|
|
|
cargoHash: string,
|
|
|
newQuotes: RawQuoteTier[],
|
|
|
quoteId?: string,
|
|
|
): Promise<void> {
|
|
|
const newStandardTotal = getStandardLowestTotal(newQuotes);
|
|
|
if (newStandardTotal === null) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const guaranteedTotal =
|
|
|
newQuotes.find(
|
|
|
(q) => q.service_level === "guaranteed" && q.rate_option === "lowest",
|
|
|
)?.raw_total ?? newStandardTotal;
|
|
|
|
|
|
const existing = await prisma.quoteCacheMeta.findUnique({
|
|
|
where: { cargoHash },
|
|
|
});
|
|
|
|
|
|
if (!existing) {
|
|
|
await prisma.quoteCacheMeta.create({
|
|
|
data: {
|
|
|
cargoHash,
|
|
|
rawTotalStandard: newStandardTotal,
|
|
|
rawTotalGuaranteed: guaranteedTotal,
|
|
|
lastQuotesJson: newQuotes as unknown as Prisma.InputJsonValue,
|
|
|
},
|
|
|
});
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const prevTotal = Number(existing.rawTotalStandard);
|
|
|
const deviation =
|
|
|
prevTotal > 0
|
|
|
? Math.abs(newStandardTotal - prevTotal) / prevTotal
|
|
|
: 0;
|
|
|
|
|
|
if (deviation >= PRICE_DEVIATION_THRESHOLD) {
|
|
|
const deviationPercent =
|
|
|
Math.round(
|
|
|
(Math.abs(newStandardTotal - prevTotal) / prevTotal) * 10_000,
|
|
|
) / 100;
|
|
|
await writeAlert("PRICE_DEVIATION", {
|
|
|
quoteId,
|
|
|
cargoHash,
|
|
|
detail: {
|
|
|
prev_total: prevTotal,
|
|
|
curr_total: newStandardTotal,
|
|
|
deviation_percent: deviationPercent,
|
|
|
},
|
|
|
});
|
|
|
}
|
|
|
|
|
|
await prisma.quoteCacheMeta.update({
|
|
|
where: { cargoHash },
|
|
|
data: {
|
|
|
rawTotalStandard: newStandardTotal,
|
|
|
rawTotalGuaranteed: guaranteedTotal,
|
|
|
lastQuotesJson: newQuotes as unknown as Prisma.InputJsonValue,
|
|
|
refreshedAt: new Date(),
|
|
|
},
|
|
|
});
|
|
|
}
|