import { PreviewResolvedHolidaysSchema } from "@capakraken/shared"; import { TRPCError } from "@trpc/server"; import { z } from "zod"; import { findUniqueOrThrow } from "../db/helpers.js"; import { asHolidayResolverDb, getResolvedCalendarHolidays } from "../lib/holiday-availability.js"; import { protectedProcedure } from "../trpc.js"; import { type HolidayReadContext } from "./holiday-calendar-shared.js"; function canManageHolidayResourceReads(ctx: HolidayReadContext): boolean { const role = ctx.dbUser?.systemRole; return role === "ADMIN" || role === "MANAGER"; } async function findOwnedHolidayResourceId(ctx: HolidayReadContext): Promise { if (!ctx.dbUser?.id) { return null; } if (!ctx.db.resource || typeof ctx.db.resource.findFirst !== "function") { return null; } const resource = await ctx.db.resource.findFirst({ where: { userId: ctx.dbUser.id }, select: { id: true }, }); return resource?.id ?? null; } async function assertCanReadHolidayResource( ctx: HolidayReadContext, resourceId: string, ): Promise { if (canManageHolidayResourceReads(ctx)) { return; } const ownedResourceId = await findOwnedHolidayResourceId(ctx); if (!ownedResourceId || ownedResourceId !== resourceId) { throw new TRPCError({ code: "FORBIDDEN", message: "You can only view holiday data for your own resource", }); } } function formatResolvedHolidayDetail(holiday: { date: string; name: string; scopeType: string; calendarName: string; sourceType: string; }) { return { date: holiday.date, name: holiday.name, scope: holiday.scopeType, calendarName: holiday.calendarName, sourceType: holiday.sourceType, }; } function summarizeResolvedHolidaysDetail(holidays: Array<{ date: string; name: string; scope: string; calendarName: string; sourceType: string; }>) { const byScope = new Map(); const bySourceType = new Map(); const byCalendar = new Map(); for (const holiday of holidays) { byScope.set(holiday.scope, (byScope.get(holiday.scope) ?? 0) + 1); bySourceType.set(holiday.sourceType, (bySourceType.get(holiday.sourceType) ?? 0) + 1); byCalendar.set(holiday.calendarName, (byCalendar.get(holiday.calendarName) ?? 0) + 1); } return { byScope: [...byScope.entries()] .sort(([left], [right]) => left.localeCompare(right)) .map(([scope, count]) => ({ scope, count })), bySourceType: [...bySourceType.entries()] .sort(([left], [right]) => left.localeCompare(right)) .map(([sourceType, count]) => ({ sourceType, count })), byCalendar: [...byCalendar.entries()] .sort(([left], [right]) => left.localeCompare(right)) .map(([calendarName, count]) => ({ calendarName, count })), }; } const ResolveHolidaysInputSchema = z.object({ periodStart: z.coerce.date(), periodEnd: z.coerce.date(), countryId: z.string().optional(), countryCode: z.string().trim().min(1).optional(), stateCode: z.string().trim().min(1).optional(), metroCityId: z.string().optional(), metroCityName: z.string().trim().min(1).optional(), }).superRefine((input, issueCtx) => { if (!input.countryId && !input.countryCode) { issueCtx.addIssue({ code: z.ZodIssueCode.custom, message: "Either countryId or countryCode is required.", path: ["countryId"], }); } if (input.periodEnd < input.periodStart) { issueCtx.addIssue({ code: z.ZodIssueCode.custom, message: "periodEnd must be on or after periodStart.", path: ["periodEnd"], }); } }); const ResolveResourceHolidaysInputSchema = z.object({ resourceId: z.string(), periodStart: z.coerce.date(), periodEnd: z.coerce.date(), }).superRefine((input, issueCtx) => { if (input.periodEnd < input.periodStart) { issueCtx.addIssue({ code: z.ZodIssueCode.custom, message: "periodEnd must be on or after periodStart.", path: ["periodEnd"], }); } }); async function readPreviewResolvedHolidaysSnapshot( ctx: HolidayReadContext, input: z.infer, ) { const country = await findUniqueOrThrow( ctx.db.country.findUnique({ where: { id: input.countryId }, select: { id: true, code: true, name: true }, }), "Country", ); const metroCity = input.metroCityId ? await ctx.db.metroCity.findUnique({ where: { id: input.metroCityId }, select: { id: true, name: true, countryId: true }, }) : null; const resolved = await getResolvedCalendarHolidays(asHolidayResolverDb(ctx.db), { periodStart: new Date(`${input.year}-01-01T00:00:00.000Z`), periodEnd: new Date(`${input.year}-12-31T00:00:00.000Z`), countryId: input.countryId, countryCode: country.code, federalState: input.stateCode?.trim().toUpperCase() ?? null, metroCityId: input.metroCityId ?? null, metroCityName: metroCity?.name ?? null, }); return { locationContext: { countryId: input.countryId, countryCode: country.code, stateCode: input.stateCode?.trim().toUpperCase() ?? null, metroCityId: input.metroCityId ?? null, metroCity: metroCity?.name ?? null, year: input.year, }, holidays: resolved.map((holiday) => ({ date: holiday.date, name: holiday.name, scopeType: holiday.scope, calendarName: holiday.calendarName, sourceType: holiday.sourceType, })), }; } async function readResolvedHolidaysSnapshot( ctx: HolidayReadContext, input: z.infer, ) { let resolvedCountryCode = input.countryCode?.trim().toUpperCase() ?? null; if (!resolvedCountryCode && input.countryId) { const country = await findUniqueOrThrow( ctx.db.country.findUnique({ where: { id: input.countryId }, select: { code: true }, }), "Country", ); resolvedCountryCode = country.code; } const metroCityName = input.metroCityId ? (await ctx.db.metroCity.findUnique({ where: { id: input.metroCityId }, select: { name: true }, }))?.name ?? null : input.metroCityName?.trim() ?? null; const resolved = await getResolvedCalendarHolidays(asHolidayResolverDb(ctx.db), { periodStart: input.periodStart, periodEnd: input.periodEnd, countryId: input.countryId ?? null, countryCode: resolvedCountryCode, federalState: input.stateCode?.trim().toUpperCase() ?? null, metroCityId: input.metroCityId ?? null, metroCityName, }); return { periodStart: input.periodStart.toISOString().slice(0, 10), periodEnd: input.periodEnd.toISOString().slice(0, 10), locationContext: { countryId: input.countryId ?? null, countryCode: resolvedCountryCode, federalState: input.stateCode?.trim().toUpperCase() ?? null, metroCityId: input.metroCityId ?? null, metroCity: metroCityName, }, holidays: resolved.map((holiday) => ({ date: holiday.date, name: holiday.name, scopeType: holiday.scope, calendarName: holiday.calendarName, sourceType: holiday.sourceType, })), }; } async function readResolvedResourceHolidaysSnapshot( ctx: HolidayReadContext, input: z.infer, ) { await assertCanReadHolidayResource(ctx, input.resourceId); const resource = await findUniqueOrThrow( ctx.db.resource.findUnique({ where: { id: input.resourceId }, select: { id: true, eid: true, displayName: true, federalState: true, countryId: true, metroCityId: true, country: { select: { code: true, name: true } }, metroCity: { select: { name: true } }, }, }), "Resource", ); const resolved = await getResolvedCalendarHolidays(asHolidayResolverDb(ctx.db), { periodStart: input.periodStart, periodEnd: input.periodEnd, countryId: resource.countryId ?? null, countryCode: resource.country?.code ?? null, federalState: resource.federalState ?? null, metroCityId: resource.metroCityId ?? null, metroCityName: resource.metroCity?.name ?? null, }); return { periodStart: input.periodStart.toISOString().slice(0, 10), periodEnd: input.periodEnd.toISOString().slice(0, 10), resource: { id: resource.id, eid: resource.eid, name: resource.displayName, country: resource.country?.name ?? resource.country?.code ?? null, countryCode: resource.country?.code ?? null, federalState: resource.federalState ?? null, metroCity: resource.metroCity?.name ?? null, }, holidays: resolved.map((holiday) => ({ date: holiday.date, name: holiday.name, scopeType: holiday.scope, calendarName: holiday.calendarName, sourceType: holiday.sourceType, })), }; } export const holidayCalendarResolutionReadProcedures = { previewResolvedHolidays: protectedProcedure .input(PreviewResolvedHolidaysSchema) .query(async ({ ctx, input }) => (await readPreviewResolvedHolidaysSnapshot(ctx, input)).holidays), previewResolvedHolidaysDetail: protectedProcedure .input(PreviewResolvedHolidaysSchema) .query(async ({ ctx, input }) => { const resolved = await readPreviewResolvedHolidaysSnapshot(ctx, input); const holidays = resolved.holidays.map(formatResolvedHolidayDetail); return { count: holidays.length, locationContext: resolved.locationContext, summary: summarizeResolvedHolidaysDetail(holidays), holidays, }; }), resolveHolidays: protectedProcedure .input(ResolveHolidaysInputSchema) .query(async ({ ctx, input }) => readResolvedHolidaysSnapshot(ctx, input)), resolveHolidaysDetail: protectedProcedure .input(ResolveHolidaysInputSchema) .query(async ({ ctx, input }) => { const resolved = await readResolvedHolidaysSnapshot(ctx, input); const holidays = resolved.holidays.map(formatResolvedHolidayDetail); return { periodStart: resolved.periodStart, periodEnd: resolved.periodEnd, locationContext: resolved.locationContext, count: holidays.length, summary: summarizeResolvedHolidaysDetail(holidays), holidays, }; }), resolveResourceHolidays: protectedProcedure .input(ResolveResourceHolidaysInputSchema) .query(async ({ ctx, input }) => readResolvedResourceHolidaysSnapshot(ctx, input)), resolveResourceHolidaysDetail: protectedProcedure .input(ResolveResourceHolidaysInputSchema) .query(async ({ ctx, input }) => { const resolved = await readResolvedResourceHolidaysSnapshot(ctx, input); const holidays = resolved.holidays.map(formatResolvedHolidayDetail); return { periodStart: resolved.periodStart, periodEnd: resolved.periodEnd, resource: resolved.resource, count: holidays.length, summary: summarizeResolvedHolidaysDetail(holidays), holidays, }; }), };