Skip to main content
Petanque Life

Notification System

F09.02 17 features Shipped

At a glance

The Notification System is a unified hub that delivers transactional email, SMS, and push messages from every Petanque Life feature. Mustache templates resolve per tenant, channel, locale, and event so any federation can override defaults without code. Dispatch is idempotent on the booking, event, and channel tuple, every recipient controls channels, frequency, and quiet hours, and a per-user delivery log keeps support staff honest.

How it works

When any part of the platform — bookings, draws, license renewals, ranking jobs — needs to reach a user, it raises a notification event rather than calling an email or SMS API directly. The hub picks the right channel mix (Email notifications via ACS/SendGrid, SMS notifications via ACS/46elks, Push notifications via FCM) based on the event type and the user's notification preferences, including chosen channels, frequency caps, and quiet hours.

Template resolution walks a four-key lookup: tenant, channel, locale, event. A custom template at the federation layer wins; otherwise the platform default applies. Notification templates are Mustache-rendered, so editors can drop in {{player.name}}, {{match.startsAt}}, or {{competition.title}} without touching code, and previews render against sample data before going live. Each tenant maintains its own template library per language, satisfying the multi-language obligation across 43+ FIPJP locales.

Dispatch is idempotent on the tuple (booking or event id, channel, recipient): if a worker retries, the recipient does not get duplicates. Every send is written to the notification delivery log with status (queued, sent, delivered, bounced, opened where allowed), so support staff can audit a single user's history end-to-end. Competition-specific notifications fire on draw published, next match imminent, and final results; license renewal reminders ramp up at T-60, T-30, T-7, and T-0 days; ranking change notifications fire after the periodic recompute when a player crosses a configured threshold.

For mass communication the same engine powers federation circular distribution — bulk sends to filtered member segments — with per-channel opt-out and unsubscribe links injected automatically. The hub respects the shared suppression list with the newsletter engine, so a hard bounce or complaint anywhere in the platform protects the user across both transactional and marketing flows.

Key capabilities

  • Unified email, SMS, and push delivery via ACS, 46elks, and FCM
  • Mustache templates with custom-over-default resolution per tenant, channel, locale, event
  • Per-user notification preferences for channels, frequency, and quiet hours
  • Idempotent dispatch keyed on event, channel, and recipient
  • Built-in event types for competitions, license renewals, and ranking changes
  • Federation circular bulk distribution with opt-out and unsubscribe links
  • Full delivery log per user for auditing and support

In practice

A licensed player in Bordeaux finishes registration for a regional doubles tournament. The competition module raises a registration-confirmed event; the hub looks up the French-language email template for that event in the FFPJP tenant, renders the player's name and bracket details, and sends it via ACS Email. Two days before the event, the draw is published — the hub fires a draw-published push to her phone (her preferred channel for in-event updates) at 09:30, respecting her 22:00–08:00 quiet hours.

On match day she receives an SMS five minutes before her court is called. Every message is recorded in her delivery log, viewable in her account.

Features in this subsystem

