Files
CapaKraken/packages/engine/src/budget/monitor.ts
T
Hartmut cd78f72f33 chore: full technical rename planarchy → capakraken
Complete rename of all technical identifiers across the codebase:

Package names (11 packages):
- @planarchy/* → @capakraken/* in all package.json, tsconfig, imports

Import statements: 277 files, 548 occurrences replaced

Database & Docker:
- PostgreSQL user/db: planarchy → capakraken
- Docker volumes: planarchy_pgdata → capakraken_pgdata
- Connection strings updated in docker-compose, .env, CI

CI/CD:
- GitHub Actions workflow: all filter commands updated
- Test database credentials updated

Infrastructure:
- Redis channel: planarchy:sse → capakraken:sse
- Logger service name: planarchy-api → capakraken-api
- Anonymization seed updated
- Start/stop/restart scripts updated

Test data:
- Seed emails: @planarchy.dev → @capakraken.dev
- E2E test credentials: all 11 spec files updated
- Email defaults: @planarchy.app → @capakraken.app
- localStorage keys: planarchy_* → capakraken_*

Documentation: 30+ .md files updated

Verification:
- pnpm install: workspace resolution works
- TypeScript: only pre-existing TS2589 (no new errors)
- Engine: 310/310 tests pass
- Staffing: 37/37 tests pass

Co-Authored-By: claude-flow <ruv@ruv.net>
2026-03-27 13:18:09 +01:00

110 lines
3.5 KiB
TypeScript

import type { Allocation, BudgetStatus, BudgetWarning } from "@capakraken/shared";
import { BUDGET_WARNING_THRESHOLDS } from "@capakraken/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"> & {
/** 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;
}