Aller au contenu

Story CP-022: Email Verification + Trial/Payment Flow

Module: control-plane Slice: Slice 8b from architecture (split from CP-008) Brand context: Papillon HR Suite (warm, guided registration UX) Priority: 8 (after CP-008) Depends on: CP-008 Estimated complexity: M


Objective

Build the email verification flow that validates the registration token and transitions the tenant to either TRIAL or PAYMENT_REQUIRED status. Includes token expiry handling (24 hours), resend verification with rate limiting (max 3 per 24 hours), and the EmailVerificationPage frontend showing all 4 token states. On successful verification, triggers the provisioning pipeline (CP-024).


Backend Scope

Entities & Records

DTOs:

public record VerifyEmailRequest(String token) {}

public record VerifyEmailResponse(
    String status,  // "VERIFIED", "EXPIRED", "INVALID", "ALREADY_VERIFIED"
    @Nullable String redirectUrl
) {}

public record ResendVerificationRequest(String email) {}

Repository Layer

EmailVerificationToken entity (platform DB):

Field Type Constraints
id UUID v7 PK
tenant_id UUID NOT NULL, FK to Tenant
token String NOT NULL, unique
email String NOT NULL
expires_at Instant NOT NULL (24 hours after creation)
is_used boolean NOT NULL, default false
used_at Instant Nullable
created_date Instant NOT NULL
  • EmailVerificationTokenRepository.java — Platform-scoped:
  • findByToken(String) → Optional<EmailVerificationToken>
  • findByEmailAndIsUsedFalse(String) → List<EmailVerificationToken>
  • countByEmailAndCreatedDateAfter(String, Instant) → int — for resend rate limiting
  • save(EmailVerificationToken) → EmailVerificationToken

Service Layer

OnboardingService.java (extend from CP-008): - verifyEmail(VerifyEmailRequest) → VerifyEmailResponse: 1. Validate token (single-use, expires 24 hours) 2. IF valid + freeTrial → transition to TRIAL, publish TenantStatusChanged event (triggers provisioning in CP-024) 3. IF valid + no freeTrial → transition to PAYMENT_REQUIRED, publish TenantStatusChanged event (triggers provisioning in CP-024) 4. IF expired → return "EXPIRED" with resend option 5. IF invalid → return "INVALID" 6. IF already verified → return "ALREADY_VERIFIED"

  • resendVerification(ResendVerificationRequest) → void:
  • Max 3 resends per 24 hours per email
  • Generates new token, invalidates old one
  • Sends new verification email

API Endpoints

Method Path Request Body Response Auth
POST /api/v1/onboarding/verify-email VerifyEmailRequest VerifyEmailResponse Public
POST /api/v1/onboarding/resend-verification ResendVerificationRequest 204 Public

Validation Rules

  • token: not blank, must match a pending verification record
  • email (for resend): valid email, must match an existing PENDING_VERIFICATION tenant
  • Resend limit: max 3 per 24 hours per email address

Multi-Tenant Considerations

  • Verification is a PRE-tenant operation — no TenantContext exists yet
  • Token lookup is by token value (globally unique), not tenant-scoped
  • Status transition triggers provisioning which creates the actual tenant context

Frontend Scope

Pages & Routes

  • /verification — Email verification landing page (receives ?token=... query param from email link)

Components

  • EmailVerificationPage.tsx — Token validation result display with 4 states

UI States (ALL REQUIRED)

Email Verification:

State Behavior French Copy
Loading Spinner + checking message "Verification en cours..."
Valid Green checkmark + auto-redirect (2s) "Adresse e-mail confirmee !"
Expired Message + resend button "Ce lien a expire." + "Renvoyer un e-mail de verification"
Invalid Message + link to registration "Ce lien n'est pas valide." + "Retour a l'inscription"
Already verified Message + login link "Votre adresse est deja confirmee." + "Se connecter"
Resend success Toast "E-mail de verification renvoye !"
Resend limit reached Toast "Nombre maximum de renvois atteint. Veuillez reessayer dans 24 heures."

