internal/channels: konkrete Zustellkanaele fuer CFG-02s Dispatcher.
TemplateStore.Resolve loest Vorlagen pro Tenant auf und faellt auf
GlobalTemplateScope zurueck, wenn ein Tenant keine eigene gesetzt hat
(Akzeptanzkriterium 3). Render nutzt text/template mit
Option("missingkey=error") — ein fehlender Platzhalter bricht das Rendering
MIT FEHLER ab, statt eine unvollstaendige Nachricht zu erzeugen
(Akzeptanzkriterium 1).
EmailSender implementiert notify.Sender: rendert ZUERST die Vorlage, bevor
ueberhaupt eine SMTP-Verbindung aufgebaut wird — schlaegt das Rendering
fehl, wird nie ein Netzwerkzugriff versucht. Ein anschliessend fehl-
schlagender SMTP-Versand liefert einen Fehler, den CFG-02s bereits
getestete Wiederholungslogik verarbeitet (kein zweiter Retry-Mechanismus
hier). InAppSender persistiert In-App-Nachrichten ueber InAppStore
(Akzeptanzkriterium 2, ueber API abrufbar/als gelesen markierbar). Router
waehlt den Kanal anhand Notification.Channel — ein neuer Kanal wird per
Register() ergaenzt, ohne Dispatcher oder Router umzubauen.
Pruefungen (ausgefuehrt auf root@192.168.1.131, go build/vet/test PASS):
1. Vorlagenrendering mit fehlenden Platzhaltern bricht kontrolliert ab —
TestRender_MissingPlaceholderAborts und
TestEmailSender_AbortsBeforeSMTPWhenTemplateMissing (Fehler kommt von der
Vorlagenaufloesung, kein SMTP-Verbindungsversuch). PASS.
2. In-App-Benachrichtigung nach Markierung als gelesen korrekt gefuehrt —
TestInAppStore_MarkReadIsReflectedCorrectly. PASS.
3. E-Mail-Versand bei nicht erreichbarem SMTP-Server loest dokumentiertes
Retry-Verhalten ueber CFG-02 aus —
TestEmailSender_TriggersDispatcherRetryOnUnreachableSMTP: echter
EmailSender gegen unerreichbaren Host, ueber notify.Dispatcher
eingereiht, nach ausgeschoepften Wiederholungen status=failed mit
korrekter Versuchszahl. PASS.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
29 lines
1.1 KiB
SQL
29 lines
1.1 KiB
SQL
-- Benachrichtigungs-Kanaele: Vorlagen (E-Mail) + In-App-Nachrichten
|
|
-- (CFG-03, siehe core-kanban/tickets/CFG-03.md). Beide leben in der
|
|
-- Registry-DB, analog zu feature_flags/config_values — modulübergreifende
|
|
-- Konfiguration/UI-Zustand, keine Mandanten-Geschaeftsdaten.
|
|
|
|
-- tenant_slug = 'global' ist der Fallback-Wert, wenn ein Tenant keine
|
|
-- eigene Vorlage gesetzt hat (Akzeptanzkriterium 3: Vorlagen pro Tenant
|
|
-- anpassbar, mit sinnvollem Default).
|
|
CREATE TABLE notification_templates (
|
|
tenant_slug TEXT NOT NULL,
|
|
key TEXT NOT NULL,
|
|
subject TEXT NOT NULL,
|
|
body TEXT NOT NULL,
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
|
PRIMARY KEY (tenant_slug, key)
|
|
);
|
|
|
|
CREATE TABLE in_app_notifications (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_slug TEXT NOT NULL,
|
|
user_id TEXT NOT NULL,
|
|
title TEXT NOT NULL,
|
|
body TEXT NOT NULL,
|
|
read_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
|
);
|
|
|
|
CREATE INDEX in_app_notifications_user_idx ON in_app_notifications (tenant_slug, user_id, created_at);
|