183 lines
6.3 KiB
TypeScript
183 lines
6.3 KiB
TypeScript
import type { Prisma } from "@capakraken/db";
|
|
import type { ExperienceMultiplierRule as EngineRule } from "@capakraken/engine";
|
|
import {
|
|
CreateExperienceMultiplierSetSchema,
|
|
UpdateExperienceMultiplierSetSchema,
|
|
} from "@capakraken/shared";
|
|
import { z } from "zod";
|
|
|
|
type CreateExperienceMultiplierSetInput = z.infer<typeof CreateExperienceMultiplierSetSchema>;
|
|
type UpdateExperienceMultiplierSetInput = z.infer<typeof UpdateExperienceMultiplierSetSchema>;
|
|
type ExperienceMultiplierRuleRowInput =
|
|
| CreateExperienceMultiplierSetInput["rules"][number]
|
|
| NonNullable<UpdateExperienceMultiplierSetInput["rules"]>[number];
|
|
|
|
type ExperienceMultiplierRuleRecord = {
|
|
chapter: string | null;
|
|
location: string | null;
|
|
level: string | null;
|
|
costMultiplier: number;
|
|
billMultiplier: number;
|
|
shoringRatio: number | null;
|
|
additionalEffortRatio: number | null;
|
|
description: string | null;
|
|
};
|
|
|
|
type DemandLineRecord = {
|
|
id: string;
|
|
name: string;
|
|
chapter: string | null;
|
|
costRateCents: number;
|
|
billRateCents: number;
|
|
hours: number;
|
|
metadata: unknown;
|
|
staffingAttributes: unknown;
|
|
};
|
|
|
|
type MultiplierSetRecord = {
|
|
id: string;
|
|
name: string;
|
|
};
|
|
|
|
type ExperienceMultiplierResultRecord = {
|
|
adjustedCostRateCents: number;
|
|
adjustedBillRateCents: number;
|
|
adjustedHours: number;
|
|
appliedRules: string[];
|
|
};
|
|
|
|
export const experienceMultiplierRuleInclude = {
|
|
rules: { orderBy: { sortOrder: "asc" as const } },
|
|
} as const;
|
|
|
|
function buildExperienceMultiplierRuleRow(
|
|
input: ExperienceMultiplierRuleRowInput,
|
|
index: number,
|
|
) {
|
|
return {
|
|
...(input.chapter ? { chapter: input.chapter } : {}),
|
|
...(input.location ? { location: input.location } : {}),
|
|
...(input.level ? { level: input.level } : {}),
|
|
costMultiplier: input.costMultiplier,
|
|
billMultiplier: input.billMultiplier,
|
|
...(input.shoringRatio !== undefined ? { shoringRatio: input.shoringRatio } : {}),
|
|
...(input.additionalEffortRatio !== undefined ? { additionalEffortRatio: input.additionalEffortRatio } : {}),
|
|
...(input.description ? { description: input.description } : {}),
|
|
sortOrder: input.sortOrder ?? index,
|
|
};
|
|
}
|
|
|
|
export function buildExperienceMultiplierNestedCreateRows(
|
|
rules: ExperienceMultiplierRuleRowInput[],
|
|
): Prisma.ExperienceMultiplierRuleUncheckedCreateWithoutMultiplierSetInput[] {
|
|
return rules.map((rule, index) => ({
|
|
...buildExperienceMultiplierRuleRow(rule, index),
|
|
}));
|
|
}
|
|
|
|
export function buildExperienceMultiplierCreateManyRows(
|
|
rules: ExperienceMultiplierRuleRowInput[],
|
|
multiplierSetId: string,
|
|
): Prisma.ExperienceMultiplierRuleCreateManyInput[] {
|
|
return rules.map((rule, index) => ({
|
|
multiplierSetId,
|
|
...buildExperienceMultiplierRuleRow(rule, index),
|
|
}));
|
|
}
|
|
|
|
export function buildExperienceMultiplierSetCreateData(
|
|
input: CreateExperienceMultiplierSetInput,
|
|
): Prisma.ExperienceMultiplierSetCreateInput {
|
|
return {
|
|
name: input.name,
|
|
...(input.description ? { description: input.description } : {}),
|
|
isDefault: input.isDefault,
|
|
rules: {
|
|
create: buildExperienceMultiplierNestedCreateRows(input.rules),
|
|
},
|
|
};
|
|
}
|
|
|
|
export function buildExperienceMultiplierSetUpdateData(
|
|
input: Omit<UpdateExperienceMultiplierSetInput, "id" | "rules">,
|
|
): Prisma.ExperienceMultiplierSetUncheckedUpdateInput {
|
|
return {
|
|
...(input.name !== undefined ? { name: input.name } : {}),
|
|
...(input.description !== undefined ? { description: input.description } : {}),
|
|
...(input.isDefault !== undefined ? { isDefault: input.isDefault } : {}),
|
|
};
|
|
}
|
|
|
|
export function toExperienceMultiplierEngineRules(
|
|
dbRules: ExperienceMultiplierRuleRecord[],
|
|
): EngineRule[] {
|
|
return dbRules.map((rule) => ({
|
|
...(rule.chapter != null ? { chapter: rule.chapter } : {}),
|
|
...(rule.location != null ? { location: rule.location } : {}),
|
|
...(rule.level != null ? { level: rule.level } : {}),
|
|
costMultiplier: rule.costMultiplier,
|
|
billMultiplier: rule.billMultiplier,
|
|
...(rule.shoringRatio != null ? { shoringRatio: rule.shoringRatio } : {}),
|
|
...(rule.additionalEffortRatio != null ? { additionalEffortRatio: rule.additionalEffortRatio } : {}),
|
|
...(rule.description != null ? { description: rule.description } : {}),
|
|
}));
|
|
}
|
|
|
|
export function buildExperienceMultiplierInput(
|
|
line: DemandLineRecord,
|
|
) {
|
|
return {
|
|
costRateCents: line.costRateCents,
|
|
billRateCents: line.billRateCents,
|
|
hours: line.hours,
|
|
...(line.chapter != null ? { chapter: line.chapter } : {}),
|
|
...(line.metadata != null && typeof line.metadata === "object" && "location" in (line.metadata as Record<string, unknown>)
|
|
? { location: (line.metadata as Record<string, unknown>).location as string }
|
|
: {}),
|
|
...(line.staffingAttributes != null && typeof line.staffingAttributes === "object" && "level" in (line.staffingAttributes as Record<string, unknown>)
|
|
? { level: (line.staffingAttributes as Record<string, unknown>).level as string }
|
|
: {}),
|
|
};
|
|
}
|
|
|
|
export function hasExperienceMultiplierChanges(
|
|
line: Pick<DemandLineRecord, "costRateCents" | "billRateCents" | "hours">,
|
|
result: ExperienceMultiplierResultRecord,
|
|
): boolean {
|
|
return (
|
|
result.adjustedCostRateCents !== line.costRateCents ||
|
|
result.adjustedBillRateCents !== line.billRateCents ||
|
|
result.adjustedHours !== line.hours
|
|
);
|
|
}
|
|
|
|
export function buildExperienceMultiplierDemandLineUpdateData(input: {
|
|
line: DemandLineRecord;
|
|
result: ExperienceMultiplierResultRecord;
|
|
multiplierSet: MultiplierSetRecord;
|
|
}): Prisma.EstimateDemandLineUncheckedUpdateInput {
|
|
const newCostTotal = Math.round(input.result.adjustedCostRateCents * input.result.adjustedHours);
|
|
const newPriceTotal = Math.round(input.result.adjustedBillRateCents * input.result.adjustedHours);
|
|
|
|
return {
|
|
costRateCents: input.result.adjustedCostRateCents,
|
|
billRateCents: input.result.adjustedBillRateCents,
|
|
hours: input.result.adjustedHours,
|
|
costTotalCents: newCostTotal,
|
|
priceTotalCents: newPriceTotal,
|
|
metadata: {
|
|
...(typeof input.line.metadata === "object" && input.line.metadata !== null
|
|
? input.line.metadata as Record<string, unknown>
|
|
: {}),
|
|
experienceMultiplier: {
|
|
setId: input.multiplierSet.id,
|
|
setName: input.multiplierSet.name,
|
|
appliedRules: input.result.appliedRules,
|
|
originalCostRateCents: input.line.costRateCents,
|
|
originalBillRateCents: input.line.billRateCents,
|
|
originalHours: input.line.hours,
|
|
},
|
|
},
|
|
};
|
|
}
|