cd78f72f33
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>
92 lines
2.3 KiB
TypeScript
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);
|
|
}
|
|
}
|