|
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
|
import { detectDeviation } from "@/modules/alert/deviation-detector";
|
|
|
|
|
|
const prismaMock = vi.hoisted(() => ({
|
|
|
quoteCacheMeta: {
|
|
|
findUnique: vi.fn(),
|
|
|
create: vi.fn(),
|
|
|
update: vi.fn(),
|
|
|
},
|
|
|
alertLog: {
|
|
|
create: vi.fn(),
|
|
|
},
|
|
|
}));
|
|
|
|
|
|
vi.mock("@/lib/prisma", () => ({ prisma: prismaMock }));
|
|
|
|
|
|
const FOUR_TIERS = [
|
|
|
{
|
|
|
service_level: "standard" as const,
|
|
|
rate_option: "lowest" as const,
|
|
|
carrier: "M",
|
|
|
transit_days: "5",
|
|
|
transit_description: "x",
|
|
|
raw_freight: 320,
|
|
|
surcharges: 0,
|
|
|
raw_total: 320,
|
|
|
},
|
|
|
{
|
|
|
service_level: "standard" as const,
|
|
|
rate_option: "fastest" as const,
|
|
|
carrier: "M",
|
|
|
transit_days: "3",
|
|
|
transit_description: "x",
|
|
|
raw_freight: 365,
|
|
|
surcharges: 0,
|
|
|
raw_total: 365,
|
|
|
},
|
|
|
{
|
|
|
service_level: "guaranteed" as const,
|
|
|
rate_option: "lowest" as const,
|
|
|
carrier: "M",
|
|
|
transit_days: "4",
|
|
|
transit_description: "x",
|
|
|
raw_freight: 380,
|
|
|
surcharges: 0,
|
|
|
raw_total: 380,
|
|
|
},
|
|
|
{
|
|
|
service_level: "guaranteed" as const,
|
|
|
rate_option: "fastest" as const,
|
|
|
carrier: "M",
|
|
|
transit_days: "3",
|
|
|
transit_description: "x",
|
|
|
raw_freight: 410,
|
|
|
surcharges: 0,
|
|
|
raw_total: 410,
|
|
|
},
|
|
|
];
|
|
|
|
|
|
describe("TC-401 cache vs RPA 一致性(task-113)", () => {
|
|
|
beforeEach(() => {
|
|
|
vi.clearAllMocks();
|
|
|
});
|
|
|
|
|
|
it("L2 raw_total 与 RPA 写入一致;偏差≥5% 触发 PRICE_DEVIATION", async () => {
|
|
|
const l2RawTotal = 320;
|
|
|
prismaMock.quoteCacheMeta.findUnique.mockResolvedValue({
|
|
|
cargoHash: "hash_cache",
|
|
|
rawTotalStandard: l2RawTotal,
|
|
|
});
|
|
|
prismaMock.quoteCacheMeta.update.mockResolvedValue({});
|
|
|
prismaMock.alertLog.create.mockResolvedValue({});
|
|
|
|
|
|
const newRpaQuotes = FOUR_TIERS.map((t) => ({ ...t }));
|
|
|
newRpaQuotes[0].raw_total = 340;
|
|
|
newRpaQuotes[0].raw_freight = 340;
|
|
|
|
|
|
await detectDeviation("hash_cache", newRpaQuotes, "QTE_CACHE");
|
|
|
|
|
|
expect(prismaMock.quoteCacheMeta.findUnique).toHaveBeenCalledWith({
|
|
|
where: { cargoHash: "hash_cache" },
|
|
|
});
|
|
|
expect(prismaMock.alertLog.create).toHaveBeenCalledWith(
|
|
|
expect.objectContaining({
|
|
|
data: expect.objectContaining({ alertType: "PRICE_DEVIATION" }),
|
|
|
}),
|
|
|
);
|
|
|
});
|
|
|
|
|
|
it("L2 与 RPA 一致(偏差<5%)→ 不触发预警", async () => {
|
|
|
prismaMock.quoteCacheMeta.findUnique.mockResolvedValue({
|
|
|
cargoHash: "hash_ok",
|
|
|
rawTotalStandard: 320,
|
|
|
});
|
|
|
prismaMock.quoteCacheMeta.update.mockResolvedValue({});
|
|
|
|
|
|
await detectDeviation("hash_ok", FOUR_TIERS, "QTE_OK");
|
|
|
|
|
|
expect(prismaMock.alertLog.create).not.toHaveBeenCalled();
|
|
|
});
|
|
|
});
|