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>
210 lines
6.0 KiB
TypeScript
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,
|
|
};
|
|
}
|