Aller au contenu

Story CP-002a: Pre-payment summary + consent capture (no PAY call yet)

Module: customer-portal Slice: CP-002 sub-slice (a) (architecture §11 — CP-002 split into a/b/c) Side: [CUSTOMER] + [BACKEND] Version target: [V0.0.1] Priority: 2 Depends on: CP-001 Can develop concurrently with: None (CP-002b builds on the consent + summary) Merge order: After CP-001, before CP-002b Estimated complexity: M PRD User Stories: CP-04 Design screen: CP-S2 (Récapitulatif + Consentement) — see docs/design/customer-portal-design.md §4 (CP-S2), §8, §10 (compliance UI) Wireframe: docs/wireframes/customer-portal/CP-002/ (prototype pending — design §12 lists CP-S2 as 2nd-priority prototype; run /wireframe)

[LEGAL-REVIEW] BLOQUANT — Magic-link as opposable consent = INCERTAIN - avocat requis. Additionally, the probative value of the un-pre-checked consent checkbox (CP-OQ-02) is INCERTAIN. Per compliance-discipline.md these block production ship until legal clears them. Renouvellement à l'identique only — any change to garanties / montant / durée is out of scope and blocks the story.


Objective

Add the pre-payment summary screen and the audited consent capture. When the customer taps "Payer" on the contract list, CP re-checks eligibility, shows a summary of the selected contracts + total, the droit de rétractation notice (CIMA art. 24), and an un-pre-checked consent checkbox. Ticking it records customer_consent_captured bound to the session with the mentions-légales content version. No PAY call yet — the "Payer (final)" action is mocked/stubbed in this slice (real initiation lands in CP-002b).


Backend Scope

Entities

Uses customer_sessions (consent_content_version, consent_ticked_at, state) + mentions_legales_versions (read content version). No new tables.

Migrations

None (schema frozen in CP-001).

Service Layer

  • SummaryService.recap(tenantId, session) — re-queries the eligible set (R-CP-001) and intersects with the persisted selection (R-CP-009). If the intersection is empty → redirect signal to the contract list with copy. If the intersection differs from the persisted selection → return the summary with a "selection changed" flag requiring explicit confirm before consent unlocks. Emits customer_summary_viewed.
  • ConsentService.capture(tenantId, session, contentVersion) — sets consent_content_version (read at tick time per CP-OQ-07 default) + consent_ticked_at on the session; transitions state SUMMARY → CONSENTED. Emits customer_consent_captured. Consent is never pre-ticked and never auto-ticks on reload (R-CP-010).

API Endpoints

Method Path Request Response Auth
GET /api/portail/session/recap 200 {contracts[], total_amount_minor, currency, content_version, selection_changed?} cookie
POST /api/portail/session/consent {content_version} 204 (consent bound to session) cookie
  • X-API-Version: 1 via apiClient.ts. JSON private, must-revalidate + ETag.

Validation Rules

  • Recap re-checks eligibility server-side; only currently-eligible items returned.
  • Consent body content_version must equal the latest published version for the tenant; mismatch → re-render summary with the current version.
  • Total is the sum of selected contract.prime (XOF integer minor units); no per-contract amount input (R-CP-008).

Multi-Tenant Considerations

tenant_id from the cookie-bound session only. mentions_legales_versions read scoped by tenant_id. Content version is tenant-specific.

Audit & Logging

  • customer_summary_viewed — on GET /recap (customer_session_id, contract_ids[], total_amount_minor, currency).
  • customer_consent_captured — on POST /consent (customer_session_id, content_version, contract_ids[], total_amount_minor, currency, ticked_at).

Frontend Scope

Pages & Routes

  • /portail/session/recapSummaryConsentPage (route-split, lazy, loaded on "Payer" intent; chunk ~8 KB per arch §8.1).

Components

  • SummaryConsentPage, per-contract ContractSummaryRow (read-only Card), Total bar, Checkbox consent (un-pre-checked, design §5 consent rules), rétractation block, BrokerHeader (sticky), StepIndicator (step 2 active, design D1), mentions-légales footer link, primary "Payer" Button (disabled until consent ticked), NetworkBanner.

UI States (ALL REQUIRED)

State Behavior French Copy
Loading ContractSummaryRow skeletons + Total skeleton; consent block hidden until data loaded
Empty If intersection empty after re-check → Toast error + redirect to list « Aucun contrat sélectionné n'est plus éligible. »
Error XHR eligibility re-check fails → InlineBanner error + Button ghost « Réessayer »; BrokerHeader still rendered « Impossible de charger le récapitulatif. Réessayez. »
Offline / low network NetworkBanner; consent tick is optimistic-local; customer_consent_captured POST retries on reconnect; Pay button enabled on local tick « Connexion instable. Vos choix sont enregistrés. »
Success Contracts read-only; checkbox un-ticked; Pay button disabled; on tick → POST audit event, Pay button enabled h1 « Récapitulatif » · sub « Vérifiez vos contrats avant de payer. » · Total « Total : {total} XOF » · Consent « Je confirme avoir pris connaissance des conditions des contrats sélectionnés et des mentions légales du cabinet. » · Pay « Payer {total} XOF par mobile money » · Back « ← Modifier ma sélection »

Droit de rétractation block (CIMA art. 24 — MUST appear between the consent checkbox and the Pay button, design §10 placement rule 3): « Vous disposez d'un délai de 14 jours pour exercer votre droit de rétractation. Contactez votre cabinet au {agency.phone}. » Selection-changed notice (R-CP-009): « Votre sélection a changé : voici le détail à jour. » (InlineBanner warning above contracts; consent checkbox hidden + Pay button disabled until the user taps « Confirmer »).