17
ID Status Features
F09.02.01 Shipped Email notifications (SendGrid) ✅ PL-F0902a
F09.02.02 Shipped SMS notifications (46elks) ✅ PL-F0902a
F09.02.03 Shipped Push notifications (FCM) ✅ PL-F0902a · Go-ombyggnad: transportkärnan LEVERERAD i 4776ad23.m-1 (D-PUSH) — enhetsregister, APNs/FCM v1-providers, synkron + asynkron fan-out (push.fan_out), token-självläkning, push_dispatch_log + DeliveryEventSink, nattlig push_stale_token_purge. 4776ad23.m-2 la den mallstyrda vägen POST /v1/push/dispatch/by-template + per-tenant-konfigurationen (GET/PUT /v1/push/config: default-ikon/ljud, anti-spam-taket max_push_per_user_per_hour, branded-build-bindning). 4776ad23.m-3 slöt de två återstående ytorna: app-sidans behörighets-onboarding/registrering/mottagning (se F15.01.07) och sys-operatörsytan — Push-hälsa (/halsa/push: härledd providerhälsa, invalid_token-andel, fan-out-dead-letter, purge-status och credential-rotationens testsändning) samt Transport-loggen (/halsa/push/logg: sökbar push_dispatch_log med Notistyp och keyset-paginering) över GET /v1/sys/push/health, GET /v1/sys/push/dispatch-log och POST /v1/sys/push/test-send (specs/api/endpoints/push-sys.md). Orkestreringen (kanalval, preferenser, quiet hours, digest, leveranshistorik) är D-COMM
F09.02.04 Shipped Notification templates (customizable per federation, per language) ✅ PL-F0902a · Push-delen ombyggd i Go: LEVERERAD i 4776ad23.m-2 (D-PUSH) — push_template per (förbund, notistyp, språk) med append-only versionshistorik, strikt {{variabel}}-rendering (saknad platshållare ⇒ 422 missing_variables), åtgärdsknappar/deep-link/bild/badge-policy, sidoeffektsfri preview av BÅDA nyttolasterna (APNs + FCM v1) och admin-editorn /kommunikation/push med serverdriven live-preview (specs/admin/views/push-mallar.md). E-post-/SMS-/in-app-mallmotorn LEVERERAD i 040878a9.m-2 (D-COMM) — notification_template per (förbund, notistyp, kanal, språk) med deklarerade variabler, append-only notification_template_version med revert/clone, sidoeffektsfri preview/validate, HTML-wrapper för e-post ({{content}}) och fallback-mall med exakt ett hopp; capability notification:template:manage, ETag/If-Match på varje mutation (specs/api/endpoints/notifications.md Del A₂). Admin-editorn för de kanalerna ligger i 040878a9.m-2.t-2. Push-mallen förblir D-PUSH:s: notification_template.channel utesluter push (§5d.1)
F09.02.05 Shipped Notification preferences per user (channels, frequency, quiet hours) ✅ PL-F0902a · Go-ombyggnad: API:t LEVERERAT i 040878a9.m-2 (D-COMM) — GET|PATCH /v1/me/notification-preferences i EFFEKTIV form (systemets + förbundets defaults, version=0 utan egen rad, notistypskatalogen med), ägar-skopat ur token-contexten (ingen rutt tar en användarreferens), ETag/If-Match, tysta tider validerade mot binärens inbyggda IANA-databas och tolkade i personens → förbundets default_timezone. Skärmen ligger i 040878a9.m-2.t-2
F09.02.06 Shipped Competition-specific notifications (draw published, next match, results) ✅ PL-F0902b
F09.02.07 Shipped License renewal reminders (notification type) ✅ PL-F0902b
F09.02.08 Shipped Ranking change notifications (notification type) ✅ PL-F0902b
F09.02.09 Shipped Federation circular distribution (bulk send) ✅ PL-F0902b · Go-ombyggnad: LEVERERAD i 040878a9.m-4 (D-COMM) — POST /v1/bulk-notifications (utkast/schemalagt/skickat, obligatorisk Idempotency-Key), outboxen GET /v1/bulk-notifications med mätta sent_count/failed_count (aggregat över bulk_id, aldrig lagrade räknare), PATCH med If-Match medan körningen är utkast/schemalagd, :send/:cancel, sidoeffektsfri :preview och per-mottagare-resultat via GET /v1/notifications/delivery-log?bulk_id=. Fan-out:en är asynkron (notification.bulk_fanout + notification_bulk_tick), chunkad om 500 och återupptagbar
F09.02.10 Shipped Notification delivery log (history per user) ✅ PL-F0902b · Go-ombyggnad: LEVERERAD i 040878a9.m-3 (D-COMM) — GET /v1/notifications/delivery-log (keyset, tenant-skopad, server-side fritext på namn eller adress, filter kanal/status/notistyp/from, to)), radens tidslinje GET .../delivery-log/{notification_id} med leverantörens kvitton + adressens effektiva spärrstatus, och räknarstatistiken GET /v1/notification-statistics (flyttalsfri; ingen öppnings-/klickmetrik — transactional spårar inte). Capability notification:delivery:read; admin-ytan /kommunikation/leveranslogg ([specs/admin/views/kommunikation-leveranslogg.md). Rader utan inkopplad kvittensväg redovisas som "Sänd — inget leveranskvitto", aldrig som levererade
F09.02.13 Shipped Delivery webhooks per provider — leverantörsneutral mottagare POST /v1/notifications/webhooks/{provider} + adapterregister (Mailjet wire:ad) ✅ 040878a9.m-3 (D-COMM) — fail-closed HMAC-verifiering i konstant tid (X-PL-Webhook-Signature, hemlighet ur PL_COMM_WEBHOOK_SECRET_<PROVIDER>, aldrig ur DB), 1 MiB-tak, okänd leverantör ⇒ 404 (registret är inget orakel), idempotens som databas-constraint, batch tillämpad per event. Speglas i notification_delivery_event (append-only) och lyfter radens status till delivered/failed (monotont). O-korrelerade kvitton lagras utan tenant och gallras av notification_delivery_event_purge
F09.02.14 Shipped Delad suppression i notispipen — spärren prövas vid dispatch och igen vid sändningen ✅ 040878a9.m-3 (D-COMM) — en notification_suppression, en beslutsfunktion (internal/suppression), delad med marketing. critical (otp/urgent) trumfar tysta tider men aldrig en spärr. Ny skip-orsak suppressed i dispatch-svaret
F09.02.11 Shipped Engagement attribution capture — EngagementAttribution-events (email_opened/clicked/bounced, webinar_attended, resource_downloaded, signup) konsumeras från notification-pipen och webinar/resource-event-bus — PL-T223 ✅ PL-T223
F09.02.12 Shipped Engagement snapshot builder — idempotent aggregat per (federation_id, club_id, period_type, period_start) med viktad engagement_score 0–100 — PL-T223 ✅ PL-T223
F09.02.15 Shipped Sys-plattformsbroadcast — operatörens utskick till förbundens användare (utkast → testskick → granskat → fan-out) med svarsinkorg ✅ 040878a9.m-9 (D-COMM) — det tenant-lösa aggregatet platform_broadcast + platform_broadcast_reply (migration 00156), sys-familjen /v1/sys/comm/broadcasts med monoton FSM (409 bär current_status), testskick direkt på MessageSender-lagret till operatörerna (noll notisrader), :mark-reviewed, :fan-out med fresh-auth + armering för schemalagd tid, sidoeffektsfri :preview och den datadrivna rollkatalogen. Fan-outen går genom m-4:s befintliga bulkmaskin — ingen andra väg runt suppression/opt-out/tysta timmar. Capability sys.broadcasts.manage. Sys-ytan /utskick i 040878a9.m-9.t-2 (specs/sys/utskick.md)
F09.02.16 Shipped Tvärs-tenant leverans- och spärrhälsa för support/drift (operatörsvy) ✅ 040878a9.m-9 (D-COMM) — GET /v1/sys/comm/health: 24 h-fönster räknat ur m-1/m-3:s riktiga rader (sända, kvitterade, hårda studsar, klagomål, retry-kö, spärrar per förbund) med serverberäknad ok|warning|no_data ur deklarerade tröskelkonstanter. Integritetslinjen är kontrakt: aggregat per förbund — aldrig en mottagare, adress, ämnesrad eller kropp; spärrar redovisas som antal. Ytan härleder procenten ur räknarna och renderar en tom mängd som tomt tillstånd, aldrig som kvot. Hävning av plattformsspärrar ingår inte (öppen skuld, kräver eget mockup-delta)
F09.02.17 Shipped Broadcaster-feedarnas latensöversikt tvärs förbund (operatörsvy) ✅ 040878a9.m-9 (D-COMM) — GET /v1/sys/comm/feed-latency exponerar m-7:s mätning ur API-processens glidande fönster: p99_ms/max_ms mot feedens budget_ms, samples och subscribers. samples == 0 ⇒ null och ytan skriver *"Ingen mätning"* — aldrig 0 ms; kolumnen etiketteras med den kvantil som faktiskt mäts (p99). Mätningen kan inte seedas — den framkallas med trafik (:test-publish)