Aller au contenu

Story CP-002c: Receipt screen + reçu download + NOTIF receipt push

Module: customer-portal Slice: CP-002 sub-slice (c) (architecture §11) Side: [CUSTOMER] + [BACKEND] Version target: [V0.0.1] Priority: 4 Depends on: CP-002b; PAY getReceiptUrls(ref) live (signed MinIO URLs); NOTIF cp_payment_receipt template registered (NOTIF-US-11) Can develop concurrently with: None (closes the v0.0.1 payment slice) Merge order: After CP-002b, before CP-003 Estimated complexity: S PRD User Stories: CP-07 Design screen: CP-S4 (Reçu — paiement confirmé) — see docs/design/customer-portal-design.md §4 (CP-S4), §8, design decisions D9 + D10 Wireframe: docs/wireframes/customer-portal/CP-002/ (prototype pending — design §12 lists CP-S4 as 2nd-priority prototype; run /wireframe)

[LEGAL-REVIEW] BLOQUANT — Magic-link opposable consent = INCERTAIN - avocat requis. Anonymous mandataire receipt acceptability (CP-OQ-05) is also INCERTAIN. Blocks production ship.


Objective

After PAY confirms the payment, render the receipt ("reçu") screen with a green success banner, a per-contract ReceiptCard breakdown, and a "Télécharger le reçu" button per contract that fetches a short-lived signed MinIO URL from PAY. Surface the consolidated contestation banner (CIMA, design D9). On PaymentConfirmed, NOTIF dispatches the cp_payment_receipt transactional template (SMS + WhatsApp) — CP consumes that artifact; PAY orchestrates the dispatch.


Backend Scope

Entities

Uses payment_attempts (status CONFIRMED). No new tables.

Migrations

None.

Service Layer

  • ReceiptService.listReceipts(tenantId, session, ref) — on CONFIRMED, calls PAY.getReceiptUrls(ref) returning per-contract signed MinIO URLs (≤ 5 min TTL, single-use). CP never proxies the PDF body (CP-NFR-07). On a download click, emits CP_RECU_DOWNLOADED; expired clicks re-fetch a fresh URL (R-CP-023).
  • NOTIF push is orchestrated by PAY on PaymentConfirmed (NOTIF-US-11); CP only consumes the published template and references the reçu via signed URL. No CP code triggers NOTIF directly.

API Endpoints

Method Path Request Response Auth
GET /api/portail/session/payments/{ref}/recus 200 {recus:[{contract_id, url, expires_at}]} cookie OR readonly
  • X-API-Version: 1. URLs are single-use within their 5-min window.

Validation Rules

  • Only a CONFIRMED transaction for the session yields receipt URLs.
  • ref must belong to the cookie-bound session's tenant.

Multi-Tenant Considerations

Receipt URLs scoped to the tenant's transaction; tenant_id from the cookie-bound session. MinIO objects are PAY/storage-owned; CP only relays signed URLs.

Audit & Logging

  • CP_RECU_DOWNLOADED — on download click (transaction_reference, contract_id, recu_object_key).

Frontend Scope

Pages & Routes

  • /portail/session/return (CONFIRMED branch) → ReceiptPage (route-split, lazy, ~5 KB per arch §8.1).

Components

  • ReceiptPage: green SuccessBanner, per-contract ReceiptCard (frontend/src/customer/components/ReceiptCard.tsx — states default / downloading / error per design §5) with a "Télécharger le reçu" button each, a single consolidated contestation InlineBanner (design D9), BrokerHeader (sticky) + click-to-call / click-to-WhatsApp chips, mentions-légales footer link, NetworkBanner. StepIndicator is hidden on CP-S4 (terminal screen, design §3.2).

UI States (ALL REQUIRED)

State Behavior French Copy
Loading SuccessBanner skeleton (green tint) + ReceiptCard skeletons while fetching signed URLs
Empty N/A (always ≥ 1 confirmed contract on this screen)
Error Reçu URL fetch fails for one contract → that ReceiptCard shows an error; other cards unaffected; broker identity still rendered « Reçu indisponible. Réessayez. »
Offline / low network NetworkBanner; download button shows the offline note when offline; URL re-fetched on reconnect « Connexion requise pour télécharger » (download) · « Connexion instable. Vos choix sont enregistrés. » (banner)
Success Green SuccessBanner + per-contract ReceiptCards + contestation banner Banner « Paiement reçu » + « Référence : {transaction_reference} » + « {total} XOF — {JJ/MM/AAAA} » · Section « Vos contrats renouvelés » · Download « ↓ Télécharger le reçu » (loading « Téléchargement… ») · Contestation « Pour toute contestation, contactez le cabinet sous 30 jours au {agency.phone}. » · NOTIF info « Vous recevrez un message de confirmation par SMS/WhatsApp. »

Responsive Behavior

Mobile-first 360 px; ≥ 44 px targets; single-column breakdown; 768 = centered 600px column; 1024–1440 = centered 500px column (design §6). Currency 15 000 XOF, date JJ/MM/AAAA.

Connectivity Behavior (CUSTOMER stories only)

  • Receipt chunk lazy-loaded only after the confirmation return.
  • Survives reload: re-mounting CP-S4 re-fetches the list of signed URLs (they expire in 5 min; single-use). The confirmed payment lives on the session, so the receipt screen is reachable on any reload within the link window.
  • Signed-link reuse: same link re-opens to the receipt while the link is alive (post-ALL_PAID re-download is CP-006/V1 via CP-S8).

