import { CreateRateCardLineSchema, CreateRateCardSchema, UpdateRateCardLineSchema, UpdateRateCardSchema, } from "@capakraken/shared"; import { z } from "zod"; import { findUniqueOrThrow } from "../db/helpers.js"; import { createAuditEntry } from "../lib/audit.js"; import type { TRPCContext } from "../trpc.js"; import { lookupBestRateMatch, rateCardLineSelect, resolveBestRate, resolveBestRateLineMatch, } from "./rate-card-read.js"; import { buildRateCardCreateAuditAfter, buildRateCardCreateData, buildRateCardLineCreateAuditAfter, buildRateCardLineCreateData, buildRateCardLineUpdateData, buildRateCardListWhere, buildRateCardNestedLineCreateData, buildRateCardReplaceLinesAuditAfter, buildRateCardUpdateData, rateCardCreateInclude, rateCardDetailInclude, rateCardSummaryInclude, } from "./rate-card-write-support.js"; type RateCardProcedureContext = Pick; function withAuditUser(userId: string | undefined) { return userId ? { userId } : {}; } export const rateCardListInputSchema = z.object({ isActive: z.boolean().optional(), search: z.string().optional(), clientId: z.string().optional(), effectiveAt: z.coerce.date().optional(), }).optional(); export const rateCardByIdInputSchema = z.object({ id: z.string() }); export const rateCardLookupBestMatchInputSchema = z.object({ clientId: z.string().optional(), chapter: z.string().optional(), managementLevelId: z.string().optional(), roleName: z.string().optional(), seniority: z.string().optional(), }); export const rateCardResolveBestRateInputSchema = z.object({ resourceId: z.string().optional(), roleName: z.string().optional(), date: z.coerce.date().optional(), }); export const rateCardUpdateInputSchema = z.object({ id: z.string(), data: UpdateRateCardSchema, }); export const rateCardAddLineInputSchema = z.object({ rateCardId: z.string(), line: CreateRateCardLineSchema, }); export const rateCardUpdateLineInputSchema = z.object({ lineId: z.string(), data: UpdateRateCardLineSchema, }); export const rateCardDeleteLineInputSchema = z.object({ lineId: z.string() }); export const rateCardReplaceLinesInputSchema = z.object({ rateCardId: z.string(), lines: z.array(CreateRateCardLineSchema), }); export const rateCardResolveLineInputSchema = z.object({ rateCardId: z.string(), roleId: z.string().optional(), chapter: z.string().optional(), location: z.string().optional(), seniority: z.string().optional(), workType: z.string().optional(), }); type RateCardListInput = z.infer; type RateCardByIdInput = z.infer; type RateCardLookupBestMatchInput = z.infer; type RateCardResolveBestRateInput = z.infer; type RateCardCreateInput = z.infer; type RateCardUpdateInput = z.infer; type RateCardDeactivateInput = z.infer; type RateCardAddLineInput = z.infer; type RateCardUpdateLineInput = z.infer; type RateCardDeleteLineInput = z.infer; type RateCardReplaceLinesInput = z.infer; type RateCardResolveLineInput = z.infer; export async function listRateCards( ctx: RateCardProcedureContext, input: RateCardListInput, ) { return ctx.db.rateCard.findMany({ where: buildRateCardListWhere(input), include: rateCardSummaryInclude, orderBy: [{ isActive: "desc" }, { effectiveFrom: "desc" }, { name: "asc" }], }); } export async function getRateCardById( ctx: RateCardProcedureContext, input: RateCardByIdInput, ) { return findUniqueOrThrow( ctx.db.rateCard.findUnique({ where: { id: input.id }, include: rateCardDetailInclude, }), "Rate card", ); } export async function lookupRateCardBestMatch( ctx: RateCardProcedureContext, input: RateCardLookupBestMatchInput, ) { return lookupBestRateMatch(ctx.db, input); } export async function resolveRateCard( ctx: RateCardProcedureContext, input: RateCardResolveBestRateInput, ) { return resolveBestRate(ctx.db, input); } export async function createRateCard( ctx: RateCardProcedureContext, input: RateCardCreateInput, ) { const { lines, name, currency } = input; const rateCard = await ctx.db.rateCard.create({ data: buildRateCardCreateData(input), include: rateCardCreateInclude, }); void createAuditEntry({ db: ctx.db, entityType: "RateCard", entityId: rateCard.id, entityName: rateCard.name, action: "CREATE", ...withAuditUser(ctx.dbUser?.id), after: buildRateCardCreateAuditAfter({ name, currency, lines }), source: "ui", }); return rateCard; } export async function updateRateCard( ctx: RateCardProcedureContext, input: RateCardUpdateInput, ) { const before = await findUniqueOrThrow( ctx.db.rateCard.findUnique({ where: { id: input.id } }), "Rate card", ); const updated = await ctx.db.rateCard.update({ where: { id: input.id }, data: buildRateCardUpdateData(input.data), include: rateCardSummaryInclude, }); void createAuditEntry({ db: ctx.db, entityType: "RateCard", entityId: input.id, entityName: updated.name, action: "UPDATE", ...withAuditUser(ctx.dbUser?.id), before: before as unknown as Record, after: updated as unknown as Record, source: "ui", }); return updated; } export async function deactivateRateCard( ctx: RateCardProcedureContext, input: RateCardDeactivateInput, ) { const deactivated = await ctx.db.rateCard.update({ where: { id: input.id }, data: { isActive: false }, }); void createAuditEntry({ db: ctx.db, entityType: "RateCard", entityId: input.id, entityName: deactivated.name, action: "DELETE", ...withAuditUser(ctx.dbUser?.id), source: "ui", summary: "Deactivated rate card", }); return deactivated; } export async function addRateCardLine( ctx: RateCardProcedureContext, input: RateCardAddLineInput, ) { const rateCard = await findUniqueOrThrow( ctx.db.rateCard.findUnique({ where: { id: input.rateCardId } }), "Rate card", ); const line = await ctx.db.rateCardLine.create({ data: buildRateCardLineCreateData(input.rateCardId, input.line), select: rateCardLineSelect, }); void createAuditEntry({ db: ctx.db, entityType: "RateCardLine", entityId: line.id, entityName: `${rateCard.name} - ${input.line.chapter ?? "line"}`, action: "CREATE", ...withAuditUser(ctx.dbUser?.id), after: buildRateCardLineCreateAuditAfter({ rateCardId: input.rateCardId, line: input.line, }), source: "ui", }); return line; } export async function updateRateCardLine( ctx: RateCardProcedureContext, input: RateCardUpdateLineInput, ) { const before = await findUniqueOrThrow( ctx.db.rateCardLine.findUnique({ where: { id: input.lineId } }), "Rate card line", ); const updated = await ctx.db.rateCardLine.update({ where: { id: input.lineId }, data: buildRateCardLineUpdateData(input.data), select: rateCardLineSelect, }); void createAuditEntry({ db: ctx.db, entityType: "RateCardLine", entityId: input.lineId, action: "UPDATE", ...withAuditUser(ctx.dbUser?.id), before: before as unknown as Record, after: updated as unknown as Record, source: "ui", }); return updated; } export async function deleteRateCardLine( ctx: RateCardProcedureContext, input: RateCardDeleteLineInput, ) { const line = await findUniqueOrThrow( ctx.db.rateCardLine.findUnique({ where: { id: input.lineId } }), "Rate card line", ); await ctx.db.rateCardLine.delete({ where: { id: input.lineId } }); void createAuditEntry({ db: ctx.db, entityType: "RateCardLine", entityId: input.lineId, action: "DELETE", ...withAuditUser(ctx.dbUser?.id), before: line as unknown as Record, source: "ui", }); return { deleted: true }; } export async function replaceRateCardLines( ctx: RateCardProcedureContext, input: RateCardReplaceLinesInput, ) { const rateCard = await findUniqueOrThrow( ctx.db.rateCard.findUnique({ where: { id: input.rateCardId } }), "Rate card", ); const result = await ctx.db.$transaction(async (tx) => { await tx.rateCardLine.deleteMany({ where: { rateCardId: input.rateCardId } }); return Promise.all( input.lines.map((line) => tx.rateCardLine.create({ data: { rateCardId: input.rateCardId, ...buildRateCardNestedLineCreateData(line), }, select: rateCardLineSelect, }), ), ); }); void createAuditEntry({ db: ctx.db, entityType: "RateCard", entityId: input.rateCardId, entityName: rateCard.name, action: "UPDATE", ...withAuditUser(ctx.dbUser?.id), after: buildRateCardReplaceLinesAuditAfter(result.length), source: "ui", summary: `Replaced all lines with ${result.length} new lines`, }); return result; } export async function resolveRateCardLine( ctx: RateCardProcedureContext, input: RateCardResolveLineInput, ) { const { rateCardId, ...criteria } = input; const lines = await ctx.db.rateCardLine.findMany({ where: { rateCardId }, select: rateCardLineSelect, }); return resolveBestRateLineMatch(lines, criteria); }