Files
CapaKraken/packages/engine/src/budget/monitor.ts
T

108 lines
3.3 KiB
TypeScript

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<Allocation, "status" | "dailyCostCents" | "startDate" | "endDate" | "hoursPerDay">[],
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 days = countWorkingDaysInRange(
new Date(alloc.startDate),
new Date(alloc.endDate),
);
const totalCents = alloc.dailyCostCents * days;
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;
}