import { VacationStatus, VacationType } from "@capakraken/db"; import { asHolidayResolverDb, getResolvedCalendarHolidays } from "../lib/holiday-availability.js"; import type { TRPCContext } from "../trpc.js"; type VacationDb = TRPCContext["db"]; export type BatchCreatePublicHolidaysInput = { year: number; federalState?: string | undefined; chapter?: string | undefined; replaceExisting: boolean; }; export async function batchCreatePublicHolidayVacations( db: VacationDb, input: BatchCreatePublicHolidaysInput, adminUserId: string, ): Promise<{ created: number; holidays: number; resources: number }> { const resources = await db.resource.findMany({ where: { isActive: true, ...(input.chapter ? { chapter: input.chapter } : {}), }, select: { id: true, federalState: true, countryId: true, metroCityId: true, country: { select: { code: true } }, metroCity: { select: { name: true } }, }, }); if (resources.length === 0) { return { created: 0, holidays: 0, resources: 0 }; } const periodStart = new Date(`${input.year}-01-01T00:00:00.000Z`); const periodEnd = new Date(`${input.year}-12-31T00:00:00.000Z`); // Step 1: Group resources by their holiday-resolution key so we call // getResolvedCalendarHolidays once per unique location combo instead of // once per resource (~5-10 calls vs. 500+). type HolidayGroup = { countryId: string | null; countryCode: string | null | undefined; federalState: string | null | undefined; metroCityId: string | null; metroCityName: string | null | undefined; resourceIds: string[]; }; const groups = new Map(); for (const resource of resources) { const effectiveFederalState = input.federalState ?? resource.federalState; const key = [ resource.countryId ?? "", resource.country?.code ?? "", effectiveFederalState ?? "", resource.metroCityId ?? "", ].join("|"); if (!groups.has(key)) { groups.set(key, { countryId: resource.countryId, countryCode: resource.country?.code, federalState: effectiveFederalState, metroCityId: resource.metroCityId, metroCityName: resource.metroCity?.name, resourceIds: [], }); } groups.get(key)!.resourceIds.push(resource.id); } // Step 2: Resolve holidays once per group (parallel). const resourceHolidays = new Map>(); await Promise.all( [...groups.values()].map(async (group) => { const holidays = await getResolvedCalendarHolidays(asHolidayResolverDb(db), { periodStart, periodEnd, countryId: group.countryId, countryCode: group.countryCode, federalState: group.federalState, metroCityId: group.metroCityId, metroCityName: group.metroCityName, }); for (const resourceId of group.resourceIds) { resourceHolidays.set(resourceId, holidays); } }), ); // Step 3: Build the full list of (resourceId, date) pairs. const allPairs: Array<{ resourceId: string; startDate: Date; endDate: Date; name: string }> = []; for (const resource of resources) { const holidays = resourceHolidays.get(resource.id) ?? []; for (const holiday of holidays) { const date = new Date(holiday.date); allPairs.push({ resourceId: resource.id, startDate: date, endDate: date, name: holiday.name }); } } const holidayCount = allPairs.length; if (holidayCount === 0) { return { created: 0, holidays: 0, resources: resources.length }; } const resourceIds = resources.map((r) => r.id); // Step 4: Batch delete for the whole period if replaceExisting (1 query). if (input.replaceExisting) { await db.vacation.deleteMany({ where: { resourceId: { in: resourceIds }, type: VacationType.PUBLIC_HOLIDAY, startDate: { gte: periodStart, lte: periodEnd }, }, }); } // Step 5: Batch fetch all existing entries for the period (1 query). const existing = await db.vacation.findMany({ where: { resourceId: { in: resourceIds }, type: VacationType.PUBLIC_HOLIDAY, startDate: { gte: periodStart, lte: periodEnd }, }, select: { resourceId: true, startDate: true }, }); const existingSet = new Set( existing.map((e) => `${e.resourceId}::${new Date(e.startDate).toISOString()}`), ); // Step 6: createMany for all new entries (1 query). const toCreate = allPairs.filter( (p) => !existingSet.has(`${p.resourceId}::${p.startDate.toISOString()}`), ); if (toCreate.length > 0) { const now = new Date(); await db.vacation.createMany({ data: toCreate.map((p) => ({ resourceId: p.resourceId, type: VacationType.PUBLIC_HOLIDAY, status: VacationStatus.APPROVED, startDate: p.startDate, endDate: p.endDate, note: p.name, requestedById: adminUserId, approvedById: adminUserId, approvedAt: now, })), skipDuplicates: true, }); } return { created: toCreate.length, holidays: holidayCount, resources: resources.length }; }