Story CP-008: Self-Registration Form + Duplicate Detection¶
Module: control-plane Slice: Slice 8a from architecture (split from original CP-008) Brand context: Papillon HR Suite (warm, guided registration UX) Priority: 8 Depends on: CP-001, CP-002, CP-003, CP-004, CP-005, CP-006, CP-007 Estimated complexity: M
Objective¶
Build the self-registration form (Step 1) with company info collection, duplicate detection (email/phone exact match blocking, fuzzy company name flagging), hCaptcha validation, and rate limiting. This is the entry point to the onboarding journey. Creates a tenant with status PENDING_VERIFICATION and sends a verification email. Email verification, trial/payment flow, module selection, and provisioning are in subsequent stories (CP-022, CP-023, CP-024).
Backend Scope¶
Entities & Records¶
DTOs:
public record SelfRegistrationRequest(
@NotBlank @Size(min=2, max=200) String companyName,
@NotBlank @Size(min=2, max=2) String countryCode, // "CI" only for MVP
@NotBlank String employeeCountRange, // "2-12", "13-29", etc.
@NotBlank @Size(min=2, max=200) String adminFullName, // Split at first space → User.first_name + User.last_name
@NotBlank @Email @Size(max=254) String adminEmail,
@NotBlank String adminPhone, // CI format: 10 digits
@Nullable @Size(max=50) String rccm,
@NotEmpty List<String> selectedModuleCodes,
boolean freeTrial,
@NotBlank String hCaptchaToken
) {}
public record RegistrationResponse(
UUID tenantId, String message
) {}
Service Layer¶
OnboardingService.java:
- register(SelfRegistrationRequest) → RegistrationResponse:
1. Validate hCaptcha token (external API call)
2. Check rate limit (3 per IP per hour) — counters stored in Caffeine local cache (single instance MVP) or Redis (multi-instance production). Key: rate_limit:register:{ip}, TTL: 1 hour, value: request count.
3. Check duplicate email/phone (exact match → BLOCK)
4. Check fuzzy company name (Levenshtein < 3 after normalization → FLAG for ops, don't block)
5. Create Tenant with status=PENDING_VERIFICATION
6. Create TenantSettings with defaults
7. Store selected modules (not yet activated)
8. Send verification email (FR-CP-002)
9. Return tenantId
DuplicateDetectionService.java:
- checkDuplicates(String email, String phone, String companyName) → DuplicateResult
- Exact match email → BLOCK
- Exact match phone → BLOCK
- Fuzzy company name (normalize: lowercase, remove legal suffixes "SARL"/"SA"/"SAS"/"SUARL", trim, Levenshtein distance < 3) → FLAG
API Endpoints¶
| Method | Path | Request Body | Response | Auth |
|---|---|---|---|---|
| POST | /api/v1/onboarding/register | SelfRegistrationRequest | RegistrationResponse | Public (hCaptcha) |
Validation Rules¶
companyName: min 2, max 200countryCode: must be "CI" (MVP)employeeCountRange: must be one of "2-12", "13-29", "30-49", "50-99", "100-199", "200-350"adminEmail: valid email, unique globallyadminPhone: CI format (10 digits), unique globallyselectedModuleCodes: at least 1, all must be valid sellable module codeshCaptchaToken: validated against hCaptcha API- Rate limit: 3 registrations per IP per hour
Multi-Tenant Considerations¶
- Registration is a PRE-tenant operation — no TenantContext exists yet
- This slice only creates the Tenant row with PENDING_VERIFICATION status; provisioning (CP-024) creates the actual tenant context
Frontend Scope¶
Pages & Routes¶
/inscription— Registration form (Step 1 only in this slice)
Components¶
RegistrationStep1Page.tsx— Company info form (company name, country, employee count range, admin name, email, phone, RCCM, free trial checkbox)StepIndicator.tsx— Component spec in design §19.1 (shows 1/3 active)
UI States (ALL REQUIRED)¶
Registration Step 1:
| State | Behavior | French Copy |
|---|---|---|
| Loading | N/A (static form) | — |
| Empty | All fields empty except country (pre-filled CI) | Title: "Créez votre compte Papillon en 5 minutes" |
| Validation error | Red border + error below field, field shakes (150ms) | Per-field error messages (see below) |
| Duplicate | Error message with login link | "Un compte existe déjà avec cet e-mail." / "Un compte existe déjà avec ce numéro." |
| Submitting | Button: spinner + "Envoi en cours…", all fields disabled | "Envoi en cours…" |
| Server error | Toast, fields remain filled | "Une erreur est survenue. Veuillez réessayer." |
| Rate limited | Toast | "Trop de tentatives. Veuillez réessayer dans quelques minutes." |
| Success | Redirect to Step 2 or email verification | — |
Responsive Behavior¶
Registration Step 1: - 360px (mobile): Single column card on warm bg (#FFFDF7). Step indicator at top. Fields stacked vertically. - 1024px+ (desktop): Split layout — left panel (50%): deep teal #0D6B6E with geometric SVG, tagline "Simplifiez la gestion de vos RH", trust badges. Right panel (50%): white card with form.
Interactions¶
- Step 1 submit: "Continuer →" validates all fields, triggers hCaptcha, submits to backend. On success, sends verification email and shows confirmation message.
- Phone auto-format: As user types, format to XX XX XX XX XX. Country code +225 pre-filled and non-editable.
- Email/phone uniqueness: Checked on submit (server-side). If duplicate → error message with login link.
- hCaptcha: Invisible challenge first; falls back to visible challenge if suspicious.
French micro-copy (registration):
| Key | French Text |
|---|---|
registration.title |
"Créez votre compte Papillon en 5 minutes" |
registration.subtitle |
"Simplifiez la gestion de vos ressources humaines" |
registration.company_name.label |
"Nom de l'entreprise" |
registration.company_name.placeholder |
"Ex : Acme SARL" |
registration.country.label |
"Pays" |
registration.employee_count.label |
"Nombre de salariés" |
registration.employee_count.placeholder |
"Sélectionnez une tranche" |
registration.admin_name.label |
"Votre nom complet" |
registration.admin_name.placeholder |
"Ex : Moussa Koné" |
registration.email.label |
"Adresse e-mail" |
registration.email.placeholder |
"[email protected]" |
registration.phone.label |
"Numéro de téléphone" |
registration.phone.placeholder |
"07 12 34 56 78" |
registration.rccm.label |
"RCCM (optionnel)" |
registration.rccm.helper |
"Vous pourrez le fournir plus tard" |
registration.trial.label |
"Démarrer un essai gratuit de 14 jours" |
registration.submit |
"Continuer →" |
registration.already_registered |
"Déjà inscrit ?" |
registration.login_link |
"Se connecter" |
registration.duplicate_email |
"Un compte existe déjà avec cet e-mail." |
registration.duplicate_phone |
"Un compte existe déjà avec ce numéro." |
registration.rate_limited |
"Trop de tentatives. Veuillez réessayer dans quelques minutes." |
registration.server_error |
"Une erreur est survenue. Veuillez réessayer." |
verification.email_sent |
"Un e-mail de vérification a été envoyé à {email}" |
verification.check_inbox |
"Vérifiez votre boîte de réception (et les spams)" |
Acceptance Criteria¶
AC-061: Successful self-registration
Given I am on the registration page
When I fill all required fields, pass hCaptcha, and submit
Then a tenant is created with status PENDING_VERIFICATION
And I receive a verification email within 60 seconds
AC-064: Duplicate email blocks registration
Given email "[email protected]" is already registered
When I try to register with that email
Then I see "Un compte existe déjà avec cet e-mail."
AC-065: Fuzzy company name flags for ops
Given a tenant "Acme SARL" already exists
When I register "ACME Sarl" (Levenshtein distance < 3 after normalization)
Then registration proceeds (NOT blocked)
And an alert is created for ALTARYS ops in admin console
AC-066: Rate limiting
Given 3 registrations have been attempted from my IP in the last hour
When I try to register again
Then I see "Trop de tentatives. Veuillez réessayer dans quelques minutes."
OHADA & Regulatory Rules¶
- Loi 2013-450: Registration collects personal data (name, email, phone). Consent is implied by form submission. CGV must include data processing clause (legal/operational, not code).
- RCCM optionality: RCCM is not mandatory at registration (many small businesses don't have it yet). It can be provided later. But it's needed for FNE-compliant invoicing (see CP-009).
- hCaptcha: Privacy-focused alternative to Google reCAPTCHA. Important for CI data protection compliance.
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— §External API calls (hCaptcha), §Application Eventsdocs/standards/api-guidelines.md— §Public endpoints, §Rate limiting, §Error responsesdocs/standards/react-typescript-guidelines.md— §Multi-step forms, §StepIndicator, §Public pagesdocs/standards/offline-sync-guidelines.md— N/A (registration always online)
Testing Requirements¶
Unit Tests¶
- DuplicateDetectionService: exact email match, exact phone match, fuzzy name match
- DuplicateDetectionService: normalization (remove SARL/SA/SAS, lowercase, trim)
- OnboardingService.register: hCaptcha validation, rate limit check, duplicate check, tenant creation
- Validation: all field constraints (companyName min/max, countryCode "CI" only, phone format, email format)
Integration Tests¶
- Full registration flow: POST /api/v1/onboarding/register → tenant created with PENDING_VERIFICATION
- Duplicate detection: email, phone, fuzzy name
- Rate limiting: 4th attempt in 1 hour → 429
- hCaptcha token validation (mocked external API)
What QA Will Validate¶
- Registration form: all fields, validation messages, phone auto-format
- Step indicator shows progress (1/3 active)
- Desktop split layout (branding left, form right)
- Mobile single-column layout
- hCaptcha appears on submit
- Trust badges visible on desktop ("Conforme OHADA", "Donnees securisees", "Support reactif")
- Duplicate email/phone shows correct error messages with login link
Out of Scope¶
- Email verification + trial/payment flow — CP-022
- Module selection + pack detection UI — CP-023
- Provisioning pipeline — CP-024
- Payment flow (Step 3 when not using free trial) — that's CP-009
- Channel B (CRM) onboarding — that's CP-013
- Channel C (Admin back-office) onboarding — V2
- Password creation flow — Keycloak handles this
- Post-registration dashboard (welcome celebration) — Host shell feature
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 (new tenant's data is isolated)
- [ ] Offline write+sync works (Papillon) — N/A (registration 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