Aller au contenu

Notifications (NOTIF) — UX / UI Design Spec

Field Value
Module notifications (prefix NOTIF)
Plane Control
Sides covered [CUSTOMER] (out-of-app: SMS + WhatsApp message bodies) · [OPERATOR] (none in v0.0.0 — design intent for V1 frozen) · [ADMIN] (none in v0.0.0 — design intent for V1 frozen)
Version scope v0.0.0 (SMS at T0 + WhatsApp cascade at T+5d, behind flag; consent UI deferred to v0.0.1; operator timeline deferred to V1)
Date 22/05/2026
PRD reference docs/prd/notifications-prd.md (V0 Envelope block + V1 §A/B/C for forward intent)
Architecture ref docs/architecture/notifications-arch.md
Stories ref docs/stories/v0.0.0/notifications/index.md (NOTIF-S0, NOTIF-001a/b/c/d, NOTIF-002/003/004)
Design system docs/standards/design-system.md (tokens, components — applies to V1 operator/admin UI only; out-of-app message bodies are governed by SMS/WA carrier renderers, not our design system)

0. Scope reality check — what "design" means for NOTIF v0.0.0

NOTIF v0.0.0 is a backend Control-plane primitive. It has no application UI in v0.0.0 — neither a customer screen nor an operator screen. The sole human-facing artifacts the module produces are:

  1. The SMS message body rendered on the customer's native SMS app (Android Messages / iOS Messages / carrier client).
  2. The WhatsApp template message body rendered on the customer's WhatsApp client at T+5d (when the flag is on and ARTCI B5+B10 are notified — production-gated).
  3. The customer reply path (STOP / ARRET / NON / NO) — UX behaviour of the inbound channel.

Everything else NOTIF "touches" lives in sibling modules and is designed there:

Surface Owned by NOTIF's role Scope
Consent checkbox + privacy-notice link on the landing page customer-portal (CP) Supplies the canonical FR copy block and the privacy-notice URL v0.0.1 (CP-owned)
Operator per-customer timeline (history + retry) operator-console (OP) Publishes Spring application events the OP UI consumes V1 (OP-owned)
Platform admin kill switch platform admin (no UI v0.0.0) Backend endpoint only; admin UI lands V1 v0.1.0 + V1
Tenant notif.sms_sender_id / notif.wa_business_id form tenant-admin (TENANT) Reads the values at dispatch time TENANT v0.0.0

This document therefore covers:

  • §1 the message-body design (real v0.0.0 deliverable).
  • §2 the cross-module UX touchpoints NOTIF contributes copy / contracts to (notably the consent block — designed forward so the CP designer can render it in v0.0.1 without re-spec).
  • §3 the operator-side V1 design intent — frozen here for forward reference; not built in v0.0.0.

The Design Principles' "5 states per screen" rule does not map cleanly to an out-of-app SMS body. I substitute a 5-outcome customer-journey matrix (§1.6) and apply the strict 5-state rule to the V1 operator timeline (§3.3) and the v0.0.1 consent block (§2.1).


1. Customer-facing artifacts (v0.0.0)

The customer never opens a Papillon app to receive these. They open whichever messaging app the carrier delivered to. Our design surface is the message body itself — its wording, its length, its link placement, its trust signals, its opt-out instruction.

1.1 SMS — Initial renewal reminder (T0)

Trigger: operator validates a renewal campaign in OP (v0.0.0 OP slice) → POST /api/notifications/dispatchSmsDispatcher after-commit → provider submit.

