feat: calculation rules engine for decoupled cost attribution and chargeability
Introduces an admin-configurable rules engine that determines per-day cost attribution (CHARGE/ZERO/REDUCE) and chargeability reporting (COUNT/SKIP) for absence types (sick, vacation, public holiday). Includes shared types, Zod schemas, Prisma model, rule matching with specificity scoring, default rules, calculator integration, CRUD API router, seed data, chargeability report integration, and admin UI. 283/283 engine tests, 209/209 API tests, 0 TS errors. Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const AbsenceTriggerEnum = z.enum(["SICK", "VACATION", "PUBLIC_HOLIDAY", "CUSTOM"]);
|
||||
export const CostEffectEnum = z.enum(["CHARGE", "ZERO", "REDUCE"]);
|
||||
export const ChargeabilityEffectEnum = z.enum(["COUNT", "SKIP"]);
|
||||
|
||||
export const CreateCalculationRuleSchema = z.object({
|
||||
name: z.string().min(1).max(200),
|
||||
description: z.string().max(1000).optional(),
|
||||
triggerType: AbsenceTriggerEnum,
|
||||
projectId: z.string().optional(),
|
||||
orderType: z.string().optional(),
|
||||
costEffect: CostEffectEnum,
|
||||
costReductionPercent: z.number().int().min(0).max(100).optional(),
|
||||
chargeabilityEffect: ChargeabilityEffectEnum,
|
||||
priority: z.number().int().min(0).max(1000).default(0),
|
||||
isActive: z.boolean().default(true),
|
||||
});
|
||||
|
||||
export const UpdateCalculationRuleSchema = CreateCalculationRuleSchema.partial().extend({
|
||||
id: z.string(),
|
||||
});
|
||||
@@ -13,3 +13,4 @@ export * from "./client.schema.js";
|
||||
export * from "./management-level.schema.js";
|
||||
export * from "./rate-card.schema.js";
|
||||
export * from "./dispo-import.schema.js";
|
||||
export * from "./calculation-rules.schema.js";
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// ─── Calculation Rules ────────────────────────────────────────────────────────
|
||||
// Admin-configurable rules that decouple cost attribution from chargeability.
|
||||
// Example: "Sick person = still chargeable, but NOT charged to project."
|
||||
|
||||
export type AbsenceTrigger = "SICK" | "VACATION" | "PUBLIC_HOLIDAY" | "CUSTOM";
|
||||
|
||||
export type CostEffect = "CHARGE" | "ZERO" | "REDUCE";
|
||||
|
||||
export type ChargeabilityEffect = "COUNT" | "SKIP";
|
||||
|
||||
export interface CalculationRule {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
|
||||
// ── Matching ──
|
||||
triggerType: AbsenceTrigger;
|
||||
projectId: string | null; // null = all projects
|
||||
orderType: string | null; // null = all order types
|
||||
|
||||
// ── Effects ──
|
||||
costEffect: CostEffect;
|
||||
costReductionPercent: number | null; // only for REDUCE (0-100)
|
||||
chargeabilityEffect: ChargeabilityEffect;
|
||||
|
||||
// ── Ordering ──
|
||||
priority: number;
|
||||
isActive: boolean;
|
||||
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
@@ -39,6 +39,12 @@ export interface ConflictDetail {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface AbsenceDay {
|
||||
date: Date;
|
||||
type: import("./calculation-rules.js").AbsenceTrigger;
|
||||
isHalfDay?: boolean;
|
||||
}
|
||||
|
||||
export interface AllocationCalculationInput {
|
||||
lcrCents: number;
|
||||
hoursPerDay: number;
|
||||
@@ -51,6 +57,14 @@ export interface AllocationCalculationInput {
|
||||
recurrence?: import("./allocation.js").RecurrencePattern;
|
||||
/** APPROVED vacation dates — these days are blocked regardless of other settings */
|
||||
vacationDates?: Date[];
|
||||
/** Typed absence days (vacation, sick, public holiday) — used by the rules engine */
|
||||
absenceDays?: AbsenceDay[];
|
||||
/** Calculation rules — when provided, absence days are evaluated against these rules */
|
||||
calculationRules?: import("./calculation-rules.js").CalculationRule[];
|
||||
/** Order type of the project — used for rule matching */
|
||||
orderType?: string;
|
||||
/** Project ID — used for rule matching */
|
||||
projectId?: string;
|
||||
}
|
||||
|
||||
export interface AllocationCalculationResult {
|
||||
@@ -59,6 +73,10 @@ export interface AllocationCalculationResult {
|
||||
totalCostCents: number;
|
||||
dailyCostCents: number;
|
||||
dailyBreakdown: DailyBreakdown[];
|
||||
/** Total hours counting toward chargeability (rules-adjusted) */
|
||||
totalChargeableHours?: number;
|
||||
/** Cost after rule adjustments (e.g. sick days zeroed out) */
|
||||
totalProjectCostCents?: number;
|
||||
}
|
||||
|
||||
export interface DailyBreakdown {
|
||||
@@ -66,6 +84,10 @@ export interface DailyBreakdown {
|
||||
isWorkday: boolean;
|
||||
hours: number;
|
||||
costCents: number;
|
||||
/** Absence type for this day (if any rule matched) */
|
||||
absenceType?: import("./calculation-rules.js").AbsenceTrigger;
|
||||
/** Hours that count toward chargeability (may differ from hours when rules apply) */
|
||||
chargeableHours?: number;
|
||||
}
|
||||
|
||||
export interface BudgetStatus {
|
||||
|
||||
@@ -18,3 +18,4 @@ export * from "./utilization-category.js";
|
||||
export * from "./client.js";
|
||||
export * from "./management-level.js";
|
||||
export * from "./dispo-import.js";
|
||||
export * from "./calculation-rules.js";
|
||||
|
||||
Reference in New Issue
Block a user