refactor(api): extract holiday calendar write support

This commit is contained in:
2026-03-31 13:25:27 +02:00
parent 8e542fd6ba
commit 0a10a440ee
3 changed files with 308 additions and 135 deletions
@@ -0,0 +1,119 @@
import { describe, expect, it, vi } from "vitest";
import {
assertHolidayCalendarEntryDateAvailable,
assertHolidayCalendarScopeConsistency,
clampHolidayCalendarDate,
normalizeHolidayCalendarScopeInput,
normalizeHolidayCalendarStateCode,
resolveHolidayCalendarUpdateScope,
} from "../router/holiday-calendar-write-support.js";
describe("holiday calendar write support", () => {
it("normalizes state codes and optional scope fields", () => {
expect(normalizeHolidayCalendarStateCode(" by ")).toBe("BY");
expect(normalizeHolidayCalendarStateCode(undefined)).toBeNull();
expect(normalizeHolidayCalendarScopeInput({
stateCode: " ny ",
metroCityId: undefined,
})).toEqual({
stateCode: "NY",
metroCityId: null,
});
});
it("reuses existing scope values for partial updates", () => {
expect(resolveHolidayCalendarUpdateScope({
existing: {
stateCode: "BY",
metroCityId: "city_1",
},
data: {
stateCode: undefined,
metroCityId: null,
},
})).toEqual({
stateCode: "BY",
metroCityId: null,
});
});
it("clamps dates to UTC midnight", () => {
expect(clampHolidayCalendarDate(new Date("2026-12-24T18:45:00.000Z")).toISOString())
.toBe("2026-12-24T00:00:00.000Z");
});
it("rejects invalid state and city scope combinations before hitting uniqueness checks", async () => {
const db = {
metroCity: { findUnique: vi.fn() },
holidayCalendar: { findFirst: vi.fn() },
} as never;
await expect(assertHolidayCalendarScopeConsistency(db, {
scopeType: "STATE",
countryId: "country_de",
stateCode: null,
metroCityId: null,
})).rejects.toMatchObject({
code: "BAD_REQUEST",
message: "State calendars require a state code",
});
await expect(assertHolidayCalendarScopeConsistency(db, {
scopeType: "CITY",
countryId: "country_de",
stateCode: null,
metroCityId: null,
})).rejects.toMatchObject({
code: "BAD_REQUEST",
message: "City calendars require a metro city",
});
expect(db.holidayCalendar.findFirst).not.toHaveBeenCalled();
});
it("rejects city scopes whose metro city belongs to a different country", async () => {
const db = {
metroCity: {
findUnique: vi.fn().mockResolvedValue({
id: "city_augsburg",
countryId: "country_us",
}),
},
holidayCalendar: { findFirst: vi.fn() },
} as never;
await expect(assertHolidayCalendarScopeConsistency(db, {
scopeType: "CITY",
countryId: "country_de",
stateCode: null,
metroCityId: "city_augsburg",
})).rejects.toMatchObject({
code: "BAD_REQUEST",
message: "Metro city must belong to the selected country",
});
});
it("rejects duplicate holiday entry dates using the clamped date", async () => {
const db = {
holidayCalendarEntry: {
findFirst: vi.fn().mockResolvedValue({ id: "entry_existing" }),
},
} as never;
await expect(assertHolidayCalendarEntryDateAvailable(db, {
holidayCalendarId: "cal_1",
date: new Date("2026-12-24T18:45:00.000Z"),
})).rejects.toMatchObject({
code: "CONFLICT",
message: "A holiday entry for this calendar and date already exists",
});
expect(db.holidayCalendarEntry.findFirst).toHaveBeenCalledWith({
where: {
holidayCalendarId: "cal_1",
date: new Date("2026-12-24T00:00:00.000Z"),
},
select: { id: true },
});
});
});
@@ -0,0 +1,158 @@
import { TRPCError } from "@trpc/server";
import { findUniqueOrThrow } from "../db/helpers.js";
import {
type HolidayCalendarDb,
} from "./holiday-calendar-shared.js";
import type { HolidayCalendarScopeInput } from "@capakraken/shared";
type HolidayCalendarScope = HolidayCalendarScopeInput;
const HOLIDAY_SCOPE = {
COUNTRY: "COUNTRY",
STATE: "STATE",
CITY: "CITY",
} as const satisfies Record<HolidayCalendarScope, HolidayCalendarScope>;
export function clampHolidayCalendarDate(date: Date): Date {
const value = new Date(date);
value.setUTCHours(0, 0, 0, 0);
return value;
}
export function normalizeHolidayCalendarStateCode(
stateCode: string | null | undefined,
): string | null {
return stateCode?.trim().toUpperCase() ?? null;
}
export function normalizeHolidayCalendarScopeInput(input: {
stateCode?: string | null | undefined;
metroCityId?: string | null | undefined;
}) {
return {
stateCode: normalizeHolidayCalendarStateCode(input.stateCode),
metroCityId: input.metroCityId ?? null,
};
}
export function resolveHolidayCalendarUpdateScope(input: {
existing: {
stateCode: string | null;
metroCityId: string | null;
};
data: {
stateCode?: string | null | undefined;
metroCityId?: string | null | undefined;
};
}) {
return {
stateCode: input.data.stateCode === undefined
? input.existing.stateCode
: normalizeHolidayCalendarStateCode(input.data.stateCode),
metroCityId: input.data.metroCityId === undefined
? input.existing.metroCityId
: input.data.metroCityId ?? null,
};
}
export async function assertHolidayCalendarEntryDateAvailable(
db: HolidayCalendarDb,
input: {
holidayCalendarId: string;
date: Date;
},
ignoreId?: string,
) {
const existing = await db.holidayCalendarEntry.findFirst({
where: {
holidayCalendarId: input.holidayCalendarId,
date: clampHolidayCalendarDate(input.date),
...(ignoreId ? { id: { not: ignoreId } } : {}),
},
select: { id: true },
});
if (existing) {
throw new TRPCError({
code: "CONFLICT",
message: "A holiday entry for this calendar and date already exists",
});
}
}
export async function assertHolidayCalendarScopeConsistency(
db: HolidayCalendarDb,
input: {
scopeType: HolidayCalendarScope;
countryId: string;
stateCode?: string | null;
metroCityId?: string | null;
},
ignoreId?: string,
) {
if (input.scopeType === HOLIDAY_SCOPE.COUNTRY) {
if (input.stateCode || input.metroCityId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Country calendars may not define a state or metro city",
});
}
}
if (input.scopeType === HOLIDAY_SCOPE.STATE) {
if (!input.stateCode) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "State calendars require a state code",
});
}
if (input.metroCityId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "State calendars may not define a metro city",
});
}
}
if (input.scopeType === HOLIDAY_SCOPE.CITY) {
if (!input.metroCityId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "City calendars require a metro city",
});
}
const metroCity = await findUniqueOrThrow(
db.metroCity.findUnique({
where: { id: input.metroCityId },
select: { id: true, countryId: true },
}),
"Metro city",
);
if (metroCity.countryId !== input.countryId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Metro city must belong to the selected country",
});
}
}
const existing = await db.holidayCalendar.findFirst({
where: {
countryId: input.countryId,
scopeType: input.scopeType,
...(input.scopeType === HOLIDAY_SCOPE.STATE ? { stateCode: input.stateCode ?? null } : {}),
...(input.scopeType === HOLIDAY_SCOPE.CITY ? { metroCityId: input.metroCityId ?? null } : {}),
...(ignoreId ? { id: { not: ignoreId } } : {}),
},
select: { id: true },
});
if (existing) {
throw new TRPCError({
code: "CONFLICT",
message: "A holiday calendar for this exact scope already exists",
});
}
}
+31 -135
View File
@@ -1,134 +1,23 @@
import { import {
CreateHolidayCalendarEntrySchema, CreateHolidayCalendarEntrySchema,
CreateHolidayCalendarSchema, CreateHolidayCalendarSchema,
type HolidayCalendarScopeInput,
UpdateHolidayCalendarEntrySchema, UpdateHolidayCalendarEntrySchema,
UpdateHolidayCalendarSchema, UpdateHolidayCalendarSchema,
} from "@capakraken/shared"; } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { z } from "zod"; import { z } from "zod";
import { findUniqueOrThrow } from "../db/helpers.js"; import { findUniqueOrThrow } from "../db/helpers.js";
import { createAuditEntry } from "../lib/audit.js"; import { createAuditEntry } from "../lib/audit.js";
import { createTRPCRouter, adminProcedure } from "../trpc.js"; import { createTRPCRouter, adminProcedure } from "../trpc.js";
import { holidayCalendarCatalogReadProcedures } from "./holiday-calendar-catalog-read.js"; import { holidayCalendarCatalogReadProcedures } from "./holiday-calendar-catalog-read.js";
import { holidayCalendarResolutionReadProcedures } from "./holiday-calendar-resolution-read.js"; import { holidayCalendarResolutionReadProcedures } from "./holiday-calendar-resolution-read.js";
import { asHolidayCalendarDb, type HolidayCalendarDb } from "./holiday-calendar-shared.js"; import { asHolidayCalendarDb } from "./holiday-calendar-shared.js";
import {
type HolidayCalendarScope = HolidayCalendarScopeInput; assertHolidayCalendarEntryDateAvailable,
assertHolidayCalendarScopeConsistency,
const HOLIDAY_SCOPE = { clampHolidayCalendarDate,
COUNTRY: "COUNTRY", normalizeHolidayCalendarScopeInput,
STATE: "STATE", resolveHolidayCalendarUpdateScope,
CITY: "CITY", } from "./holiday-calendar-write-support.js";
} as const satisfies Record<HolidayCalendarScope, HolidayCalendarScope>;
function clampDate(date: Date): Date {
const value = new Date(date);
value.setUTCHours(0, 0, 0, 0);
return value;
}
async function assertEntryDateAvailable(
db: HolidayCalendarDb,
input: {
holidayCalendarId: string;
date: Date;
},
ignoreId?: string,
) {
const existing = await db.holidayCalendarEntry.findFirst({
where: {
holidayCalendarId: input.holidayCalendarId,
date: clampDate(input.date),
...(ignoreId ? { id: { not: ignoreId } } : {}),
},
select: { id: true },
});
if (existing) {
throw new TRPCError({
code: "CONFLICT",
message: "A holiday entry for this calendar and date already exists",
});
}
}
async function assertScopeConsistency(
db: HolidayCalendarDb,
input: {
scopeType: HolidayCalendarScope;
countryId: string;
stateCode?: string | null;
metroCityId?: string | null;
},
ignoreId?: string,
) {
if (input.scopeType === HOLIDAY_SCOPE.COUNTRY) {
if (input.stateCode || input.metroCityId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Country calendars may not define a state or metro city",
});
}
}
if (input.scopeType === HOLIDAY_SCOPE.STATE) {
if (!input.stateCode) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "State calendars require a state code",
});
}
if (input.metroCityId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "State calendars may not define a metro city",
});
}
}
if (input.scopeType === HOLIDAY_SCOPE.CITY) {
if (!input.metroCityId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "City calendars require a metro city",
});
}
const metroCity = await findUniqueOrThrow(
db.metroCity.findUnique({
where: { id: input.metroCityId },
select: { id: true, countryId: true },
}),
"Metro city",
);
if (metroCity.countryId !== input.countryId) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Metro city must belong to the selected country",
});
}
}
const existing = await db.holidayCalendar.findFirst({
where: {
countryId: input.countryId,
scopeType: input.scopeType,
...(input.scopeType === HOLIDAY_SCOPE.STATE ? { stateCode: input.stateCode ?? null } : {}),
...(input.scopeType === HOLIDAY_SCOPE.CITY ? { metroCityId: input.metroCityId ?? null } : {}),
...(ignoreId ? { id: { not: ignoreId } } : {}),
},
select: { id: true },
});
if (existing) {
throw new TRPCError({
code: "CONFLICT",
message: "A holiday calendar for this exact scope already exists",
});
}
}
export const holidayCalendarRouter = createTRPCRouter({ export const holidayCalendarRouter = createTRPCRouter({
...holidayCalendarCatalogReadProcedures, ...holidayCalendarCatalogReadProcedures,
@@ -147,11 +36,18 @@ export const holidayCalendarRouter = createTRPCRouter({
"Country", "Country",
); );
await assertScopeConsistency(db, { await assertHolidayCalendarScopeConsistency(db, {
scopeType: input.scopeType, scopeType: input.scopeType,
countryId: input.countryId, countryId: input.countryId,
stateCode: input.stateCode?.trim().toUpperCase() ?? null, ...normalizeHolidayCalendarScopeInput({
metroCityId: input.metroCityId ?? null, stateCode: input.stateCode,
metroCityId: input.metroCityId,
}),
});
const normalizedScope = normalizeHolidayCalendarScopeInput({
stateCode: input.stateCode,
metroCityId: input.metroCityId,
}); });
const created = await db.holidayCalendar.create({ const created = await db.holidayCalendar.create({
@@ -159,8 +55,8 @@ export const holidayCalendarRouter = createTRPCRouter({
name: input.name, name: input.name,
scopeType: input.scopeType, scopeType: input.scopeType,
countryId: input.countryId, countryId: input.countryId,
...(input.stateCode ? { stateCode: input.stateCode.trim().toUpperCase() } : {}), ...(normalizedScope.stateCode ? { stateCode: normalizedScope.stateCode } : {}),
...(input.metroCityId ? { metroCityId: input.metroCityId } : {}), ...(normalizedScope.metroCityId ? { metroCityId: normalizedScope.metroCityId } : {}),
isActive: input.isActive ?? true, isActive: input.isActive ?? true,
priority: input.priority ?? 0, priority: input.priority ?? 0,
}, },
@@ -194,14 +90,12 @@ export const holidayCalendarRouter = createTRPCRouter({
"Holiday calendar", "Holiday calendar",
); );
const stateCode = input.data.stateCode === undefined const { stateCode, metroCityId } = resolveHolidayCalendarUpdateScope({
? existing.stateCode existing,
: input.data.stateCode?.trim().toUpperCase() ?? null; data: input.data,
const metroCityId = input.data.metroCityId === undefined });
? existing.metroCityId
: input.data.metroCityId ?? null;
await assertScopeConsistency(db, { await assertHolidayCalendarScopeConsistency(db, {
scopeType: existing.scopeType, scopeType: existing.scopeType,
countryId: existing.countryId, countryId: existing.countryId,
stateCode, stateCode,
@@ -280,7 +174,7 @@ export const holidayCalendarRouter = createTRPCRouter({
"Holiday calendar", "Holiday calendar",
); );
await assertEntryDateAvailable(db, { await assertHolidayCalendarEntryDateAvailable(db, {
holidayCalendarId: input.holidayCalendarId, holidayCalendarId: input.holidayCalendarId,
date: input.date, date: input.date,
}); });
@@ -288,7 +182,7 @@ export const holidayCalendarRouter = createTRPCRouter({
const created = await db.holidayCalendarEntry.create({ const created = await db.holidayCalendarEntry.create({
data: { data: {
holidayCalendarId: input.holidayCalendarId, holidayCalendarId: input.holidayCalendarId,
date: clampDate(input.date), date: clampHolidayCalendarDate(input.date),
name: input.name, name: input.name,
isRecurringAnnual: input.isRecurringAnnual ?? false, isRecurringAnnual: input.isRecurringAnnual ?? false,
...(input.source ? { source: input.source } : {}), ...(input.source ? { source: input.source } : {}),
@@ -317,9 +211,11 @@ export const holidayCalendarRouter = createTRPCRouter({
db.holidayCalendarEntry.findUnique({ where: { id: input.id } }), db.holidayCalendarEntry.findUnique({ where: { id: input.id } }),
"Holiday calendar entry", "Holiday calendar entry",
); );
const nextDate = input.data.date !== undefined ? clampDate(input.data.date) : existing.date; const nextDate = input.data.date !== undefined
? clampHolidayCalendarDate(input.data.date)
: existing.date;
await assertEntryDateAvailable(db, { await assertHolidayCalendarEntryDateAvailable(db, {
holidayCalendarId: existing.holidayCalendarId, holidayCalendarId: existing.holidayCalendarId,
date: nextDate, date: nextDate,
}, existing.id); }, existing.id);