b41c1d2501
CI / Architecture Guardrails (push) Successful in 2m38s
CI / Assistant Split Regression (push) Successful in 3m33s
CI / Typecheck (push) Successful in 3m51s
CI / Lint (push) Successful in 5m2s
CI / E2E Tests (push) Has been cancelled
CI / Fresh-Linux Docker Deploy (push) Has been cancelled
CI / Release Images (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
rename(phase 1): CapaKraken → Nexus across code, UI, docs, CI (#61) Co-authored-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com> Co-committed-by: Hartmut Nörenberg <hn@hartmut-noerenberg.com>
197 lines
6.0 KiB
TypeScript
197 lines
6.0 KiB
TypeScript
import type {
|
|
EstimateDemandLine,
|
|
EstimateDemandLineCalculationMetadata,
|
|
EstimateDemandLineRateMode,
|
|
EstimateDemandSummary,
|
|
} from "@nexus/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,
|
|
};
|
|
}
|