Aller au contenu

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
}
- MVP payment methods: - MOBILE_MONEY: Provider adapter TBD (Flutterwave, Orange Money, Wave CI, or MTN Money — first confirmed partner) - BANK_TRANSFER: Manual confirmation by ALTARYS admin - MANUAL: Admin override (marks any invoice as paid) - Payment flow: 1. Tenant selects payment method → system calls 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 EmployeeUpdated event 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."
}
- Module check is performed AFTER tenant resolution and authentication (the request must have a valid JWT with tenant_id before module gating is checked)

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_verified flag 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 = false until 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)
)
- In-app notifications: - Delivered via WebSocket when user is online - Queued and delivered on next login when user was offline - Read/unread status, mark-all-as-read - Notification bell shows unread count badge - Email notifications: - Sent via email delivery service (provider TBD: Brevo, Mailjet, or Amazon SES) - Templates stored in DB (editable by ALTARYS admin without deployment) - All emails Papillon-branded (MVP). V2: tenant logo customization. - French language

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_id column (except reference data tables)
  • Mandatory TenantAwareRepository: All module repositories extend TenantAwareRepository base class. Impossible to execute a query without tenant scoping. Enforced at the repository layer — "pit of success" design.
  • Spring Security filter extracts tenant_id from JWT → sets TenantContext (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
  • TenantAwareRepository still 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)
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)

END OF DOCUMENT