feat(planning): ship holiday-aware planning and assistant upgrades
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
import { PrismaClient, type HolidayCalendarEntry } from "@prisma/client";
|
||||
import { buildHolidayCalendarSeedDefinitions } from "./holiday-calendar-seed-data.js";
|
||||
import { loadWorkspaceEnv } from "./load-workspace-env.js";
|
||||
|
||||
loadWorkspaceEnv();
|
||||
|
||||
const prisma = new PrismaClient();
|
||||
|
||||
const YEARS = [2026, 2027];
|
||||
const SEED_SOURCE = "seed:holiday-calendars:2026-2027";
|
||||
|
||||
type ExistingCalendar = {
|
||||
id: string;
|
||||
entries: Pick<HolidayCalendarEntry, "id" | "date" | "source">[];
|
||||
};
|
||||
|
||||
function toUtcDate(isoDate: string): Date {
|
||||
return new Date(`${isoDate}T00:00:00.000Z`);
|
||||
}
|
||||
|
||||
function dateKey(value: Date): string {
|
||||
return value.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
async function findScopedCalendar(input: {
|
||||
countryId: string;
|
||||
scopeType: "COUNTRY" | "STATE" | "CITY";
|
||||
stateCode?: string | undefined;
|
||||
metroCityId?: string | undefined;
|
||||
}): Promise<ExistingCalendar | null> {
|
||||
return prisma.holidayCalendar.findFirst({
|
||||
where: {
|
||||
countryId: input.countryId,
|
||||
scopeType: input.scopeType,
|
||||
stateCode: input.scopeType === "STATE" ? input.stateCode ?? null : null,
|
||||
metroCityId: input.scopeType === "CITY" ? input.metroCityId ?? null : null,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
entries: {
|
||||
select: {
|
||||
id: true,
|
||||
date: true,
|
||||
source: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log("Seeding holiday calendars for 2026-2027...");
|
||||
|
||||
const countries = await prisma.country.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
code: true,
|
||||
name: true,
|
||||
metroCities: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: { code: "asc" },
|
||||
});
|
||||
|
||||
const activeGermanStatesRows = await prisma.resource.findMany({
|
||||
where: {
|
||||
isActive: true,
|
||||
country: { code: "DE" },
|
||||
federalState: { not: null },
|
||||
},
|
||||
select: { federalState: true },
|
||||
distinct: ["federalState"],
|
||||
});
|
||||
|
||||
const definitions = buildHolidayCalendarSeedDefinitions({
|
||||
availableCountryCodes: countries.map((country) => country.code),
|
||||
availableCitiesByCountry: Object.fromEntries(
|
||||
countries.map((country) => [
|
||||
country.code,
|
||||
country.metroCities.map((city) => city.name),
|
||||
]),
|
||||
),
|
||||
activeGermanStates: activeGermanStatesRows
|
||||
.map((row) => row.federalState)
|
||||
.filter((stateCode): stateCode is string => Boolean(stateCode)),
|
||||
years: YEARS,
|
||||
});
|
||||
|
||||
const countryByCode = new Map(countries.map((country) => [country.code, country]));
|
||||
const cityByCountryAndName = new Map(
|
||||
countries.flatMap((country) =>
|
||||
country.metroCities.map((city) => [`${country.code}:${city.name}`, city] as const),
|
||||
),
|
||||
);
|
||||
|
||||
let createdCalendars = 0;
|
||||
let reusedCalendars = 0;
|
||||
let createdEntries = 0;
|
||||
let updatedEntries = 0;
|
||||
let skippedManualEntries = 0;
|
||||
|
||||
for (const definition of definitions) {
|
||||
const country = countryByCode.get(definition.countryCode);
|
||||
if (!country) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const metroCity = definition.cityName
|
||||
? cityByCountryAndName.get(`${definition.countryCode}:${definition.cityName}`)
|
||||
: null;
|
||||
|
||||
if (definition.scopeType === "CITY" && !metroCity) {
|
||||
console.warn(
|
||||
`Skipping city calendar ${definition.name}: city ${definition.cityName ?? "?"} not found.`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
let calendar = await findScopedCalendar({
|
||||
countryId: country.id,
|
||||
scopeType: definition.scopeType,
|
||||
stateCode: definition.stateCode,
|
||||
metroCityId: metroCity?.id,
|
||||
});
|
||||
|
||||
if (!calendar) {
|
||||
calendar = await prisma.holidayCalendar.create({
|
||||
data: {
|
||||
name: definition.name,
|
||||
scopeType: definition.scopeType,
|
||||
countryId: country.id,
|
||||
stateCode: definition.scopeType === "STATE" ? definition.stateCode ?? null : null,
|
||||
metroCityId: definition.scopeType === "CITY" ? metroCity?.id ?? null : null,
|
||||
priority: definition.priority,
|
||||
isActive: true,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
entries: {
|
||||
select: {
|
||||
id: true,
|
||||
date: true,
|
||||
source: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
createdCalendars += 1;
|
||||
} else {
|
||||
reusedCalendars += 1;
|
||||
}
|
||||
|
||||
const entriesByDate = new Map(calendar.entries.map((entry) => [dateKey(entry.date), entry]));
|
||||
|
||||
for (const entry of definition.entries) {
|
||||
const existingEntry = entriesByDate.get(entry.date);
|
||||
|
||||
if (!existingEntry) {
|
||||
await prisma.holidayCalendarEntry.create({
|
||||
data: {
|
||||
holidayCalendarId: calendar.id,
|
||||
date: toUtcDate(entry.date),
|
||||
name: entry.name,
|
||||
isRecurringAnnual: entry.isRecurringAnnual,
|
||||
source: SEED_SOURCE,
|
||||
},
|
||||
});
|
||||
createdEntries += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (existingEntry.source && existingEntry.source !== SEED_SOURCE) {
|
||||
skippedManualEntries += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
await prisma.holidayCalendarEntry.update({
|
||||
where: { id: existingEntry.id },
|
||||
data: {
|
||||
name: entry.name,
|
||||
isRecurringAnnual: entry.isRecurringAnnual,
|
||||
source: SEED_SOURCE,
|
||||
},
|
||||
});
|
||||
updatedEntries += 1;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(` calendars created: ${createdCalendars}`);
|
||||
console.log(` calendars reused: ${reusedCalendars}`);
|
||||
console.log(` entries created: ${createdEntries}`);
|
||||
console.log(` entries updated: ${updatedEntries}`);
|
||||
console.log(` manual entries preserved: ${skippedManualEntries}`);
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
})
|
||||
.finally(() => prisma.$disconnect());
|
||||
Reference in New Issue
Block a user