Files
CapaKraken/packages/engine/src/rules/engine.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

92 lines
2.3 KiB
TypeScript

/**
* Calculation Rules Engine — matches absence days against rules
* to determine cost and chargeability effects.
*
* Pure function — no DB imports.
*/
import type {
AbsenceTrigger,
CalculationRule,
CostEffect,
ChargeabilityEffect,
} from "@capakraken/shared";
export interface RuleMatch {
rule: CalculationRule;
costEffect: CostEffect;
chargeabilityEffect: ChargeabilityEffect;
costReductionPercent: number | null;
}
/**
* Specificity score for a rule — more specific filters = higher score.
*/
function specificityScore(rule: CalculationRule): number {
let score = 0;
if (rule.projectId) score += 2;
if (rule.orderType) score += 1;
return score;
}
/**
* Find the best matching rule for a given absence day.
*
* Matching:
* 1. triggerType must match
* 2. isActive must be true
* 3. projectId must match (null = all projects)
* 4. orderType must match (null = all order types)
*
* Ranking: highest specificity wins, then highest priority.
*/
export function findMatchingRule(
rules: CalculationRule[],
triggerType: AbsenceTrigger,
projectId?: string | null,
orderType?: string | null,
): RuleMatch | null {
const candidates = rules.filter((r) => {
if (!r.isActive) return false;
if (r.triggerType !== triggerType) return false;
if (r.projectId && r.projectId !== projectId) return false;
if (r.orderType && r.orderType !== orderType) return false;
return true;
});
if (candidates.length === 0) return null;
// Sort by specificity (desc), then priority (desc)
candidates.sort((a, b) => {
const specDiff = specificityScore(b) - specificityScore(a);
if (specDiff !== 0) return specDiff;
return b.priority - a.priority;
});
const best = candidates[0]!;
return {
rule: best,
costEffect: best.costEffect,
chargeabilityEffect: best.chargeabilityEffect,
costReductionPercent: best.costReductionPercent,
};
}
/**
* Apply cost effect to a cost value.
*/
export function applyCostEffect(
normalCostCents: number,
costEffect: CostEffect,
reductionPercent: number | null,
): number {
switch (costEffect) {
case "CHARGE":
return normalCostCents;
case "ZERO":
return 0;
case "REDUCE":
return Math.round(normalCostCents * (100 - (reductionPercent ?? 0)) / 100);
}
}