Story AB-006: Absence Settings Configuration¶
Module: absence-management Slice: VS-AB-017 from architecture Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 6 Depends on: AB-002 Can develop concurrently with: AB-007, AB-008 Merge order: Any order with AB-007, AB-008 Estimated complexity: S PRD User Stories: N/A - Technical Story
Objective¶
Allow HR managers and DGs to configure tenant-level absence settings (deduction unit, reference period type, carry-over policy) through a backend API and frontend settings page. Settings are seeded with CI legal defaults at module activation.
Backend Scope¶
Entities & Records¶
AbsenceSettings (from AB-002, tenant DB):
| Field | Type | Constraints |
|---|---|---|
| id | UUID v7 | PK, server-generated |
| tenant_id | UUID | NOT NULL |
| deduction_unit | DeductionUnit enum | NOT NULL, default JOURS_CALENDAIRES |
| reference_period_type | ReferencePeriodType enum | NOT NULL, default CALENDAR_YEAR |
| carry_over_policy | CarryOverPolicy enum | NOT NULL, default STRICT_EXPIRY |
| partial_carry_over_max_days | BigDecimal | Nullable, used when PARTIAL_CARRY_OVER |
| created_at | Instant | NOT NULL |
| updated_at | Instant | NOT NULL |
Enums:
public enum DeductionUnit { JOURS_CALENDAIRES, JOURS_OUVRES, JOURS_OUVRABLES }
public enum ReferencePeriodType { CALENDAR_YEAR, ANNIVERSARY }
public enum CarryOverPolicy { STRICT_EXPIRY, PARTIAL_CARRY_OVER, UNLIMITED }
DTOs:
public record AbsenceSettingsResponse(
UUID id,
String deductionUnit,
String referencePeriodType,
String carryOverPolicy,
BigDecimal partialCarryOverMaxDays
) {}
public record UpdateAbsenceSettingsRequest(
@Nullable String deductionUnit,
@Nullable String referencePeriodType,
@Nullable String carryOverPolicy,
@Nullable BigDecimal partialCarryOverMaxDays
) {}
Repository Layer¶
AbsenceSettingsRepository.java:findByTenantId(UUID tenantId) → Optional<AbsenceSettings>
Service Layer¶
AbsenceSettingsService.java:
- getSettings(UUID tenantId) → AbsenceSettings — Returns current settings or seeds defaults if none exist
- updateSettings(UUID tenantId, UpdateAbsenceSettingsRequest request) → AbsenceSettings — Partial update, publishes AbsenceSettingsUpdated event
API Endpoints¶
| Method | Path | Request Body | Response | Auth |
|---|---|---|---|---|
| GET | /api/v1/absence/config/settings | — | AbsenceSettingsResponse | HR_MANAGER, DG |
| PATCH | /api/v1/absence/config/settings | UpdateAbsenceSettingsRequest | AbsenceSettingsResponse | HR_MANAGER, DG |
Validation Rules¶
- If
carryOverPolicy=PARTIAL_CARRY_OVER, thenpartialCarryOverMaxDaysmust be > 0 - If
carryOverPolicy!=PARTIAL_CARRY_OVER,partialCarryOverMaxDaysis ignored (set to null) deductionUnitmust be one of:JOURS_CALENDAIRES,JOURS_OUVRES,JOURS_OUVRABLESreferencePeriodTypemust be one of:CALENDAR_YEAR,ANNIVERSARYcarryOverPolicymust be one of:STRICT_EXPIRY,PARTIAL_CARRY_OVER,UNLIMITED- Changing
deductionUnitdoes NOT alter stored jours ouvrables values (display conversion only)
Multi-Tenant Considerations¶
- AbsenceSettings is stored in tenant DB, scoped by
tenant_id - All queries must include
tenant_idfilter - One row per tenant — upsert semantics on seed
Frontend Scope¶
Pages & Routes¶
/absences/parametres/types— Settings page (S11 in design spec, general policy section)
Components¶
AbsenceSettingsPage.tsx— Container page for all absence settings- Select component for
deductionUnit(3 options) - Select component for
referencePeriodType(2 options) - Select component for
carryOverPolicy(3 options) - Input (
type=number) forpartialCarryOverMaxDays— conditionally shown whencarryOverPolicy=PARTIAL_CARRY_OVER - Button (
variant="primary") for save
UI States (ALL REQUIRED)¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | Skeleton shimmer for 3 select fields | — |
| Empty | N/A (settings always seeded) | — |
| Error | Toast error on save failure | "Impossible d'enregistrer les paramètres. Réessayez." |
| Offline | Settings read from cache, save blocked | "Cette action nécessite une connexion internet" |
| Success | Toast on save | "Paramètres enregistrés" |
Responsive Behavior¶
- 360px (mobile): Single column, full-width selects, full-width save button
- 768px (tablet): Same layout, slightly wider margins
- 1024px+ (desktop): Card container within sidebar layout, max-width 600px
Interactions¶
- Page title: "Paramètres des congés"
- Deduction unit label: "Unité de décompte"
- Options: "Jours calendaires", "Jours ouvrés", "Jours ouvrables"
- Reference period label: "Période de référence"
- Options: "Année civile (jan-déc)", "Anniversaire d'embauche"
- Carry-over label: "Report de congés"
- Options: "Expiration stricte (loi)", "Report partiel", "Report illimité"
- Max days label: "Nombre maximum de jours reportables" (shown only when carry-over = "Report partiel")
- Save button: "Enregistrer"
Acceptance Criteria¶
AC-017: GET returns current settings
Given the ABSMGT module is active for a CI tenant
When HR calls GET /api/v1/absence/config/settings
Then the response contains current settings (reference period, carry-over, deduction unit) with CI defaults if never modified
AC-018: PATCH updates settings
Given HR is logged in with HR_MANAGER role
When they PATCH /api/v1/absence/config/settings with { deductionUnit: "JOURS_OUVRES" }
Then the setting is updated and the response reflects the new value
AC-019: CI defaults seeded at module activation
Given a new CI tenant activates the ABSMGT module
When the module initializes
Then AbsenceSettings is created with deductionUnit=JOURS_CALENDAIRES, referencePeriodType=CALENDAR_YEAR, carryOverPolicy=STRICT_EXPIRY
AC-020: Changing deduction unit does not alter stored values
Given employee balances are stored in jours ouvrables (source of truth)
When HR changes the deduction unit from JOURS_CALENDAIRES to JOURS_OUVRES
Then the stored annual_balance_ouvrables values remain unchanged (only display conversion changes)
OHADA & Regulatory Rules¶
- FR-ABS-004: Tenant configures deduction unit preference. Internal storage ALWAYS in jours ouvrables.
- FR-ABS-020: Reference period: CALENDAR_YEAR (default) or ANNIVERSARY.
- FR-ABS-022: Carry-over policies: STRICT_EXPIRY (default per CI law), PARTIAL_CARRY_OVER, UNLIMITED.
- FR-ABS-023: Compensatory payment in lieu of untaken leave is FORBIDDEN during employment (Art. 25.12 CT 2015).
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— §Entities, §Service layer, §Enumsdocs/standards/database-guidelines.md— §Multi-tenant repository pattern, §Tenant isolationdocs/standards/api-guidelines.md— §PATCH semantics, §Standard envelopedocs/standards/react-typescript-guidelines.md— §Form components, §Select patterndocs/standards/design-system.md— §Select, §Button, §Toast components
Testing Requirements¶
Unit Tests¶
- AbsenceSettingsService:
getSettings()seeds defaults when none exist - AbsenceSettingsService:
updateSettings()validates PARTIAL_CARRY_OVER requires max days > 0 - AbsenceSettingsService:
updateSettings()ignorespartialCarryOverMaxDayswhen policy is not PARTIAL_CARRY_OVER - AbsenceSettingsService:
updateSettings()rejects invalid enum values
Integration Tests¶
- GET /api/v1/absence/config/settings — returns seeded defaults for new tenant
- PATCH /api/v1/absence/config/settings — updates and returns new values
- PATCH with invalid carryOverPolicy + missing partialCarryOverMaxDays → 422
- Tenant isolation: tenant A settings not visible to tenant B
What QA Will Validate¶
- Settings page displays correctly on all breakpoints (360px, 768px, 1024px)
- All 3 selects populate with correct French options
- partialCarryOverMaxDays input shows/hides based on carry-over selection
- Save button triggers toast on success and error
- Offline state blocks save with appropriate message
Out of Scope¶
- Carry-over processing at period end (V2)
- Leave balance recalculation when deduction unit changes
- Per-leave-type settings (AB-007 handles leave type config)
- Audit log UI for settings changes (V2)
Definition of Done¶
- [ ] All backend endpoints implemented and returning correct responses
- [ ] All frontend pages render correctly at 360px, 768px, 1024px
- [ ] All 5 UI states implemented (loading, empty, error, offline, success)
- [ ] French micro-copy matches design spec exactly
- [ ] All acceptance criteria pass manually
- [ ] Unit tests written and passing
- [ ] Integration tests written and passing
- [ ] Multi-tenant isolation verified (no cross-tenant data leaks)
- [ ] Offline write+sync works (Papillon) — save blocked offline, read from cache
- [ ] No TypeScript
any— all types explicit - [ ] No Java raw types — all generics explicit
- [ ] API responses follow standard envelope format
- [ ] Audit trail entries created for every data mutation