Responsive Behavior

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

Connectivity Behavior (CUSTOMER stories only)

  • Summary chunk lazy-loaded only on "Payer" intent (keeps landing path light).
  • Survives reload: consent state lives on the session; a reload re-reads it. But consent must be freshly ticked for every fresh payment attempt — it never auto-ticks on reload (R-CP-010).
  • Survives disconnect: consent POST retried with backoff; summary re-fetched on screen re-entry (CP-NFR-12).

Interactions

  • "Payer" on the list navigates to /recap (lazy chunk).
  • Consent checkbox toggles the primary button enabled/disabled; state CONSENTED ⇄ SUMMARY on tick/untick.
  • In this slice, the final "Payer" action is stubbed (mock) — wired to PAY in CP-002b.

Acceptance Criteria

AC-002a.1: Summary renders the selected set with total, rétractation notice and an un-pre-checked consent box
Given I tapped "Payer" on the contract list with a non-empty selection
When the summary screen renders (GET /recap)
Then I see h1 « Récapitulatif », one read-only row per selected contract (branche, n° police, période, prime), the aggregated « Total : {total} XOF », the BrokerHeader, the StepIndicator on step 2, the droit-de-rétractation notice « Vous disposez d'un délai de 14 jours… » BETWEEN the consent checkbox and the Pay button, the mentions-légales footer link, and a single un-pre-checked consent checkbox; the primary action is disabled until it is ticked; customer_summary_viewed is emitted
AC-002a.2: Eligibility re-checked on summary entry
Given the eligible set changed between the list and the summary
When the summary opens
Then the server re-queries eligibility; if the intersection is empty it redirects to the list with « Aucun contrat sélectionné n'est plus éligible. »; if it differs it shows « Votre sélection a changé : voici le détail à jour. » and requires explicit « Confirmer » before the consent box unlocks
AC-002a.3: Consent capture is audited with content version
Given the summary screen
When I tick the consent checkbox (POST /consent)
Then consent_content_version (read at tick time) + consent_ticked_at are bound to my session, the state becomes CONSENTED, the "Payer" button enables, and customer_consent_captured is emitted with content_version, contract_ids[], total_amount_minor, currency, ticked_at; the box is never pre-ticked and never auto-ticks on reload

Compliance Rules

  • Magic-link opposable consent + probative value of checkbox consent (CP-OQ-02) = INCERTAIN - avocat requis ([LEGAL-REVIEW], BLOQUANT) — see header.
  • CIMA Reg. 01-24 mandatory pre-contractual information (VÉRIFIÉ per PRD: arts. 22-23 + Loi 2013-546 art. 19): mentions précontractuelles surfaced before payment.
  • Droit de rétractation 14 j affiché AVANT paiement (VÉRIFIÉ, CIMA art. 24): the rétractation block MUST render between the consent checkbox and the Pay button (design §10 placement rule 3) — never after.
  • Double-clic obligatoire (VÉRIFIÉ, Loi 2013-546 art. 20): consent tick (action 1) + a subsequent explicit "Payer" action (action 2) constitute the double action (initiation completed in CP-002b).
  • Consent content-version capture is the basis for the audit trail (CP-TV-02).

Standards & Conventions

  • docs/standards/critical-rules.md — money (XOF minor units), audit on action, tenant isolation.
  • docs/standards/api-guidelines.md — envelope, ETag, header versioning.
  • docs/standards/connectivity-low-bandwidth.md — lazy chunk on intent, server-side state.
  • docs/standards/react-typescript-guidelines.md — TanStack Query mutation, apiClient.ts.
  • docs/standards/design-system.md + design-principles.md — checkbox, button states, no silent failure.
  • docs/design/customer-portal-design.md — §4 CP-S2 (5 states + rétractation placement), §8 micro-copy, §10 compliance UI.
  • docs/standards/compliance-discipline.md[LEGAL-REVIEW] markers.

Testing Requirements

Unit Tests

  • Recap intersection logic: empty → redirect; differs → selection-changed flag; equal → normal.
  • Consent capture sets version + timestamp; reload does not auto-tick.

Integration Tests

  • GET /recap returns only currently-eligible items + emits customer_summary_viewed.
  • POST /consent binds content_version + emits customer_consent_captured.
  • Tenant isolation on recap + consent (tenant B cannot reach tenant A's session).

What QA Will Validate

  • Renders at 360 / 768 / 1024 / 1440; all 5 UI states; French copy exact; rétractation block sits between consent and Pay; Slow-3G smoke; consent never pre-ticked, re-tick required after reload.

Out of Scope

  • Real PAY initiation, redirect, idempotency, PaymentConfirmed listener (CP-002b).
  • Receipt screen + downloads (CP-002c).
  • Status polling / pending screen (CP-003).
  • Full mentions-légales page render (CP-S9, CP-006 / V1) — only the footer link + content-version read are in scope here.

Definition of Done

  • [ ] GET /recap + POST /consent implemented
  • [ ] Summary + consent 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 (h1, Total, consent, rétractation, Pay, Back)
  • [ ] Droit-de-rétractation block renders between consent checkbox and Pay button
  • [ ] StepIndicator step 2 + BrokerHeader present
  • [ ] AC-002a.1 … AC-002a.3 pass manually
  • [ ] customer_summary_viewed + customer_consent_captured emitted with required fields
  • [ ] Consent never pre-ticked; re-tick required for every fresh attempt
  • [ ] Tenant isolation verified (both SHARED + BYO profiles)
  • [ ] Summary chunk lazy-loaded only on "Payer" intent
  • [ ] Flow tested under throttled "Slow 3G"
  • [ ] No silent failures on network errors