Story CP-005: Organization Structure + Reference Data¶
Module: control-plane Slice: Slice 5 from architecture Brand context: [BOTH] Papillon HR Suite + ALTARYS ENTERPRISE HR Suite Priority: 5 Depends on: CP-001, CP-002, CP-004 Estimated complexity: M
Objective¶
Deliver the organizational backbone and reference data that every HR module depends on: the hierarchical org unit tree (Entity → Sites → Departments), the position catalog with escape-hatch creation, employee categories, and contract types. This slice establishes the master data structures. The Employee entity (CRUD, matricule, offline) is in CP-017 and CP-018.
Backend Scope¶
Entities & Records¶
OrgUnit entity (tenant DB):
| Field | Type | Constraints |
|---|---|---|
| id | UUID v7 | PK |
| tenant_id | UUID | NOT NULL |
| name | String | NOT NULL, min 2, max 200 |
| type | OrgUnitType enum | NOT NULL |
| parent_id | UUID | Nullable (null = root Entity) |
| status | OrgUnitStatus enum | NOT NULL, default ACTIVE |
| created_date | Instant | NOT NULL |
| deactivated_date | Instant | Nullable |
OrgUnitType: ENTITY, SITE, DEPARTMENT
OrgUnitStatus: ACTIVE, INACTIVE
Position entity (tenant DB):
| Field | Type | Constraints |
|---|---|---|
| id | UUID v7 | PK |
| tenant_id | UUID | NOT NULL |
| title | String | NOT NULL, min 2, max 200 |
| description | String | Nullable, max 500 |
| category_id | UUID | NOT NULL, FK to EmployeeCategory |
| department_id | UUID | Nullable, FK to OrgUnit (DEPARTMENT) |
| status | String | NOT NULL, default "ACTIVE" |
| is_custom | boolean | NOT NULL, default false |
| created_date | Instant | NOT NULL |
EmployeeCategory entity (tenant DB):
| Field | Type | Constraints |
|---|---|---|
| id | UUID v7 | PK |
| tenant_id | UUID | NOT NULL |
| country_code | String(2) | NOT NULL |
| name | String | NOT NULL, max 100 |
| code | String | NOT NULL, max 10 |
| description | String | Nullable |
| sort_order | int | NOT NULL |
| is_system | boolean | NOT NULL, default true |
| created_date | Instant | NOT NULL |
CI seed data: Cadre (CAD), Agent de maîtrise (AM), Employé (EMP), Ouvrier (OUV)
ContractType entity (tenant DB):
| Field | Type | Constraints |
|---|---|---|
| id | UUID v7 | PK |
| tenant_id | UUID | NOT NULL |
| country_code | String(2) | NOT NULL |
| name | String | NOT NULL, max 200 |
| code | String | NOT NULL, max 10 |
| max_duration_months | Integer | Nullable |
| description | String | Nullable |
| is_system | boolean | NOT NULL, default true |
| status | String | NOT NULL, default "ACTIVE" |
| created_date | Instant | NOT NULL |
CI seed data: CDI, CDD (max 24), Stage (max 24), Apprentissage (max 36), Journalier, CTT (max 24)
Note: Employee entity, ManagerHistory entity, and all Employee DTOs are in CP-017. EmployeeStatus enum (
PRE_HIRE, ACTIVE, SUSPENDED, TERMINATED) is defined in CP-017.
DTOs:
// OrgUnit
public record OrgUnitTreeResponse(UUID id, String name, OrgUnitType type, String status,
int employeeCount, List<OrgUnitTreeResponse> children) {}
public record CreateOrgUnitRequest(String name, OrgUnitType type, @Nullable UUID parentId) {}
// Position
public record PositionResponse(UUID id, String title, String categoryCode, String categoryName,
@Nullable String departmentName, boolean isCustom) {}
public record CreatePositionRequest(String title, UUID categoryId, @Nullable UUID departmentId,
@Nullable String description) {}
// Category
public record CategoryResponse(UUID id, String name, String code, String description, int sortOrder, boolean isSystem) {}
// ContractType
public record ContractTypeResponse(UUID id, String name, String code, @Nullable Integer maxDurationMonths,
String description, boolean isSystem, String status) {}
Repository Layer¶
OrgUnitRepository.java— Tenant-aware: findByParentId, findAll, savePositionRepository.java— Tenant-aware: findAll, findByTitle(search), saveEmployeeCategoryRepository.java— Tenant-aware: findAll, findByCodeContractTypeRepository.java— Tenant-aware: findAll, findByCode
Note: EmployeeRepository and ManagerHistoryRepository are in CP-017.
Service Layer¶
OrgUnitService.java:
- getTree(UUID tenantId) → List<OrgUnitTreeResponse> — Returns full tree with employee counts
- createUnit(CreateOrgUnitRequest) → OrgUnitTreeResponse — Validates hierarchy rules (Site parent = Entity, Dept parent = Entity or Site), name unique within parent
- deactivateUnit(UUID unitId) → void — Sets status=INACTIVE. Does NOT affect existing employee assignments. Blocks new assignments. Publishes OrgUnitDeactivated.
PositionService.java:
- list(UUID tenantId, @Nullable String search) → List<PositionResponse> — Search by title
- create(CreatePositionRequest) → PositionResponse — Creates a position. Sets is_custom=true for escape-hatch created positions.
Note: EmployeeService, MatriculeService, and all employee endpoints are in CP-017.
API Endpoints¶
| Method | Path | Request Body | Response | Auth |
|---|---|---|---|---|
| GET | /api/v1/org-units/tree | — | List |
Any authenticated |
| POST | /api/v1/org-units | CreateOrgUnitRequest | OrgUnitTreeResponse | ROLE_DG, ROLE_RH_MANAGER |
| PATCH | /api/v1/org-units/{id}/deactivate | — | 204 | ROLE_DG, ROLE_RH_MANAGER |
| GET | /api/v1/positions | ?search= | List |
Any authenticated |
| POST | /api/v1/positions | CreatePositionRequest | PositionResponse | ROLE_DG, ROLE_RH_MANAGER |
| GET | /api/v1/categories | — | List |
Any authenticated |
| GET | /api/v1/contract-types | — | List |
Any authenticated |
Note: All employee endpoints (
/api/v1/employees/*) are in CP-017.
Validation Rules¶
- OrgUnit: Site parent must be ENTITY. Department parent must be ENTITY or SITE. Name unique within parent.
- Position: title min 2, max 200. category_id required and must exist.
- EmployeeCategory: system categories (is_system=true) cannot be deleted, only deactivated.
- Financial calculations: N/A for this slice (no money fields).
Multi-Tenant Considerations¶
- ALL entities in this slice are in the tenant DB and tenant-scoped
- Every repository extends TenantAwareRepository
Flyway Migrations (Tenant DB)¶
V003__create_org_unit.sql—org_unitstableV004__create_position_category.sql—positions,employee_categories,contract_typestables + seed CI data
Note:
V005__create_employee.sql(employees, matricule_counters) is in CP-017.V006__create_manager_history.sqlis in CP-018.
Frontend Scope¶
Pages & Routes¶
/admin/settings/org-structure— Org tree page/admin/settings/categories— Category management/admin/settings/positions— Position catalog/admin/settings/contract-types— Contract type list
Note: All employee pages (
/rh/employees/*) are in CP-017.
Components¶
OrgTreePage.tsx,OrgTreeNode.tsx(component spec in design §19.4)CategoryListPage.tsxPositionCatalogPage.tsx,PositionCombobox.tsx(component spec in design §19.5)ContractTypeListPage.tsx
Note: EmployeeListPage, EmployeeCard, EmployeeRow, EmployeeDetailPage, EmployeeFormPage are in CP-017.
UI States (ALL REQUIRED)¶
Note: Employee List UI states are in CP-017.
Org Structure:
| State | Behavior | French Copy |
|---|---|---|
| Loading | Skeleton: tree outline | — |
| Empty | Message + CTA | "Votre structure organisationnelle est vide.\nCommencez par ajouter des départements." CTA: "Ajouter un département" |
| Error | Red toast | "Une erreur est survenue. Veuillez réessayer." |
| Offline (read) | Tree from IndexedDB, orange badge | "Hors ligne — données locales" |
| Offline (write) | Add unit queues for sync | "Unité enregistrée hors ligne ●" |
Position Catalog:
| State | Behavior | French Copy |
|---|---|---|
| Loading | Skeleton: 5 position items | — |
| Empty | Message | "Aucun poste défini.\nLes postes seront créés automatiquement lorsque vous ajouterez des employés." |
| Error | Red toast | "Une erreur est survenue. Veuillez réessayer." |
| Offline | List from IndexedDB | "Hors ligne — données locales" |
Responsive Behavior¶
Note: Employee List responsive behavior is in CP-017.
Org Structure: - 360px: Indented list with expand/collapse. Long-press → add child. Tap → bottom sheet with details. - 1024px+: Master (40%, tree with search filter) + detail panel (60%, selected unit info, inline edit, "Ajouter un sous-département" button).
Interactions¶
Note: Employee creation interactions (online + offline) are in CP-017 and CP-018.
Org tree interactions (mobile): - Tap node → toggle expand/collapse - Long-press parent → "Ajouter sous [name]" action - Tap node name → bottom sheet: name, type badge, status, employee count - In bottom sheet: edit name, deactivate toggle
Note: Employee card states (Active/Pre-hire/Suspended/Terminated/Pending sync) are in CP-017.
Acceptance Criteria¶
AC-036: Position escape-hatch creation
Given I am on the position catalog or employee form, position field
When I type "Technicien réseau" and no matching position exists
Then I see an option "Technicien réseau — Ajouter comme nouveau poste"
When I select it
Then the position is auto-created in the catalog (is_custom=true)
And I can select a category for the new position
AC-038: Org tree hierarchy rules
Given the root Entity "Acme SARL" exists
When I try to add a Site with parent = a Department
Then the validation rejects: Site parent must be the Entity
AC-042: Deactivated org unit blocks new assignments
Given department "Informatique" is deactivated
When I try to assign a new employee to "Informatique"
Then the validation rejects: cannot assign to inactive org unit
And existing employees in "Informatique" are NOT affected
AC-043: System categories cannot be deleted
Given the system category "Cadre" (is_system=true) exists
When I attempt to delete it
Then the operation is rejected
And I can only deactivate it
Moved ACs: AC-034, AC-037, AC-040, AC-041 → CP-017. AC-035, AC-039, AC-044, AC-045 → CP-018.
OHADA & Regulatory Rules¶
- Code du Travail Art. 92.3 (Registre d'employeur): The Employee entity stores baseline fields required for the employer register. Full register → HR Core module.
- CDD 2-year limit (Art. 15.10): CDD contracts exceeding 2 years cumulative automatically become CDI. The rule is stored in ContractType.max_duration_months. Alerts at 90/30/7 days — enforcement in Employee Management module, but the data model supports it from day one.
- Convention Collective Interprofessionnelle (CCI): Defines 4 employee categories (Cadre, AM, Employé, Ouvrier). Seeded as system categories. Affects CNPS contribution rates.
- Décret 2024-901: Companies with >10 workers must elect staff representatives (délégués du personnel). Employee count tracking enables this threshold check.
- Employee data is personal data (Loi 2013-450): first_name, last_name, email, phone are PII. Audit trail tracks all modifications. Terminated employees are never hard-deleted (AUDCIF Art. 24).
Standards & Conventions¶
docs/standards/java-spring-guidelines.md— §Entities, §UUID v7, §Optimistic locking, §Application Events, §Tree structuresdocs/standards/database-guidelines.md— §Self-referencing FKs, §Counter tables (FOR UPDATE), §Tenant-scoped unique constraintsdocs/standards/api-guidelines.md— §CRUD patterns, §Pagination, §Filteringdocs/standards/react-typescript-guidelines.md— §List/card/table components, §Form components, §Combobox pattern, §FABdocs/standards/offline-sync-guidelines.md— §IndexedDB sync queue, §UUID v7 client generation, §Conflict detection, §"Numéro en attente" pattern
Testing Requirements¶
Unit Tests¶
- OrgUnitService: hierarchy validation (Site parent = Entity, Dept parent = Entity/Site)
- OrgUnitService: name uniqueness within parent
- OrgUnitService: deactivation does not affect existing assignments
- PositionService: escape-hatch creation sets is_custom=true
- EmployeeCategoryService: system categories cannot be deleted
Integration Tests¶
- GET /api/v1/org-units/tree — correct tree structure with employee counts
- POST /api/v1/org-units — hierarchy validation
- POST /api/v1/positions — escape-hatch creation
- GET /api/v1/categories — seed data present
- GET /api/v1/contract-types — seed data present
- Tenant isolation: all entities scoped to tenant
What QA Will Validate¶
- Org tree: expand/collapse, add child, deactivate
- Position catalog: search, escape-hatch creation
- Category list: system categories cannot be deleted
- Contract type list displays correctly
- All pages render correctly at 360px, 768px, 1024px
Out of Scope¶
- Employee CRUD + Matricule — CP-017
- Employee Offline + Manager Hierarchy + PRE_HIRE Transition — CP-018
- Full employee dossier (photo, emergency contacts, bank info, documents) — HR Core module
- Employee onboarding/offboarding workflows — Employee Management module
- Bulk employee import — V2
- Employee self-service profile view — V2
- Org tree reordering (drag & drop) — V2
- Multi-entity support (multiple legal entities per tenant) — 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)
- [ ] Seed data present (CI categories + contract types)
- [ ] 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