From 609804a33472718cd710c5c8c4721b9f61abda68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hartmut=20N=C3=B6renberg?= Date: Tue, 31 Mar 2026 14:24:46 +0200 Subject: [PATCH] refactor(api): extract holiday calendar support --- .../holiday-calendar-support.test.ts | 102 ++++++++++++++++++ .../router/holiday-calendar-catalog-read.ts | 35 ++---- .../src/router/holiday-calendar-support.ts | 89 +++++++++++++++ packages/api/src/router/holiday-calendar.ts | 65 +++++------ 4 files changed, 227 insertions(+), 64 deletions(-) create mode 100644 packages/api/src/__tests__/holiday-calendar-support.test.ts create mode 100644 packages/api/src/router/holiday-calendar-support.ts diff --git a/packages/api/src/__tests__/holiday-calendar-support.test.ts b/packages/api/src/__tests__/holiday-calendar-support.test.ts new file mode 100644 index 0000000..f5a4101 --- /dev/null +++ b/packages/api/src/__tests__/holiday-calendar-support.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from "vitest"; +import { + buildHolidayCalendarCreateData, + buildHolidayCalendarEntryCreateData, + buildHolidayCalendarEntryUpdateData, + buildHolidayCalendarUpdateData, + holidayCalendarDetailInclude, + holidayCalendarEntryOrderBy, + holidayCalendarListInclude, +} from "../router/holiday-calendar-support.js"; + +describe("holiday calendar support", () => { + it("exposes shared include definitions", () => { + expect(holidayCalendarEntryOrderBy).toEqual([ + { date: "asc" }, + { name: "asc" }, + ]); + + expect(holidayCalendarDetailInclude).toEqual({ + country: { select: { id: true, code: true, name: true } }, + metroCity: { select: { id: true, name: true } }, + entries: { orderBy: holidayCalendarEntryOrderBy }, + }); + + expect(holidayCalendarListInclude).toEqual({ + country: { select: { id: true, code: true, name: true } }, + metroCity: { select: { id: true, name: true } }, + entries: { orderBy: holidayCalendarEntryOrderBy }, + _count: { select: { entries: true } }, + }); + }); + + it("builds calendar create and sparse update payloads", () => { + expect(buildHolidayCalendarCreateData({ + name: "Bayern Feiertage", + scopeType: "STATE", + countryId: "country_de", + stateCode: "by", + isActive: undefined, + priority: undefined, + normalizedScope: { + stateCode: "BY", + metroCityId: null, + }, + })).toEqual({ + name: "Bayern Feiertage", + scopeType: "STATE", + countryId: "country_de", + stateCode: "BY", + isActive: true, + priority: 0, + }); + + expect(buildHolidayCalendarUpdateData({ + data: { + name: "Augsburg lokal", + metroCityId: null, + priority: 7, + }, + resolvedScope: { + stateCode: null, + metroCityId: null, + }, + })).toEqual({ + name: "Augsburg lokal", + metroCityId: null, + priority: 7, + }); + }); + + it("builds holiday entry create and sparse update payloads", () => { + const normalizedDate = new Date("2026-01-06T00:00:00.000Z"); + + expect(buildHolidayCalendarEntryCreateData({ + data: { + holidayCalendarId: "cal_by", + date: new Date("2026-01-06T13:15:00.000Z"), + name: "Heilige Drei Koenige", + isRecurringAnnual: undefined, + source: "state", + }, + date: normalizedDate, + })).toEqual({ + holidayCalendarId: "cal_by", + date: normalizedDate, + name: "Heilige Drei Koenige", + isRecurringAnnual: false, + source: "state", + }); + + expect(buildHolidayCalendarEntryUpdateData({ + data: { + date: normalizedDate, + source: null, + }, + date: normalizedDate, + })).toEqual({ + date: normalizedDate, + source: null, + }); + }); +}); diff --git a/packages/api/src/router/holiday-calendar-catalog-read.ts b/packages/api/src/router/holiday-calendar-catalog-read.ts index b9a6211..68e7ebc 100644 --- a/packages/api/src/router/holiday-calendar-catalog-read.ts +++ b/packages/api/src/router/holiday-calendar-catalog-read.ts @@ -3,6 +3,10 @@ import { z } from "zod"; import { findUniqueOrThrow } from "../db/helpers.js"; import { adminProcedure } from "../trpc.js"; import { asHolidayCalendarDb, type HolidayReadContext } from "./holiday-calendar-shared.js"; +import { + holidayCalendarDetailInclude, + holidayCalendarListInclude, +} from "./holiday-calendar-support.js"; const HolidayCalendarCatalogInputSchema = z.object({ includeInactive: z.boolean().optional(), @@ -100,12 +104,7 @@ async function readCalendarsSnapshot( return db.holidayCalendar.findMany({ where, - include: { - country: { select: { id: true, code: true, name: true } }, - metroCity: { select: { id: true, name: true } }, - _count: { select: { entries: true } }, - entries: { orderBy: [{ date: "asc" }, { name: "asc" }] }, - }, + include: holidayCalendarListInclude, orderBy: [ { country: { name: "asc" } }, { scopeType: "asc" }, @@ -121,32 +120,20 @@ async function readCalendarByIdentifierSnapshot(ctx: HolidayReadContext, identif let calendar = await db.holidayCalendar.findUnique({ where: { id: trimmedIdentifier }, - include: { - country: { select: { id: true, code: true, name: true } }, - metroCity: { select: { id: true, name: true } }, - entries: { orderBy: [{ date: "asc" }, { name: "asc" }] }, - }, + include: holidayCalendarDetailInclude, }); if (!calendar) { calendar = await db.holidayCalendar.findFirst({ where: { name: { equals: trimmedIdentifier, mode: "insensitive" } }, - include: { - country: { select: { id: true, code: true, name: true } }, - metroCity: { select: { id: true, name: true } }, - entries: { orderBy: [{ date: "asc" }, { name: "asc" }] }, - }, + include: holidayCalendarDetailInclude, }); } if (!calendar) { calendar = await db.holidayCalendar.findFirst({ where: { name: { contains: trimmedIdentifier, mode: "insensitive" } }, - include: { - country: { select: { id: true, code: true, name: true } }, - metroCity: { select: { id: true, name: true } }, - entries: { orderBy: [{ date: "asc" }, { name: "asc" }] }, - }, + include: holidayCalendarDetailInclude, }); } @@ -191,11 +178,7 @@ export const holidayCalendarCatalogReadProcedures = { return findUniqueOrThrow( db.holidayCalendar.findUnique({ where: { id: input.id }, - include: { - country: { select: { id: true, code: true, name: true } }, - metroCity: { select: { id: true, name: true } }, - entries: { orderBy: [{ date: "asc" }, { name: "asc" }] }, - }, + include: holidayCalendarDetailInclude, }), "Holiday calendar", ); diff --git a/packages/api/src/router/holiday-calendar-support.ts b/packages/api/src/router/holiday-calendar-support.ts new file mode 100644 index 0000000..4fa22f1 --- /dev/null +++ b/packages/api/src/router/holiday-calendar-support.ts @@ -0,0 +1,89 @@ +import type { Prisma } from "@capakraken/db"; +import { + CreateHolidayCalendarEntrySchema, + CreateHolidayCalendarSchema, + UpdateHolidayCalendarEntrySchema, + UpdateHolidayCalendarSchema, +} from "@capakraken/shared"; +import { z } from "zod"; + +type CreateHolidayCalendarInput = z.infer; +type UpdateHolidayCalendarInput = z.infer; +type CreateHolidayCalendarEntryInput = z.infer; +type UpdateHolidayCalendarEntryInput = z.infer; + +export const holidayCalendarEntryOrderBy = [ + { date: "asc" }, + { name: "asc" }, +] as const; + +export const holidayCalendarDetailInclude = { + country: { select: { id: true, code: true, name: true } }, + metroCity: { select: { id: true, name: true } }, + entries: { orderBy: holidayCalendarEntryOrderBy }, +} as const; + +export const holidayCalendarListInclude = { + ...holidayCalendarDetailInclude, + _count: { select: { entries: true } }, +} as const; + +export function buildHolidayCalendarCreateData( + input: CreateHolidayCalendarInput & { + normalizedScope: { + stateCode: string | null; + metroCityId: string | null; + }; + }, +): Prisma.HolidayCalendarUncheckedCreateInput { + return { + name: input.name, + scopeType: input.scopeType, + countryId: input.countryId, + ...(input.normalizedScope.stateCode ? { stateCode: input.normalizedScope.stateCode } : {}), + ...(input.normalizedScope.metroCityId ? { metroCityId: input.normalizedScope.metroCityId } : {}), + isActive: input.isActive ?? true, + priority: input.priority ?? 0, + }; +} + +export function buildHolidayCalendarUpdateData(input: { + data: UpdateHolidayCalendarInput; + resolvedScope: { + stateCode: string | null; + metroCityId: string | null; + }; +}): Prisma.HolidayCalendarUncheckedUpdateInput { + return { + ...(input.data.name !== undefined ? { name: input.data.name } : {}), + ...(input.data.stateCode !== undefined ? { stateCode: input.resolvedScope.stateCode } : {}), + ...(input.data.metroCityId !== undefined ? { metroCityId: input.resolvedScope.metroCityId } : {}), + ...(input.data.isActive !== undefined ? { isActive: input.data.isActive } : {}), + ...(input.data.priority !== undefined ? { priority: input.data.priority } : {}), + }; +} + +export function buildHolidayCalendarEntryCreateData(input: { + data: CreateHolidayCalendarEntryInput; + date: Date; +}): Prisma.HolidayCalendarEntryUncheckedCreateInput { + return { + holidayCalendarId: input.data.holidayCalendarId, + date: input.date, + name: input.data.name, + isRecurringAnnual: input.data.isRecurringAnnual ?? false, + ...(input.data.source ? { source: input.data.source } : {}), + }; +} + +export function buildHolidayCalendarEntryUpdateData(input: { + data: UpdateHolidayCalendarEntryInput; + date: Date; +}): Prisma.HolidayCalendarEntryUncheckedUpdateInput { + return { + ...(input.data.date !== undefined ? { date: input.date } : {}), + ...(input.data.name !== undefined ? { name: input.data.name } : {}), + ...(input.data.isRecurringAnnual !== undefined ? { isRecurringAnnual: input.data.isRecurringAnnual } : {}), + ...(input.data.source !== undefined ? { source: input.data.source ?? null } : {}), + }; +} diff --git a/packages/api/src/router/holiday-calendar.ts b/packages/api/src/router/holiday-calendar.ts index 556fecb..17eb878 100644 --- a/packages/api/src/router/holiday-calendar.ts +++ b/packages/api/src/router/holiday-calendar.ts @@ -11,6 +11,13 @@ import { createTRPCRouter, adminProcedure } from "../trpc.js"; import { holidayCalendarCatalogReadProcedures } from "./holiday-calendar-catalog-read.js"; import { holidayCalendarResolutionReadProcedures } from "./holiday-calendar-resolution-read.js"; import { asHolidayCalendarDb } from "./holiday-calendar-shared.js"; +import { + buildHolidayCalendarCreateData, + buildHolidayCalendarEntryCreateData, + buildHolidayCalendarEntryUpdateData, + buildHolidayCalendarUpdateData, + holidayCalendarDetailInclude, +} from "./holiday-calendar-support.js"; import { assertHolidayCalendarEntryDateAvailable, assertHolidayCalendarScopeConsistency, @@ -51,20 +58,11 @@ export const holidayCalendarRouter = createTRPCRouter({ }); const created = await db.holidayCalendar.create({ - data: { - name: input.name, - scopeType: input.scopeType, - countryId: input.countryId, - ...(normalizedScope.stateCode ? { stateCode: normalizedScope.stateCode } : {}), - ...(normalizedScope.metroCityId ? { metroCityId: normalizedScope.metroCityId } : {}), - isActive: input.isActive ?? true, - priority: input.priority ?? 0, - }, - include: { - country: { select: { id: true, code: true, name: true } }, - metroCity: { select: { id: true, name: true } }, - entries: true, - }, + data: buildHolidayCalendarCreateData({ + ...input, + normalizedScope, + }), + include: holidayCalendarDetailInclude, }); void createAuditEntry({ @@ -104,18 +102,14 @@ export const holidayCalendarRouter = createTRPCRouter({ const updated = await db.holidayCalendar.update({ where: { id: input.id }, - data: { - ...(input.data.name !== undefined ? { name: input.data.name } : {}), - ...(input.data.stateCode !== undefined ? { stateCode } : {}), - ...(input.data.metroCityId !== undefined ? { metroCityId } : {}), - ...(input.data.isActive !== undefined ? { isActive: input.data.isActive } : {}), - ...(input.data.priority !== undefined ? { priority: input.data.priority } : {}), - }, - include: { - country: { select: { id: true, code: true, name: true } }, - metroCity: { select: { id: true, name: true } }, - entries: { orderBy: [{ date: "asc" }, { name: "asc" }] }, - }, + data: buildHolidayCalendarUpdateData({ + data: input.data, + resolvedScope: { + stateCode, + metroCityId, + }, + }), + include: holidayCalendarDetailInclude, }); void createAuditEntry({ @@ -180,13 +174,10 @@ export const holidayCalendarRouter = createTRPCRouter({ }); const created = await db.holidayCalendarEntry.create({ - data: { - holidayCalendarId: input.holidayCalendarId, + data: buildHolidayCalendarEntryCreateData({ + data: input, date: clampHolidayCalendarDate(input.date), - name: input.name, - isRecurringAnnual: input.isRecurringAnnual ?? false, - ...(input.source ? { source: input.source } : {}), - }, + }), }); void createAuditEntry({ @@ -222,12 +213,10 @@ export const holidayCalendarRouter = createTRPCRouter({ const updated = await db.holidayCalendarEntry.update({ where: { id: input.id }, - data: { - ...(input.data.date !== undefined ? { date: nextDate } : {}), - ...(input.data.name !== undefined ? { name: input.data.name } : {}), - ...(input.data.isRecurringAnnual !== undefined ? { isRecurringAnnual: input.data.isRecurringAnnual } : {}), - ...(input.data.source !== undefined ? { source: input.data.source ?? null } : {}), - }, + data: buildHolidayCalendarEntryUpdateData({ + data: input.data, + date: nextDate, + }), }); void createAuditEntry({