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 alsoINCERTAIN. 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, callsPAY.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, emitsCP_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.
refmust 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: greenSuccessBanner, per-contractReceiptCard(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 contestationInlineBanner(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