Aller au contenu

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, then partialCarryOverMaxDays must be > 0
  • If carryOverPolicy != PARTIAL_CARRY_OVER, partialCarryOverMaxDays is ignored (set to null)
  • deductionUnit must be one of: JOURS_CALENDAIRES, JOURS_OUVRES, JOURS_OUVRABLES
  • referencePeriodType must be one of: CALENDAR_YEAR, ANNIVERSARY
  • carryOverPolicy must be one of: STRICT_EXPIRY, PARTIAL_CARRY_OVER, UNLIMITED
  • Changing deductionUnit does 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_id filter
  • 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) for partialCarryOverMaxDays — conditionally shown when carryOverPolicy = 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, §Enums
  • docs/standards/database-guidelines.md — §Multi-tenant repository pattern, §Tenant isolation
  • docs/standards/api-guidelines.md — §PATCH semantics, §Standard envelope
  • docs/standards/react-typescript-guidelines.md — §Form components, §Select pattern
  • docs/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() ignores partialCarryOverMaxDays when 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