import { CreateRateCardLineSchema, CreateRateCardSchema, UpdateRateCardLineSchema, UpdateRateCardSchema, } from "@capakraken/shared"; import { z } from "zod"; import { findUniqueOrThrow } from "../db/helpers.js"; import { createTRPCRouter, controllerProcedure, managerProcedure } from "../trpc.js"; import { createAuditEntry } from "../lib/audit.js"; import { lookupBestRateMatch, rateCardLineSelect, resolveBestRate, resolveBestRateLineMatch, } from "./rate-card-read.js"; import { buildRateCardCreateAuditAfter, buildRateCardCreateData, buildRateCardLineCreateAuditAfter, buildRateCardLineCreateData, buildRateCardLineUpdateData, buildRateCardListWhere, buildRateCardReplaceLinesAuditAfter, buildRateCardUpdateData, rateCardCreateInclude, rateCardDetailInclude, buildRateCardNestedLineCreateData, rateCardSummaryInclude, } from "./rate-card-write-support.js"; export const rateCardRouter = createTRPCRouter({ list: controllerProcedure .input( z.object({ isActive: z.boolean().optional(), search: z.string().optional(), clientId: z.string().optional(), effectiveAt: z.coerce.date().optional(), }).optional(), ) .query(async ({ ctx, input }) => { return ctx.db.rateCard.findMany({ where: buildRateCardListWhere(input), include: rateCardSummaryInclude, orderBy: [{ isActive: "desc" }, { effectiveFrom: "desc" }, { name: "asc" }], }); }), getById: controllerProcedure .input(z.object({ id: z.string() })) .query(async ({ ctx, input }) => { const rateCard = await findUniqueOrThrow( ctx.db.rateCard.findUnique({ where: { id: input.id }, include: rateCardDetailInclude, }), "Rate card", ); return rateCard; }), lookupBestMatch: controllerProcedure .input(z.object({ clientId: z.string().optional(), chapter: z.string().optional(), managementLevelId: z.string().optional(), roleName: z.string().optional(), seniority: z.string().optional(), })) .query(async ({ ctx, input }) => lookupBestRateMatch(ctx.db, input)), resolveBestRate: controllerProcedure .input(z.object({ resourceId: z.string().optional(), roleName: z.string().optional(), date: z.coerce.date().optional(), })) .query(async ({ ctx, input }) => resolveBestRate(ctx.db, input)), create: managerProcedure .input(CreateRateCardSchema) .mutation(async ({ ctx, input }) => { 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", userId: ctx.dbUser?.id, after: buildRateCardCreateAuditAfter({ name, currency, lines }), source: "ui", }); return rateCard; }), update: managerProcedure .input(z.object({ id: z.string(), data: UpdateRateCardSchema })) .mutation(async ({ ctx, input }) => { 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", userId: ctx.dbUser?.id, before: before as unknown as Record, after: updated as unknown as Record, source: "ui", }); return updated; }), deactivate: managerProcedure .input(z.object({ id: z.string() })) .mutation(async ({ ctx, input }) => { 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", userId: ctx.dbUser?.id, source: "ui", summary: "Deactivated rate card", }); return deactivated; }), // ─── Line CRUD ───────────────────────────────────────────────────────────── addLine: managerProcedure .input(z.object({ rateCardId: z.string(), line: CreateRateCardLineSchema })) .mutation(async ({ ctx, input }) => { 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", userId: ctx.dbUser?.id, after: buildRateCardLineCreateAuditAfter({ rateCardId: input.rateCardId, line: input.line, }), source: "ui", }); return line; }), updateLine: managerProcedure .input(z.object({ lineId: z.string(), data: UpdateRateCardLineSchema })) .mutation(async ({ ctx, input }) => { 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", userId: ctx.dbUser?.id, before: before as unknown as Record, after: updated as unknown as Record, source: "ui", }); return updated; }), deleteLine: managerProcedure .input(z.object({ lineId: z.string() })) .mutation(async ({ ctx, input }) => { 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", userId: ctx.dbUser?.id, before: line as unknown as Record, source: "ui", }); return { deleted: true }; }), // ─── Batch operations ────────────────────────────────────────────────────── replaceLines: managerProcedure .input(z.object({ rateCardId: z.string(), lines: z.array(CreateRateCardLineSchema), })) .mutation(async ({ ctx, input }) => { 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 } }); const created = await Promise.all( input.lines.map((line) => tx.rateCardLine.create({ data: { rateCardId: input.rateCardId, ...buildRateCardNestedLineCreateData(line), }, select: rateCardLineSelect, }), ), ); return created; }); void createAuditEntry({ db: ctx.db, entityType: "RateCard", entityId: input.rateCardId, entityName: rateCard.name, action: "UPDATE", userId: ctx.dbUser?.id, after: buildRateCardReplaceLinesAuditAfter(result.length), source: "ui", summary: `Replaced all lines with ${result.length} new lines`, }); return result; }), // ─── Rate resolution ─────────────────────────────────────────────────────── resolveRateLine: controllerProcedure .input(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(), })) .query(async ({ ctx, input }) => { const { rateCardId, ...criteria } = input; // Find the most specific matching line (most criteria matched wins) const lines = await ctx.db.rateCardLine.findMany({ where: { rateCardId }, select: rateCardLineSelect, }); return resolveBestRateLineMatch(lines, criteria); }), });