Interactions

  • "Télécharger le reçu" → button loading (« Téléchargement… ») → fetch fresh signed URL → open directly from MinIO (browser handles download) → button returns to default.
  • Expired URL on click → transparent re-fetch (R-CP-023). On failure → « Reçu indisponible. Réessayez. ».

Acceptance Criteria

AC-002c.1: Receipt screen renders on confirmation
Given PAY status is CONFIRMED
When the reçu screen renders
Then I see a green SuccessBanner « Paiement reçu » with « Référence : {transaction_reference} » and « {total} XOF — {JJ/MM/AAAA} », the section « Vos contrats renouvelés », a per-contract ReceiptCard with a « ↓ Télécharger le reçu » button each, a single consolidated contestation banner « Pour toute contestation, contactez le cabinet sous 30 jours au {agency.phone}. », the NOTIF info line « Vous recevrez un message de confirmation par SMS/WhatsApp. », and the BrokerHeader with click-to-call / click-to-WhatsApp chips
AC-002c.2: Reçu download uses a short-lived signed URL and is audited
Given the receipt screen
When I tap "Télécharger le reçu" for a contract
Then the button shows « Téléchargement… », CP fetches a single-use signed MinIO URL (≤ 5 min) from PAY, the browser downloads directly (CP never proxies the PDF), and CP_RECU_DOWNLOADED is emitted with transaction_reference + contract_id; an expired URL re-fetches transparently and a failure shows « Reçu indisponible. Réessayez. »
AC-002c.3: Confirmation triggers the NOTIF receipt push
Given PAY emits PaymentConfirmed
When the confirmation is applied
Then NOTIF dispatches the cp_payment_receipt template via SMS and WhatsApp (when WA is on file) carrying the reçu re-download link and reference; CP consumes the published template and references the reçu via signed URL (PAY orchestrates the dispatch)

Compliance Rules

  • Magic-link opposable consent + anonymous mandataire receipt acceptability (CP-OQ-05) = INCERTAIN - avocat requis ([LEGAL-REVIEW], BLOQUANT).
  • Contestation procedure (VÉRIFIÉ, V1 PRD §16.2 / CIMA art. 26): surfaced on the reçu screen as a single consolidated banner (30-day window + agency phone, design D9 + §10 placement rule 2 — agency phone, never the platform number).
  • Reçu PDF retention + integrity (CP-OQ-03): owned by PAY; CP consumes via signed URL only.
  • ARTCI / Loi 2013-450 sub-processor disclosure: NOTIF cross-border block named on mentions-légales (CP-006); receipt push uses SMS aggregator + WhatsApp BSP (NOTIF-owned).

Standards & Conventions

  • docs/standards/critical-rules.md — money, tenant isolation, audit, no silent failure.
  • docs/standards/api-guidelines.md — envelope, header versioning.
  • docs/standards/connectivity-low-bandwidth.md — lazy receipt chunk, no PDF proxy.
  • docs/standards/react-typescript-guidelines.md — apiClient.ts, direct MinIO download.
  • docs/standards/design-system.md + design-principles.md — success banner, buttons.
  • docs/design/customer-portal-design.md — §4 CP-S4 (5 states + download interaction), §8 micro-copy, decisions D9/D10.
  • docs/standards/compliance-discipline.md[LEGAL-REVIEW] markers.

Testing Requirements

Unit Tests

  • Receipt list only returned for CONFIRMED tx of the session.
  • Download click emits CP_RECU_DOWNLOADED; expired URL → re-fetch.

Integration Tests

  • GET /payments/{ref}/recus returns per-contract signed URLs (WireMock PAY/MinIO).
  • PaymentConfirmed → NOTIF cp_payment_receipt dispatch consumed (stub NOTIF).
  • Tenant isolation: tenant B cannot fetch tenant A's receipts.

What QA Will Validate

  • Renders at 360 / 768 / 1024 / 1440; all 5 UI states; French copy exact (banner structure, section, download loading/error, contestation, NOTIF line); Slow-3G smoke; download works; URL single-use within 5 min.

Out of Scope

  • Status polling + pending screen (CP-003).
  • Post-ALL_PAID re-download surface CP-S8/CP-13 + cp_readonly_sessions cookie (CP-006 / V1).
  • Phone-last-4, per-contract toggle (CP-003 / v0.0.2).

Definition of Done

  • [ ] GET /payments/{ref}/recus implemented (signed MinIO URLs, no PDF proxy)
  • [ ] Receipt screen renders at 360 / 768 / 1024 px (500px column at 1440)
  • [ ] All 5 UI states implemented (loading, empty, error, offline/low-network, success)
  • [ ] French micro-copy matches design §8 exactly (banner, section, download loading/error, contestation, NOTIF info)
  • [ ] Per-contract ReceiptCard + single consolidated contestation banner present
  • [ ] AC-002c.1 … AC-002c.3 pass manually
  • [ ] CP_RECU_DOWNLOADED emitted on download
  • [ ] NOTIF cp_payment_receipt push consumed on PaymentConfirmed
  • [ ] Tenant isolation verified (both SHARED + BYO profiles)
  • [ ] Receipt chunk lazy-loaded only after confirmation return
  • [ ] Flow tested under throttled "Slow 3G"
  • [ ] No silent failures on network errors