Story CP-030: Channel B — Opt-Out + Commission + Provisioning¶
Module: control-plane Slice: Slice 13b from architecture (split from CP-013) Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 15 Depends on: CP-013 Estimated complexity: S
Objective¶
Complete the Channel B onboarding path by implementing the opt-out mechanism (allowing prospects to cancel unsolicited registrations), salesperson commission attribution on the tenant record, and the provisioning pipeline for CRM-originated tenants. After CP-013 creates the tenant and sends emails, this story ensures the recipient can opt out, the salesperson link is preserved for future commission tracking, and the full 9-step provisioning pipeline runs for Channel B tenants.
Backend Scope¶
Opt-Out Handling¶
Opt-out link in the activation email (sent by CP-013): papillon.ci/onboarding/opt-out?token=xxx
Opt-out token: JWT or opaque token tied to the PENDING tenant. Generated during tenant creation (CP-013) and embedded in email. Expires after 30 days.
Opt-out flow:
1. Validate token (not expired, tenant still PENDING)
2. Soft-delete the PENDING tenant and all associated data (Keycloak user, schema if created)
3. Create audit entry: TENANT_OPT_OUT
4. Return confirmation page
Salesperson Commission Attribution¶
salesperson_idis stored on the tenant record (set by CP-013)- This story ensures the
salesperson_idis preserved through the full tenant lifecycle and available on payment events - When a tenant eventually pays, the
salesperson_idis included in thePaymentReceivedfor downstream commission tracking (CRM module concern)
Provisioning Pipeline for Channel B¶
- Reuse the same 9-step provisioning pipeline from CP-008 (Channel A)
- Trigger provisioning after tenant creation (event-driven, listens to
ProspectConvertedToTenantfrom CP-013) - Channel B difference: tenant starts as PENDING (not PENDING_VERIFICATION) — no email verification needed since CRM already verified the prospect
- All 9 provisioning steps complete: schema creation, Keycloak realm setup, default roles, country config seeding, module activation, etc.
API Endpoints¶
| Method | Path | Request Body | Response | Auth |
|---|---|---|---|---|
| GET | /api/v1/onboarding/opt-out | ?token=xxx | HTML confirmation page | Public (token-validated) |
Validation Rules¶
- Opt-out token must be valid and not expired
- Tenant must still be in PENDING status (cannot opt out after activation)
- Opt-out is idempotent: calling twice with same token returns success
Multi-Tenant Considerations¶
- Opt-out endpoint is public (token-validated, no auth required)
- Opt-out soft-deletes tenant data across all isolation layers (schema, Keycloak, etc.)
Frontend Scope¶
Pages & Routes¶
/onboarding/opt-out— Opt-out confirmation page (public, in Papillon frontend)
Components¶
OptOutPage.tsx— Simple centered confirmation page
UI States¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | Spinner | — |
| Valid token | Confirmation message | "Votre inscription a été annulée. Vous ne recevrez plus de messages." |
| Invalid/expired token | Error message | "Ce lien n'est plus valide." |
| Already opted out | Same as valid (idempotent) | "Votre inscription a été annulée. Vous ne recevrez plus de messages." |
Responsive Behavior¶
Simple centered page — same layout at all breakpoints. Minimal styling, Papillon brand colors.
Acceptance Criteria¶
AC-103: Opt-out link works
Given the tenant admin received the activation email
When they click the opt-out link
Then the PENDING tenant is soft-deleted
And they see "Votre inscription a été annulée."
AC-104: Salesperson commission attribution
Given a tenant created via CRM with salesperson_id="sales-001"
When the tenant eventually pays
Then the salesperson_id is available on the tenant record for commission tracking
AC-106: Provisioning pipeline runs for Channel B
Given a tenant is created via CRM
When the provisioning pipeline runs
Then all 9 steps complete (same as Channel A)
And the tenant is ready for the admin to set their password and pay
OHADA & Regulatory Rules¶
- Opt-out link: The activation email MUST include an opt-out mechanism (the recipient did not request this registration). This aligns with data protection best practices (Loi 2013-450). Opt-out must soft-delete all personal data associated with the PENDING tenant.
- Salesperson commission: Tracked on payment, not registration (PRD FR-CP-005). Control Plane stores salesperson_id; commission calculation is a CRM module concern.
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— §Application Events, §Provisioning pipelinedocs/standards/api-guidelines.md— §Public endpoints, §Token validationdocs/standards/react-typescript-guidelines.md— §Simple pages, §Error states
Testing Requirements¶
Unit Tests¶
- Opt-out: token validation (valid, expired, invalid), soft-delete of tenant data
- Opt-out idempotency: second call with same token succeeds
- Commission attribution: salesperson_id preserved on payment event
- Provisioning: Channel B triggers same 9 steps as Channel A
Integration Tests¶
- Opt-out flow: valid token → tenant soft-deleted → confirmation page
- Opt-out with expired token → error page
- Opt-out on non-PENDING tenant → error (cannot opt out after activation)
- Provisioning pipeline: CRM tenant creation → all 9 steps complete
- salesperson_id available after provisioning completes
What QA Will Validate¶
- Opt-out page displays correctly at all breakpoints
- Opt-out actually deletes tenant data (verify in DB)
- Provisioning completes for CRM-originated tenants
- salesperson_id visible on tenant record in admin console (CP-012)
Out of Scope¶
- CRM internal API and tenant creation — CP-013 (already built)
- Email sending — CP-013 (emails sent during creation)
- CRM module itself — Sprint 3
- Salesperson commission calculation — CRM module
- CRM manager approval gate — Explicitly NOT included (FR-CP-005)
- Channel C (admin back-office creation) — V2
Definition of Done¶
- [ ] All backend endpoints implemented and returning correct responses
- [ ] All frontend pages render correctly at 360px, 768px, 1024px
- [ ] All UI states implemented (loading, error, 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 (opt-out cleans up all tenant data)
- [ ] No TypeScript
any— all types explicit - [ ] No Java raw types — all generics explicit
- [ ] API responses follow standard envelope format
- [ ] Audit trail entries created for opt-out action