Responsive Behavior

  • 360px (mobile): Centered card on warm bg (#FFFDF7). Icon + message + action button stacked vertically.
  • 1024px+ (desktop): Same centered card layout. Max-width 480px.

Interactions

  • Page auto-calls POST /api/v1/onboarding/verify-email with token from URL on mount
  • Valid → 2 second delay → auto-redirect to password creation (Keycloak) or payment page
  • Expired → "Renvoyer" button calls POST /api/v1/onboarding/resend-verification
  • Invalid → "Retour a l'inscription" link navigates to /inscription
  • Already verified → "Se connecter" link navigates to Keycloak login

French micro-copy:

Key French Text
verification.loading "Verification en cours..."
verification.success "Adresse e-mail confirmee !"
verification.expired "Ce lien a expire."
verification.resend "Renvoyer un e-mail de verification"
verification.resend_success "E-mail de verification renvoye !"
verification.resend_limit "Nombre maximum de renvois atteint. Veuillez reessayer dans 24 heures."
verification.invalid "Ce lien n'est pas valide."
verification.back_to_register "Retour a l'inscription"
verification.already_done "Votre adresse est deja confirmee."
verification.login "Se connecter"

Acceptance Criteria

AC-062: Free trial activation
Given I checked "Demarrer un essai gratuit de 14 jours" during registration
When I click the email verification link
Then my tenant status is TRIAL
And all selected modules are active
And I am redirected to password creation -> Dashboard

AC-063: Registration without trial
Given I did NOT check free trial
When I click the email verification link
Then my tenant status is PAYMENT_REQUIRED
And modules are read-only
And I am redirected to payment choice page (Step 3)

AC-067: Email verification token expires
Given I received a verification email
When I click the link after 24 hours
Then I see "Ce lien a expire." with a "Renvoyer" button

AC-068: Resend verification limit
Given I have already resent the verification email 3 times in 24 hours
When I try to resend again
Then the request is rejected

AC-069: Email verification token single-use enforcement
Given I received a verification email with a valid token
When I click the link and verify my email successfully
Then the token is marked as used (used_at set to current time)
When I try to click the same link again
Then I receive status "ALREADY_VERIFIED"
And I am shown "Votre adresse est deja confirmee." with a login link

OHADA & Regulatory Rules

  • Loi 2013-450: Email verification is part of the consent flow. User must verify email before accessing the platform.
  • No specific OHADA requirements for email verification.

Standards & Conventions

  • docs/standards/java-spring-guidelines.md — §Application Events (TenantStatusChanged), §Token management
  • docs/standards/api-guidelines.md — §Public endpoints, §Error responses
  • docs/standards/react-typescript-guidelines.md — §Public pages, §Auto-redirect pattern

Testing Requirements

Unit Tests

  • OnboardingService.verifyEmail: valid token + free trial → TRIAL status
  • OnboardingService.verifyEmail: valid token + no trial → PAYMENT_REQUIRED status
  • OnboardingService.verifyEmail: expired token → EXPIRED response
  • OnboardingService.verifyEmail: invalid token → INVALID response
  • OnboardingService.verifyEmail: already verified → ALREADY_VERIFIED response
  • OnboardingService.resendVerification: success within limit
  • OnboardingService.resendVerification: rejected at limit (4th attempt)

Integration Tests

  • Full flow: verify email with valid token → tenant status updated
  • Token expiry: verify after 24 hours → EXPIRED
  • Resend: 3 resends succeed, 4th rejected
  • TenantStatusChanged event published on successful verification

What QA Will Validate

  • Email verification page: all 4 states render correctly
  • Auto-redirect after successful verification
  • Resend button works and shows success/limit toast
  • Mobile and desktop layouts

Out of Scope

  • Registration form (Step 1) — CP-008
  • Module selection (Step 2) — CP-023
  • Provisioning pipeline — CP-024 (triggered by TenantStatusChanged event from this slice)
  • Payment flow — CP-009
  • Password creation — Keycloak handles this

Definition of Done

  • [ ] All backend endpoints implemented and returning correct responses
  • [ ] All frontend pages render correctly at 360px, 768px, 1024px
  • [ ] All 5 UI states implemented (loading, empty, error, offline, success)
  • [ ] French micro-copy matches design spec exactly
  • [ ] All acceptance criteria pass manually
  • [ ] Unit tests written and passing
  • [ ] Integration tests written and passing
  • [ ] Multi-tenant isolation verified — N/A (pre-tenant operation)
  • [ ] Offline write+sync works — N/A (verification always online)
  • [ ] No TypeScript any — all types explicit
  • [ ] No Java raw types — all generics explicit
  • [ ] API responses follow standard envelope format
  • [ ] Audit trail entries created for every data mutation