/** * Kern-Helfer zum Erzeugen von In-App-Benachrichtigungen. * * Prinzipien: * - Wirft NIE. Ein fehlgeschlagenes Insert (DB weg, o.ä.) darf niemals eine * Buchung, Stornierung oder Kursabsage kippen. Alles in try/catch, Fehler * landen nur im Log - gleiches Pattern wie der Mailversand. * - Respektiert die pro-Business-Opt-outs aus Business.notificationSettings, * wenn ein businessId übergeben wird. Ohne businessId (kein Business-Kontext) * wird immer zugestellt. * - Empfänger ist die Manager-ID des Business (kein Employee-Fanout im MVP). * - Sendet zusätzlich Web-Push an alle Geräte des Empfängers (zweiter * Zustellkanal für dieselbe Benachrichtigung, kein eigenes Opt-out - wer * Push aktiviert hat, bekommt dieselben Typen wie in-app). */ import { prisma } from "@/src/lib/prisma"; import { sendPushToRecipient } from "@/src/lib/push/send-push"; import { NOTIFICATION_TYPES, resolveNotificationSettings, type NotificationType, } from "./notification-types"; interface CreateNotificationInput { recipientId: string; type: NotificationType; title: string; body: string; linkUrl?: string; } /** * Prüft, ob ein Benachrichtigungstyp für ein Business aktiv ist. * Fällt bei fehlendem Business / fehlendem Key auf den Registry-Default zurück. */ export async function isNotificationEnabled( businessId: string, type: NotificationType ): Promise { try { const business = await prisma.business.findUnique({ where: { id: businessId }, select: { notificationSettings: true }, }); if (!business) return NOTIFICATION_TYPES[type].defaultOn; return resolveNotificationSettings(business.notificationSettings)[type]; } catch (error) { console.error("isNotificationEnabled fehlgeschlagen:", error); // Im Zweifel zustellen - eine verpasste Benachrichtigung ist schlimmer // als eine, die trotz Opt-out durchrutscht. return NOTIFICATION_TYPES[type].defaultOn; } } /** * Legt eine einzelne Benachrichtigung an. Gibt true zurück, wenn ein Datensatz * geschrieben wurde, false wenn übersprungen (Opt-out) oder fehlgeschlagen. */ export async function createNotification(input: CreateNotificationInput): Promise { try { await prisma.notification.create({ data: { recipientId: input.recipientId, type: input.type, title: input.title, body: input.body, linkUrl: input.linkUrl ?? null, }, }); // Fire-and-forget: Push-Fehler dürfen das erfolgreiche In-App-Insert // oben nicht rückgängig machen oder den Aufrufer blockieren. void sendPushToRecipient(input.recipientId, { title: input.title, body: input.body, url: input.linkUrl, }); return true; } catch (error) { console.error("createNotification fehlgeschlagen:", error); return false; } } /** * Legt eine Benachrichtigung für den Manager eines Business an - sofern der * Typ für dieses Business nicht abgeschaltet wurde. * * Bewusst fire-and-forget-tauglich: Rückgabe wird von den Aufrufern (Webhook, * Storno-Action, ...) nicht ausgewertet, der Aufruf hängt an einem * .catch(console.error) und blockiert den Request nicht. */ export async function notifyBusinessManager( businessId: string | null | undefined, input: Omit ): Promise { try { if (!businessId) return; const business = await prisma.business.findUnique({ where: { id: businessId }, select: { managerId: true, notificationSettings: true }, }); if (!business) return; const enabled = resolveNotificationSettings(business.notificationSettings)[input.type]; if (!enabled) return; await createNotification({ ...input, recipientId: business.managerId }); } catch (error) { console.error("notifyBusinessManager fehlgeschlagen:", error); } }