import type { Allocation, BudgetStatus, BudgetWarning } from "@planarchy/shared"; import { BUDGET_WARNING_THRESHOLDS } from "@planarchy/shared"; /** * Computes budget status for a project given its allocations. * Pure function — all data passed as parameters. */ export function computeBudgetStatus( budgetCents: number, winProbability: number, allocations: (Pick & { /** When provided (from rules engine), used instead of dailyCostCents * days */ adjustedTotalCostCents?: number; })[], projectStartDate: Date, projectEndDate: Date, ): BudgetStatus { const activeStatuses = new Set(["CONFIRMED", "ACTIVE"]); const proposedStatuses = new Set(["PROPOSED"]); let confirmedCents = 0; let proposedCents = 0; for (const alloc of allocations) { const totalCents = alloc.adjustedTotalCostCents ?? (alloc.dailyCostCents * countWorkingDaysInRange( new Date(alloc.startDate), new Date(alloc.endDate), )); if (activeStatuses.has(alloc.status)) { confirmedCents += totalCents; } else if (proposedStatuses.has(alloc.status)) { proposedCents += totalCents; } } const allocatedCents = confirmedCents + proposedCents; const remainingCents = budgetCents - allocatedCents; const utilizationPercent = budgetCents > 0 ? (allocatedCents / budgetCents) * 100 : 0; const winProbabilityWeightedCents = Math.round(allocatedCents * (winProbability / 100)); const warnings: BudgetWarning[] = []; if (utilizationPercent >= BUDGET_WARNING_THRESHOLDS.CRITICAL) { warnings.push({ level: "critical", code: "BUDGET_CRITICAL", message: `Budget utilization at ${utilizationPercent.toFixed(1)}% — critical threshold exceeded`, thresholdPercent: BUDGET_WARNING_THRESHOLDS.CRITICAL, currentPercent: utilizationPercent, }); } else if (utilizationPercent >= BUDGET_WARNING_THRESHOLDS.WARNING) { warnings.push({ level: "warning", code: "BUDGET_WARNING", message: `Budget utilization at ${utilizationPercent.toFixed(1)}% — approaching limit`, thresholdPercent: BUDGET_WARNING_THRESHOLDS.WARNING, currentPercent: utilizationPercent, }); } else if (utilizationPercent >= BUDGET_WARNING_THRESHOLDS.INFO) { warnings.push({ level: "info", code: "BUDGET_INFO", message: `Budget utilization at ${utilizationPercent.toFixed(1)}%`, thresholdPercent: BUDGET_WARNING_THRESHOLDS.INFO, currentPercent: utilizationPercent, }); } if (allocatedCents > budgetCents) { warnings.push({ level: "critical", code: "BUDGET_EXCEEDED", message: `Budget exceeded by ${((allocatedCents - budgetCents) / 100).toFixed(2)} EUR`, thresholdPercent: 100, currentPercent: utilizationPercent, }); } return { budgetCents, allocatedCents, confirmedCents, proposedCents, remainingCents, utilizationPercent, winProbabilityWeightedCents, warnings, }; } /** Simple working day counter (Mon-Fri) for budget calculations */ function countWorkingDaysInRange(startDate: Date, endDate: Date): number { let count = 0; const current = new Date(startDate); current.setHours(0, 0, 0, 0); const end = new Date(endDate); end.setHours(0, 0, 0, 0); while (current <= end) { const dow = current.getDay(); if (dow !== 0 && dow !== 6) { count++; } current.setDate(current.getDate() + 1); } return count; }