Kuma/src/modules/notifications/lib/create-notification.ts
2026-09-14 16:26:18 +02:00

115 lines
3.8 KiB
TypeScript

/**
* 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<boolean> {
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<boolean> {
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<CreateNotificationInput, "recipientId">
): Promise<void> {
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);
}
}