Control Plane — PRD+FSD Hybrid¶
Module: Control Plane (CP) Product: [BOTH] (Papillon HR Suite + ALTARYS ENTERPRISE HR Suite) Country Scope: CI (Côte d'Ivoire) — MVP Version: 1.0 Date: 26/02/2026 Author: ANALYST (Claude Code) Input documents: - docs/specs/control-plane.md (Tier 2 human-written spec) - docs/specs/platform-vision.md (Tier 1 platform vision spec) - docs/prd/platform-vision-prd.md (Platform Vision PRD v1.0) - docs/specs/tarifs_papillon_hr_suite.md (pricing grid) - docs/research/3_validated/control-plane-legal-validated.md (legal research) - docs/research/2_audit/control-plane-legal-audit.md (legal audit gaps)
PART A — Product Requirements (PRD)¶
A.1 Executive Summary¶
The Control Plane is the platform foundation of ALTARYS ENTERPRISE's multi-tenant SaaS platform. Every sellable HR/Finance module depends on it for authentication, tenant resolution, user identity, billing, and master data.
The Problem¶
OHADA-zone businesses — from 2-person startups to 350-employee SMEs — need a cloud platform that: - Onboards them quickly (no multi-week setup process) - Handles payment via Mobile Money (the dominant payment method in West Africa, where credit card penetration is <5%) - Enforces tenant data isolation (cross-tenant leakage is an existential risk for an HR SaaS storing salary data) - Provides the organizational structure (departments, positions, categories) that every HR module needs - Complies with OHADA Uniform Acts, Ivorian data protection law (Loi 2013-450), and FNE electronic invoicing requirements
What the Control Plane Does¶
The Control Plane is the SaaS operator layer (borrowed from the AWS SaaS Builder Toolkit architecture). It is NOT a tenant-facing HR/Finance module. It manages:
| Responsibility | Description |
|---|---|
| Tenant Management | Onboarding, lifecycle, provisioning, suspension, deletion |
| Billing & Subscriptions | Plans, invoices (FNE-ready), payments (provider-agnostic), metering |
| User Management | Users, invitations, multi-tenant access, Keycloak integration |
| Authentication | JWT-based auth, tenant resolution, token management |
| RBAC | Roles, permissions, module-gated access control |
| Organization & Master Data | Org units, positions, categories, contract types, manager hierarchy |
| Employee Entity (Baseline) | Minimal employee record consumed by all modules |
| Country Configuration Engine | Tax tables, social contribution rates, leave types, holidays |
| Module Gating | Track subscribed modules, enforce access, upsell display |
| Audit Trail | Immutable logging for all data mutations across all modules |
| Notification Service | Shared in-app + email notification pipeline for all modules |
| Admin Console | ALTARYS operator tools (separate app) |
What the Control Plane Does NOT Do¶
- It does NOT process leave requests (Absence Management module)
- It does NOT compute payroll (Payroll module)
- It does NOT manage the full employee dossier (HR Core module, post-MVP)
- It does NOT provide administrative process management (Employee Management module, post-MVP)
- It does NOT enforce AUDCIF Art. 22 period locking or hash chains (Accounting module)
- It does NOT provide document versioning, retention, or search (Advanced Document Management, post-MVP)
A.2 User Stories¶
US-CP-001: Tenant Self-Onboarding [BOTH] [CI]¶
As a prospective customer visiting papillon.ci, I want to register my company and start using the platform immediately, So that I don't waste time on administrative setup before seeing value.
Acceptance Criteria: - Given I am on the registration page, When I fill in: company name, country (CI), declared employee count, admin email, admin name, phone, RCCM (optional), And I pass hCaptcha, And I submit the form, Then I receive a verification email within 60 seconds. - Given I clicked the email verification link, When I chose the 14-day free trial, Then my tenant status is TRIAL, all selected modules are active, and I can start using the platform immediately. - Given I clicked the email verification link, When I chose NOT to use the free trial, Then my tenant status is PAYMENT_REQUIRED, I can log in but all modules are read-only, and I have 14 days to pay. - Given my tenant is in PAYMENT_REQUIRED status, When I have not yet requested a trial, Then I can request my one-time 14-day free trial at any time. - Given my admin email already exists in the system, When I attempt to register, Then the system shows "Un compte existe déjà avec cet email."
US-CP-002: CRM Salesperson Creates Tenant [BOTH]¶
As an ALTARYS freelance salesperson, I want to register a prospect as a tenant directly from my CRM, So that the prospect can start paying and using the platform without delays.
Acceptance Criteria: - Given I have prospect information in the CRM, When I trigger "Convertir en client" (convert to tenant), Then the system creates a tenant in PENDING status. - Given a tenant is created from CRM, When the system processes the creation, Then the tenant admin receives email #1 (create password link) and email #2 (payment link). - Given the activation email, When the recipient did not request this registration, Then they can click "Annuler cette inscription" (opt-out link) to delete the PENDING tenant. - Given the tenant admin has set their password and paid, When payment is confirmed, Then tenant status transitions to ACTIVE and all subscribed modules are available. - Given the salesperson's ID, When the tenant pays, Then the salesperson ID is recorded on the tenant for commission attribution (commission tracked on payment, not registration).
US-CP-003: Tenant Admin Manages Users [BOTH]¶
As a tenant administrator, I want to invite users to my company's platform account, So that my employees and external collaborators can access the modules they need.
Acceptance Criteria: - Given I am a tenant admin, When I invite a user by email, Then the user receives an invitation email with a password setup link, and their status is INVITED. - Given an invited user clicks the link and sets their password, When they complete registration, Then their status changes to ACTIVE and they can log in with the roles I assigned. - Given I assign a role to a user, When the role includes permissions for an inactive module, Then those permissions are silently ignored (module gating overrides role permissions).
US-CP-004: Multi-Tenant User (Cabinet Comptable) [BOTH]¶
As an accountant managing 5 client companies, I want to log in once and switch between my clients' tenants, So that I don't need separate accounts with different emails for each company.
Acceptance Criteria: - Given I belong to multiple tenants, When I log in, Then I land on my home tenant's dashboard with a "Mes entreprises" section showing thumbnails/logos of all my client tenants. - Given I click on a client tenant's thumbnail, When the tenant switch initiates, Then a new JWT is issued for the selected tenant (token exchange, no re-authentication), and I operate in that tenant's context. - Given I am operating in a client tenant's context, When I want to switch to another client or go home, Then I can switch via a persistent tenant selector without re-logging in.
US-CP-005: ALTARYS Admin Manages Platform [BOTH]¶
As an ALTARYS platform administrator, I want to view all tenants, confirm payments, and manage subscriptions, So that I can operate the platform and support customers.
Acceptance Criteria: - Given I access the admin console (separate app, non-public URL), When I view the tenant list, Then I see all tenants with their status, subscription details, employee count, and payment status. - Given a tenant paid by bank transfer, When I confirm the payment manually, Then the tenant status transitions from ACTIVE_PAYMENT_PENDING to ACTIVE. - Given I need to suspend a tenant, When I trigger manual suspension, Then the tenant status becomes SUSPENDED and all modules become read-only for that tenant.
US-CP-006: Employee Management (Baseline) [BOTH]¶
As a tenant administrator, I want to create and manage employee records, So that modules like Absence, Attendance, and Payroll can reference my employees.
Acceptance Criteria:
- Given I am creating an employee online,
When I submit the form,
Then the employee is created with a UUID v7 ID (client-generated) and a server-assigned matricule (format: {PREFIX}-{YEAR}-{MM}-{SEQ}, e.g. ALT-2026-03-001).
- Given I am creating an employee offline,
When I submit the form,
Then the employee is saved to IndexedDB with UUID v7, matricule shows "Numéro en attente", and it syncs when connectivity returns.
- Given I assign a position to an employee,
When the position doesn't exist in the catalog,
Then I can type a custom position name which auto-adds to the catalog for future reuse.
- Given an employee has no manager assigned,
When any module routes an approval request for that employee,
Then the request routes to the tenant's Default Approver.
US-CP-007: Module Upsell Display [PAPILLON ONLY]¶
As an employee using Papillon, I want to see what other modules my company could subscribe to, So that I can recommend useful tools to my admin.
Acceptance Criteria: - Given my tenant has Absence but not Payroll, When I view the navigation menu, Then Payroll appears greyed/locked with a brief description and a "Souscrire" button. - Given I click "Souscrire", When I am a regular employee (not admin), Then I see "Contactez votre administrateur pour activer ce module." - Given I click "Souscrire" as a tenant admin, When I click through, Then I am directed to the billing/subscription management page.
A.3 User Personas¶
P-CP-01: Propriétaire / DG (CEO-Owner) — SME [PAPILLON]¶
- Context: Owns a 20-employee trading company in Abidjan. Currently manages everything via WhatsApp + Excel.
- Goal: Onboard quickly (< 5 minutes), start tracking absences, eventually add payroll.
- Pain point: "Je veux que ça marche tout de suite. Pas de formation compliquée."
- Device: Android phone (Samsung A-series, 3G/4G).
- Interaction with CP: Self-onboarding, user invitations, company settings, billing.
P-CP-02: Responsable RH (HR Manager) — Mid-size SME [PAPILLON]¶
- Context: HR manager at a 150-employee manufacturing company in San Pedro.
- Goal: Manage employee records centrally, ensure labor code compliance.
- Pain point: "J'ai 3 cahiers et 5 fichiers Excel. Je perds des informations."
- Device: Desktop (primary), phone for approvals.
- Interaction with CP: Employee creation, org structure setup, role management.
P-CP-03: Comptable Externe (External Accountant) — Cabinet Comptable [BOTH]¶
- Context: Accountant at a cabinet comptable managing 12 SME clients.
- Goal: Access all clients from one account. View payroll data, validate budgets.
- Pain point: "12 mots de passe pour 12 clients, c'est ingérable."
- Device: Desktop (dual screen).
- Interaction with CP: Multi-tenant login, tenant switching (thumbnail dashboard), role-per-tenant.
P-CP-04: Directeur Financier (CFO) — Large Company [ENTERPRISE ONLY]¶
- Context: CFO at a 500-employee company with multiple sites and departments.
- Goal: Dense data views, batch operations, advanced reporting.
- Interaction with CP: Company settings, org structure (multi-site), billing oversight.
- Note: Enterprise persona. V2 frontend serves this user with denser UI.
P-CP-05: Administrateur Plateforme ALTARYS [BOTH]¶
- Context: ALTARYS operations staff managing the platform.
- Goal: Monitor tenant health, resolve payment issues, onboard tenants manually.
- Device: Desktop (admin console — separate app).
- Interaction with CP: Admin console for tenant management, payment confirmation, suspension/reactivation, platform metrics.
P-CP-06: Commercial Freelance (Freelance Salesperson) [BOTH]¶
- Context: ALTARYS freelance salesperson prospecting businesses across CI.
- Goal: Register prospects quickly from the CRM, track commission on conversion.
- Device: Phone (always on the move).
- Interaction with CP: Channel B onboarding (CRM → tenant creation).
A.4 Non-Functional Requirements¶
| Requirement | Target | Notes |
|---|---|---|
| Page load time | < 2 seconds on 3G | Control Plane pages (settings, user management, org structure) |
| Onboarding completion time | < 5 minutes | From first form field to TRIAL or PAYMENT_REQUIRED status |
| Tenant provisioning time | < 30 seconds (Profile A), < 2 minutes (Profile C) | Includes DB provisioning, Keycloak setup, seed data |
| API response time | < 500ms (p95) for auth endpoints, < 1s for CRUD | Auth is the hot path — every request passes through it |
| Offline capability | Employee creation, org structure browse | Employee forms queue offline. Settings/billing require online. |
| Uptime | 99.5% (auth endpoints: 99.9%) | Auth is critical path for all modules |
| Concurrent tenants | 100,000+ within 5 years | DB design must handle this scale |
| Concurrent users per tenant | 500 at launch, scalable to 5,000 | Keycloak sizing must support this |
| Data isolation | Zero cross-tenant data leakage — auditable and testable | Enforced at repository layer (TenantAwareRepository). Every query scoped. |
| Audit retention | 10 years minimum | AUDCIF Art. 24 compliance |
| Backup | Daily automated, 30-day retention | Per-tenant backup for Profile C |
| Browser support | Chrome 90+, Firefox 90+, Safari 15+ | 95%+ of West African users |
| Device support | Android phones (8+ year old models), tablets, desktops | Primary: 360px mobile |
A.5 MVP Scope¶
In MVP¶
- Tenant Management: Full lifecycle (10 statuses), 2 active channels (A + B), provisioning pipeline
- DB Profiles: All 4 profiles — A (Shared DB), B (Schema-per-tenant), C (DB-per-tenant), D (BYO-DB). Auto-selection by employee count.
- Billing: Provider-agnostic payment adapter, manual fallback, FNE-ready invoice model, monthly billing, annual billing (14 months for price of 12), segment-based pricing (S1-S6), packs as pricing discount rules, pending payments dashboard
- User Management: Invite, RBAC, default roles (DG, RH Manager, Comptable, Employé, Manager), multi-tenant users with home_tenant_id and thumbnail dashboard
- Authentication: Keycloak single realm, JWT (user_id, tenant_id, country_code, roles[]), hybrid permissions (JWT roles + cached permission matrix), token exchange for tenant switching
- Organization & Master Data: 3-level flexible tree (Entity → optional Site → Department), position catalog with escape hatch, 4 employee categories (CI) + top 5 sector salary grids, contract types (CDI, CDD, Stage, Apprentissage, Journalier, CTT), manager hierarchy (N/N+1), Default Approver
- Employee Entity: Baseline with statuses (PRE_HIRE, ACTIVE, SUSPENDED, TERMINATED), UUID v7 client-side, server-assigned configurable matricule
- Country Configuration Engine: CI only — ITS brackets (temporarily validated), CNPS rates (temporarily validated), leave types, public holidays, SYSCOHADA chart, categories + 5 sector grids, contract types, CDD 2-year rule (90/30/7 alerts)
- Module Gating: Per-module status tracking, API + frontend enforcement, upsell display (visible + locked + CTA)
- Audit Trail: Immutable logging for all modules, queryable by tenant admins, 10-year retention
- Notification Service: Shared service for all modules, in-app (notification bell) + email, ~10 DB-stored Papillon-branded templates
- Admin Console: Separate lightweight app (desktop-only, no offline) — tenant list, payment management, suspend/reactivate, module override, basic metrics
- Channel B Stub: Interface contract defined + stub endpoint for CRM integration
- Security: hCaptcha on public endpoints, rate limiting, duplicate detection (email + phone + fuzzy name), manual review alerts for suspicious patterns
- 14-day free trial: Optional, Channel A only, one-time per tenant
- French only, XOF only, CI only
Out of MVP (V2+)¶
- Per-module suspension: Data model ready in MVP, enforcement logic in V2
- Channel C: Admin back-office tenant creation
- Custom roles: Tenant-created roles (MVP has fixed default roles)
- SSO / OAuth / realm-per-enterprise-tenant: V2 Enterprise
- SMS notifications: V2 (important for West Africa)
- Usage-based billing / metering dashboards: V2
- Mid-cycle proration: V2 (MVP: changes take effect next billing cycle)
- Module trial periods: Individual module trials (MVP only has tenant-level trial)
- Tenant data export tool: Self-service export before deletion (MVP: manual via admin)
- FNE DGI API integration: MVP stores FNE-ready data, V2 transmits to DGI
- CinetPay / Stripe: Specific provider integrations (when partnerships confirmed)
- Multi-country configs: BEN + CMR at month 9, RDC at month 15
- XAF currency support: V2
- English / Portuguese: V2+
- HR Core module: Full employee dossier (post-MVP sellable module)
- Employee Management module: Administrative process layer (post-MVP)
- Advanced Document Management: Versioning, retention, search (post-MVP)
- Cost centers: V2 (§ documented in Platform Vision)
- Multi-entity / group structures: V2
- Organigramme graphique: V2
- Grades, classifications, job families: V2-V3
- Matrix organisations: V3 Enterprise
A.6 Open Questions¶
| ID | Question | Priority | Impact |
|---|---|---|---|
| OQ-CP-01 | Délégués du personnel threshold: Is the >10 worker threshold per establishment (site) or per company? Affects org unit employee count tracking. | HIGH | Country Config, Employee entity |
| OQ-CP-02 | RICF formula: Réduction d'Impôt pour Charges de Famille — specific calculation formula and deduction amounts per number of parts. Needed for Country Config Engine. | HIGH | Country Config, Payroll |
| OQ-CP-03 | ARTCI déclaration préalable: Filing not started. Required before processing CI residents' personal data. Cross-border transfer authorization for Hetzner (Germany) also required. PRE-LAUNCH LEGAL BLOCKER. | CRITICAL | Legal compliance |
| OQ-CP-04 | DGI FNE API: Endpoint, onboarding process, and technical requirements for transmitting FNE-compliant invoices to DGI. Needed for V2 integration. | MEDIUM | Billing V2 |
| OQ-CP-05 | BCEAO aggregator limits: Actual per-transaction and monthly limits for Flutterwave/Orange Money/Wave merchant channels. Affects payment flow design. | MEDIUM | Billing |
| OQ-CP-06 | National holidays API: Find an API or method to provide national public holidays for all 17 OHADA countries programmatically. BACKLOG ITEM. | MEDIUM | Country Config |
| OQ-CP-07 | Payslip confidentiality source: Legal source for payslip confidentiality requirement acknowledged as SOURCE UNKNOWN in research. | LOW | Notification, Access Control |
| OQ-CP-08 | Labour inspection document production deadline: No legal source found for the deadline to produce documents for labour inspectors. | LOW | Document management |
| OQ-CP-09 | DB-per-tenant auto-selection threshold: At what declared employee count should auto-selection recommend Profile C over Profile A? | HIGH | Tenant provisioning |
A.7 Temporarily Validated¶
| ID | Item | Value | Status | Notes |
|---|---|---|---|---|
| TV-CP-01 | CNPS Prestations Familiales rate | 5% (employer only, no ceiling) | Temporarily validated — source: CNPS Guide Employeur, unverified decree | Implement in Country Config, flag in DB as unverified. Update when decree identified. |
| TV-CP-02 | CNPS Maternité rate | 0.75% (employer only, no ceiling) | Temporarily validated | Same as above |
| TV-CP-03 | CNPS AT/MP rate | 2–5% (employer only, variable by risk class) | Temporarily validated | Risk class mapping to rate not fully defined |
| TV-CP-04 | CNPS Assurance Vieillesse rate | 14% total (7.7% employer + 6.3% employee), ceiling 3,375,000 FCFA/month | Temporarily validated — source: CNPS Communiqué 2023 | Critical for Payroll. Ceiling and rate must be verified. |
| TV-CP-05 | ITS brackets (CI) | 0–75K: 0%, 75K–240K: 16%, 240K–800K: 21%, 800K–2.4M: 24%, 2.4M–8M: 28%, >8M: 32% | Temporarily validated — source: Ordonnance n° 2023-719 | Must verify no Loi de Finances 2025/2026 modified these. |
| TV-CP-06 | CDD max duration | 2 years (renewable once) — Art. 15.10 Code du Travail CI | Temporarily validated | Alert thresholds: 90/30/7 days before limit. |
PART B — Functional Specifications (FSD)¶
B.1 Functional Rules — Tenant Management¶
B.1.1 Tenant Onboarding¶
FR-CP-001 [BOTH] [CI]: Tenant Self-Registration Data - The self-registration form (Channel A) collects: - Company name (required, min 2 chars, max 200 chars) - Country code (required, ISO 3166-1 alpha-2; MVP: "CI" only, dropdown pre-selected) - Declared employee count range (required, one of: "2-12", "13-29", "30-49", "50-99", "100-199", "200-350") - Admin email (required, valid email format, unique across all tenants) - Admin full name (required, min 2 chars) - Admin phone number (required, CI format: +225 XX XX XX XX XX, unique across all tenants) - RCCM number (optional — present in form, NOT mandatory. Never blocks onboarding or payment.) - Module selection (required, at least 1 module) - Free trial option (optional, checkbox: "Démarrer un essai gratuit de 14 jours") - IF admin email OR phone already exists in system THEN show "Un compte existe déjà avec ces informations" and block registration - IF fuzzy company name match detected (Levenshtein distance < 3 OR normalized match) THEN flag for ALTARYS ops review (do NOT block registration) - hCaptcha validation required on submit - Rate limit: max 3 registration attempts per IP per hour
FR-CP-002 [BOTH]: Email Verification - IF self-registration form submitted successfully THEN send verification email with a single-use link (expires in 24 hours) - Verification email sent within 60 seconds of form submission - Tenant status = PENDING_VERIFICATION until link is clicked - IF link expired THEN tenant can request a new verification email (max 3 resends per 24h)
FR-CP-003 [BOTH]: Post-Verification Routing - IF tenant clicked free trial checkbox during registration THEN status transitions to TRIAL (see FR-CP-010) - IF tenant did NOT click free trial checkbox THEN status transitions to PAYMENT_REQUIRED (see FR-CP-011) - In both cases, trigger tenant provisioning pipeline (FR-CP-008)
FR-CP-004 [BOTH]: Channel B — CRM Salesperson Onboarding - IF salesperson triggers "Convert prospect to tenant" from CRM THEN create tenant with status = PENDING AND record salesperson_id on tenant entity AND send two emails to tenant admin: - Email #1: password creation link (expires 7 days) - Email #2: payment link (with module selection summary and pricing) AND activation email includes an opt-out link: "Si cette inscription est une erreur, cliquez ici pour annuler" - IF opt-out link clicked THEN soft-delete the PENDING tenant and all associated data - ALTARYS ops receives weekly report of all CRM-created tenants for spot-check
FR-CP-005 [BOTH]: Channel B — No Approval Gate - Channel B does NOT require CRM manager approval before tenant creation - Salesperson commission is attributed ONLY when tenant payment is confirmed (not at registration) - This is a business policy documented in the CRM module; Control Plane tracks salesperson_id on tenant
FR-CP-006 [BOTH]: Channel C — Admin Back-Office (Post-MVP) - NOT in MVP scope - Interface contract: the admin console will support tenant creation with all fields (including overrides) - V2 implementation
FR-CP-007 [BOTH]: Duplicate Tenant Detection - On every tenant registration (Channel A, B, or C), check for duplicates: - Exact match on admin email → BLOCK registration, show error - Exact match on admin phone → BLOCK registration, show error - Fuzzy match on company name (Levenshtein distance < 3 after normalization: lowercase, remove "SARL", "SA", "SAS", "SUARL", trim whitespace) → FLAG for ops review, do NOT block - IF duplicate detected and flagged THEN create an alert in admin console for ALTARYS ops
B.1.2 Tenant Provisioning Pipeline¶
FR-CP-008 [BOTH]: Provisioning Steps - Triggered when tenant transitions to TRIAL, PAYMENT_REQUIRED, or PENDING (Channel B) - Pipeline executes the following steps in order: 1. Select DB profile: Auto-select based on declared employee count (see FR-CP-009). All 4 profiles available: A (Shared), B (Schema-per-tenant), C (DB-per-tenant), D (BYO-DB). 2. Provision database resources: - Profile A: No database creation needed. Seed data inserted with tenant_id. - Profile C: Create new PostgreSQL database for tenant. Run migrations. Seed data. 3. Seed country-specific reference data (from Country Configuration Engine): - ITS brackets (CI) - CNPS rates and ceilings (CI) - Default leave types per CI labor code - Public holidays for CI (current year + next year) - SYSCOHADA chart of accounts - Default employee categories (CI): Cadre, Agent de maîtrise, Employé, Ouvrier - Default contract types: CDI, CDD, Stage, Apprentissage, Journalier, CTT - Top 5 sector salary grids (CI) 4. Seed default organisational structure: - Create root Entity (type: ENTITY, name: tenant's company name) - Seed 6 default departments under root Entity: Direction Générale, Ressources Humaines, Comptabilité, Commercial, Production, Informatique - Departments are editable/deletable by tenant admin 5. Configure Keycloak: - Create Keycloak client for tenant (within single realm) - Create admin user with DG role - Seed default roles: DG, RH Manager, Comptable, Employé, Manager 6. Initialize module gating: Activate only the modules the tenant subscribed to 7. Initialize MinIO bucket: Create tenant-scoped storage bucket for document attachments 8. Send activation communication: Content varies per channel (see FR-CP-002, FR-CP-004) 9. Update tenant status: Based on channel and payment status (see state machine) - IF any provisioning step fails THEN: - Mark tenant as PROVISIONING_FAILED (internal status, not visible to tenant) - Alert ALTARYS ops via admin console - Retry automatically up to 3 times with exponential backoff - After 3 failures, require manual intervention
FR-CP-009 [BOTH]: DB Profile Auto-Selection (MVP) - IF declared employee count <= [threshold TBD — see OQ-CP-09] THEN Profile A (Shared) - IF declared employee count > [threshold TBD] THEN Profile C (DB-per-tenant) - Tenant can override the auto-selected profile during onboarding - MVP default: start with threshold = 200 (tentative, to be confirmed) - All 4 profiles (A, B, C, D) are available from MVP. Refine thresholds based on operational data.
B.1.3 Tenant Lifecycle State Machine¶
FR-CP-010 [BOTH]: Trial Status - IF tenant selected free trial AND has not used their one-time trial THEN status = TRIAL - Trial duration: 14 calendar days from activation - During TRIAL: all subscribed modules are fully active (read + write) - IF trial expires without payment THEN status → SUSPENDED - IF payment received during trial THEN status → ACTIVE (or ACTIVE_INCOMPLETE if legal info missing) - IF bank transfer chosen during trial THEN status → ACTIVE_PAYMENT_PENDING - Trial is one-time per tenant. Cannot be requested a second time.
FR-CP-011 [BOTH]: Payment Required Status - IF tenant did not select trial (Channel A) OR API-method payment failed THEN status = PAYMENT_REQUIRED - During PAYMENT_REQUIRED: - Tenant CAN log in - All modules are read-only - Tenant CAN invite users (users also see read-only) - Tenant CAN still request their one-time trial if not yet used - 14-day window to complete payment - IF payment received THEN status → ACTIVE (or ACTIVE_INCOMPLETE if RCCM/NIF missing) - IF bank transfer chosen THEN status → ACTIVE_PAYMENT_PENDING - IF 14 days elapsed without payment AND no trial requested THEN status → SUSPENDED
FR-CP-012 [BOTH]: Active Payment Pending Status - IF tenant chose bank transfer payment method THEN status = ACTIVE_PAYMENT_PENDING - During ACTIVE_PAYMENT_PENDING: - All subscribed modules are fully active (read + write) for 10 calendar days - Tenant can choose to pay via API-method at any time (replaces bank transfer expectation) - IF 10 days elapsed without payment confirmation THEN status → SUSPENDED - IF payment confirmed (manually by admin OR via webhook if available) THEN status → ACTIVE (or ACTIVE_INCOMPLETE)
FR-CP-013 [BOTH]: Active Incomplete Status - IF tenant has paid BUT has not provided RCCM and/or NIF THEN status = ACTIVE_INCOMPLETE - During ACTIVE_INCOMPLETE: - All subscribed modules are fully active (read + write) - System displays a persistent, dismissible banner: "Veuillez compléter vos informations légales (RCCM, NIF) dans les paramètres de votre entreprise." - Invoices issued during this period are proforma invoices (not FNE-compliant, as buyer NIF is missing) - IF RCCM and NIF provided THEN status → ACTIVE - IF 3 months elapsed without RCCM/NIF THEN: - Status transitions to SUSPENDED (read-only access) - Banner becomes non-dismissible: "Accès en lecture seule. Complétez vos informations légales pour restaurer l'accès complet." - A TenantStatusChanged event is published - Tenant receives a notification explaining read-only mode and how to restore full access
FR-CP-014 [BOTH]: Active Status - Status = ACTIVE when: - Payment confirmed AND - Legal info complete (RCCM + NIF provided) - All subscribed modules fully active - Subscription period tracked. System sends renewal alerts at 30, 15, and 7 days before expiry.
FR-CP-015 [BOTH]: Suspended Status
- IF subscription expired AND 10-day grace period elapsed (from expiry date)
OR IF admin manually suspends tenant
THEN status = SUSPENDED
- During SUSPENDED (MVP — strict read-only):
- ALL modules are read-only for ALL users (no new data creation of any kind)
- No new employees can be created
- No transactions (leave requests, payroll runs, commitments) can be initiated
- Existing data is viewable
- Tenant admin can still access billing/payment to reactivate
- IF payment received THEN status → ACTIVE
- IF 30 days elapsed without payment THEN status → DELETED (soft)
- V2: Per-module suspension — tenant_modules.status supports SUSPENDED from day one (data model ready), enforcement logic enabled in V2
FR-CP-016 [BOTH]: Deleted Status (Soft Delete) - DELETED is a soft delete. The tenant record and metadata are retained. - Data handling on deletion: 1. Accounting/payroll/financial data: retained for 10 years (AUDCIF Art. 24, mandatory) 2. Non-financial personal data (names, photos, contact info): anonymized within 90 days 3. Data archived to cold storage (MinIO) after anonymization 4. Archival storage is free for the 10-year legal period (cost absorbed by ALTARYS) - Before deletion: - Tenant can export ALL their data (data portability right) - MVP: export via admin support request. V2: self-service export tool. - IF admin manually requests restoration within 30 days of soft delete THEN tenant can be reactivated to SUSPENDED (pending payment)
FR-CP-017 [BOTH]: Manual Suspension by Admin - ALTARYS admin can suspend any tenant at any time from the admin console - Reason for suspension must be recorded in audit trail - Suspension email sent to tenant admin with reason - Reactivation also requires admin action (or payment, depending on reason)
B.1.4 Tenant State Machine Diagram¶
stateDiagram-v2
[*] --> PENDING_VERIFICATION : Channel A (self-register)
[*] --> PENDING : Channel B (CRM)
PENDING_VERIFICATION --> TRIAL : Email verified + trial selected
PENDING_VERIFICATION --> PAYMENT_REQUIRED : Email verified + no trial
PENDING --> PAYMENT_REQUIRED : Password set, no payment yet
PENDING --> ACTIVE_PAYMENT_PENDING : Password set + bank transfer
PENDING --> ACTIVE : Password set + API payment collected
TRIAL --> ACTIVE : Payment received
TRIAL --> ACTIVE_INCOMPLETE : Payment received, legal info missing
TRIAL --> ACTIVE_PAYMENT_PENDING : Bank transfer chosen
TRIAL --> SUSPENDED : 14 days expired, no payment
PAYMENT_REQUIRED --> TRIAL : One-time trial requested
PAYMENT_REQUIRED --> ACTIVE : Payment received
PAYMENT_REQUIRED --> ACTIVE_INCOMPLETE : Payment received, legal info missing
PAYMENT_REQUIRED --> ACTIVE_PAYMENT_PENDING : Bank transfer chosen
PAYMENT_REQUIRED --> SUSPENDED : 14 days expired
ACTIVE_PAYMENT_PENDING --> ACTIVE : Payment confirmed
ACTIVE_PAYMENT_PENDING --> ACTIVE_INCOMPLETE : Payment confirmed, legal info missing
ACTIVE_PAYMENT_PENDING --> SUSPENDED : 10 days expired
ACTIVE_INCOMPLETE --> ACTIVE : Legal info completed
note right of ACTIVE_INCOMPLETE : After 3 months → read-only sub-state
ACTIVE --> SUSPENDED : Subscription expired + grace
ACTIVE --> SUSPENDED : Admin manual suspension
SUSPENDED --> ACTIVE : Payment received / admin reactivation
SUSPENDED --> DELETED : 30 days no action
DELETED --> [*]
note right of DELETED : Soft delete. 10-year archive for financial data.
B.2 Functional Rules — Billing & Subscriptions¶
FR-CP-020 [BOTH] [CI]: Pricing Segments - Tenants are assigned to a pricing segment based on active employee count (employees with status ACTIVE or PRE_HIRE):
| Segment | Employee Count | Pricing Model |
|---|---|---|
| S1 | 2 – 12 | Flat fee / month |
| S2 | 13 – 29 | Flat fee / month |
| S3 | 30 – 49 | Flat fee / month |
| S4 | 50 – 99 | Per-user/month + Platform Fee |
| S5 | 100 – 199 | Per-user/month + Platform Fee |
| S6 | 200 – 350 | Per-user/month + Platform Fee |
- Segment determined at billing time by snapshot of active employee count
- IF employee count crosses a segment boundary mid-month THEN new segment applies on next billing cycle (no mid-cycle proration in MVP)
FR-CP-021 [BOTH] [CI]: Platform Fee Calculation (S4-S6)
- Platform Fee formula: FP = [1 + (x - 1) × 0.15] × base_fee
where x = number of active modules at billing time
- Base fees per segment per year (from pricing grid):
| Segment | Base Fee (annual) |
|---|---|
| S4 (50-99) | 360,000 FCFA |
| S5 (100-199) | 540,000 FCFA |
| S6 (200-350) | 660,000 FCFA |
- Platform Fee is a monthly charge: annual base / 12 × multiplier
FR-CP-022 [BOTH]: Pack Pricing (Discount Rules) - Packs are NOT first-class subscription entities - Packs are pricing rules: IF a tenant's subscribed modules match a pack combination THEN apply the discounted pack price instead of sum of individual prices - Pack detection runs at billing time - IF multiple packs could apply (overlapping modules) THEN apply the pack that gives the tenant the greatest discount - Pack definitions:
| Pack | Modules Included | Discount vs. Individual |
|---|---|---|
| Pack Contrôle | Absence + Présence QR | ~12-30% depending on segment |
| Pack Finance | NDF + Engagement/Budget | ~5-15% |
| Pack Essentiel | Core RH + Absences | ~17-20% |
| Pack Standard | Core RH + Absences + Présence QR | Incremental discount |
| Pack Complet | Core RH + Absences + Présence QR + NDF + Eval. Performance | Highest discount |
- Pack pricing details hardcoded in application code for MVP (DB-driven in V2)
FR-CP-023 [BOTH]: Invoice Generation
- Monthly invoice generated on the tenant's billing date (anniversary of first payment)
- Invoice entity includes FNE-ready fields from day one:
- Invoice number: sequential per fiscal year, format FAC-{YYYY}-{sequence_zero_padded} (e.g., FAC-2026-00001)
- Invoice date
- Seller: ALTARYS ENTERPRISE company details (name, RCCM, NIF, address)
- Buyer: tenant company details (name, RCCM if available, NIF if available)
- Line items: per module subscribed (name, quantity, unit price, total)
- Subtotal HT (hors taxes)
- TVA amount (18% for CI)
- Total TTC (toutes taxes comprises)
- QR code field (empty until FNE DGI integration in V2)
- FNE fiscal number field (empty until V2)
- Payment method
- Payment status: DRAFT | SENT | PAID | OVERDUE | CANCELLED
- IF tenant is ACTIVE_INCOMPLETE (no NIF/RCCM) THEN invoice is flagged as "Facture proforma" with buyer NIF field empty
- All amounts in XOF — zero decimal places (e.g., 15 000 FCFA, never 15 000,00)
- Invoice stored immutably. Corrections via credit notes (new invoice), never by editing an existing invoice.
FR-CP-024 [BOTH]: Payment Processing (Provider-Agnostic)
- The billing engine uses an abstract PaymentProvider interface:
interface PaymentProvider {
initiatePayment(invoiceId, amount, currency, returnUrl) → PaymentSession
handleWebhook(webhookPayload) → PaymentConfirmation
getPaymentStatus(paymentSessionId) → PaymentStatus
}
initiatePayment()
2. Provider returns redirect URL or widget → tenant completes payment
3. Provider sends webhook → system calls handleWebhook()
4. Payment confirmed → update invoice status to PAID → update tenant status per state machine
- IF webhook confirms payment THEN update occurs automatically
- IF bank transfer THEN admin confirms manually via admin console → payment recorded with admin's user_id in audit trail
- Manual fallback ALWAYS available regardless of active provider integrations
FR-CP-025 [BOTH]: Pending Payments Dashboard (Admin Console) - Admin console shows a "Paiements en attente" dashboard with: - All tenants in ACTIVE_PAYMENT_PENDING status (awaiting bank transfer) - All tenants in PAYMENT_REQUIRED status - All tenants with OVERDUE invoices - Days remaining until suspension for each - "Confirmer le paiement" action button per tenant - Dashboard refreshes automatically (WebSocket push or polling)
FR-CP-026 [BOTH]: Metering - Track per tenant per month: - Active employee count (status = ACTIVE or PRE_HIRE) - Active module list (module codes with status = ACTIVE) - Storage used (MinIO — relevant for future ADVDOC pricing) - Metering data used for: billing calculation, segment determination, platform metrics
B.3 Functional Rules — User Management & Authentication¶
FR-CP-030 [BOTH]: User Entity
- User attributes:
- id (UUID v7, server-generated for users — users are always created online)
- email (unique globally, login identifier)
- first_name
- last_name
- phone
- status: ACTIVE | DISABLED | INVITED
- home_tenant_id (FK to Tenant — the user's primary/home tenant)
- created_date, last_login_date
- A User is NOT necessarily an Employee. Example: external accountant is a User but not in the Employee registry.
- A User can belong to multiple tenants (via user_tenant_memberships table)
- Each membership has: user_id, tenant_id, role_ids[], joined_date
FR-CP-031 [BOTH]: User Invitation - Tenant admin invites user by email - IF email does not exist in system THEN: - Create user with status = INVITED - Send invitation email with password setup link (expires 7 days) - Add membership to current tenant with specified roles - IF email already exists (user in another tenant) THEN: - Add a new membership to the current tenant with specified roles - Send notification: "Vous avez été ajouté à l'entreprise {companyName}" - User can now access both tenants via tenant switcher - IF invited user does not complete registration within 7 days THEN link expires; admin can re-invite
FR-CP-032 [BOTH]: Keycloak Integration
- Single Keycloak realm for all tenants (MVP)
- Login flow:
1. User submits email + password → Keycloak authenticates
2. IF user belongs to 1 tenant → JWT issued with that tenant_id
3. IF user belongs to multiple tenants → system returns list of tenant memberships with tenant names + logos. User lands on home_tenant_id by default.
4. JWT claims: user_id (UUID), tenant_id (UUID), country_code (ISO), roles[] (role names), iat, exp
5. Access token expiry: 15 minutes
6. Refresh token expiry: 7 days
- Token exchange for tenant switching:
- User requests switch to tenant B
- System validates user has membership in tenant B
- New JWT issued for tenant B (no re-authentication required)
- Old JWT for tenant A is invalidated
- Password recovery: via Keycloak email flow
FR-CP-033 [BOTH]: Hybrid Permission Model
- JWT contains: user_id, tenant_id, country_code, roles[] (role names only, NOT individual permissions)
- On first login (or role change), frontend fetches the full permission matrix for the user's roles:
GET /api/v1/auth/my-permissions → returns { permissions: ["absence.leave_request.create", "absence.leave_request.approve", ...] }
- Frontend caches this permission matrix in IndexedDB (works offline)
- Frontend checks cached permissions for UI rendering (show/hide buttons, nav items)
- Backend ALWAYS validates permissions server-side on every request (never trusts frontend)
- Cache invalidation: on role change (notified via WebSocket), on app reload, on tenant switch
- Permission format: {module_code}.{entity}.{action}
- Examples: absence.leave_request.approve, payroll.payslip.read, cp.employee.create, cp.tenant.manage
- Actions: create, read, update, delete, approve, export
FR-CP-034 [BOTH]: Default Roles (Seeded per Tenant) | Role | Key Permissions | Scope | |---|---|---| | DG (Directeur Général) | Full access to all active modules within tenant | Tenant-wide | | RH Manager (Responsable RH) | Employee management, absence, attendance, payroll oversight | Tenant-wide | | Comptable (Accountant) | Budget, commitments, expenses, payroll (read), accounting | Tenant-wide | | Employé (Employee) | Self-service only: own leave, own payslips, own profile (read), clock in/out | Own data only | | Manager (Chef de Service) | Team-scoped approvals: approve leave for direct reports, view team data | Direct reports |
- Default roles are immutable (cannot be deleted or renamed by tenant admin)
- V2: Tenant admin can create custom roles with custom permission sets
FR-CP-035 [BOTH]: Module-Gated Permissions - A permission is effective ONLY IF: 1. The user's role includes it, AND 2. The corresponding module is ACTIVE for the tenant (per tenant_modules table), AND 3. The tenant status allows write operations (not SUSPENDED, not read-only mode) - IF module is INACTIVE for tenant THEN all permissions for that module are silently denied (no error — just hidden in UI, 403 on API) - IF tenant is SUSPENDED THEN all write permissions are denied; read permissions remain active
B.4 Functional Rules — Organization & Master Data¶
FR-CP-040 [BOTH]: Organisational Unit Tree - Each tenant has a hierarchical tree of organisational units - Unit types: ENTITY (root), SITE (optional, intermediate), DEPARTMENT (leaf) - Data model per unit: - id (UUID v7), tenant_id, name, type (ENTITY | SITE | DEPARTMENT), parent_id (FK to self), status (ACTIVE | INACTIVE), created_date, deactivated_date - Hierarchy rules: - Entity is always the root (parent_id = null) - MVP: one Entity per tenant. V2: multi-entity for corporate groups. - Site's parent must be an Entity - Department's parent can be an Entity (if no sites) OR a Site - No hard deletes — deactivation only (historical data integrity) - Deactivated units: existing employees remain assigned but new assignments are blocked - Seeded at onboarding: root Entity (from company name) + 6 default departments (Direction Générale, RH, Comptabilité, Commercial, Production, Informatique) - Default departments can be renamed, deactivated, or have new departments added by tenant admin
FR-CP-041 [BOTH]: Position Catalog - Each tenant has a position catalog - Data model per position: - id (UUID v7), tenant_id, title, description (optional), category_id (FK to EmployeeCategory), department_id (optional FK to OrgUnit — default assignment), status (ACTIVE | INACTIVE), is_custom (boolean) - Structured with escape hatch: - Tenant admin can create positions via the catalog management screen - When assigning a position to an employee, IF the desired position doesn't exist, the user can type a custom title → system auto-creates a new position in the catalog (is_custom = true) - Custom positions are available for reuse (appear in dropdown for future assignments) - Each position links to an EmployeeCategory (required). This link is critical: CNPS rates depend on category.
FR-CP-042 [BOTH] [CI]: Employee Categories - Categories are country-specific reference data - CI categories (seeded at onboarding): - Cadre (code: CAD) - Agent de maîtrise (code: AM) - Employé (code: EMP) - Ouvrier (code: OUV) - Data model per category: - id (UUID v7), tenant_id, country_code, name, code, description (optional), sort_order, is_system (boolean) - System-seeded categories (is_system = true) cannot be deleted. Can be deactivated. - Tenant admin can add custom categories (is_system = false) - Top 5 sector salary grids for CI (to be populated in Country Config): - Commerce, BTP (Bâtiment et Travaux Publics), Industrie, Services, Agriculture - Each sector has minimum salary per category - Grid stored in Country Config table (see FR-CP-070)
FR-CP-043 [BOTH]: Contract Type Reference - Contract types are per-country reference data - CI contract types (seeded at onboarding): - CDI — Contrat à Durée Indéterminée (max_duration_months: null) - CDD — Contrat à Durée Déterminée (max_duration_months: 24, renewable_once: true) - Stage — Internship (max_duration_months: 24) - Apprentissage — Apprenticeship (max_duration_months: 36) - Journalier — Daily worker (max_duration_months: null) - CTT — Contrat de Travail Temporaire (max_duration_months: 24) - Data model: id (UUID v7), tenant_id, country_code, name, code, max_duration_months (nullable), description, is_system, status - CDD requalification rule: IF cumulative CDD duration for one employee reaches 2 years THEN system generates alerts at 90, 30, and 7 days before limit (consumed by Employee Management module when built; rule stored in Country Config now)
FR-CP-044 [BOTH]: Manager Hierarchy (N/N+1)
- Each employee has 0 or 1 direct manager (N+1)
- Manager is an FK from Employee to Employee (self-referencing)
- Hierarchy is flat (N/N+1 only) in MVP. No multi-level chain traversal.
- Manager assignment is historised:
- manager_history table: employee_id, manager_id, start_date, end_date
- On manager change: previous record gets end_date = today, new record created with start_date = today
- History queryable: "Who was X's manager on date Y?"
- NULL manager allowed (see FR-CP-045 Default Approver)
- Event published: ManagerChanged (employeeId, tenantId, oldManagerId, newManagerId, effectiveDate)
FR-CP-045 [BOTH]: Default Approver
- Every tenant has a Default Approver (Approbateur par défaut)
- Default Approver is set during onboarding = the tenant admin user
- Configurable in company settings (can be changed to RH Manager or any other user)
- The Default Approver receives approval requests when the normal N+1 chain is broken:
- Employee has no manager assigned (manager_id = null)
- Employee IS the DG/CEO (no N+1 exists) — IF the DG is ALSO the Default Approver THEN their requests auto-approve
- Employee's manager has been terminated — requests route to Default Approver until HR reassigns
- When routing to Default Approver, system generates an alert to RH Manager: "L'employé {name} n'a pas de responsable assigné. Veuillez assigner un N+1."
- data model: tenant_settings.default_approver_user_id (FK to User)
B.5 Functional Rules — Employee Entity (Baseline)¶
FR-CP-050 [BOTH]: Employee Entity — Control Plane Baseline
- The Control Plane owns a minimal Employee entity. This is the baseline consumed by ALL modules.
- Attributes :
- id: UUID v7 (client-generated — works offline)
- tenant_id: UUID (FK to Tenant, mandatory)
- matricule: String - Unique sequential identifier for an employee within a tenant. Format: {PREFIX}-{YEAR}-{MM}-{SEQ}.
- first_name: String (required, min 2 chars)
- last_name: String (required, min 2 chars)
- email: String (optional — not all employees have email, especially ouvriers)
- phone: String (optional)
- contract_type_id: UUID (FK to ContractType reference — NOT free-text)
- org_unit_id: UUID (FK to OrganisationalUnit — department/site assignment)
- position_id: UUID (FK to Position catalog — NOT free-text)
- category_id: UUID (derived from position → category, OR directly assigned)
- hire_date: Date (required)
- manager_id: UUID (FK to Employee, self-referencing — nullable, see FR-CP-045)
- status: Enum (PRE_HIRE | ACTIVE | SUSPENDED | TERMINATED)
- status_effective_date: Date (when current status took effect)
- created_date, updated_date
- ON_LEAVE is NOT a status. Leave status is derived: IF an ACTIVE employee has an approved leave record overlapping today's date THEN UI displays "En congé" badge but underlying status remains ACTIVE.
FR-CP-051 [BOTH]: Employee Status Transitions
stateDiagram-v2
[*] --> PRE_HIRE : Employee created with future hire date
[*] --> ACTIVE : Employee created with today or past hire date
PRE_HIRE --> ACTIVE : Hire date reached
ACTIVE --> SUSPENDED : Administrative suspension (disciplinary, etc.)
SUSPENDED --> ACTIVE : Suspension lifted
ACTIVE --> TERMINATED : Employment ended
SUSPENDED --> TERMINATED : Employment ended during suspension
TERMINATED --> [*]
note right of TERMINATED : Soft delete. Data retained for compliance.
- PRE_HIRE → ACTIVE: Automatic transition on hire_date (batch job runs daily at 00:01 tenant local time)
- TERMINATED employees are never hard-deleted. Data retained per OHADA requirements.
- Status changes produce
EmployeeUpdatedevent with changedFields including "status"
FR-CP-052 [BOTH]: UUID v7 Platform-Wide Pattern - ALL entities created by users (employees, org units, positions, leave requests, attendance records, etc.) use UUID v7 as their primary key - UUID v7 is generated CLIENT-SIDE at creation time (including offline) - Benefits: - No ID conflicts between offline clients - Time-sortable (database index performance) - Permanent from birth — no temp-ID-to-real-ID remapping on sync - The server NEVER generates IDs for user-created entities. It validates UUID v7 format on receipt. - Server-generated entities (audit entries, system events, billing records) also use UUID v7 but generated server-side.
FR-CP-053 [BOTH]: Employee Matricule (Server-Assigned)
- Matricule is a human-readable employee number, separate from the UUID primary key
- Assigned by server on first sync (or immediately if created online)
- Format: {PREFIX}-{YEAR}-{MM}-{SEQ}.
- Prefix: {PREFIX} is a trigram or a 4-to-6 letter word derived from company name
- Year : {YEAR} current year of employee registration
- Month : {MM} is current month of employee registration
- Sequence: {SEQ} per-tenant, monthly auto-incrementing, zero-padded to 3 digits minimum, start over from 001 every month
- Tenant admin can configure: prefix, padding length
- While offline (before sync): UI shows "Numéro en attente" where matricule would appear
- Matricule is unique within tenant. Not globally unique.
- Matricule is immutable once assigned (never changes, even if employee is terminated)
FR-CP-054 [BOTH]: Offline Employee Creation - Employee form works offline (PWA + IndexedDB) - Offline creation flow: 1. User fills form → client generates UUID v7 → saves to IndexedDB 2. Matricule field shows "Numéro en attente" 3. Employee is immediately available for other offline operations (e.g., leave request submission references the UUID) 4. On connectivity: sync queue sends employee to server 5. Server validates, assigns matricule, persists to PostgreSQL 6. Server returns matricule → client updates IndexedDB record 7. Any entities referencing this employee's UUID remain valid (UUID was permanent from step 1) - Conflict detection on sync: - Duplicate email within tenant → flag for admin resolution - Duplicate phone within tenant → flag for admin resolution - No UUID conflicts possible (client-generated UUID v7)
B.6 Functional Rules — Module Gating¶
FR-CP-060 [BOTH]: Module Status Tracking
- tenant_modules table: tenant_id, module_code, status, activated_date, deactivated_date
- Module status values (MVP): ACTIVE | INACTIVE | TRIAL
- Module status values (V2): + SUSPENDED (per-module suspension)
- Sellable module codes: HRCORE, EMPMGT, ABSMGT, QRCONTR, TIMACT, EXPMGT, COMMIT, PAYROL, HSEMGT, PERFOB, GPEC, LTRAIN, ATSMGT, ADVDOC, CRMMGT
- Non-sellable: BUDMGT (auto-included — see FR-CP-061)
- Control Plane and Organization & Master Data are always active (platform foundation, not gated)
FR-CP-061 [BOTH]: Budget Lines Auto-Inclusion - IF EXPMGT OR COMMIT is ACTIVE THEN BUDMGT is automatically set to ACTIVE - IF NEITHER EXPMGT NOR COMMIT is ACTIVE THEN BUDMGT is automatically set to INACTIVE - BUDMGT never appears in module selection UI (not sellable) - BUDMGT status changes are recorded in audit trail with reason "Auto-inclusion rule"
FR-CP-062 [BOTH]: API-Level Module Gating
- Every API endpoint for a gated module checks: tenant_modules.status = ACTIVE for the relevant module
- IF module is INACTIVE for the requesting tenant THEN return HTTP 403 with body:
{
"error": "MODULE_NOT_SUBSCRIBED",
"module": "PAYROL",
"message": "Module non souscrit. Contactez votre administrateur pour activer ce module."
}
FR-CP-063 [PAPILLON ONLY]: Frontend Upsell Display - Unsubscribed modules appear in the navigation menu, greyed out and locked - Each locked module shows: - Module icon (greyed) - Module name - Brief one-line description - "Souscrire" (Subscribe) CTA button - IF user is tenant admin AND clicks "Souscrire" THEN redirect to billing/subscription management - IF user is NOT admin AND clicks "Souscrire" THEN show: "Contactez votre administrateur pour activer ce module." - [ENTERPRISE ONLY]: Enterprise frontend may use a different upsell pattern (V2 design decision)
FR-CP-064 [BOTH]: Module Deactivation (Downgrade) - When a tenant deactivates a module: 1. System checks for downstream dependencies and warns: - "Le module Paie utilise les données d'Absences. La désactivation d'Absences rendra ces données inaccessibles à la Paie." 2. IF admin confirms THEN module status → INACTIVE 3. Data from the deactivated module remains: - Readable by the tenant (read-only access to historical data) - Exportable by the tenant (data portability) - Readable by other active modules (e.g., Payroll can still read historical absence data) - NOT writable — no new data can be created for an inactive module 4. Module can be reactivated at any time → full access restored
B.7 Functional Rules — Country Configuration Engine¶
FR-CP-070 [BOTH] [CI]: Country Config Data Model - Country configuration is stored in DB tables (not hardcoded). Updatable by ALTARYS admin without redeployment. - Configuration is versioned per fiscal year (a country can change rates mid-year) - All application plane modules read country config from this engine via internal API
FR-CP-071 [BOTH] [CI]: Tax Configuration — ITS Brackets
- [TEMPORARILY VALIDATED — TV-CP-05]
- Stored as: country_tax_brackets table
- country_code, fiscal_year, bracket_lower, bracket_upper, rate_percent, effective_date
- CI ITS brackets (Ordonnance n° 2023-719 — unverified):
| Lower (FCFA/month) | Upper (FCFA/month) | Rate |
|---|---|---|
| 0 | 75,000 | 0% |
| 75,001 | 240,000 | 16% |
| 240,001 | 800,000 | 21% |
| 800,001 | 2,400,000 | 24% |
| 2,400,001 | 8,000,000 | 28% |
| 8,000,001 | ∞ | 32% |
- RICF (Réduction d'Impôt pour Charges de Famille): [OPEN — OQ-CP-02] — formula and deduction amounts not yet defined. Must be researched before Payroll module implementation.
is_verifiedflag on each rate record. ALTARYS admin dashboard shows unverified rates in orange.
FR-CP-072 [BOTH] [CI]: Social Contribution Configuration — CNPS Rates
- [TEMPORARILY VALIDATED — TV-CP-01 through TV-CP-04]
- Stored as: country_social_contributions table
- country_code, fiscal_year, branch_code, branch_name, employer_rate, employee_rate, ceiling_monthly (nullable), risk_class (nullable), effective_date, is_verified
| Branch | Code | Employer | Employee | Ceiling (FCFA/month) | Notes |
|---|---|---|---|---|---|
| Prestations Familiales | PF | 5.00% | 0% | No ceiling | |
| Maternité | MAT | 0.75% | 0% | No ceiling | |
| Accidents du Travail / Maladies Prof. | ATMP | 2.00–5.00% | 0% | No ceiling | Rate varies by risk class |
| Assurance Vieillesse | AV | 7.70% | 6.30% | 3,375,000 | Ceiling source: CNPS Communiqué 2023 (unverified) |
- AT/MP risk class: stored as a separate lookup table
atmp_risk_classes(class_code, rate, description). Tenant selects their risk class during company settings. - All rates flagged
is_verified = falseuntil decree numbers are confirmed.
FR-CP-073 [BOTH] [CI]: Employee Category Salary Grids
- Top 5 sector salary grids stored in: country_sector_salary_grids
- country_code, sector_code, sector_name, category_code, minimum_salary_monthly, effective_date
- Sectors: Commerce (COM), BTP (BTP), Industrie (IND), Services (SVC), Agriculture (AGR)
- Grid values to be populated during legal research for Payroll module
- Used by Payroll for minimum wage validation per category per sector
FR-CP-074 [BOTH] [CI]: Leave Types, Public Holidays, and SYSCOHADA - Leave types seeded per country: to be detailed in Absence Management PRD. Control Plane stores the reference data. - Public holidays for CI seeded per year. [BACKLOG: OQ-CP-06] — Find an API or method to provide national holidays for all 17 OHADA countries programmatically. - SYSCOHADA chart of accounts: standard OHADA chart (same across 17 countries), seeded at tenant onboarding for the Accounting module to consume.
B.8 Functional Rules — Audit Trail¶
FR-CP-080 [BOTH]: Audit Entry Structure - Every create/update/delete operation across ALL modules produces an immutable audit entry - Audit entry attributes: - id: UUID v7 (server-generated) - tenant_id: UUID - user_id: UUID (who performed the action) - timestamp: Instant (UTC) - entity_type: String (e.g., "Employee", "LeaveRequest", "Tenant") - entity_id: UUID - module_code: String (e.g., "CP", "ABSMGT", "PAYROL") - action: Enum (CREATE | UPDATE | DELETE) - before_value: JSONB (entity snapshot before mutation — null for CREATE) - after_value: JSONB (entity snapshot after mutation — null for DELETE) - metadata: JSONB (optional — extra context like "reason for suspension") - Audit entries are: - Immutable: no delete, no modify, ever - Tenant-scoped: tenant admin sees only their tenant's audit trail - Platform-scoped for admin: ALTARYS admin can query across all tenants - Retained 10 years minimum (AUDCIF Art. 24)
FR-CP-081 [BOTH]: Audit Trail Query API
- GET /api/v1/audit?entity_type=Employee&date_from=2026-01-01&date_to=2026-02-28
- Filterable by: entity_type, entity_id, user_id, action, module_code, date range
- Paginated (default 50 per page)
- Tenant-scoped automatically via TenantContext
- Export to CSV (V2)
FR-CP-082 [BOTH]: AUDCIF Art. 22 — NOT in Control Plane - AUDCIF-specific features (monthly period locking, quarterly closure, sequential numbering with hash chains) are the responsibility of the Accounting module - The Control Plane audit trail provides immutable, timestamped logging — this satisfies general traceability requirements - The Accounting module will implement additional integrity mechanisms (hash chains, period locks) on top of its own journal entries
B.9 Functional Rules — Notification Service¶
FR-CP-090 [BOTH]: Shared Notification Service - The Control Plane provides a centralized notification service used by ALL modules - Notification channels (MVP): in-app (bell icon) + email - Any module publishes a notification via internal API:
NotificationService.send(
tenantId, recipientUserId, channel (IN_APP | EMAIL | BOTH),
templateCode, templateParams, priority (LOW | NORMAL | HIGH | URGENT)
)
FR-CP-091 [BOTH] [CI]: MVP Email Templates (~10)
| Template Code | Trigger | Channel |
|---|---|---|
| ONBOARDING_WELCOME_A | Channel A: email verification | Email |
| ONBOARDING_WELCOME_B_PASSWORD | Channel B: password creation link | Email |
| ONBOARDING_WELCOME_B_PAYMENT | Channel B: payment link | Email |
| EMAIL_VERIFICATION | Email address verification (resend) | Email |
| PASSWORD_RESET | Password recovery request | Email |
| INVOICE_GENERATED | Monthly invoice created | Email |
| PAYMENT_RECEIVED | Payment confirmed | Email + In-App |
| PAYMENT_FAILED | API payment attempt failed | Email + In-App |
| SUBSCRIPTION_EXPIRING | 30/15/7 days before subscription expiry | Email + In-App |
| ACCOUNT_SUSPENDED | Tenant suspended | Email |
| ACCOUNT_REACTIVATED | Tenant reactivated after payment | Email + In-App |
| USER_INVITATION | User invited to a tenant | Email |
| MISSING_LEGAL_INFO | Reminder to complete RCCM/NIF (ACTIVE_INCOMPLETE) | Email + In-App |
B.10 Functional Rules — Admin Console (ALTARYS Operators)¶
FR-CP-100 [BOTH]: Admin Console — Separate Application
- The admin console is a separate React application:
- URL: non-public (e.g., admin.altarys.internal or IP-restricted)
- Desktop-only, no mobile-first requirement
- No offline support (ALTARYS staff have reliable connectivity)
- No PWA, no service workers
- Keycloak authentication with Platform Admin role (separate from tenant roles)
- Access restricted to ALTARYS staff with Platform Admin role
FR-CP-101 [BOTH]: Admin Console — MVP Features | Feature | Description | |---|---| | Tenant List | All tenants with: name, status, segment, employee count, active modules, created date, payment status. Filterable, sortable, searchable. | | Tenant Detail | Full tenant info: company details, subscription, module list, admin user, billing history, audit trail. | | Payment Management | Pending payments dashboard (FR-CP-025). Manual payment confirmation. Invoice history. | | Suspend / Reactivate | Manually suspend or reactivate a tenant. Reason required (recorded in audit). | | Module Override | Grant a tenant access to a module (e.g., trial module, promotional access). Override recorded in audit. | | CRM Registration Review | Weekly report of CRM-created tenants. Flag suspicious registrations. | | Basic Metrics | Total tenants by status, total active employees, revenue (MRR), top modules by subscription count. | | Country Config Management | View/edit tax rates, CNPS rates, leave types, holidays. Flag unverified rates. |
B.11 Data Requirements — Key Entities Summary¶
| Entity | Key Fields | Tenant-Scoped? | Module |
|---|---|---|---|
| Tenant | id, name, country_code, status, db_profile, segment, onboarding_channel, salesperson_id, default_approver_user_id | No (platform-level) | CP |
| TenantModule | tenant_id, module_code, status, activated_date, deactivated_date | Yes | CP |
| User | id, email, first_name, last_name, phone, status, home_tenant_id | No (cross-tenant) | CP |
| UserTenantMembership | user_id, tenant_id, role_ids[], joined_date | Yes | CP |
| Role | id, name, code, permissions[], is_system | No (platform-global) | CP — Roles are platform-level (SUPER_ADMIN, TENANT_ADMIN, MANAGER, EMPLOYEE, etc.). Tenants select which roles they use via UserTenantMembership but cannot create custom ones. |
| OrganisationalUnit | id, tenant_id, name, type, parent_id, status | Yes | CP (Org) |
| Position | id, tenant_id, title, category_id, department_id, is_custom, status | Yes | CP (Org) |
| EmployeeCategory | id, tenant_id, country_code, name, code, is_system | Yes | CP (Org) |
| ContractType | id, tenant_id, country_code, name, code, max_duration_months | Yes | CP (Org) |
| Employee | id, tenant_id, matricule, first_name,last_name, email, phone, contract_type_id, org_unit_id, position_id, category_id, hire_date, manager_id, status | Yes | CP |
| ManagerHistory | employee_id, manager_id, start_date, end_date | Yes | CP |
| Invoice | id, tenant_id, invoice_number, date, seller_, buyer_, line_items[], subtotal_ht, tva_amount, total_ttc, qr_code, fne_fiscal_number, payment_status | Yes | CP (Billing) |
| Payment | id, tenant_id, invoice_id, provider, amount, currency, status, provider_reference, confirmed_by_user_id | Yes | CP (Billing) |
| AuditEntry | id, tenant_id, user_id, timestamp, entity_type, entity_id, module_code, action, before_value, after_value | Yes | CP |
| Notification | id, tenant_id, recipient_user_id, template_code, channel, priority, read, created_date | Yes | CP |
| CountryTaxBracket | country_code, fiscal_year, bracket_lower, bracket_upper, rate, is_verified | No (reference) | CP (Config) |
| CountrySocialContribution | country_code, fiscal_year, branch_code, employer_rate, employee_rate, ceiling, is_verified | No (reference) | CP (Config) |
| CountrySectorSalaryGrid | country_code, sector_code, category_code, minimum_salary, effective_date | No (reference) | CP (Config) |
| PublicHoliday | country_code, year, date, name | No (reference) | CP (Config) |
| TenantSettings | tenant_id, fiscal_year_start_month, working_days_per_week, working_hours_per_day, currency, logo_url, matricule_prefix, matricule_padding, default_approver_user_id, atmp_risk_class | Yes | CP |
B.12 Multi-Tenant Implications¶
Profile A — Shared Database (MVP)¶
- All tenants in one PostgreSQL database
- Every table has
tenant_idcolumn (except reference data tables) - Mandatory TenantAwareRepository: All module repositories extend
TenantAwareRepositorybase class. Impossible to execute a query without tenant scoping. Enforced at the repository layer — "pit of success" design. - Spring Security filter extracts
tenant_idfrom JWT → setsTenantContext(ThreadLocal/ScopedValue) → every downstream query uses it - Indexes: all primary query patterns include tenant_id (composite indexes)
Profile C — DB-per-Tenant (MVP)¶
- Each tenant gets its own PostgreSQL database
- Tenant provisioning creates a new database, runs all migrations, seeds reference data
- Connection routing: DataSource resolved dynamically from tenant_id → database connection mapping
TenantAwareRepositorystill enforces tenant scoping (defense in depth — even though the DB itself is isolated)- Migrations must run against each tenant database individually (migration orchestrator)
- Backup/restore is per-tenant
Profile B (Schema-per-Tenant) and Profile D (BYO-DB)¶
- Data model designed to support all 4 profiles from day one
- Profile B: tenant_id → schema name mapping. Same DB, different schemas.
- Profile D: tenant provides their own DB connection string. ALTARYS manages no infra for that tenant's data.
B.13 Cross-Module Dependencies¶
Events Published by Control Plane¶
| Event | Trigger | Consumed By | Payload |
|---|---|---|---|
TenantProvisioned |
Provisioning pipeline complete | All modules (initialization) | tenantId, countryCode, dbProfile, subscribedModules[] |
TenantStatusChanged |
Any tenant status transition | All modules (enforce read-only, suspension) | tenantId, oldStatus, newStatus, reason |
TenantModuleActivated |
Module activated for tenant | Target module | tenantId, moduleCode, activatedDate |
TenantModuleDeactivated |
Module deactivated for tenant | Target module | tenantId, moduleCode, deactivatedDate |
EmployeeCreated |
New employee created | Absence, Attendance, Payroll | tenantId, employeeId, matricule, fullName, contractTypeCode, hireDate, categoryCode, orgUnitId |
EmployeeUpdated |
Employee data changed | Modules caching employee data | employeeId, tenantId, changedFields[] |
EmployeeStatusChanged |
Employee status transition | Absence, Attendance, Payroll | employeeId, tenantId, oldStatus, newStatus, effectiveDate |
OrganisationUnitCreated |
New dept/site created | Budget Lines, Reporting | unitId, tenantId, type, name, parentId |
OrganisationUnitDeactivated |
Unit deactivated | Budget Lines, Reporting | unitId, tenantId, deactivatedDate |
PositionCreated |
New position added | GPEC (V2) | positionId, tenantId, title, categoryId |
ManagerChanged |
Employee's N+1 updated | Absence, Commitment, Employee Mgmt | employeeId, tenantId, oldManagerId, newManagerId, effectiveDate |
UserRoleChanged |
User's roles updated | Frontend (cache invalidation) | userId, tenantId, newRoles[] |
ProspectConvertedToTenant |
CRM converts prospect (Channel B) | Control Plane (provisioning) | prospectId, tenantData, salespersonId |
APIs Exposed by Control Plane (Consumed by Other Modules)¶
| Endpoint | Method | Consumed By | Purpose |
|---|---|---|---|
/api/v1/cp/employees/{id} |
GET | Payroll, Absence, Attendance, all | Get employee data |
/api/v1/cp/employees?org_unit_id={id} |
GET | Absence (team view), Payroll | Get employees by department |
/api/v1/cp/employees/{id}/manager |
GET | Absence, Commitment | Get approval chain |
/api/v1/cp/org-units/{id} |
GET | Budget, Commitment, Reporting | Get org unit details |
/api/v1/cp/org-units/{id}/tree |
GET | Reporting, HR Core | Get org subtree |
/api/v1/cp/country-config/{countryCode}/tax-brackets |
GET | Payroll | Get ITS brackets |
/api/v1/cp/country-config/{countryCode}/social-contributions |
GET | Payroll | Get CNPS rates |
/api/v1/cp/country-config/{countryCode}/leave-types |
GET | Absence | Get leave type reference |
/api/v1/cp/country-config/{countryCode}/holidays/{year} |
GET | Absence, Payroll | Get public holidays |
/api/v1/auth/my-permissions |
GET | Frontend (all modules) | Get permission matrix for current user's roles |
/api/v1/cp/tenant/modules |
GET | Frontend (navigation) | Get active/inactive modules for current tenant |
/api/v1/cp/tenant/settings |
GET | All modules | Get tenant configuration (fiscal year, working days, etc.) |
/api/v1/cp/tenant/default-approver |
GET | Absence, Commitment, Expense | Get fallback approver |
/api/v1/cp/notifications |
POST | All modules | Publish notification through shared service |
Channel B Stub Endpoint (CRM Integration)¶
| Endpoint | Method | Purpose |
|---|---|---|
/api/v1/internal/tenants/from-prospect |
POST | Create tenant from CRM prospect data. Accepts: company name, country, employee count, admin email, admin name, phone, module selection, salesperson_id. Returns: tenantId, status. |
- This endpoint is "internal" — not exposed publicly. Authenticated with a service-to-service JWT (CRM module → Control Plane).
- For MVP testing: can be triggered manually via admin console or Swagger.
PART C — Constraints & Boundaries¶
C.1 Regulatory Compliance [CI]¶
C.1.1 Data Protection — Loi n° 2013-450 du 19 juin 2013¶
| Requirement | Implementation | Status |
|---|---|---|
| Déclaration préalable to ARTCI | ALTARYS must file before processing CI residents' personal data | NOT STARTED — PRE-LAUNCH BLOCKER (OQ-CP-03) |
| Cross-border transfer authorization | Required for Hetzner (Germany) hosting — non-CEDEAO country | NOT STARTED — PRE-LAUNCH BLOCKER (OQ-CP-03) |
| Data minimization (Art. 36) | Collect only data necessary for the declared purpose | Enforced by CP baseline entity design (minimal fields) |
| Consent | Tenant admin consents on behalf of their company at registration | CGV/CGU must include data processing consent clause |
| Data subject rights (access, rectification, deletion) | Employees can view their data (self-service); deletion overridden by AUDCIF 10-year rule for financial data | Implemented in Employee entity + audit trail |
| DPO requirement | ALTARYS should appoint a DPO given the volume and sensitivity of data | Legal/organizational action required |
| Breach notification | ARTCI must be notified within 48 hours of a data breach | Incident response procedure required (operational, not CP code) |
C.1.2 Electronic Invoicing — FNE (Facture Normalisée Électronique)¶
| Requirement | Implementation | Status |
|---|---|---|
| FNE mandatory since June 2025 (Arrêté n° 0337 du 9 mai 2025) | ALTARYS subscription invoices must be FNE-compliant | MVP: FNE-ready data model. V2: DGI API integration. |
| Sequential invoice numbering | FAC-{YYYY}-{zero_padded_sequence} |
Implemented from day one |
| Buyer NIF on invoice | Required for full FNE compliance | ACTIVE_INCOMPLETE tenants get proforma invoices until NIF provided |
| QR code + fiscal number | Required when DGI integration is live | Fields exist in invoice entity (empty until V2) |
| DGI API integration | Required to transmit invoices electronically | V2 — see OQ-CP-04 |
C.1.3 AUDCIF (Acte Uniforme relatif au Droit Comptable et à l'Information Financière)¶
| Requirement | Implementation | Status |
|---|---|---|
| Art. 24: 10-year retention | All financial/accounting records retained 10 years minimum | Enforced for: audit entries, invoices, payroll data (when Payroll ships) |
| Art. 22: Immutable records | Accounting entries cannot be modified or deleted | Control Plane audit trail is immutable. AUDCIF-specific period locking → Accounting module |
| Art. 20: Error correction | Errors corrected via negative entry (écriture de contrepassation), not by editing | Accounting module responsibility |
| Tenant deletion vs. retention | AUDCIF Art. 24 overrides data protection deletion rights | Soft delete + 10-year archive for financial data + anonymization of non-financial PII |
C.1.4 OHADA Labor Code — Employee Data¶
| Requirement | Implementation | Status |
|---|---|---|
| Code du Travail Art. 92.3: Registre d'employeur | Employer must maintain employee register with mandatory fields | CP Employee entity stores baseline fields. Full register → HR Core / Employee Management. |
| Employee categories (CCI) | Convention Collective Interprofessionnelle de CI defines 4 categories | Seeded in Country Config. Affects CNPS rates. |
| CDD 2-year limit (Art. 15.10) | CDD exceeding 2 years cumulative automatically becomes CDI | Rule stored in Country Config. Alerts at 90/30/7 days. Enforcement → Employee Management module. |
| Délégués du personnel (Décret 2024-901) | Companies with >10 workers must elect staff representatives | Threshold tracked. Per-establishment vs. per-company: OPEN (OQ-CP-01) |
C.1.5 Pre-Launch Legal Blockers¶
| Blocker | Action Required | Owner |
|---|---|---|
| ARTCI déclaration préalable | File declaration with ARTCI for personal data processing | Lawyer + ALTARYS management |
| ARTCI cross-border authorization | File for Hetzner (Germany) hosting authorization | Lawyer + ALTARYS management |
| CGV/DPA drafting | CGV must include AUDCIF liability disclaimers, data processing terms | Lawyer |
| DPO appointment | Appoint a Data Protection Officer | ALTARYS management |
C.2 Localization Requirements [CI]¶
| Aspect | Requirement |
|---|---|
| Language | French only (MVP). All UI strings externalized in translation files. |
| Currency | XOF — zero decimal places. Format: 15 000 FCFA (space as thousands separator) |
| Date format | DD/MM/YYYY (e.g., 26/02/2026) |
| Number format | Space as thousands separator: 1 000 000 |
| Phone format | CI: +225 XX XX XX XX XX (10 digits after country code) |
| Business hours | Default: Mon-Fri, 08:00-17:00 (configurable per tenant) |
| Timezone | GMT+0 (Abidjan — no daylight saving time in CI) |
C.3 Security Requirements¶
| Requirement | Implementation |
|---|---|
| Tenant data isolation | Mandatory TenantAwareRepository pattern. Every query scoped by tenant_id. Zero cross-tenant data leakage. |
| Authentication | Keycloak OIDC/OAuth2. JWT-based. No session cookies. |
| Authorization | Hybrid: JWT carries roles, backend validates permissions server-side on every request. |
| Encryption in transit | TLS 1.2+ on all endpoints. HSTS enforced. |
| Encryption at rest | PostgreSQL disk encryption. MinIO bucket encryption. |
| Password policy | Min 8 chars, must include uppercase + lowercase + digit. No common passwords. |
| Brute force protection | Keycloak: account lockout after 5 failed attempts (15-minute cooldown). |
| CAPTCHA | hCaptcha on public endpoints (self-registration, password reset). |
| Rate limiting | 3 registrations/IP/hour. API rate limiting TBD per-tenant (V2). |
| Audit trail | Every data mutation logged immutably. 10-year retention. |
| Admin console isolation | Separate app, non-public URL, Platform Admin role only. |
| Input validation | All API endpoints validate input (type, length, format, range). |
| SQL injection prevention | Parameterized queries via Spring Data JDBC. No raw SQL concatenation. |
| XSS prevention | React's default escaping. Content-Security-Policy headers. |
C.4 Out of Scope¶
| Item | Reason | When |
|---|---|---|
| HR Core module (full employee dossier) | Separate sellable module. CP provides baseline entity only. | Post-MVP (Order 7) |
| Employee Management module (administrative processes) | Separate sellable module. CP stores employee statuses but doesn't manage onboarding/offboarding workflow. | Post-MVP (Order 8) |
| Advanced Document Management (versioning, retention, search) | Separate sellable module. CP provides basic MinIO upload/download only. | Post-MVP (Order 10) |
| Payroll calculations | Payroll module responsibility. CP stores rates in Country Config but does not compute salaries. | MVP Order 5 |
| Leave request processing | Absence Management module. CP stores leave types in Country Config. | MVP Order 2a |
| Attendance recording | QR Attendance module. | MVP Order 2b |
| AUDCIF period locking + hash chains | Accounting module. CP audit trail is immutable but not AUDCIF Art. 22 compliant for accounting specifically. | Post-MVP (Order 9) |
| DB Profile B (Schema-per-tenant) | MVP — all 4 profiles ship in MVP. | MVP |
| DB Profile D (BYO-DB) | MVP — all 4 profiles ship in MVP. | MVP |
| SSO / OAuth (external) | Keycloak email+password for MVP. | V2 Enterprise |
| SMS notifications | Important for West Africa but not MVP. Email + in-app first. | V2 |
| Custom roles | Tenant-created roles. MVP has fixed 5 default roles. | V2 |
| Per-module suspension | Data model supports it from day one. Enforcement in V2. | V2 (within 6 months) |
| Mid-cycle proration | MVP: segment changes on next billing cycle. | V2 |
| Self-service data export | MVP: export via admin support. V2: self-service export tool. | V2 |
| FNE DGI API integration | MVP stores FNE-ready data. V2 transmits to DGI. | V2 |
| Multi-country configs | CI only for MVP. BEN + CMR at month 9. | Month 9 |
| XAF currency | V2 (CEMAC zone). | V2 |
| English / Portuguese | V2+ (Cameroun bilingual, Guinée Bissau). | V2+ |
| Cost centers | V2 data model expansion. | V2 |
| Multi-entity / group structures | V2 (corporate groups with subsidiaries). | V2 |
| Organigramme graphique | V2 (visual org chart rendering). | V2 |
| Tenant-facing invoicing tool | The platform generating FNE-compliant invoices FOR the tenant's own customers. | V2+ (new module) |