Story AB-002 : Modèle de données + Migrations Flyway¶
Module : absence-management Slice : VS-AB-002 from architecture Brand context : [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority : 2 Depends on : AB-001 Can develop concurrently with : Aucune Merge order : Après AB-001, Avant AB-003 Estimated complexity : M PRD User Stories : N/A - Technical Story
Objectif¶
Créer toutes les tables de base de données et les entités Spring Data JDBC pour le module Absence Management via des migrations Flyway, établissant la fondation de données pour toutes les stories suivantes. Ce slice définit le schéma canonique pour les demandes de congé, les soldes, les profils employé, les tables de configuration et les jours fériés — chaque story AB à partir de AB-003 dépend de ce schéma.
Périmètre Backend¶
Entités & Records¶
1. LeaveRequest
| Field | Type | Constraints |
|---|---|---|
| id | UUID v7 | PK, server-generated |
| tenant_id | UUID | NOT NULL |
| employee_id | UUID | NOT NULL |
| leave_type_code | VARCHAR(20) | NOT NULL |
| event_type_code | VARCHAR(30) | Nullable (pour le sous-type de congé exceptionnel) |
| start_date | DATE | NOT NULL |
| end_date | DATE | NOT NULL |
| half_day_start | BOOLEAN | NOT NULL, default false |
| half_day_end | BOOLEAN | NOT NULL, default false |
| number_of_days | DECIMAL | NOT NULL (jours ouvrables) |
| number_of_days_ouvrables | DECIMAL | NOT NULL |
| status | VARCHAR(30) | NOT NULL (DRAFT, PENDING_APPROVAL, APPROVED, REJECTED, CANCELLED) |
| reason | TEXT | Nullable |
| rejection_reason | TEXT | Nullable |
| cancellation_reason | TEXT | Nullable |
| approver_id | UUID | Nullable, FK conceptuel vers User |
| is_retroactive | BOOLEAN | NOT NULL, default false |
| supporting_document_id | UUID | Nullable |
| submitted_offline | BOOLEAN | NOT NULL, default false |
| created_at | TIMESTAMP WITH TIME ZONE | NOT NULL |
| updated_at | TIMESTAMP WITH TIME ZONE | NOT NULL |
| version | INT | NOT NULL, default 0 (optimistic locking) |
| created_by | UUID | NOT NULL |
2. LeaveEmployeeProfile
| Field | Type | Constraints |
|---|---|---|
| id | UUID v7 | PK, server-generated |
| tenant_id | UUID | NOT NULL |
| employee_id | UUID | NOT NULL, UNIQUE par tenant |
| annual_balance_ouvrables | DECIMAL | NOT NULL, default 0 |
| exceptional_days_used_this_period | DECIMAL | NOT NULL, default 0 |
| sick_days_current_rolling_12m | DECIMAL | NOT NULL, default 0 |
| sick_leave_phase | VARCHAR(20) | Nullable (FULL_PAY, HALF_PAY, UNPAID) |
| number_of_children_under_21 | INTEGER | NOT NULL, default 0 |
| reference_period_start | DATE | NOT NULL |
| reference_period_end | DATE | NOT NULL |
| last_accrual_date | DATE | Nullable |
| created_at | TIMESTAMP WITH TIME ZONE | NOT NULL |
| updated_at | TIMESTAMP WITH TIME ZONE | NOT NULL |
| version | INT | NOT NULL, default 0 |
3. LeaveBalanceLedger
| Field | Type | Constraints |
|---|---|---|
| id | UUID v7 | PK, server-generated |
| tenant_id | UUID | NOT NULL |
| employee_id | UUID | NOT NULL |
| leave_type | VARCHAR(20) | NOT NULL |
| transaction_type | VARCHAR(30) | NOT NULL (MONTHLY_ACCRUAL, SENIORITY_BONUS, CHILDREN_BONUS, DEDUCTION, CARRY_OVER, MANUAL_ADJUSTMENT) |
| amount | DECIMAL | NOT NULL (signé : positif = crédit, négatif = débit) |
| amount_calendaires | DECIMAL | NOT NULL |
| amount_ouvres | DECIMAL | NOT NULL |
| reference_id | UUID | Nullable (FK vers LeaveRequest ou autre source) |
| reason | TEXT | NOT NULL |
| created_at | TIMESTAMP WITH TIME ZONE | NOT NULL |
| created_by | UUID | NOT NULL |
Index : idx_balance_ledger_lookup(tenant_id, employee_id, leave_type, created_at)
4. MaternityDeclaration
| Field | Type | Constraints |
|---|---|---|
| id | UUID v7 | PK, server-generated |
| tenant_id | UUID | NOT NULL |
| employee_id | UUID | NOT NULL |
| expected_delivery_date | DATE | NOT NULL |
| actual_delivery_date | DATE | Nullable |
| leave_request_id | UUID | NOT NULL, FK vers LeaveRequest |
| created_at | TIMESTAMP WITH TIME ZONE | NOT NULL |
| created_by | UUID | NOT NULL |
5. ExceptionalLeaveConfig
| Field | Type | Constraints |
|---|---|---|
| id | UUID v7 | PK, server-generated |
| tenant_id | UUID | NOT NULL |
| event_type_code | VARCHAR(30) | NOT NULL |
| duration_days | DECIMAL | NOT NULL |
| cci_minimum_days | DECIMAL | NOT NULL |
| country_code | VARCHAR(2) | NOT NULL |
| created_at | TIMESTAMP WITH TIME ZONE | NOT NULL |
| updated_at | TIMESTAMP WITH TIME ZONE | NOT NULL |
Unique : (tenant_id, event_type_code)
6. PublicHoliday
| Field | Type | Constraints |
|---|---|---|
| id | UUID v7 | PK, server-generated |
| tenant_id | UUID | Nullable (null = valeur par défaut au niveau plateforme) |
| country_code | VARCHAR(2) | NOT NULL |
| year | INTEGER | NOT NULL |
| date | DATE | NOT NULL |
| name_fr | VARCHAR(100) | NOT NULL |
| holiday_code | VARCHAR(30) | NOT NULL |
| is_fixed | BOOLEAN | NOT NULL |
| source | VARCHAR(20) | NOT NULL (SYSTEM, TENANT_CUSTOM) |
| created_at | TIMESTAMP WITH TIME ZONE | NOT NULL |
Unique : (tenant_id, country_code, year, holiday_code) — Note : tenant_id nullable dans la contrainte unique, utiliser COALESCE ou index partiel.
7. CachedEmployeeData
| Field | Type | Constraints |
|---|---|---|
| id | UUID v7 | PK, server-generated |
| tenant_id | UUID | NOT NULL |
| employee_id | UUID | NOT NULL |
| employee_matricule | VARCHAR | NOT NULL |
| first_name | VARCHAR | NOT NULL |
| last_name | VARCHAR | NOT NULL |
| category | VARCHAR | NOT NULL |
| hire_date | DATE | NOT NULL |
| org_unit_id | UUID | NOT NULL |
| current_manager_id | UUID | Nullable |
| employee_status | VARCHAR | NOT NULL |
| synced_at | TIMESTAMP WITH TIME ZONE | NOT NULL |
Index : idx_cached_employee_lookup(tenant_id, employee_id)
8. LeaveTypeConfig
| Field | Type | Constraints |
|---|---|---|
| id | UUID v7 | PK, server-generated |
| tenant_id | UUID | NOT NULL |
| code | VARCHAR(20) | NOT NULL |
| name_fr | VARCHAR(100) | NOT NULL |
| is_paid | BOOLEAN | NOT NULL |
| requires_document | BOOLEAN | NOT NULL |
| requires_approval | BOOLEAN | NOT NULL |
| accrual_based | BOOLEAN | NOT NULL |
| max_days_per_year | DECIMAL | Nullable |
| is_system | BOOLEAN | NOT NULL, default true |
| is_active | BOOLEAN | NOT NULL, default true |
| country_code | VARCHAR(2) | NOT NULL |
| created_at | TIMESTAMP WITH TIME ZONE | NOT NULL |
| updated_at | TIMESTAMP WITH TIME ZONE | NOT NULL |
Unique : (tenant_id, code)
9. AbsenceSettings
| Field | Type | Constraints |
|---|---|---|
| id | UUID v7 | PK, server-generated |
| tenant_id | UUID | NOT NULL, UNIQUE |
| deduction_unit | VARCHAR | NOT NULL (OUVRABLES, OUVRES, CALENDAIRES) |
| reference_period_type | VARCHAR | NOT NULL (CALENDAR_YEAR, CUSTOM) |
| carry_over_policy | VARCHAR | NOT NULL (NO_CARRY_OVER, FULL_CARRY_OVER, PARTIAL_CARRY_OVER) |
| partial_carry_over_max_days | DECIMAL | Nullable (requis quand carry_over_policy = PARTIAL_CARRY_OVER) |
| created_at | TIMESTAMP WITH TIME ZONE | NOT NULL |
| updated_at | TIMESTAMP WITH TIME ZONE | NOT NULL |
10. SickLeaveIndemnizationRule
| Field | Type | Constraints |
|---|---|---|
| id | UUID v7 | PK, server-generated |
| tenant_id | UUID | NOT NULL |
| country_code | VARCHAR(2) | NOT NULL |
| employee_category | VARCHAR | NOT NULL |
| seniority_min_years | INT | NOT NULL |
| seniority_max_years | INT | Nullable |
| full_pay_months | INT | NOT NULL |
| half_pay_months | INT | NOT NULL |
| total_months | INT | NOT NULL |
| created_at | TIMESTAMP WITH TIME ZONE | NOT NULL |
| updated_at | TIMESTAMP WITH TIME ZONE | NOT NULL |
Unique : (tenant_id, country_code, employee_category, seniority_min_years)
Couche Repository¶
Tous les repositories sont des interfaces Spring Data JDBC dans le package com.altarys.papillon.modules.absence.repository :
LeaveRequestRepositoryextendsCrudRepository<LeaveRequest, UUID>:findByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId) -> List<LeaveRequest>findByTenantIdAndStatus(UUID tenantId, String status) -> List<LeaveRequest>-
findByTenantIdAndApproverId(UUID tenantId, UUID approverId) -> List<LeaveRequest> -
LeaveEmployeeProfileRepositoryextendsCrudRepository<LeaveEmployeeProfile, UUID>: -
findByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId) -> Optional<LeaveEmployeeProfile> -
LeaveBalanceLedgerRepositoryextendsCrudRepository<LeaveBalanceLedger, UUID>: -
findByTenantIdAndEmployeeIdAndLeaveType(UUID tenantId, UUID employeeId, String leaveType) -> List<LeaveBalanceLedger> -
MaternityDeclarationRepositoryextendsCrudRepository<MaternityDeclaration, UUID> -
ExceptionalLeaveConfigRepositoryextendsCrudRepository<ExceptionalLeaveConfig, UUID>: -
findByTenantIdAndEventTypeCode(UUID tenantId, String eventTypeCode) -> Optional<ExceptionalLeaveConfig> -
PublicHolidayRepositoryextendsCrudRepository<PublicHoliday, UUID>: findByCountryCodeAndYear(String countryCode, int year) -> List<PublicHoliday>-
findByTenantIdAndCountryCodeAndYear(UUID tenantId, String countryCode, int year) -> List<PublicHoliday> -
CachedEmployeeDataRepositoryextendsCrudRepository<CachedEmployeeData, UUID>: -
findByTenantIdAndEmployeeId(UUID tenantId, UUID employeeId) -> Optional<CachedEmployeeData> -
LeaveTypeConfigRepositoryextendsCrudRepository<LeaveTypeConfig, UUID>: findByTenantIdAndCode(UUID tenantId, String code) -> Optional<LeaveTypeConfig>-
findByTenantIdAndIsActiveTrue(UUID tenantId) -> List<LeaveTypeConfig> -
AbsenceSettingsRepositoryextendsCrudRepository<AbsenceSettings, UUID>: -
findByTenantId(UUID tenantId) -> Optional<AbsenceSettings> -
SickLeaveIndemnizationRuleRepositoryextendsCrudRepository<SickLeaveIndemnizationRule, UUID>: findByTenantIdAndCountryCodeAndEmployeeCategory(UUID tenantId, String countryCode, String category) -> List<SickLeaveIndemnizationRule>
Couche Service¶
N/A — Pas de logique métier dans ce slice. Uniquement schéma + entités + repositories.
Endpoints API¶
N/A — Aucun endpoint dans ce slice.
Règles de validation¶
- Tous les champs
DECIMALdoivent utiliserBigDecimalen Java — jamaisdoubleoufloat - Toutes les colonnes
DATEcorrespondent àjava.time.LocalDatedans les entités - Toutes les colonnes
TIMESTAMP WITH TIME ZONEcorrespondent àjava.time.Instantdans les entités tenant_idestNOT NULLsur toutes les entités saufPublicHoliday(nullable pour les valeurs par défaut au niveau plateforme)- Le champ
versionsurLeaveRequestetLeaveEmployeeProfileactive l'optimistic locking via@Version - Les contraintes d'unicité doivent inclure
tenant_idpour l'isolation tenant
Considérations multi-tenant¶
- Chaque table inclut une colonne
tenant_id(sauf PublicHoliday où elle est nullable pour les valeurs par défaut de la plateforme) - Toutes les contraintes d'unicité incluent
tenant_idpour éviter les collisions inter-tenant - Toutes les méthodes de requête des repositories incluent le paramètre
tenantId - Le schéma fonctionne sur les 4 profils DB : shared (filtrage par colonne tenant_id), schema-per-tenant (colonne tenant_id + isolation par schéma), DB-per-tenant (colonne tenant_id + isolation par DB), BYO-DB (colonne tenant_id + DB externe)
Migrations Flyway¶
Toutes les migrations dans db/tenant-migration/ (base de données tenant) :
V001__create_leave_request_table.sqlV002__create_leave_employee_profile_table.sqlV003__create_leave_balance_ledger_table.sqlV004__create_maternity_declaration_table.sqlV005__create_exceptional_leave_config_table.sqlV006__create_public_holiday_table.sqlV007__create_cached_employee_data_table.sqlV008__create_indexes.sqlV009__create_leave_type_config_table.sqlV010__create_absence_settings_table.sqlV011__create_sick_leave_indemnization_rule_table.sql
Périmètre Frontend¶
N/A — Story d'infrastructure backend.
Pages & Routes¶
N/A
Composants¶
N/A
États UI (TOUS REQUIS)¶
N/A — Backend uniquement.
Comportement responsive¶
N/A
Interactions¶
N/A
Critères d'acceptation¶
AC-004: All tables created with correct schema
Given the application starts with Flyway enabled
When all migrations run
Then all 10 tables are created with correct columns, types, constraints, and indexes
AC-005: Spring Data JDBC entities map correctly
Given the Spring Data JDBC repositories are defined
When a LeaveRequest entity is saved and retrieved
Then all fields round-trip correctly including BigDecimal amounts and DATE types
AC-006: Migrations are reversible
Given a migration has been applied
When a rollback is needed
Then each migration has a corresponding undo script or the migration is safely re-runnable
Règles OHADA & réglementaires¶
- Types de données : Tous les champs monétaires et de comptage de jours utilisent
DECIMAL/BigDecimalconformément aux exigences de calcul financier de la CI — jamaisfloatoudouble - Système à trois unités :
LeaveBalanceLedgerstockeamount(unité principale, ouvrables),amount_calendairesetamount_ouvrespour supporter les trois unités de calcul selon le droit du travail ivoirien - Jours ouvrables : Le champ
number_of_days_ouvrablessurLeaveRequestest l'unité canonique de déduction selon le Code du Travail Art. 25.2
Standards & conventions¶
docs/standards/java-spring-guidelines.md— §Records & sealed types, §Spring Data JDBC entitiesdocs/standards/database-guidelines.md— §Flyway migration naming, §Column types, §Timestamp types (utiliser TIMESTAMP WITH TIME ZONE, pas TIMESTAMP), §Multi-tenant unique constraintsdocs/standards/multi-datasource-patterns.md— §Tenant-scoped repositories, §Avoid CrudRepository for platform tablesdocs/standards/api-guidelines.md— §UUID v7 generation
Exigences de test¶
Tests unitaires¶
- Mapping des champs d'entité : tous les champs BigDecimal initialisés correctement
- Validation d'entité : le champ version commence à 0
- Valeurs d'enum : les enums status, transaction_type, deduction_unit contiennent toutes les valeurs attendues
Tests d'intégration¶
- Les migrations Flyway s'exécutent sans erreur sur une base de données vierge
- Les 10 tables existent après migration avec les colonnes et types corrects
- Le round-trip save + findById de LeaveRequest préserve correctement tous les champs (y compris la précision BigDecimal)
- La contrainte d'unicité de LeaveEmployeeProfile sur (tenant_id, employee_id) rejette les doublons
- L'index de LeaveBalanceLedger est utilisé pour les requêtes typiques (EXPLAIN ANALYZE)
Ce que la QA validera¶
- N/A — Pas de fonctionnalité visible par l'utilisateur. Le développeur vérifie les migrations et les mappings d'entités via les tests d'intégration.
Hors périmètre¶
- Logique métier, services ou endpoints API — uniquement tables + entités + repositories
- Données de seed pour LeaveTypeConfig ou ExceptionalLeaveConfig (fait dans les stories de configuration)
- Configuration frontend de quelque nature que ce soit
- Event listeners (AB-003)
- Logique d'accumulation (AB-004)
Définition de Done¶
- [ ] Tous les endpoints backend implémentés et retournant les réponses correctes — N/A (pas d'endpoints)
- [ ] Toutes les pages frontend s'affichent correctement à 360px, 768px, 1024px — N/A (backend uniquement)
- [ ] Les 5 états UI implémentés (loading, empty, error, offline, success) — N/A
- [ ] Le micro-copy français correspond exactement à la spec de design — N/A
- [ ] Tous les critères d'acceptation passent manuellement
- [ ] Tests unitaires écrits et passants
- [ ] Tests d'intégration écrits et passants
- [ ] Isolation multi-tenant vérifiée — Les contraintes d'unicité incluent tenant_id
- [ ] L'écriture + synchronisation offline fonctionne (Papillon) / N/A (Enterprise) — N/A (backend uniquement)
- [ ] Pas de TypeScript
any— N/A - [ ] Pas de Java raw types — tous les generics explicites
- [ ] Les réponses API suivent le format d'enveloppe standard — N/A (pas d'endpoints)
- [ ] Entrées de piste d'audit pour chaque mutation de données — N/A (pas de mutation de données dans ce slice)