Sync trash calendar from official Nackenheim/KAW waste collection API
Adds trashCalendarSyncJob.ts, which pulls Restmüll/Biomüll/Gelbe Tonne/ Papiertonne dates for Nackenheim (CityId 35, Verbandsgemeinde Bodenheim) from the public, keyless API behind lk.kaw-mainz-bingen.de's official waste calendar, and upserts them into trash_schedule. Adds a unique (type, date) constraint to prevent duplicate entries on reruns. Run via `npm run trash-sync:run`, same standalone-script pattern as the other reminder jobs.
This commit is contained in:
parent
3b505d231a
commit
39220bddf4
@ -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",
|
||||
|
||||
99
backend/src/jobs/trashCalendarSyncJob.ts
Normal file
99
backend/src/jobs/trashCalendarSyncJob.ts
Normal file
@ -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<number, TrashType> = {
|
||||
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<KawCollectionDate[]> {
|
||||
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);
|
||||
});
|
||||
}
|
||||
@ -590,6 +590,7 @@ model TrashSchedule {
|
||||
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
@@unique([type, date])
|
||||
@@index([date])
|
||||
@@map("trash_schedule")
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user