diff --git a/packages/api/src/__tests__/calculation-rule-support.test.ts b/packages/api/src/__tests__/calculation-rule-support.test.ts new file mode 100644 index 0000000..1a4893b --- /dev/null +++ b/packages/api/src/__tests__/calculation-rule-support.test.ts @@ -0,0 +1,77 @@ +import { describe, expect, it } from "vitest"; +import { + buildCalculationRuleCreateData, + buildCalculationRuleUpdateData, +} from "../router/calculation-rule-support.js"; + +describe("calculation-rule-support", () => { + it("buildCalculationRuleCreateData omits undefined optional fields", () => { + const result = buildCalculationRuleCreateData({ + name: "Sick Days", + triggerType: "SICK", + costEffect: "ZERO", + chargeabilityEffect: "SKIP", + priority: 0, + isActive: true, + }); + + expect(result).toEqual({ + name: "Sick Days", + triggerType: "SICK", + costEffect: "ZERO", + chargeabilityEffect: "SKIP", + priority: 0, + isActive: true, + }); + expect(result).not.toHaveProperty("description"); + expect(result).not.toHaveProperty("projectId"); + expect(result).not.toHaveProperty("orderType"); + expect(result).not.toHaveProperty("costReductionPercent"); + }); + + it("buildCalculationRuleCreateData keeps provided optional fields", () => { + const result = buildCalculationRuleCreateData({ + name: "Vacation Discount", + description: "Reduce cost for vacation", + triggerType: "VACATION", + projectId: "project_1", + orderType: "CHARGEABLE", + costEffect: "REDUCE", + costReductionPercent: 25, + chargeabilityEffect: "COUNT", + priority: 10, + isActive: false, + }); + + expect(result).toEqual({ + name: "Vacation Discount", + description: "Reduce cost for vacation", + triggerType: "VACATION", + projectId: "project_1", + orderType: "CHARGEABLE", + costEffect: "REDUCE", + costReductionPercent: 25, + chargeabilityEffect: "COUNT", + priority: 10, + isActive: false, + }); + }); + + it("buildCalculationRuleUpdateData only includes provided fields and preserves falsy values", () => { + const result = buildCalculationRuleUpdateData({ + description: "", + costReductionPercent: 0, + priority: 0, + isActive: false, + }); + + expect(result).toEqual({ + description: "", + costReductionPercent: 0, + priority: 0, + isActive: false, + }); + expect(result).not.toHaveProperty("name"); + expect(result).not.toHaveProperty("projectId"); + }); +}); diff --git a/packages/api/src/router/calculation-rule-support.ts b/packages/api/src/router/calculation-rule-support.ts new file mode 100644 index 0000000..5791f60 --- /dev/null +++ b/packages/api/src/router/calculation-rule-support.ts @@ -0,0 +1,43 @@ +import type { Prisma } from "@capakraken/db"; +import { + CreateCalculationRuleSchema, + UpdateCalculationRuleSchema, +} from "@capakraken/shared"; +import { z } from "zod"; + +type CreateCalculationRuleInput = z.infer; +type UpdateCalculationRuleInput = Omit, "id">; + +export function buildCalculationRuleCreateData( + input: CreateCalculationRuleInput, +): Prisma.CalculationRuleUncheckedCreateInput { + return { + name: input.name, + triggerType: input.triggerType, + costEffect: input.costEffect, + chargeabilityEffect: input.chargeabilityEffect, + ...(input.description !== undefined ? { description: input.description } : {}), + ...(input.projectId !== undefined ? { projectId: input.projectId } : {}), + ...(input.orderType !== undefined ? { orderType: input.orderType as never } : {}), + ...(input.costReductionPercent !== undefined ? { costReductionPercent: input.costReductionPercent } : {}), + priority: input.priority, + isActive: input.isActive, + }; +} + +export function buildCalculationRuleUpdateData( + input: UpdateCalculationRuleInput, +): Prisma.CalculationRuleUncheckedUpdateInput { + return { + ...(input.name !== undefined ? { name: input.name } : {}), + ...(input.description !== undefined ? { description: input.description } : {}), + ...(input.triggerType !== undefined ? { triggerType: input.triggerType } : {}), + ...(input.projectId !== undefined ? { projectId: input.projectId } : {}), + ...(input.orderType !== undefined ? { orderType: input.orderType as never } : {}), + ...(input.costEffect !== undefined ? { costEffect: input.costEffect } : {}), + ...(input.costReductionPercent !== undefined ? { costReductionPercent: input.costReductionPercent } : {}), + ...(input.chargeabilityEffect !== undefined ? { chargeabilityEffect: input.chargeabilityEffect } : {}), + ...(input.priority !== undefined ? { priority: input.priority } : {}), + ...(input.isActive !== undefined ? { isActive: input.isActive } : {}), + }; +} diff --git a/packages/api/src/router/calculation-rules.ts b/packages/api/src/router/calculation-rules.ts index 08baf31..ac62a16 100644 --- a/packages/api/src/router/calculation-rules.ts +++ b/packages/api/src/router/calculation-rules.ts @@ -7,6 +7,10 @@ import { findUniqueOrThrow } from "../db/helpers.js"; import { PROJECT_BRIEF_SELECT } from "../db/selects.js"; import { createTRPCRouter, controllerProcedure, managerProcedure } from "../trpc.js"; import { createAuditEntry } from "../lib/audit.js"; +import { + buildCalculationRuleCreateData, + buildCalculationRuleUpdateData, +} from "./calculation-rule-support.js"; export const calculationRuleRouter = createTRPCRouter({ list: controllerProcedure.query(async ({ ctx }) => { @@ -40,18 +44,7 @@ export const calculationRuleRouter = createTRPCRouter({ .input(CreateCalculationRuleSchema) .mutation(async ({ ctx, input }) => { const rule = await ctx.db.calculationRule.create({ - data: { - name: input.name, - triggerType: input.triggerType, - costEffect: input.costEffect, - chargeabilityEffect: input.chargeabilityEffect, - ...(input.description !== undefined ? { description: input.description } : {}), - ...(input.projectId !== undefined ? { projectId: input.projectId } : {}), - ...(input.orderType !== undefined ? { orderType: input.orderType as never } : {}), - ...(input.costReductionPercent !== undefined ? { costReductionPercent: input.costReductionPercent } : {}), - priority: input.priority, - isActive: input.isActive, - }, + data: buildCalculationRuleCreateData(input), }); void createAuditEntry({ @@ -77,22 +70,9 @@ export const calculationRuleRouter = createTRPCRouter({ "CalculationRule", ); - // Build update data using exactOptionalPropertyTypes pattern - const updateData: Record = {}; - if (data.name !== undefined) updateData.name = data.name; - if (data.description !== undefined) updateData.description = data.description; - if (data.triggerType !== undefined) updateData.triggerType = data.triggerType; - if (data.projectId !== undefined) updateData.projectId = data.projectId; - if (data.orderType !== undefined) updateData.orderType = data.orderType; - if (data.costEffect !== undefined) updateData.costEffect = data.costEffect; - if (data.costReductionPercent !== undefined) updateData.costReductionPercent = data.costReductionPercent; - if (data.chargeabilityEffect !== undefined) updateData.chargeabilityEffect = data.chargeabilityEffect; - if (data.priority !== undefined) updateData.priority = data.priority; - if (data.isActive !== undefined) updateData.isActive = data.isActive; - const updated = await ctx.db.calculationRule.update({ where: { id }, - data: updateData, + data: buildCalculationRuleUpdateData(data), }); void createAuditEntry({