Aller au contenu

Architecture Progress — ALTARYS Platform

Purpose: Living document read and updated by every ARCHITECT session. New ARCHITECT sessions read this FIRST to reuse decisions and avoid re-reading all prior *-arch.md files.


Completed Modules

Module Arch Doc Date Key Decisions
Control Plane docs/architecture/control-plane-arch.md 2026-02-27 6 Modulith modules (tenant, identity, billing, organization, audit, notification). ScopedValue for TenantContext. Dual DataSource (platform + tenant). Shadcn/ui + Tailwind for frontend. 13 vertical slices.
Budget Lines Management (BUDMGT) docs/architecture/budget-lines-management-arch.md 2026-02-28 Application Plane module. Pessimistic locking (SELECT FOR UPDATE) for budget mutations. Hybrid API: Java interface (sync) for COMMIT/EXPMGT + Application Events (async) for notifications. On-the-fly SQL aggregation for dashboard. 11 vertical slices in 6 tiers.
Absence Management (ABSMGT) docs/architecture/absence-management-arch.md 2026-03-12 CI MVP only. Ledger-based balance tracking (append-only, auditability). Event-driven employee cache (no Java interfaces, independent scaling). Pessimistic locking (SELECT FOR UPDATE) for balance mutations. Scheduled monthly accrual cron job. 19 vertical slices in 4 tiers. Supports all 4 DB profiles (Shared/Schema/DB-per-tenant/BYO-DB). ADR-14 revised: event-driven read models as default pattern; Java interfaces only for atomic sync operations (e.g., BudgetMutationApi).
Platform Vision (shared infra) docs/architecture/platform-vision-arch.md 2026-03-13 Cross-cutting infrastructure patterns for ALL modules. TenantContext propagation (ScopedValue + ThreadLocal fallback). Shared event catalog (com.altarys.papillon.events). Tenant ID enforcement (convention + RLS + CI test). Global error handling (single @ControllerAdvice + module error codes). Module gating interceptor (URL prefix → module code). Audit via events (zero coupling). Offline sync shared hook (useOfflineQueue). i18n namespace-per-module JSON (frontend-only). Per-module API versioning. Shared test-support module. docker-compose with 5 services. 8 vertical slices (5 Tier 0 + 3 Tier 1).
QR Attendance (QRCONTR) docs/architecture/qr-attendance-arch.md 2026-03-31 CI MVP only. Own CachedEmployeeData (ADR-14 pattern). Stored DailyAttendance table (pre-computed). Public holidays via events from ABSMGT (new PublicHolidayUpdated). @Scheduled + tenant iteration for batch (auto-close + weekly overtime). ZXing for QR generation. Client-generated UUID for offline sync. HMAC secret in tenant config table. Single module with package-based internal organization. 24 vertical slices in 6 tiers (T0-T5). Supports all 4 DB profiles.

Cross-Module Architectural Decisions

Patterns chosen that apply to ALL modules. Future ARCHITECTs MUST reuse these — do not re-decide.