Channel: SMS only at T0 in v0.0.0. WhatsApp is the T+5d cascade (§1.2), not a parallel mirror (the PRD's V1 "WA-first" rule is explicitly replaced in v0.0.0 per NOTIF-ADR-02 / NOTIF-003).

Canonical FR body (locked v0.0.0):

Bonjour {customer_first_name}, votre contrat {contract_ref} chez {tenant_name} arrive
à échéance le {due_date}. Montant : {amount_xof_formatted}. Renouvelez ici :
{signed_link}
Infos & vie privée : {privacy_url}
Pour ne plus recevoir de message, répondez STOP.

Variable formatting (locked):

Variable Rule Example
{customer_first_name} First given name only. Title-case. Strip diacritics ONLY if the GSM-7 budget bites (§1.4). Aïcha (UCS-2) or Aicha (GSM-7)
{contract_ref} As-is from ING. Uppercase preserved. AUTO-2025-0042
{tenant_name} tenant.display_name (TENANT-owned). May be 1–32 chars. Cabinet NSIA Cocody
{due_date} DD/MM/YYYY (Africa/Abidjan). 15/06/2026
{amount_xof_formatted} Integer, space thousands separator, XOF suffix, 0 decimals. 185 000 XOF
{signed_link} Short HTTPS URL produced by AUTH SignedLinkService. Length budget ≤ 48 chars. https://p.btr.ci/r/AbCd9XyZ
{privacy_url} Platform-default https://papillon.ci/confidentialite in v0.0.0. Tenant-override is V1. https://papillon.ci/confidentialite

Length budget — GSM-7 vs UCS-2:

Encoding Per segment First-name accents present? Typical body length Segments billed
GSM-7 160 chars NO (é/è/à stripped) ~190 chars 2
UCS-2 70 chars YES ~190 chars 3

PRD OQ-NOTIF-06 captures the cost decision (strip accents or not). v0.0.0 default: ship with accents (UCS-2, 3 segments). Rationale: the brand-trust gain of correct French diacritics outweighs the ~50% incremental cost during pilot. Founder may flip via a deploy flag without code change. The decision is reversible.

Why this body shape (educational notes for the PO):

  • Open with the customer's first name — Don Norman's "personalisation = trust" principle. A reminder that knows your name reads less like spam.
  • Tenant name appears in the first sentence — the customer paid this broker last year; they recognise the name and don't dismiss the SMS as fraud.
  • Link on its own line — most Android SMS clients auto-link a URL only if it sits cleanly; embedded mid-sentence sometimes fails to highlight.
  • Privacy notice on its own line, separate from the renewal link — keeps the regulatory disclosure visible without competing with the CTA. ARTCI mitigation #3 (prd-v0.md §6.2) requires the link in every send.
  • STOP instruction last — Fitts's-Law-flavoured: the primary action (the renewal link) is in the most prominent position; the secondary action (opt out) is at the bottom of the message where the customer's eye lands after reading.
  • No emoji, no special punctuation — defensive against GSM-7 edge cases and carriers that mangle UTF-8 across SS7.

1.2 WhatsApp — Cascade at T+5d

Trigger: WhatsAppCascadeFirer scheduled task picks rows with status = 'AWAITING_CASCADE' AND wa_scheduled_for <= now() AND customer_clicked_at IS NULL. AUTH reissues the signed link (72 h TTL). NOTIF submits via Meta WA Cloud API.

Channel gating in v0.0.0: ships behind FEATURE_NOTIF_WHATSAPP_CASCADE_ENABLED. SMS-only is the default for pilots until ARTCI B5 + B10 are notified (PRD V0 Envelope BLOQUANTs).

Meta WhatsApp template (category: utility, locked v0.0.0):

Bonjour {{1}}, votre contrat {{2}} chez {{3}} n'est pas encore renouvelé.
Échéance le {{4}} — montant {{5}}.
Renouvelez en deux clics : {{6}}

Infos & vie privée : {{7}}
Pour ne plus recevoir : répondez STOP.
  • {{1}} first name · {{2}} contract ref · {{3}} tenant name · {{4}} due date · {{5}} amount · {{6}} reissued signed link · {{7}} privacy URL.
  • One header (text-only, no media in v0.0.0 — keeps Meta approval simple).
  • One CTA button: "Renouveler" linking to {{6}} (deep-link button — surfaces above the text on most Android WA renderings).
  • Quick-reply button: "STOP" (Meta-native opt-out; mirrors the body instruction for users who never type).

Template-naming convention: papillon_renewal_cascade_fr_v1 — versioned so we can roll a v2 without breaking in-flight sends.

Why the WA body differs slightly from SMS:

  • "n'est pas encore renouvelé" — emphasises that this is a follow-up (5 days have passed, the SMS didn't land or wasn't acted on). Doesn't shame the customer; states a fact.
  • Two CTAs (link button + STOP button) — WA's native interaction model rewards button placements; users tap, not type.
  • Same privacy URL, same opt-out semantics — regulatory parity with the SMS.

1.3 Inbound STOP — customer reply UX

The customer replies STOP (or ARRET, NON, NO) by any of:

  • Typing in their SMS app and pressing send.
  • Tapping WA's native quick-reply button.
  • Replying in any case / with leading/trailing whitespace (normalisation per FR-11).

Customer-side outcome (no Papillon screen — what they see in their messaging app):

Reply token NOTIF action Customer sees
STOP / ARRET / NON / NO (exact, normalised) Suppression row written (tenant_id, phone, channel=SMS, reason=customer_stop) Nothing. v0.0.0 sends no acknowledgement back. (Auto-reply considered V1; explicit decision below.)
arrête s'il vous plaît (sentence) No suppression (per FR-11 — exact-token match only). Nothing.
Any other inbound text Dropped silently at the inbound webhook. Nothing.

Design decision — no STOP confirmation SMS in v0.0.0.

  • Pros: zero extra cost; zero risk of a confirmation SMS itself being seen as harassment by the customer who just opted out.
  • Cons: customer has no read receipt of their opt-out; may worry it didn't work and contact the broker.
  • Decision: live with the silence in v0.0.0. The pre-flight suppression check (NOTIF-001c) guarantees the next campaign won't reach them; that is the proof. Re-evaluate at v0.1.0 / V1 when we have operator visibility into the rate of "did my STOP work?" support calls.

(If founder flips this later, the auto-reply body would be: "Vous ne recevrez plus de message de {tenant_name}. Merci." — locked here so we don't redebate it.)

1.4 Privacy notice landing — https://papillon.ci/confidentialite

ARTCI mitigation #3 requires the link in every outbound message. v0.0.0 ships a platform-default page (single FR markdown render, no tenant branding). Tenant-override is V1.

Content outline (drafted here for LEGAL to validate — not designed visually; it's a static page that ships as part of the platform marketing site):

Confidentialité et traitement de vos données — Papillon Collection Solution

Pourquoi vous recevez ce message
Votre courtier {generic ref} a souscrit notre service pour vous accompagner
dans le renouvellement de votre contrat d'assurance. Le message que vous venez
de recevoir vous a été envoyé en son nom, sur la base de l'intérêt légitime
prévu par la loi n°2013-450.

Qui voit vos données
- Votre courtier (responsable du traitement).
- Papillon Collection Solution SAS (sous-traitant technique).
- Notre opérateur SMS partenaire (sous-traitant ultérieur).
- Meta Platforms (WhatsApp Cloud API) lorsque le message vous parvient par
  WhatsApp — incluant un transfert hors UE / hors Côte d'Ivoire.

Vos droits
- Vous opposer à recevoir d'autres messages : répondez STOP au SMS, ou
  appuyez sur STOP dans la conversation WhatsApp.
- Accéder, corriger, ou supprimer vos données : écrivez à
  [email protected].

Durée de conservation
Les traces de chaque envoi sont conservées 10 ans, conformément aux
obligations CIMA et ARTCI.

Plus d'informations
Visitez https://papillon.ci/legal — version FR / version EN.

Visual treatment: single-column, 16 px body min, mobile-first, no media. Static HTML to keep the budget per V1 PRD §30 < 50 KB (well under the 200 KB landing-page cap). Page must be reachable on 3G in < 2 s.

1.5 What the customer experiences on screen (low-fi mockups)

Android Messages — incoming SMS (360 dp render approximation):

┌─────────────────────────────────────────┐
│ ← +225 27 ** ** ** ** (Papillon)        │
├─────────────────────────────────────────┤
│                                         │
│   ┌─────────────────────────────────┐   │
│   │ Bonjour Aïcha, votre contrat    │   │
│   │ AUTO-2025-0042 chez Cabinet     │   │
│   │ NSIA Cocody arrive à échéance   │   │
│   │ le 15/06/2026.                  │   │
│   │ Montant : 185 000 XOF.          │   │
│   │ Renouvelez ici :                │   │
│   │ ▸ https://p.btr.ci/r/AbCd9XyZ   │   │  ← carrier auto-link
│   │                                 │   │
│   │ Infos & vie privée :            │   │
│   │ https://papillon.ci/...         │   │  ← second auto-link
│   │ Pour ne plus recevoir de        │   │
│   │ message, répondez STOP.         │   │
│   │                          09:14  │   │
│   └─────────────────────────────────┘   │
│                                         │
│ [ Tapez un message ...              ▷ ] │
└─────────────────────────────────────────┘

WhatsApp — incoming cascade at T+5d:

┌─────────────────────────────────────────┐
│ ← Cabinet NSIA Cocody (via Papillon) ⓘ │
│   En ligne · entreprise vérifiée        │
├─────────────────────────────────────────┤
│                                         │
│   ┌─────────────────────────────────┐   │
│   │ Bonjour Aïcha, votre contrat    │   │
│   │ AUTO-2025-0042 chez Cabinet     │   │
│   │ NSIA Cocody n'est pas encore    │   │
│   │ renouvelé.                      │   │
│   │ Échéance le 15/06/2026 —        │   │
│   │ montant 185 000 XOF.            │   │
│   │ Renouvelez en deux clics :      │   │
│   │ https://p.btr.ci/r/Ef33QwRt     │   │
│   │                                 │   │
│   │ Infos & vie privée :            │   │
│   │ https://papillon.ci/...         │   │
│   │ Pour ne plus recevoir :         │   │
│   │ répondez STOP.                  │   │
│   ├─────────────────────────────────┤   │
│   │  [    Renouveler    ]           │   │  ← CTA button (link)
│   │  [       STOP       ]           │   │  ← quick-reply button
│   │                          14:02 ✓│   │
│   └─────────────────────────────────┘   │
└─────────────────────────────────────────┘

These mockups are descriptive, not prescriptive — the actual rendering is the carrier's / Meta's. Our job is to write a body that survives every reasonable renderer.

1.6 Customer-journey state matrix (substitute for the per-screen 5-state rule)

# Outcome NOTIF state What the customer sees on their phone Recovery / next step
1 Delivered, clicked within 5 days DELIVEREDCASCADE_CANCELLED T0 SMS arrives; customer taps link; CP loads. CP takes over (signed-link flow).
2 Delivered, NOT clicked, cascade fires at T+5d SMS_SENTAWAITING_CASCADEDELIVERED (WA) T0 SMS arrives; 5 days later a WA message arrives with same link reissued. WA cascade-cancel race handled (§6.2 arch); customer can still click.
3 SMS provider hard-error (e.g. invalid number) FAILED Customer sees nothing. Operator visibility is v0.0.0-deferred; OP timeline in V1. Founder accepts.
4 Delivered, customer replies STOP DELIVERED + suppression row Customer's reply enters their thread; no Papillon acknowledgement. All future sends suppressed for (tenant, phone). Cross-tenant scoped.
5 Webhook lost — delivery unknown Stays at SMS_SENT indefinitely Customer experience same as outcome 1 or 2 (NOTIF can't tell). Honest-scope v0.0.0 gap (arch §6.3); V1 NOTIF-US-06 adds polling.

1.7 Connectivity behaviour (carrier / Meta side — not Papillon side)

NOTIF is server-side. The "low-bandwidth / drop / reload" rules from the DESIGNER personality apply to CP (the link target), not to NOTIF. The connectivity concerns NOTIF must handle are:

Event Behaviour Reference
Carrier drops the SMS in transit NOTIF only sees SENT (provider 2xx); no DELIVERED webhook. Outcome #5 above. arch §5 + §6.3
Customer phone off / out of coverage Carrier queues for up to 24–72 h. Webhook fires when delivered (or after a "failed" if timed out). provider-side; opaque
Meta WA outage Cascade firer hits 10 s timeout → notification FAILED. No retry in v0.0.0 (v0.1.0 adds policy). arch §5 row 5
Customer reads SMS / WA in offline mode Link tap is queued by the OS; CP load deferred until reconnection. NOTIF is no longer in the loop. CP-side concern
Customer replies STOP from airplane mode Carrier delivers when network returns; suppression effective from inbound-webhook time, not reply-compose time. arch §6.4

2. Cross-module surfaces NOTIF shapes (designed-forward, NOT built in v0.0.0)

This is the explicit consent checkbox + privacy-notice link that v0.0.1 must render on the customer's first arrival in CP. The CP designer will implement it. NOTIF owns the canonical FR copy block so the spec doesn't drift between modules.

Where it appears: CP landing page, immediately above the primary CTA ("Voir mon contrat"), as a sticky inline element. The customer cannot proceed without ticking it on first visit; on subsequent visits the same browser/session-token combination remembers the consent and the block collapses to a discreet footer link.

Wireframe (mobile-first, 360 dp):

┌──────────────────────────────────────┐
│  [logo cabinet]    Papillon          │
├──────────────────────────────────────┤
│                                      │
│  Bonjour Aïcha 👋                    │
│  Votre contrat AUTO-2025-0042        │
│  arrive à échéance le 15/06/2026.    │
│                                      │
│  ┌────────────────────────────────┐  │
│  │ Montant à régler               │  │
│  │  185 000 XOF                   │  │
│  └────────────────────────────────┘  │
│                                      │
│  ┌────────────────────────────────┐  │
│  │ ☐  J'ai lu et j'accepte que    │  │
│  │    Cabinet NSIA Cocody et      │  │
│  │    Papillon traitent mes       │  │
│  │    données pour ce             │  │
│  │    renouvellement.             │  │
│  │    En savoir plus              │  │  ← link to privacy page
│  └────────────────────────────────┘  │
│                                      │
│  [   Voir mon contrat        →   ]   │  ← disabled until checkbox ticked
│                                      │
│  Besoin d'aide ? +225 27 …           │
└──────────────────────────────────────┘

5-state spec for the consent block (DESIGNER personality rule — applied strictly here because it IS a screen the user interacts with):

State Behaviour
Loading Skeleton card with shimmer line where the label sits. Checkbox slot empty. Page CTA disabled. ≤ 600 ms target.
Empty (initial) Unticked checkbox (Checkbox component, default state). CTA disabled. Helper text below CTA: "Cochez la case ci-dessus pour continuer."
Error If consent_save API fails: keep the checkbox visually ticked, show InlineBanner variant="warning": "Nous n'avons pas pu enregistrer votre choix. Réessayez." + "Réessayer" inline button. Do NOT block CTA — retry happens lazily.
Offline / low-network If the page detects offline (navigator.onLine === false) and the user just ticked: store the consent locally (sessionStorage) + show subtle "Hors ligne — votre choix sera sauvegardé dès la reconnexion." caption under the checkbox. CTA stays enabled.
Success Checkbox ticked, CTA enabled, no banner. On click → navigates to the policy summary.

French micro-copy catalog (verbatim — locked here so CP renders these exact strings):

Key FR copy
consent.checkbox.label J'ai lu et j'accepte que {tenant_name} et Papillon traitent mes données pour ce renouvellement.
consent.checkbox.learn_more En savoir plus
consent.checkbox.helper_off Cochez la case ci-dessus pour continuer.
consent.error.save_failed Nous n'avons pas pu enregistrer votre choix. Réessayez.
consent.error.retry_button Réessayer
consent.offline.banner Hors ligne — votre choix sera sauvegardé dès la reconnexion.
consent.collapsed.footer Vie privée · vos choix
privacy.page.link_label Confidentialité et vos données

Component spec — Checkbox reuse + new variant:

  • Use the existing Checkbox from frontend/src/components/ui/Checkbox.tsx (per design-system inventory).
  • Wrap the label + "En savoir plus" link in a Card with padding="compact" for visual grouping.
  • 48 × 48 px touch target on the checkbox (already enforced by the component).
  • New requirement on Checkbox: support helperText below the checkbox (currently the design-system Checkbox doesn't expose one; tracked as a minor extension for v0.0.1 sprint).

Compliance UI: The block satisfies ARTCI mitigation #2 (explicit consent capture) and #3 (link to privacy notice). The "En savoir plus" link MUST open the privacy page at {privacy_url} (see §1.4) — same URL as the SMS body, single source of truth.

2.2 Cascade-cancel signal (CP → NOTIF)

When the customer opens CP via the signed link before T+5d, CP publishes the Spring application event customer.session_opened (OQ-NOTIF-14 — CP-side slice required). NOTIF consumes it and flips the row to CASCADE_CANCELLED. There is no UI here — it's a backend handoff. Documented for completeness because the customer's perceived behaviour ("I clicked the SMS link, so I shouldn't get a WA reminder 3 days later") depends on this contract holding.

If the contract breaks (CP event lost), the customer gets a WA reminder for a renewal they already started. Recovery: the WA message body is still accurate ("not yet renewed" until payment confirmed); customer-side annoyance is bounded. v0.0.0 risk accepted.


3. Operator + Admin surfaces — V1 design intent (frozen, NOT built v0.0.0)

The PRD lists NOTIF-US-05 (operator timeline + retry) and NOTIF-US-10 (admin kill switch). Both land in V1. I freeze the design intent here so the V1 designer doesn't redebate the structure.

3.1 Operator — per-customer notification timeline (V1)

Where it lives: inside the OP customer-detail page, as a Tabs panel "Notifications" alongside the existing "Contrats" / "Activité" tabs.

Layout (desktop 1280 dp baseline; tablet collapses tab to a stack):

┌──── Customer: Aïcha Diabaté ────────────────────────────────────────────┐
│  Contrats │ Activité │ Notifications │ Documents                         │
│                       ▔▔▔▔▔▔▔▔▔▔▔▔▔                                     │
│                                                                          │
│  [Filtrer ▼ tous]  [Période ▼ 30 derniers jours]                         │
│                                                                          │
│  ⚠ OPT-OUT (canal SMS, depuis le 18/05/2026)                            │
│                                                                          │
│  ┌──────────────────────────────────────────────────────────────────┐   │
│  │ 15/05 14:02 │ Rappel hebdo  │ WA  │ ● Livré       │ ↪ Voir       │   │
│  │ 10/05 09:14 │ Rappel initial│ SMS │ ◐ Envoyé      │ ↪ Voir       │   │
│  │ 03/05 11:42 │ Rappel hebdo  │ SMS │ ✕ Échec       │ ↻ Réessayer  │   │
│  │             │               │     │   (n° invalide)│              │   │
│  └──────────────────────────────────────────────────────────────────┘   │
│                                                                          │
│  3 notifications · 1 échec · 1 opt-out                                   │
└──────────────────────────────────────────────────────────────────────────┘

Status pill semantics (locked):

Status pill (FR) Internal status Color token Icon
Envoyé SMS_SENT / DISPATCHING_WA --color-info ◐ half-circle
Livré DELIVERED --color-success ● filled
Échec FAILED --color-error ✕ cross
Opt-out OPT_OUT --color-text-tertiary ⊘ no-entry
En attente any PENDING_* --color-warning ⏱ clock
Annulé (clic client) CASCADE_CANCELLED (reason=clicked) --color-text-secondary ↪ arrow

Color is never the only indicator — pill carries both a French word and a glyph.

3.2 Admin — kill switch (V1)

A single platform-admin page with one toggle, one reason field, one history strip. Modelled on the Switch component (needed-but-not-yet-built per design-system inventory) wrapped in a confirmation Modal. Out-of-scope for this doc to specify further; it's a 1-screen feature trivial to design once the rest of admin lands.

3.3 Five-state spec — V1 operator timeline

State Render
Loading 3 skeleton rows of the timeline (mimic the row height + the four columns). Filter bar visible but disabled. ≤ 800 ms target.
Empty EmptyState component: 📭 + "Aucune notification envoyée à ce client." No retry CTA needed. Helper text: "Les rappels seront listés ici dès le prochain envoi."
Error InlineBanner variant="error" at the top of the tab: "Impossible de charger l'historique. Réessayez." + "Réessayer" button. Skeleton rows stay below the banner so the operator sees the shell loaded.
Offline / low-network NOTIF is operator-side, desktop-primary. Per V1 PRD §20: rely on the OP module's minimum-offline base. Show a non-blocking toast: "Connexion intermittente — l'historique peut être incomplet." Last successfully fetched rows remain visible.
Success Rows render with their status pills. The opt-out banner (per AC-07.5) sits above the rows when applicable. Retry button only on Échec rows, with the 3-retry counter shown if any prior retries (e.g. "Réessayer (2/3)").

3.4 French micro-copy catalog — V1 operator timeline (frozen)

Key FR copy
timeline.tab.label Notifications
timeline.filter.all Tous
timeline.filter.period Période
timeline.empty.title Aucune notification envoyée à ce client.
timeline.empty.helper Les rappels seront listés ici dès le prochain envoi.
timeline.error.load_failed Impossible de charger l'historique. Réessayez.
timeline.error.retry Réessayer
timeline.offline.toast Connexion intermittente — l'historique peut être incomplet.
timeline.action.retry Réessayer
timeline.action.retry.counted Réessayer ({used}/3)
timeline.action.retry.limit Limite de relance manuelle atteinte
timeline.action.retry.opt_out Client en opposition (STOP)
timeline.optout.banner OPT-OUT (canal {channel}, depuis le {date})
timeline.row.see_details Voir les détails

4. Mermaid user-flow diagrams

4.1 v0.0.0 happy path (customer view + system view, intertwined)

flowchart TD
    A([OP validates campaign]) --> B[NOTIF /dispatch accepts]
    B --> C{Suppression?<br/>tenant, phone}
    C -- yes --> Z1([Suppressed silently<br/>NOTIF_SUPPRESSED_OPT_OUT])
    C -- no --> D[SMS submit to provider]
    D -- 2xx --> E[notification = SMS_SENT<br/>wa_scheduled = T+5d]
    D -- 4xx / timeout --> F([FAILED<br/>NOTIF_FAILURE<br/>no retry v0.0.0])
    E --> G{Customer clicks<br/>within 5 days?}
    G -- yes --> H[CP customer.session_opened]
    H --> I([CASCADE_CANCELLED])
    G -- no --> J[T+5d: WhatsAppCascadeFirer]
    J --> K{WA flag on?<br/>FEATURE_NOTIF_WHATSAPP_CASCADE_ENABLED}
    K -- no --> L([Stop at AWAITING_CASCADE<br/>SMS-only pilot])
    K -- yes --> M[AUTH reissues signed link]
    M --> N[WA submit via Meta]
    N -- 2xx --> O([DELIVERED via WA])
    N -- fail --> P([FAILED])

4.2 STOP path

flowchart LR
    A[Customer replies STOP] --> B[SMS provider<br/>inbound webhook]
    B --> C{HMAC valid?}
    C -- no --> D[401<br/>WARN log]
    C -- yes --> E{Body in<br/>STOP/ARRET/NON/NO?}
    E -- no --> F[200 no-op<br/>conversational text dropped]
    E -- yes --> G[UPSERT suppression<br/>tenant, phone, SMS]
    G --> H[200 to provider]
    H --> I[Next dispatch to this phone<br/>skipped silently]

4.3 Webhook-lost (honest-scope v0.0.0 gap)

flowchart LR
    A[SMS submit 2xx] --> B[notification.status = SMS_SENT]
    B --> C{Delivery webhook<br/>arrives?}
    C -- yes --> D([DELIVERED])
    C -- no, ever --> E[Stuck at SMS_SENT]
    E --> F{Customer clicks?}
    F -- yes --> G([CASCADE_CANCELLED<br/>indirect confirmation])
    F -- no --> H[T+5d cascade still fires]
    H --> I([Eventual outcome via WA])

5. Compliance UI checklist (v0.0.0)

Obligation Surface Status Reference
Privacy notice link in every message body SMS body line 4 · WA body line 4 VÉRIFIÉ ARTCI mitigation #3 (prd-v0.md §6.2)
Opt-out keyword in every message body SMS body last line · WA body last line + quick-reply button VÉRIFIÉ ARTCI mitigation #1 + FR-11
Tenant name shown to the customer SMS line 1 · WA line 1 VÉRIFIÉ CIMA Reg. 01-24 transparency intent
Explicit consent capture CP landing-page checkbox (v0.0.1) PRÉLIMINAIRE — render in v0.0.1 ARTCI mitigation #2
Sub-processor disclosure (Meta, SMS aggregator) https://papillon.ci/confidentialite static page PRÉLIMINAIRE — lawyer pin on OQ-NOTIF-02 Legal-audit gap #6
Audit trail per send Backend only (NOTIF_SENDING/NOTIF_SENT/NOTIF_FAILURE outbox) VÉRIFIÉ ARTCI mitigation #4
Per-tenant opt-out isolation Backend only (suppression table scoped by tenant_id) VÉRIFIÉ ARTCI mitigation #5 + AC-07.4

Blocking compliance items for v0.0.0 design: none — the customer-side artifacts (SMS body, WA body, privacy page) carry every ARTCI mitigation in inert text form. The dynamic consent capture is deferred to v0.0.1 by design.


6. Component & token usage summary

v0.0.0 builds no UI. The forward design refs are:

Surface (forward) Existing components used New components needed
v0.0.1 consent block (CP) Checkbox, Card, Button, InlineBanner (needed-list) Add helperText to Checkbox; build InlineBanner (already needed per design-system)
V1 operator timeline (OP) Tabs (needed), DataTable (needed), Badge (needed), EmptyState (needed), Button, Card None NOTIF-specific — relies on the OP shell
V1 admin kill switch Switch (needed), Modal, Button None
Privacy notice page None — static HTML page on the marketing site None

All future implementations MUST consume design tokens (--color-*, --space-*, --font-size-*) per docs/standards/design-system.md. No hex literals in component CSS.


7. Wireframe generation plan (Phase 3)

NOTIF v0.0.0 ships zero application UI → Phase 3 (/wireframe) is skipped for v0.0.0.

Forward-looking wireframes to generate when those sub-versions enter design:

Wireframe target Trigger sub-version Module owner Notes
Consent block (mobile + tablet) v0.0.1 CP designer Use copy block + 5-state spec from §2.1 verbatim
Operator timeline tab (desktop + tablet) V1 OP designer Use the layout, status pills, and copy from §3.1–§3.4
Admin kill-switch card V1 platform admin shell designer Trivial; not pre-wireframed in this doc

8. Open Questions surfaced by this design pass

OQ ID Question Owner v0.0.0 blocker?
DESIGN-NOTIF-01 Accent stripping in SMS body — UCS-2 (3 segments) vs GSM-7 (2 segments) trade-off. Default: keep accents. Founder No (reversible)
DESIGN-NOTIF-02 STOP-confirmation auto-reply SMS — v0.0.0 ships silent. Re-evaluate at v0.1.0 once support-call volume known. Locked copy: "Vous ne recevrez plus de message de {tenant_name}. Merci." Founder + OP analyst No
DESIGN-NOTIF-03 Tenant-branded privacy notice — v0.0.0 ships platform-default page only. Override surface is V1 (TENANT-owned setting). TENANT analyst No
DESIGN-NOTIF-04 papillon_renewal_cascade_fr_v1 Meta template approval — copy locked here; submission to Meta required before WA cascade flag flips on. ARCHITECT (ops) Blocks WA cascade flag, not pilot SMS-only
DESIGN-NOTIF-05 Sender-ID display name on SMS (Papillon vs Papillon-NSIA vs broker short code) — v0.0.0 uses one platform sender ID per provider OQ-NOTIF-01 pick; per-tenant sender V2. Founder + ARCHITECT No (cosmetic / trust)

9. References

  • docs/prd/notifications-prd.md — V0 Envelope block + V1 §A/B/C
  • docs/prd/prd-v0.md — §4 (NOTIF row), §5 (anchors), §6.2 (ARTCI mitigations)
  • docs/architecture/notifications-arch.md — §0, §5, §6 (sequence diagrams), §11 (slices)
  • docs/standards/design-system.md — tokens + component inventory
  • docs/standards/design-principles.md — 5-state rule, mobile-first 360 dp, French primary
  • docs/standards/connectivity-low-bandwidth.md — applies to CP, not NOTIF; cited for the consent-block 5-state spec
  • docs/legal/memo-conformite-CI-v2.md §B5 + §B10 — ARTCI authorisation BLOQUANTs gating the WA cascade flag
  • Sibling design specs: docs/design/op-design.md, docs/design/tenant-admin-design.md