import 'server-only' import type { Quelle } from '@/generated/prisma/enums' import { uebernahmeEntscheiden, type Bestand, type Fund } from '@/lib/integration' import { auditSchreiben } from '@/server/audit' import { db } from '@/server/db' import { jetzt } from '@/server/zeitgeber' /** * Die gemeinsame Aufnahmestelle für alle Integrationen. * * Jede Integration liefert eine Liste von Funden; was daraus wird, entscheidet * für alle dieselbe Stelle. Damit verhält sich Zammad wie Telegram wie der * Spiegel — und eine neue Integration muss nur noch abholen können. * * Hier hängt später auch der Wartungsplaner ein: Er braucht eine Funktion, die * seine Daten in `Fund` übersetzt, sonst nichts. */ export type Aufnahmebericht = { angelegt: number aktualisiert: number geschlossen: number unveraendert: number } const LEER: Aufnahmebericht = { angelegt: 0, aktualisiert: 0, geschlossen: 0, unveraendert: 0, } /** Wer die Aufnahme angestoßen hat – für das Protokoll. */ export type Anstoss = { id: string | null; email: string } /** * Nimmt eine Runde Funde auf. * * Anschließend gilt: Zu jedem Fund gibt es genau eine Aufgabe mit * `[quelle, ref]`, und keine Aufgabe wurde in Feldern angefasst, die hier * gepflegt werden statt drüben. */ export async function fundeAufnehmen( quelle: Quelle, funde: readonly Fund[], anstoss: Anstoss, ): Promise { const bericht = { ...LEER } const zeitpunkt = jetzt() for (const fund of funde) { const vorhanden = await db.aufgabe.findUnique({ where: { quelle_quelleRef: { quelle, quelleRef: fund.ref } }, select: { id: true, titel: true, notiz: true, quelleUrl: true, faelligAm: true, prioritaet: true, aufwandMinuten: true, status: true, }, }) const bestand: Bestand | null = vorhanden ? { titel: vorhanden.titel, notiz: vorhanden.notiz, quelleUrl: vorhanden.quelleUrl, faelligAm: vorhanden.faelligAm, prioritaet: vorhanden.prioritaet, aufwandMinuten: vorhanden.aufwandMinuten, erledigt: vorhanden.status === 'ERLEDIGT' || vorhanden.status === 'VERWORFEN', } : null const entscheidung = uebernahmeEntscheiden(fund, bestand) if (entscheidung.tun === 'nichts') { bericht.unveraendert++ continue } if (entscheidung.tun === 'anlegen') { const angelegt = await db.aufgabe.create({ data: { titel: fund.titel, notiz: fund.notiz ?? null, quelle, quelleRef: fund.ref, quelleUrl: fund.url ?? null, faelligAm: fund.faelligAm ?? null, ...(fund.prioritaet ? { prioritaet: fund.prioritaet } : {}), ...(fund.aufwandMinuten ? { aufwandMinuten: fund.aufwandMinuten } : {}), }, select: { id: true }, }) bericht.angelegt++ await auditSchreiben({ akteur: anstoss, aktion: 'integration.angelegt', objektTyp: 'Aufgabe', objektId: angelegt.id, // Der Titel steht schon in der Aufgabe; hier genügt die Herkunft. details: { quelle, ref: fund.ref }, }) continue } if (entscheidung.tun === 'schliessen') { await db.aufgabe.update({ where: { id: vorhanden?.id ?? '' }, data: { status: 'ERLEDIGT', erledigtAm: zeitpunkt }, }) bericht.geschlossen++ await auditSchreiben({ akteur: anstoss, aktion: 'integration.geschlossen', objektTyp: 'Aufgabe', objektId: vorhanden?.id ?? null, details: { quelle, ref: fund.ref }, }) continue } await db.aufgabe.update({ where: { id: vorhanden?.id ?? '' }, data: entscheidung.felder, }) bericht.aktualisiert++ await auditSchreiben({ akteur: anstoss, aktion: 'integration.aktualisiert', objektTyp: 'Aufgabe', objektId: vorhanden?.id ?? null, details: { quelle, ref: fund.ref, felder: Object.keys(entscheidung.felder) }, }) } return bericht } // --- Zustand je Integration ------------------------------------------------ /** Liest den Merker, an dem eine Integration weitermacht. */ export async function merkerLesen(quelle: Quelle): Promise { const zeile = await db.integrationszustand.findUnique({ where: { quelle }, select: { merker: true }, }) return zeile?.merker ?? null } /** Hält fest, dass ein Lauf geklappt hat. */ export async function laufGeglueckt(quelle: Quelle, merker?: string): Promise { const zeitpunkt = jetzt() const werte = { letzterLauf: zeitpunkt, letzterErfolg: zeitpunkt, letzterFehler: null, fehlversuche: 0, ...(merker === undefined ? {} : { merker }), } await db.integrationszustand.upsert({ where: { quelle }, update: werte, create: { quelle, ...werte }, }) } /** * Hält einen Fehlschlag fest und zählt hoch. * * Nur die Meldung, nie die Antwort des fremden Systems: Darin stehen gern * Tokens, Kopfzeilen und ganze Datensätze. */ export async function laufGescheitert(quelle: Quelle, fehler: unknown): Promise { const meldung = fehler instanceof Error ? fehler.message : 'unbekannter Fehler' const zeitpunkt = jetzt() const zeile = await db.integrationszustand.upsert({ where: { quelle }, update: { letzterLauf: zeitpunkt, letzterFehler: meldung.slice(0, 500), fehlversuche: { increment: 1 }, }, create: { quelle, letzterLauf: zeitpunkt, letzterFehler: meldung.slice(0, 500), fehlversuche: 1, }, select: { fehlversuche: true }, }) return zeile.fehlversuche }