diff --git a/backend/package.json b/backend/package.json index 77a3efa..f0b4d8b 100644 --- a/backend/package.json +++ b/backend/package.json @@ -12,6 +12,7 @@ "dunning:run": "ts-node -r dotenv/config src/jobs/dailyDunningJob.ts", "cleaning-reminder:run": "ts-node -r dotenv/config src/jobs/cleaningReminderJob.ts", "trash-reminder:run": "ts-node -r dotenv/config src/jobs/trashReminderJob.ts", + "trash-sync:run": "ts-node -r dotenv/config src/jobs/trashCalendarSyncJob.ts", "notice-deadline-reminder:run": "ts-node -r dotenv/config src/jobs/noticeDeadlineReminderJob.ts", "test:webhook": "ts-node -r dotenv/config scripts/send-test-webhook.ts", "prisma:generate": "prisma generate --schema=../prisma/schema.prisma", diff --git a/backend/src/jobs/trashCalendarSyncJob.ts b/backend/src/jobs/trashCalendarSyncJob.ts new file mode 100644 index 0000000..6ced94f --- /dev/null +++ b/backend/src/jobs/trashCalendarSyncJob.ts @@ -0,0 +1,99 @@ +import 'dotenv/config'; +import { PrismaClient, TrashType } from '@prisma/client'; + +const prisma = new PrismaClient(); + +/** + * Öffentliche Schnittstelle der Kommunalen Abfallwirtschaft Mainz und + * Mainz-Bingen AöR (https://lk.kaw-mainz-bingen.de/de/Abfallentsorgung/Abfallkalender), + * kein API-Key/Login nötig. CityId 35 = Nackenheim, DistrictId 5 = + * Verbandsgemeinde Bodenheim (per Browser-Formular ermittelt). + */ +const API_BASE = 'https://abfallkalender-api-lk.kaw-mainz-bingen.de/public/frontend/collectiondates'; +const CITY_ID = 35; +const DISTRICT_ID = 5; +const SYNC_MONTHS_AHEAD = 6; + +// Deren WasteTypeId -> unser TrashType. Sperrmüll (5) und Problemmüllbus (6) +// kennt unser Schema nicht und werden übersprungen. +const WASTE_TYPE_MAP: Record = { + 1: 'RESTMUELL', + 2: 'BIOMUELL', + 3: 'GELBER_SACK', + 4: 'PAPIER', +}; + +interface KawCollectionDate { + WasteTypeId: number; + WasteTypeName: string; + Date: string; // "TT.MM.JJJJ" +} + +interface KawResponse { + DataList: KawCollectionDate[]; + TotalRecords: number; +} + +function parseGermanDate(value: string): Date { + const [day, month, year] = value.split('.').map(Number); + return new Date(Date.UTC(year, month - 1, day)); +} + +async function fetchAllPages(dateFrom: string, dateTo: string): Promise { + const results: KawCollectionDate[] = []; + let page = 1; + while (true) { + const url = + `${API_BASE}?Filter.CityId=${CITY_ID}&Filter.DistrictId=${DISTRICT_ID}` + + `&Filter.DateFrom=${dateFrom}&Filter.DateTo=${dateTo}&Filter.CurrentPage=${page}`; + const res = await fetch(url); + if (!res.ok) throw new Error(`KAW-API antwortete mit Status ${res.status}`); + const body = (await res.json()) as KawResponse; + results.push(...body.DataList); + if (results.length >= body.TotalRecords || body.DataList.length === 0) break; + page += 1; + } + return results; +} + +export async function runTrashCalendarSyncJob(referenceDate: Date = new Date()) { + const dateFrom = referenceDate.toISOString().slice(0, 10); + const dateTo = new Date(referenceDate); + dateTo.setMonth(dateTo.getMonth() + SYNC_MONTHS_AHEAD); + const dateToStr = dateTo.toISOString().slice(0, 10); + + const entries = await fetchAllPages(dateFrom, dateToStr); + + let created = 0; + let skippedUnknownType = 0; + + for (const entry of entries) { + const type = WASTE_TYPE_MAP[entry.WasteTypeId]; + if (!type) { + skippedUnknownType += 1; + continue; + } + const date = parseGermanDate(entry.Date); + const existing = await prisma.trashSchedule.findUnique({ where: { type_date: { type, date } } }); + if (existing) continue; + await prisma.trashSchedule.create({ data: { type, date } }); + created += 1; + } + + return { fetched: entries.length, created, skippedUnknownType }; +} + +if (require.main === module) { + runTrashCalendarSyncJob() + .then((result) => { + console.log( + `[trashCalendarSyncJob] ${result.fetched} Termine von der KAW-API geladen, ` + + `${result.skippedUnknownType} unbekannte Mülltypen übersprungen.`, + ); + return prisma.$disconnect(); + }) + .catch((err) => { + console.error('[trashCalendarSyncJob] Fehlgeschlagen:', err); + process.exit(1); + }); +} diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3d58f76..846502f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -590,6 +590,7 @@ model TrashSchedule { createdAt DateTime @default(now()) @map("created_at") + @@unique([type, date]) @@index([date]) @@map("trash_schedule") }