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.
100 lines
3.2 KiB
TypeScript
100 lines
3.2 KiB
TypeScript
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);
|
|
});
|
|
}
|