# Decision Choice Rationale
ADR-01 TenantContext propagation ScopedValue (Java 25) Immutable within scope, virtual-thread safe, no context leaking
ADR-02 Backend deployment Single Spring Boot deployable (Control Plane + Application Plane) Team size, operational simplicity, Spring Modulith boundaries
ADR-03 Entity scoping Platform DB + Tenant DB separation BYO-DB clean split, tenant owns their data, platform data stays on ALTARYS infra
ADR-04 Caching strategy Redis (tenant config, module gating) + Caffeine (permissions, country config) Tiered by access pattern: speed for hot path, consistency for shared state
ADR-05 Offline sync conflict resolution Optimistic locking (version field) + side-by-side conflict UI Simplicity, works offline, user resolves edge cases
ADR-06 Frontend design system Shadcn/ui + Tailwind CSS Claude frontend-design compatibility, small bundle, CSS variable theming for dual-brand
ADR-07 Invoice numbering Per-tenant counter table, FOR UPDATE + single transaction FNE gap-free compliance
ADR-08 Notification delivery Spring WebSocket + STOMP + SockJS SockJS fallback for African proxy networks
ADR-09 Dual DataSource wiring Named DataSources with @Qualifier binding Explicit wiring, MVP both point to same DB, Profile C routing transparent
ADR-10 Audit trail storage Time-partitioned table in tenant data space Tenant ownership, partition pruning for 10-year scale
ADR-11 Provisioning pipeline Async event-driven (9 idempotent steps) Non-blocking UX, independently retryable steps
ADR-12 Pagination Cursor-based (keyset) pagination Consistent ordering with offline sync, no offset drift
ADR-13 Financial balance mutation locking Pessimistic locking (SELECT FOR UPDATE) Gold standard for balance mutations. Single-row lock, <50ms hold time. Prevents double-spending. Optimistic locking reserved for offline sync (ADR-05), not server-side concurrency.
ADR-14 Cross-module sync API pattern Event-driven read models (default) + Java interface for atomic sync (exception) Revised from "Java interface + events": Default pattern is Spring Application Events with event-driven caching for read-only data (employee hire date, org structure, manager assignments). Zero compile-time coupling, independent team scaling, eventual consistency acceptable for reads. Exception: Use Java interface (BudgetMutationApi pattern) ONLY for synchronous request-reply operations requiring atomic cross-module transactions (e.g., budget reserve/consume with immediate locking guarantee). This keeps monolith deployments simple while enabling future service splits. Note: control-plane-arch.md (section 1.2) references "REST API + Application Events" — written before this revision. CP arch should be updated in its next revision to document event-driven consumption pattern. BudgetMutationApi (BUDMGT) is a grandfathered exception; new modules MUST NOT create Java interfaces for read-only data access.
ADR-15 Dashboard aggregation strategy On-the-fly SQL (no materialized views) At MVP scale (≤500 rows per tenant), SUM is trivial (<2ms). No stale data risk. Add materialized views in V2 if Enterprise tenants exceed 10,000 lines.
ADR-16 TenantContext propagation (detailed) ScopedValue (Java 25) + ThreadLocal fallback ScopedValue for HTTP requests (immutable, virtual-thread safe). ThreadLocal fallback for @Scheduled, tests, Flyway, @Async. Dual-read pattern: TENANT_CONTEXT.isBound() ? TENANT_CONTEXT.get() : THREAD_LOCAL_FALLBACK.get(). See platform-vision-arch.md §3.
ADR-17 Cross-module event contracts Shared event module (com.altarys.papillon.events) Dedicated package with ALL cross-module event records. Spring Modulith shared kernel. Organized by domain subdirectory. Every module depends on this thin catalog. Enables future microservice extraction as shared Maven dependency.
ADR-18 Tenant ID enforcement Convention + RLS + CI test (3 layers) Every @Query includes WHERE tenant_id = :tenantId (convention). PostgreSQL RLS as defense-in-depth for Profile A (CI scan fails if tenant_id missing). No AOP/custom annotation magic. Explicit, debuggable, safe.
ADR-19 Error handling strategy Global @ControllerAdvice + module error codes Single GlobalExceptionHandler guarantees uniform { data, meta, errors } envelope. Module error codes: <MODULE>_<NNN>_<DESCRIPTION>. Backend sends codes only, frontend translates.
ADR-20 Offline sync framework Shared useOfflineQueue() hook Single hook in packages/shared-components/. Modules provide endpoint + optional conflict resolver. IndexedDB persistence, FIFO, exponential backoff, 409 conflict detection. Workbox Background Sync as safety net.
ADR-21 Audit trail integration Application Events (audit module as consumer) Zero coupling — modules publish events (per ADR-14), Audit module listens. Guaranteed delivery via EVENT_PUBLICATION table. No explicit audit service calls. Coverage = event coverage.
ADR-22 i18n architecture Namespace-per-module JSON (frontend-only) One JSON file per module per locale (locales/fr/absence.json). Backend sends error codes + params, never translated text. API stays language-agnostic. react-i18next with namespace lazy-loading.
ADR-23 Module gating Spring ModuleGatingInterceptor Single interceptor maps URL prefix → module code. Checks TenantModule status from Redis-cached lookup. Returns 403 if not active. CP endpoints not gated. Zero code in business modules.
ADR-24 API versioning Per-module independent versioning Each module versions independently (/api/v2/absence/* coexists with /api/v1/budget/*). Old version stays alive until all clients migrate. Industry standard (Stripe, Twilio).
ADR-25 Shared test infrastructure test-support module AbstractTenantIntegrationTest base class, TestTenantFactory, MockJwtDecoder, TestEventCollector, QueryTenantIdScanner (CI enforcement). All module tests extend same foundation.
ADR-26 Module initialization pattern Core rule: Each module MUST consume ModuleActivated filtered by its own moduleCode to seed its tenant reference tables from corresponding country_*_defaults platform table. No lazy-init in service methods — config state is guaranteed by activation listener. Listener must be idempotent (check-then-insert). Reference implementation: AttendanceModuleActivationListener (QR-004b). Platform DB read rule: A module MAY read via @Qualifier("platformJdbcClient") the Platform DB tables it owns (i.e., declared with its name in the Shared Entities Registry). It MUST NOT read directly Platform DB tables owned by another module (CP, billing, etc.) — use Spring Application Events or REST API for cross-module data access. This is NOT a Spring Modulith boundary violation (no Java import involved) but an explicit ownership rule. ModuleInitializer interface rule: Each module MUST define its own <Module>ModuleInitializer strategy interface within its own module package (e.g., com.altarys.papillon.modules.attendance.config.AttendanceModuleInitializer). Do NOT place this interface in the shared events package (com.altarys.papillon.events) — that package is reserved for event records only (ADR-17). Cross-module consistency is enforced by this ADR (convention), not by a shared Java type (which would cause all modules' initializers to be injected together via Spring collection autowiring). All implementations MUST declare @Order.
ADR-27 Cross-cutting access control layering Three-layer model: (1) HTTP entry layer (Control Plane owned) enforces "is this tenant allowed to do anything at all right now": TenantResolutionFilter validates tenant exists + is operable; ModuleGatingInterceptor (CP-007) returns 403 MODULE_NOT_SUBSCRIBED if module inactive; suspended-tenant write denial (CP-020) returns 403 TENANT_SUSPENDED for POST/PUT/PATCH/DELETE while reads remain allowed. (2) Service domain layer enforces per-entity domain rules: tenant ID matches every query (ADR-18), employee status is correct for the operation, target entity is in valid state. (3) Database layer (RLS) is the last-resort defense for Profile A. Rule: CP-owned checks happen once per HTTP request and MUST NOT be re-checked in service methods (would cause Modulith boundary violations and per-event duplication). Domain-owned checks MUST be re-checked in every service method, because services are also called from batch sync, scheduled jobs, and event consumers where the HTTP interceptors did not run. No shared @RequiresActiveEmployee-style annotation — each module owns its own domain guard against its own CachedEmployeeData projection (per-module policy variance, see java-spring-guidelines.md §16). See platform-vision-arch.md §12.5 for the security layers.

ABSMGT-Specific Architectural Decisions (Locked for Module)

These decisions are frozen for ABSMGT; future modules should follow ABSMGT's pattern unless there's a documented exception.

Decision Choice Rationale
Balance mutation concurrency Pessimistic locking (SELECT FOR UPDATE) on leave_employee_profile Prevents double-approval. Lock hold < 50ms. Matches ADR-13/BUDMGT pattern. Guarantees correctness.
Accrual execution Scheduled batch cron (monthly, idempotent via last_accrual_date check) No write side-effects on read, easier ops, handles all 4 DB profiles cleanly.
Cross-module employee data Event-driven read model (cache via EmployeeCreated, EmployeeUpdated, ManagerChanged) Zero compile-time coupling, independent team scaling. No Java interface imports. Eventual consistency (ms-level staleness) acceptable for read-only.
Public holidays storage Tenant DB only (seeded from platform defaults at tenant creation) Simplifies queries (single DB join), works with BYO-DB perfectly. Tenant overrides stored in same table with source=TENANT_OVERRIDE.
Offline leave requests Queued locally (IndexedDB), auto-submit on reconnect with submitted_offline=true flag Prevents data loss, server can log offline submissions separately.
Version column scope Offline sync conflict detection only (not server-side concurrency, that's the pessimistic lock's job) Optimistic locking reserved for offline (ADR-05); server mutations locked (ADR-13).

QRCONTR-Specific Architectural Decisions (Locked for Module)

Decision Choice Rationale
Employee data access Own CachedEmployeeData table (event-driven read model) Zero coupling to ABSMGT. Module works standalone. Follows ADR-14. Listens to EmployeeCreated/Updated/Terminated/ManagerChanged events.
DailyAttendance storage Stored (materialized) table, recomputed on clock event or regularization O(1) dashboard reads. Monthly reports are simple SELECTs. Recomputation is infrequent (regularization approval).
Public holidays Consume via PublicHolidayUpdated from ABSMGT + own cache table Zero coupling. Works standalone (falls back to platform-seeded holidays if ABSMGT not active). New event added to shared catalog.
Batch processing @Scheduled + tenant iteration (daily auto-close + weekly overtime) Same pattern as ABSMGT's monthly accrual. ThreadLocal TenantContext fallback. Idempotent via date/week markers.
QR generation library ZXing (com.google.zxing) Mature, Apache 2.0, widely used. Generates BufferedImage → PNG/SVG.
Offline sync dedup Client-generated UUID as idempotency key Proven offline-first pattern. Server returns success if UUID already exists.
HMAC secret storage qr_hmac_secret column in AttendanceModuleConfig (tenant DB) Simple, works across all 4 DB profiles. 256-bit random value, hex-encoded. BYO-DB: secret in tenant's own database. Generated at module activation by AttendanceModuleActivationListener (ADR-26). Never lazy-created.
Module initialization ModuleActivated listener seeds AttendanceModuleConfig + OvertimeConfig bands from country_attendance_config_defaults. No lazy-init. Idempotent. Follows ADR-26. Guarantees config state at activation time. Eliminates anti-pattern of first-access lazy creation.
Internal module organization Packages within single Modulith module One module (com.altarys.papillon.modules.attendance) with internal packages (.site, .schedule, .clockevent, etc.). No sub-modules.
Per-module domain guards Each service method that consumes employee data MUST call its own requireActiveEmployee(tenantId, employeeId) against AttendanceCachedEmployeeData (the module's own event-driven read model). No shared @RequiresActiveEmployee annotation. Loose coupling (modular monolith trade-off): each module decides what "active enough for this operation" means. Attendance accepts only ACTIVE; future modules may accept ACTIVE OR ON_LEAVE. Centralizing would require Set<Status> parameters that collapse back to per-module logic. Pattern documented in docs/standards/java-spring-guidelines.md §16. Reference implementation: ClockEventService.recordClockEvent (QR-006).

Shared Entities Registry

Entities defined so far. Future modules reference these — do NOT redefine.

Entity Owning Module Key Fields Storage
Tenant tenant id (UUID7), company_name, status (10-state FSM), country_code, db_profile Platform DB
TenantSettings tenant tenant_id, locale, timezone, fiscal_year_start_month, matricule_prefix, matricule_padding (INTEGER, default 3), working_days_per_week, working_hours_per_day, currency, logo_url, default_approver_user_id, atmp_risk_class Platform DB
TenantModule tenant tenant_id, module_code, status, activated_at Platform DB
CountryTaxBracket tenant country_code, fiscal_year, lower_bound, upper_bound, rate, is_verified Platform DB
CountrySocialContribution tenant country_code, contribution_type, employee_rate, employer_rate, ceiling Platform DB
CountrySectorSalaryGrid tenant country_code, sector_code, category, echelon, base_salary Platform DB
User identity id, email, full_name, status, home_tenant_id Platform DB
UserTenantMembership identity user_id, tenant_id, roles, joined_at Platform DB
Role identity (platform-global) id, code, label, permissions[], isSystem Platform DB — no tenant_id. Roles are platform-level (SUPER_ADMIN, TENANT_ADMIN, MANAGER, EMPLOYEE, etc.). Tenants select which roles they use but cannot create custom ones.
Permission identity code, module, description Platform DB
Employee organization id (UUID7), tenant_id, matricule, first_name, last_name, email, status, org_unit_id, position_id, manager_id, version Tenant DB
OrgUnit organization id, tenant_id, name, parent_id, level, status Tenant DB
Position organization id, tenant_id, title, org_unit_id Tenant DB
EmployeeCategory organization id, tenant_id, code, label Tenant DB
ContractType organization id, tenant_id, code, label, is_fixed_term Tenant DB
ManagerHistory organization employee_id, manager_id, effective_date, reason Tenant DB
MatriculeCounter organization tenant_id, prefix, next_value Tenant DB
Invoice billing id, tenant_id, invoice_number, period_start, period_end, total_amount, status Tenant DB
InvoiceLine billing invoice_id, module_code, unit_price, quantity, line_total Tenant DB
Payment billing id, invoice_id, amount, method, status, confirmed_at Tenant DB
PricingSegment billing code (S1-S6), min_employees, max_employees Platform DB
PackRule billing module_set, discount_percent Platform DB
TenantInvoiceCounter billing tenant_id, fiscal_year, next_sequence Tenant DB
AuditEntry audit id, tenant_id, entity_type, entity_id, action, user_id, timestamp, payload Tenant DB (partitioned)
Notification notification id, tenant_id, user_id, type, title, body, read, created_at Tenant DB
NotificationTemplate notification id, code, channel, subject_template, body_template Platform DB
Budget budget id, tenant_id, fiscal_year_start, fiscal_year_end, effective_start, label, status (DRAFT/ACTIVE/CLOSED), validated_by, closed_by, version Tenant DB
BudgetCategory budget id, tenant_id, name, code, sycohadaAccount_prefix, is_system, status, sort_order, version Tenant DB
BudgetLine budget id, tenant_id, budget_id, category_id, label, allocation (BigDecimal), committed, spent, org_unit_id, enforcement_level, thresholds, version Tenant DB
BudgetTransfer budget id, tenant_id, budget_id, source_line_id, dest_line_id, amount, justification, status (PENDING/APPROVED/REJECTED/CANCELLED), requested_by, approved_by Tenant DB
BudgetOverrideRequest budget id, tenant_id, budget_line_id, operation_type, requested_amount, available_at_request, source_entity_type, source_entity_id, status Tenant DB
BudgetSettings budget id, tenant_id (UNIQUE), default_enforcement_level, warn_threshold_pct, alert_threshold_pct, action_threshold_pct Tenant DB
ThresholdAlert budget id, tenant_id, budget_line_id, threshold_level, consumption_pct_at_trigger, resolved Tenant DB
LeaveRequest absence id (UUID7), tenant_id, employee_id, leave_type_code, start_date, end_date, number_of_days, number_of_days_ouvrables, status (FSM), approver_id, version Tenant DB
LeaveEmployeeProfile absence id, tenant_id, employee_id (UNIQUE), annual_balance_ouvrables (cached), reference_periods_start, last_accrual_date, version Tenant DB
LeaveBalanceLedger absence id, tenant_id, employee_id, leave_type, transaction_type (MONTHLY_ACCRUAL, LEAVE_DEBIT, etc.), amount (ouvrables, immutable), reference_id, created_at, created_by Tenant DB (append-only)
MaternityDeclaration absence id, tenant_id, employee_id, expected_delivery_date, actual_delivery_date, leave_request_id Tenant DB
CachedEmployeeData absence id, tenant_id, employee_id (event-synced), hire_date, org_unit_id, current_manager_id, category, status Tenant DB (read-model, synced via events)
ExceptionalLeaveConfig absence id, tenant_id, event_type_code, duration_days, cci_minimum_days, country_code Tenant DB
PublicHoliday absence id, tenantId, countryCode, year, date, holidayCode, source (PLATFORM | TENANT_OVERRIDE) Platform DB (reference) + Tenant DB (overrides seeded at tenant creation) — Absence module owns tenant copy, Control Plane seeds platform reference per country
LeaveTypeConfig absence id, tenant_id, code, name_fr, is_paid, requires_document, requires_approval, accrual_based, max_days_per_year, is_system, is_active, country_code Tenant DB
AbsenceSettings absence id, tenant_id (UNIQUE), deduction_unit, reference_period_type, carry_over_policy, partial_carry_over_max_days Tenant DB
SickLeaveIndemnizationRule absence id, tenant_id, country_code, employee_category, seniority_min_years, seniority_max_years, full_pay_months, half_pay_months, total_months Tenant DB
Site attendance id, tenant_id, name (unique per tenant), address, qr_token, qr_generated_at, is_active Tenant DB
Clock attendance id (client-generated UUID), tenant_id, employee_id, site_id, event_type (IN/OUT/BREAK_OUT/BREAK_IN), timestamp (device), sync_timestamp (server), device_id_hash, source, is_duplicate, auto_close_reason Tenant DB
DailyAttendance attendance id, tenant_id, employee_id, attendance_date (UNIQUE per employee+date), status, first_clock_in, last_clock_out, worked_minutes, scheduled_minutes, overtime_minutes, late_minutes, early_departure_minutes, is_auto_closed, has_anomaly, anomaly_details (JSONB) Tenant DB
WorkSchedule attendance id, tenant_id, name, is_active Tenant DB
WorkScheduleDay attendance id, schedule_id, day_of_week, is_working_day, start_time, end_time, break_start_time, break_end_time, break_tracking_enabled Tenant DB
EmployeeScheduleAssignment attendance id, tenant_id, employee_id, schedule_id, effective_from, effective_to Tenant DB
DepartmentScheduleAssignment attendance id, tenant_id, department_id, schedule_id, effective_from, effective_to Tenant DB
EmployeeSiteAssignment attendance id, tenant_id, employee_id, site_id, is_primary, effective_from, effective_to Tenant DB
RegularizationRequest attendance id, tenant_id, employee_id, request_date, reason, status (PENDING/APPROVED/REJECTED), corrected_clock_in, corrected_clock_out, is_direct_create, override_of Tenant DB
RemoteWorkRequest attendance id, tenant_id, employee_id, request_date, reason, status (PENDING/APPROVED/REJECTED/CANCELLED) Tenant DB
AttendancePublicHoliday attendance id, country_code, year, holiday_date, name_fr, holiday_code, is_fixed, source, tenant_id (nullable) Tenant DB (cache from ABSMGT events)
CompanyNonWorkingDay attendance id, tenant_id, day_date, name, is_recurring Tenant DB
OvertimeConfig attendance id, tenant_id, band_code, weekly_threshold_start, weekly_threshold_end, night_start_time, night_end_time, country_code Tenant DB
AttendanceModuleConfig attendance id, tenant_id (UNIQUE), qr_hmac_secret, default_late_tolerance_minutes, overtime_week_start_day, min_remote_work_advance_days, max_remote_work_per_week/month, auto_close_time, delegues_consultation_* Tenant DB
DepartmentAttendanceConfig attendance id, tenant_id, department_id, late_tolerance_minutes Tenant DB
WeeklyOvertimeSummary attendance id, tenant_id, employee_id, week_start_date (UNIQUE per employee+week), total_worked_minutes, band_1/2/3_minutes, night_minutes, sunday_holiday_minutes Tenant DB
AttendanceCachedEmployeeData attendance id, tenant_id, employee_id (UNIQUE), matricule, first_name, last_name, department_id, current_manager_id, hire_date, status Tenant DB (event-driven read model)
country_attendance_config_defaults attendance (reference, platform-owned) country_code (PK), default_late_tolerance_minutes, overtime_week_start_day, min_remote_work_advance_days, max_remote_work_per_week, max_remote_work_per_month, auto_close_time Platform DB (created by CP-001bis V006, managed by ALTARYS, seeded per country, read-only for tenants)
country_overtime_band_defaults attendance (reference, platform-owned) id (UUID PK), country_code, band_code, weekly_threshold_start, weekly_threshold_end, night_start_time, night_end_time — UNIQUE(country_code, band_code) Platform DB (created by CP-001bis V007, managed by ALTARYS, seeded per country, read-only for tenants — source for OvertimeBandsInitializer)

Event Contracts Registry

Spring Application Events published/consumed across module boundaries.

Early-stubbed event classes (contract exists on main, publishing logic still owed by an in-flight story):

Event Class Stub Created Branch Publishing Owner
EmployeeCreated 2026-04-22 feat/control-plane-employee-events CP-017
EmployeeUpdated 2026-04-22 feat/control-plane-employee-events CP-017
EmployeeTerminated 2026-04-22 feat/control-plane-employee-events CP-017 (termination op)
ManagerChanged 2026-04-22 feat/control-plane-employee-events CP-018

Stubs created early to unblock AB-003 / AB-022 / QR-002 compile paths. Do NOT mark CP-017 or CP-018 as done when stubs merge — publishing + integration tests are still owed by those stories.

Event Class Publisher Payload Consumers
TenantProvisioned tenant tenantId, companyName, countryCode, dbProfile identity (create Keycloak client + admin user), organization (seed org structure)
TenantStatusChanged tenant tenantId, oldStatus, newStatus billing (sync billing status)
ModuleActivated tenant tenantId, moduleCode, countryCode, activatedDate budget (seed categories + settings when BUDMGT; auto-activate BUDMGT when EXPMGT/COMMIT), absence, attendance (seed AttendanceModuleConfig + OvertimeConfig bands)
ModuleDeactivated tenant tenantId, moduleCode, deactivatedDate budget (auto-deactivate BUDMGT when neither EXPMGT nor COMMIT active)
UserRoleChanged identity tenantId, userId, oldRoles, newRoles audit
EmployeeCreated organization tenantId, employeeId, matricule, fullName, contractTypeCode, hireDate, categoryCode, orgUnitId audit, notification, absence, payroll
EmployeeUpdated organization tenantId, employeeId, changedFields audit
EmployeeStatusChanged organization (Control Plane module) tenantId, employeeId, oldStatus, newStatus audit, billing (employee count change)
EmployeeTerminated organization tenantId, employeeId, terminationDate, noticeEndDate absence (AB-022 settlement), qr-attendance (QR-002 cache), payroll
OrgUnitCreated organization tenantId, orgUnitId, name, parentId audit
OrgUnitDeactivated organization tenantId, orgUnitId audit, budget (flag affected budget lines)
PositionCreated organization tenantId, positionId, title audit
ManagerChanged organization tenantId, employeeId, oldManagerId, newManagerId, effectiveDate absence, commitment, expense (update approval chains)
EmployeeReassigned organization tenantId, employeeId, oldOrgUnitId, newOrgUnitId absence (update approver for pending requests), commitment, expense
PaymentConfirmed billing tenantId, invoiceId, paymentId, amount tenant (status sync), notification
ProspectConvertedToTenant CRM (future) prospectId, companyName, contactEmail tenant (create tenant via Channel B)
BudgetThresholdReached budget tenantId, budgetLineId, budgetLineName, thresholdLevel (WARN/ALERT/ACTION), consumptionPct, availableAmount notification
BudgetExhausted budget tenantId, budgetLineId, budgetLineName, enforcementLevel notification, commitment, expense
BudgetOverrideRequested budget tenantId, budgetLineId, overrideRequestId, requestedAmount, availableAmount, sourceEntityType, sourceEntityId notification
BudgetOverrideApproved budget tenantId, overrideRequestId, budgetLineId, amount, sourceEntityType, sourceEntityId commitment (unblock PENDING_BUDGET_OVERRIDE), expense
BudgetOverrideRejected budget tenantId, overrideRequestId, budgetLineId, sourceEntityType, sourceEntityId commitment (cancel), expense
BudgetTransferRequested budget tenantId, transferId, sourceLineName, destLineName, amount, requestedByName notification
BudgetTransferDecided budget tenantId, transferId, decision (APPROVED/REJECTED), decidedByName notification
BudgetValidated budget tenantId, budgetId, fiscalYear, validatedByName notification
BudgetClosed budget tenantId, budgetId, fiscalYear, closedAt commitment (flag open commitments)
LeaveApproved absence tenantId, employeeId, leaveRequestId, leaveType, startDate, endDate, numberOfDays (calendaires), numberOfDaysOuvrables (source of truth), isPaid, payRate (100, 50, 0), sickLeavePhase (FULL_PAY|HALF_PAY|ZERO_PAY|null), halfDayStart, halfDayEnd payroll (calculate deductions/indemnities), notification
LeaveCancelled absence tenantId, employeeId, leaveRequestId, leaveType, startDate, endDate, numberOfDaysCredited payroll (reverse deductions), notification
LeaveBalanceSettlement absence tenantId, employeeId, remainingBalance (calendaires), lastAccrualDate, terminationDate payroll (calculate indemnité compensatrice de congés payés)
LeaveRequestSubmittedNotification absence tenantId, employeeId, leaveRequestId, approverId, leaveType, startDate, endDate notification
LeaveRejectedNotification absence tenantId, employeeId, leaveRequestId, rejectionReason notification
MaternityLeaveCreated absence tenantId, employeeId, expectedDeliveryDate, leaveRequestId, preDeliveryStart, postDeliveryEnd notification, employee-mgmt (dismiss protection)
PublicHolidayUpdated absence tenantId (nullable for platform-wide), countryCode, year, holidays[] (date, nameFr, holidayCode, isFixed) attendance (cache refresh)
AttendanceRecorded attendance tenantId, employeeId, date, firstClockIn, lastClockOut, workedMinutes, overtimeMinutes, band1/2/3Minutes, nightMinutes, sundayHolidayMinutes, lateMinutes, earlyDepartureMinutes, status, isAutoClose payroll (future)
WeeklyOvertimeComputed attendance tenantId, employeeId, weekStartDate, weekEndDate, totalOvertimeMinutes, band1/2/3Minutes, nightMinutes, sundayHolidayMinutes payroll (future)

API Namespace Registry

/api/v1/ prefixes claimed by each module. New modules MUST NOT collide.

Prefix Module Scope
/api/v1/cp/tenant tenant Tenant-scoped tenant info & settings
/api/v1/cp/users identity User management within tenant
/api/v1/cp/org-units organization Org structure CRUD
/api/v1/cp/positions organization Position management
/api/v1/cp/categories organization Employee categories
/api/v1/cp/contract-types organization Contract types
/api/v1/cp/employees organization Employee CRUD
/api/v1/cp/country-config tenant Country reference data (read-only for tenants)
/api/v1/cp/billing billing Subscription, invoices, payments
/api/v1/cp/audit audit Audit trail queries
/api/v1/cp/notifications notification In-app notifications
/api/v1/auth/* identity (Control Plane module, no /cp/ prefix) Login, refresh, switch-tenant, permissions
/api/v1/public/onboarding tenant Self-registration (unauthenticated)
/api/v1/admin/* tenant, billing Platform admin console
/api/v1/budget/* budget Budget CRUD, lines, transfers, overrides, dashboard, settings, categories, export
/api/v1/absence/* absence Leave requests, balances, team calendar, HR dashboard, maternity, configuration, public holidays
/api/v1/qrcontr/* attendance Sites, QR codes, clock events, schedules, daily attendance, regularizations, remote work, overtime, reports, config, holidays, compliance, dashboard

API Footnotes: - BudgetMutationApi (Java interface, BUDMGT → COMMIT/EXPMGT): Grandfathered exception to ADR-14 for atomic cross-module transactions (budget reserve/consume with immediate locking). New modules MUST NOT create Java interfaces for read-only data access — use Spring Application Events + event-driven caching instead.


Open Questions

Decisions deferred to future modules. Future ARCHITECTs should resolve these when they become relevant.

# Question Context Deferred To
OQ-01 Full document management (versioning, retention policies) Control Plane stores basic files in MinIO. Advanced DMS deferred. HR Core / Document module
OQ-02 Advanced reporting / analytics engine Current modules expose query APIs only. BI layer TBD. Reporting module
OQ-03 ARTCI personal data filing Legal blocker for production. Does not block development. Pre-launch legal checklist
OQ-04 Multi-currency support beyond XOF/XAF Current design handles XOF (CI) and XAF (CM/BJ). EUR/USD TBD. International expansion
OQ-05 CQRS / event sourcing for high-write modules Current design uses simple CRUD + events. May need CQRS for Payroll at scale. Payroll module
OQ-06 Materialized views for budget dashboard at enterprise scale Current design uses on-the-fly SQL aggregation (ADR-15). If Enterprise tenants exceed 10,000 budget lines, may need materialized views or pre-computed summaries. BUDMGT V2 / Enterprise
OQ-07 Multi-currency budget support (XOF + XAF) MVP is XOF only. Bénin/Cameroun expansion (Month 9) needs XAF. Multi-currency budgets (Enterprise V2) need conversion rates and base currency. BUDMGT V2 + Country expansion
OQ-08 COMMIT module PENDING_BUDGET_OVERRIDE state BUDMGT returns OverrideRequired to COMMIT. COMMIT must implement PENDING_BUDGET_OVERRIDE commitment state and listen to BudgetOverrideApproved/Rejected events. COMMIT module architecture
OQ-09 BJ and CM leave accrual rules ABSMGT MVP is CI only (2.2 j./month CCI). Bénin and Cameroun may have different accrual rates, leave types, public holidays. Legal research needed. Absence V2 / Country expansion (Month 9)
OQ-10 Offline version conflict resolution UX Absence architecture supports 409 Conflict response with version mismatch. UX for side-by-side conflict resolution (show both versions, user choose) TBD in Design phase. ABSMGT Design → Stories
OQ-11 Payroll integration contracts Absence publishes LeaveApproved, Payroll consumes. Exact payload contract (sick leave indemnization phase, seniority bonus timing, maternity CNPS interaction) to be finalized in Payroll architecture. Payroll module design
OQ-12 CI overtime band verification (TV-QR-002) QRCONTR ships with configurable CI overtime band defaults (Band 1: 40-46h, Band 2: 46-48h, Band 3: >48h). These are temporarily validated and need verification against the CCI text and Art. 21.2-21.3 implementing decrees. Bands are tenant-configurable, so incorrect defaults can be corrected without code change. Pre-launch legal checklist
OQ-13 ARTCI declaration for QR attendance (OQ-QR-001) Is prior ARTCI declaration required before a company can deploy QR-based attendance? If yes, module onboarding must include ARTCI filing checklist. Currently ships compliance reminder only. Pre-launch legal checklist
OQ-14 Attendance data retention period (TV-QR-003) QRCONTR applies 10-year OHADA general retention. Needs verification whether attendance scan logs specifically fall under 10-year rule or 5-year registre d'employeur rule. 10 years is the conservative choice. Pre-launch legal checklist
OQ-15 PublicHolidayUpdated from ABSMGT QRCONTR consumes this new event. ABSMGT arch must be updated to publish it when public holidays are created/updated/deleted. If ABSMGT is not active for a tenant, QRCONTR falls back to platform-seeded holidays. ABSMGT arch revision
OQ-16 Weekly overtime recalculation on retroactive regularization When a regularization is approved for a date in a past week, the WeeklyOvertimeSummary for that week needs recalculation. This chain (regularization → DailyAttendance → WeeklyOvertime → re-publish event) needs careful testing. QRCONTR implementation
OQ-17 Apache POI dependency for Excel export QRCONTR needs Apache POI for .xlsx export. This is a large dependency (~10MB). Consider alternatives (FastExcel, EasyExcel) or evaluate if POI is already in the dependency tree. QRCONTR implementation
OQ-18 DDD event naming convention — drop "Event" suffix ModuleActivated adopts pure past-tense DDD naming (no "Event" suffix). All other events still use "Event" suffix (EmployeeCreated, LeaveApproved, etc.). Decision needed: apply new convention to all existing events at Month 9 expansion, or live with inconsistency. Low urgency — no runtime impact. Separate refactoring PR planned. Next architecture review
OQ-19 Module gating cache invalidation on TenantStatusChanged CP-007 specifies Redis cache (5 min TTL) + Caffeine fallback (1 min) for ModuleGatingService.isModuleActive(). Stale cache during a tenant SUSPENDED transition would let a non-paying tenant retain access for up to 5 minutes — security risk. Listener-based flush on TenantStatusChanged + ModuleActivated / ModuleDeactivated is needed; the TTL ceiling should also be lowered (≤2 min) defensively. Drafted as CP-007bis (docs/stories/control-plane/CP-007bis.md) for human review/conversion. CP-007bis (draft awaiting PO conversion)
OQ-20 Connection pooling behavior under SUSPENDED tenants (Profiles C/D) CP-020 enforces "reads allowed, writes denied" at the HTTP interceptor for SUSPENDED tenants. For Profile C (DB-per-tenant) and Profile D (BYO-DB), this needs a clear pattern at the DataSource layer — should the pool be marked read-only? Reduced size? Same as ACTIVE? Currently the interceptor catches it, but defense-in-depth at the connection layer is unspecified. Not MVP-blocking (HTTP layer is sufficient for now). Profile C/D activation (Enterprise tier)