import { describe, expect, it } from "vitest"; import { findMatchingRule, applyCostEffect } from "../rules/engine.js"; import { DEFAULT_CALCULATION_RULES } from "../rules/default-rules.js"; import type { CalculationRule } from "@capakraken/shared"; const now = new Date(); function makeRule(overrides: Partial): CalculationRule { return { id: "rule_1", name: "Test Rule", description: null, triggerType: "SICK", projectId: null, orderType: null, costEffect: "ZERO", costReductionPercent: null, chargeabilityEffect: "COUNT", priority: 0, isActive: true, createdAt: now, updatedAt: now, ...overrides, }; } describe("findMatchingRule", () => { it("matches by triggerType", () => { const rules = [makeRule({ triggerType: "SICK" })]; const match = findMatchingRule(rules, "SICK"); expect(match).not.toBeNull(); expect(match!.costEffect).toBe("ZERO"); }); it("returns null when no rules match", () => { const rules = [makeRule({ triggerType: "SICK" })]; const match = findMatchingRule(rules, "VACATION"); expect(match).toBeNull(); }); it("skips inactive rules", () => { const rules = [makeRule({ triggerType: "SICK", isActive: false })]; const match = findMatchingRule(rules, "SICK"); expect(match).toBeNull(); }); it("prefers more specific rules (projectId match)", () => { const global = makeRule({ id: "global", triggerType: "SICK", projectId: null }); const specific = makeRule({ id: "specific", triggerType: "SICK", projectId: "proj_1", costEffect: "CHARGE" }); const match = findMatchingRule([global, specific], "SICK", "proj_1"); expect(match!.rule.id).toBe("specific"); expect(match!.costEffect).toBe("CHARGE"); }); it("does not match project-specific rule to wrong project", () => { const specific = makeRule({ triggerType: "SICK", projectId: "proj_1" }); const match = findMatchingRule([specific], "SICK", "proj_2"); expect(match).toBeNull(); }); it("prefers higher specificity over higher priority", () => { const highPriority = makeRule({ id: "hp", triggerType: "SICK", priority: 100 }); const specific = makeRule({ id: "sp", triggerType: "SICK", projectId: "proj_1", priority: 0 }); const match = findMatchingRule([highPriority, specific], "SICK", "proj_1"); expect(match!.rule.id).toBe("sp"); }); it("breaks specificity ties with priority", () => { const lowP = makeRule({ id: "low", triggerType: "SICK", priority: 5 }); const highP = makeRule({ id: "high", triggerType: "SICK", priority: 10 }); const match = findMatchingRule([lowP, highP], "SICK"); expect(match!.rule.id).toBe("high"); }); it("matches orderType filter", () => { const rule = makeRule({ triggerType: "VACATION", orderType: "CHARGEABLE" as never, costEffect: "REDUCE", costReductionPercent: 50 }); const match = findMatchingRule([rule], "VACATION", null, "CHARGEABLE"); expect(match).not.toBeNull(); expect(match!.costEffect).toBe("REDUCE"); }); it("does not match wrong orderType", () => { const rule = makeRule({ triggerType: "VACATION", orderType: "CHARGEABLE" as never }); const match = findMatchingRule([rule], "VACATION", null, "INTERNAL"); expect(match).toBeNull(); }); it("specificity: projectId + orderType > projectId only", () => { const projOnly = makeRule({ id: "proj", triggerType: "SICK", projectId: "p1" }); const both = makeRule({ id: "both", triggerType: "SICK", projectId: "p1", orderType: "CHARGEABLE" as never, costEffect: "REDUCE" }); const match = findMatchingRule([projOnly, both], "SICK", "p1", "CHARGEABLE"); expect(match!.rule.id).toBe("both"); }); }); describe("applyCostEffect", () => { it("CHARGE returns full cost", () => { expect(applyCostEffect(1000, "CHARGE", null)).toBe(1000); }); it("ZERO returns 0", () => { expect(applyCostEffect(1000, "ZERO", null)).toBe(0); }); it("REDUCE applies percentage", () => { expect(applyCostEffect(1000, "REDUCE", 30)).toBe(700); }); it("REDUCE with 100% returns 0", () => { expect(applyCostEffect(1000, "REDUCE", 100)).toBe(0); }); it("REDUCE with 0% returns full cost", () => { expect(applyCostEffect(1000, "REDUCE", 0)).toBe(1000); }); it("REDUCE with null percent returns full cost", () => { expect(applyCostEffect(1000, "REDUCE", null)).toBe(1000); }); }); describe("DEFAULT_CALCULATION_RULES", () => { it("provides vacation, sick, and public holiday rules", () => { expect(DEFAULT_CALCULATION_RULES).toHaveLength(3); const triggers = DEFAULT_CALCULATION_RULES.map((r) => r.triggerType); expect(triggers).toContain("VACATION"); expect(triggers).toContain("SICK"); expect(triggers).toContain("PUBLIC_HOLIDAY"); }); it("vacation: zero cost, count chargeability", () => { const rule = DEFAULT_CALCULATION_RULES.find((r) => r.triggerType === "VACATION")!; expect(rule.costEffect).toBe("ZERO"); expect(rule.chargeabilityEffect).toBe("COUNT"); }); it("sick: zero cost, count chargeability", () => { const rule = DEFAULT_CALCULATION_RULES.find((r) => r.triggerType === "SICK")!; expect(rule.costEffect).toBe("ZERO"); expect(rule.chargeabilityEffect).toBe("COUNT"); }); it("public holiday: zero cost, skip chargeability", () => { const rule = DEFAULT_CALCULATION_RULES.find((r) => r.triggerType === "PUBLIC_HOLIDAY")!; expect(rule.costEffect).toBe("ZERO"); expect(rule.chargeabilityEffect).toBe("SKIP"); }); });