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.mdfiles.
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) |