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

210 lines
6.0 KiB
TypeScript

import type {
EstimateDemandLine,
EstimateDemandLineCalculationMetadata,
EstimateDemandLineRateMode,
EstimateDemandSummary,
} from "@capakraken/shared";
export interface EstimateDemandLineRateSnapshot {
resourceId?: string | null;
currency?: string | null;
lcrCents: number;
ucrCents: number;
}
export interface EstimateDemandLineForCalculation {
resourceId?: string | null | undefined;
hours: number;
rateSource?: string | null | undefined;
costRateCents: number;
billRateCents: number;
currency?: string | null | undefined;
costTotalCents: number;
priceTotalCents: number;
metadata?: Record<string, unknown> | null | undefined;
}
type ParsedDemandLineMetadata = Record<string, unknown> & {
calculation?: Partial<EstimateDemandLineCalculationMetadata>;
};
function parseRateMode(value: unknown): EstimateDemandLineRateMode | undefined {
return value === "resource" || value === "manual" ? value : undefined;
}
function parseDemandLineMetadata(
metadata: Record<string, unknown> | null | undefined,
): ParsedDemandLineMetadata {
const safeMetadata =
typeof metadata === "object" && metadata !== null && !Array.isArray(metadata)
? metadata
: {};
const rawCalculation =
typeof safeMetadata.calculation === "object" &&
safeMetadata.calculation !== null &&
!Array.isArray(safeMetadata.calculation)
? (safeMetadata.calculation as Record<string, unknown>)
: undefined;
if (!rawCalculation) {
return safeMetadata as ParsedDemandLineMetadata;
}
const calculation: Partial<EstimateDemandLineCalculationMetadata> = {};
const costRateMode = parseRateMode(rawCalculation.costRateMode);
if (costRateMode) {
calculation.costRateMode = costRateMode;
}
const billRateMode = parseRateMode(rawCalculation.billRateMode);
if (billRateMode) {
calculation.billRateMode = billRateMode;
}
if (rawCalculation.totalMode === "computed") {
calculation.totalMode = "computed";
}
if (typeof rawCalculation.liveCostRateCents === "number") {
calculation.liveCostRateCents = rawCalculation.liveCostRateCents;
}
if (typeof rawCalculation.liveBillRateCents === "number") {
calculation.liveBillRateCents = rawCalculation.liveBillRateCents;
}
if (typeof rawCalculation.liveCurrency === "string") {
calculation.liveCurrency = rawCalculation.liveCurrency;
}
return {
...safeMetadata,
...(Object.keys(calculation).length > 0 ? { calculation } : {}),
};
}
function inferRateMode(
resourceSnapshot: EstimateDemandLineRateSnapshot | null | undefined,
effectiveRateCents: number,
liveRateCents: number | undefined,
explicitMode: EstimateDemandLineRateMode | undefined,
): EstimateDemandLineRateMode {
if (explicitMode) {
return explicitMode;
}
if (!resourceSnapshot || liveRateCents == null) {
return "manual";
}
return effectiveRateCents === liveRateCents ? "resource" : "manual";
}
export function getEstimateDemandLineCalculationMetadata(
line: Pick<
EstimateDemandLineForCalculation,
"resourceId" | "costRateCents" | "billRateCents" | "metadata"
>,
options?: {
resourceSnapshot?: EstimateDemandLineRateSnapshot | null | undefined;
},
): EstimateDemandLineCalculationMetadata {
const parsedMetadata = parseDemandLineMetadata(line.metadata);
const explicitCalculation = parsedMetadata.calculation;
const resourceSnapshot = options?.resourceSnapshot;
const liveCostRateCents = resourceSnapshot?.lcrCents;
const liveBillRateCents = resourceSnapshot?.ucrCents;
return {
costRateMode: inferRateMode(
resourceSnapshot,
line.costRateCents,
liveCostRateCents,
explicitCalculation?.costRateMode,
),
billRateMode: inferRateMode(
resourceSnapshot,
line.billRateCents,
liveBillRateCents,
explicitCalculation?.billRateMode,
),
totalMode: "computed",
liveCostRateCents: liveCostRateCents ?? null,
liveBillRateCents: liveBillRateCents ?? null,
liveCurrency: resourceSnapshot?.currency ?? null,
};
}
export function normalizeEstimateDemandLine<T extends EstimateDemandLineForCalculation>(
line: T,
options?: {
resourceSnapshot?: EstimateDemandLineRateSnapshot | null | undefined;
defaultCurrency?: string;
},
): T {
const resourceSnapshot = options?.resourceSnapshot;
const calculation = getEstimateDemandLineCalculationMetadata(line, {
resourceSnapshot,
});
const effectiveCostRateCents =
calculation.costRateMode === "resource" && resourceSnapshot
? resourceSnapshot.lcrCents
: line.costRateCents;
const effectiveBillRateCents =
calculation.billRateMode === "resource" && resourceSnapshot
? resourceSnapshot.ucrCents
: line.billRateCents;
const currency =
((calculation.costRateMode === "resource" ||
calculation.billRateMode === "resource") &&
resourceSnapshot?.currency
? resourceSnapshot.currency
: line.currency) ||
resourceSnapshot?.currency ||
options?.defaultCurrency ||
"EUR";
const metadata = parseDemandLineMetadata(line.metadata);
return {
...line,
costRateCents: effectiveCostRateCents,
billRateCents: effectiveBillRateCents,
currency,
costTotalCents: Math.round(line.hours * effectiveCostRateCents),
priceTotalCents: Math.round(line.hours * effectiveBillRateCents),
metadata: {
...metadata,
calculation,
},
};
}
export function summarizeEstimateDemandLines(
demandLines: Pick<
EstimateDemandLine,
"hours" | "costTotalCents" | "priceTotalCents"
>[],
): EstimateDemandSummary {
const totalHours = demandLines.reduce((sum, line) => sum + line.hours, 0);
const totalCostCents = demandLines.reduce(
(sum, line) => sum + line.costTotalCents,
0,
);
const totalPriceCents = demandLines.reduce(
(sum, line) => sum + line.priceTotalCents,
0,
);
const marginCents = totalPriceCents - totalCostCents;
const marginPercent =
totalPriceCents > 0 ? Math.round((marginCents / totalPriceCents) * 100) : 0;
return {
totalHours,
totalCostCents,
totalPriceCents,
marginCents,
marginPercent,
};
}