From d45d623259e266f09f1976b24d15a906ca0635e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 13:38:10 +0000 Subject: [PATCH] Turn the plan into an executable build specification Rewrites PLAN.md as a normative spec an orchestrator can build from end to end, rather than a proposal. Scope locked per owner decision: multi-location, Silae as the downstream payroll system, no time clock. Timeclock entities are dropped; actual hours are now manager-entered on the shift, with planned hours authoritative when absent. Adds the full Prisma schema, the permission catalogue, 17 compliance rule codes, the leave ledger contract, the Silae CSV format (UTF-8, semicolon, HS-/AB-/EV- prefixes), the route inventory, and 12 work packages with testable acceptance criteria. Collective-agreement values and Silae rubric codes are declared stop signals: the spec forbids inventing them and requires human input before production. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv --- PLAN.md | 927 ++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 775 insertions(+), 152 deletions(-) diff --git a/PLAN.md b/PLAN.md index b8c77a7..6cb3da3 100644 --- a/PLAN.md +++ b/PLAN.md @@ -1,236 +1,859 @@ -# PlanFlow — plan de réimplémentation +# PlanFlow — spécification de construction -> **Statut :** proposition, en attente de validation sur le périmètre et la stack (§2). -> **Source de vérité fonctionnelle :** [`Audit Combo/`](Audit%20Combo/INDEX.md) — 58 écrans, cartographie fonctionnelle et inventaire de crawl du 7 août 2026. -> **Régime :** clean room. Ce plan est une liste de capacités et d'invariants, pas un modèle de code ni de présentation. Voir §11. +> **Destinataire : un orchestrateur automatisé.** Ce document est la seule source d'instructions nécessaire pour construire l'application. Il est normatif : ce qui y est écrit fait foi, ce qui n'y est pas doit être demandé, jamais inventé. +> +> **Source fonctionnelle :** [`Audit Combo/`](Audit%20Combo/INDEX.md) — 58 écrans, cartographie fonctionnelle et inventaire de crawl du 7 août 2026. +> +> **Régime : clean room.** L'audit et ce plan décrivent des *capacités* et des *invariants*. Ne recopier ni code, ni CSS, ni icônes, ni illustrations, ni wording propriétaire au-delà des libellés métier nécessaires. Concevoir une API, un schéma, des textes et une interface originaux. + +--- + +## 0. Mode d'emploi pour l'orchestrateur + +1. Lire les sections 1 à 9 en entier avant d'écrire la moindre ligne. Elles définissent des invariants transverses ; les découvrir au lot 3 impose de refaire les lots 0 à 2. +2. Exécuter les lots de travail (§10) **dans l'ordre**. Chaque lot déclare ses dépendances, ses livrables et ses **critères d'acceptation**. Un lot n'est terminé que si tous ses critères passent en test automatisé. +3. **Signaux d'arrêt.** Interrompre et demander un arbitrage humain dans ces cas : + - une valeur numérique de convention collective est nécessaire (§6) ; + - un code de rubrique Silae est nécessaire (§8) ; + - une donnée personnelle réelle serait requise pour tester ; + - un point marqué **`À VALIDER`** dans ce document bloque l'avancement. +4. Ne jamais inventer de paramètre légal, de code de paie ou de règle métier. L'absence d'information est un signal d'arrêt, pas une invitation à choisir. +5. Langue : **interface et libellés en français**, **identifiants de code en anglais**, commentaires en anglais. --- ## 1. Contexte -Le dépôt contenait uniquement l'audit ; il n'y a aucun code applicatif. L'objectif est de construire **PlanFlow**, une application de gestion du personnel et des plannings reprenant les capacités de Combo pour l'organisation auditée. +Le dépôt ne contient que l'audit ; il n'y a aucun code. L'objectif est de construire **PlanFlow**, une application de gestion du personnel et des plannings multi-établissements, reprenant les capacités de Combo pour l'organisation auditée, avec la paie **exportée vers Silae**. -L'audit remplace la recherche documentaire publique et corrige plusieurs hypothèses de travail. Trois constats changent la conception : +L'audit corrige trois hypothèses qu'une lecture de la documentation publique de Combo aurait induites : -**1. La convention collective n'est pas HCR.** Le compte audité est **FROUARD DISTRIBUTION / La Foir'Fouille**, configuré sur **« Commerces de détail non alimentaires (IDCC 1517) — JF 50 % et Dimanche 100 % »** (`settings--collective-agreement`). C'est du commerce de détail, pas de la restauration. Les règles de première implémentation sont donc celles de l'IDCC 1517 et ses majorations jours fériés / dimanche — pas les durées maximales et coupures propres à l'hôtellerie-restauration. +**La convention collective n'est pas HCR.** Le compte audité est **FROUARD DISTRIBUTION / La Foir'Fouille**, configuré sur **« Commerces de détail non alimentaires (IDCC 1517) — JF 50 % et Dimanche 100 % »**. C'est du commerce de détail. Les durées maximales, coupures et majorations propres à l'hôtellerie-restauration ne s'appliquent pas. -**2. L'autorisation est par capacités, pas par rôles.** L'audit relève des permissions granulaires (`role-config.assign-owner-level`, droits distincts pour voir ses propres compteurs et ceux d'autrui, accès à tous les établissements, modification de périmètre) et des rôles **configurables** via `/settings/roles-permissions/:roleKey`. `Role`, `Permission` et `Scope` doivent être séparés dès le premier jour ; les écrans ne doivent jamais tester un nom de rôle. +**L'autorisation est par capacités, pas par rôles.** L'audit relève des permissions granulaires et des rôles **configurables par le client** (`/settings/roles-permissions/:roleKey`). `Role`, `Permission` et `Scope` sont trois notions distinctes dès le lot 0. Aucun écran ne teste un nom de rôle. -**3. Les compteurs de congés sont un registre d'écritures, pas un solde.** L'audit identifie `Counter` / `LedgerOperation` avec ajustements manuels protégés et prévision (`/paid_leave/user_contracts/:id/forecast`). Un solde stocké serait un contresens : c'est le cumul des écritures qui fait foi, et l'ajustement manuel doit être une écriture tracée comme les autres. +**Les compteurs de congés sont un registre d'écritures.** L'audit identifie `Counter` / `LedgerOperation` avec ajustements protégés et prévision. Un solde stocké serait un contresens : le solde est le cumul des écritures. -### Sur les identifiants transmis - -Je n'ai pas utilisé le login et le mot de passe fournis en message, et l'audit rend cet accès inutile. **Ce mot de passe doit être changé** : il a circulé en clair dans une conversation. - -### Écart de documentation - -`Audit Combo/INDEX.md` référence `../../matrice-conformite-rh-france-2026.md` (matrice de conformité juridique française), qui **n'est pas dans le dépôt**. Ce document conditionne §10 ; il faut l'ajouter ou corriger le lien. +### Écart de documentation — `À VALIDER` +`Audit Combo/INDEX.md` référence `../../matrice-conformite-rh-france-2026.md`, absent du dépôt. Ce document conditionne §12. Le demander avant le lot 2. --- -## 2. Décisions à valider +## 2. Périmètre — décisions arrêtées -Ces choix sont des hypothèses de travail, pas des acquis. Ils sont peu coûteux à changer maintenant, coûteux plus tard. +| Sujet | Décision | +|---|---| +| **Établissements** | **Multi-établissements** avec équipes. Un salarié peut être rattaché à plusieurs établissements. Reporting consolidé et par établissement. | +| **Paie** | **Export vers Silae** (§8). Aucun moteur de paie, aucun bulletin, aucune DSN dans PlanFlow. | +| **Pointeuse** | **Hors périmètre.** Pas de borne, pas de PWA kiosque, pas de pointage matériel. Les heures réelles sont saisies et validées par le manager (§7.3). | +| **Convention d'amorce** | **IDCC 1517**, moteur paramétrable pour en ajouter d'autres. | +| **Stack** | Next.js 15 (App Router, TypeScript strict) · PostgreSQL 16 + Prisma · Auth.js v5 · Tailwind + shadcn/ui · Zod · Vitest + Playwright · pnpm · `docker-compose` auto-hébergé. | +| **Mobile** | PWA installable, responsive. Pas d'application native. | -| Sujet | Proposition | Raison | -|---|---|---| -| **Périmètre v1** | Lots 0 à 2 (§7) : socle, planning, absences, dossier RH. Paie en **export**, pas en moteur. | L'audit montre que Combo lui-même sépare le produit paie (`/combo-pay-onboarding/*`, connecteurs Silae/ADP). Un moteur de paie + DSN est un second produit. | -| **Stack** | Next.js 15 (App Router, TS) · PostgreSQL + Prisma · Auth.js · Tailwind + shadcn/ui | Un déploiement, itération rapide, PWA sans stores. L'original est un SPA React/Vite + API Rails ; rien n'oblige à reprendre ce découpage. | -| **Déploiement** | `docker-compose` (app + Postgres) auto-hébergé, mobile en PWA | Données chez toi, pas d'ops cloud. | -| **CCN d'amorce** | **IDCC 1517**, moteur paramétrable pour en ajouter d'autres | Constat de l'audit. | +**Hors périmètre v1**, à ne pas construire : moteur de paie, DSN, bulletins de paie, distribution de bulletins, signature électronique qualifiée, transmission DPAE à l'URSSAF, connecteurs de caisse, abonnement et facturation, planning prédictif, auto-assignation. -**Questions ouvertes :** -1. Combien d'établissements et d'équipes réels faut-il couvrir en v1 ? -2. Un seul IDCC suffit-il, ou d'autres enseignes du groupe relèvent-elles d'autres conventions ? -3. Quel logiciel de paie en aval (l'audit voit Silae et ADP côté Combo) — c'est lui qui dicte le format d'export. -4. La pointeuse est-elle utilisée ? `settings/timeclock` est une option payante ; l'audit ne prouve pas qu'elle est active. +**Conservés mais différés au lot 5** : articles, conversations, analyses RH avancées. --- -## 3. Architecture +## 3. Architecture et invariants transverses -### Multi-tenant -`Account` → `Location` → `Team`. Base unique, **scoping par ligne**, appliqué par une extension Prisma qui injecte le périmètre de la session, plus **RLS PostgreSQL** en défense en profondeur. L'audit insiste : *« isolation multi-tenant et filtrage par périmètre côté serveur »* — le filtre ne doit jamais dépendre du client. +Ces sept invariants s'appliquent à tout le code. Ils ne sont pas négociables et chacun fait l'objet de tests dédiés. -### Autorisation par capacités -Trois tables distinctes : -- `Permission` — capacité stable nommée `ressource.action.qualificatif` (ex. `planning.publish`, `planning.create.on_published`, `payroll.period.lock`, `counters.read.others`) -- `Role` — jeu de capacités, **configurable par le client**, identifié par une clé stable -- `Scope` — périmètre du membership : établissements et équipes, plus un indicateur « tous établissements » +### 3.1 Isolation multi-tenant +Hiérarchie `Account` → `Location` → `Team`. Base unique, **scoping par ligne**. +- Une extension Prisma injecte `accountId` et le périmètre de la session sur **chaque** requête. +- **RLS PostgreSQL** activée sur toutes les tables portant `accountId`, en défense en profondeur. +- Le périmètre ne provient **jamais** d'un paramètre client. Il est dérivé de la session serveur. -Un unique point d'entrée serveur `can(membership, permission, resource)` ; **contrôle à chaque mutation**, jamais uniquement à l'affichage. +### 3.2 Autorisation +Point d'entrée unique `can(membership, permissionCode, resource?)` dans `src/domain/access/`. +- Vérification **à chaque mutation**, côté serveur, avant tout effet. +- L'affichage conditionnel est un confort, jamais une sécurité. +- Toute Server Action commence par un `can(...)` ; une action sans contrôle est un défaut bloquant. -Familles de capacités relevées par l'audit, à reprendre telles quelles comme référentiel de départ : planning (créer sur publié/non publié, éditer publié, voir non publié, publier, dépublier, valider, invalider, supprimer, dupliquer, imprimer, labels, notes, pauses, alertes, compteurs, non-assignés, actions de masse, planning prédictif), paie (accès, exports, CRUD période, période alternative, verrouillage, export Silae/brut, synchronisation), contrats (CRUD, suppression dans le passé, éligibilité DPAE, voir salaires, documents, demande de signature), absences (gestion, prévision, contournement du délai minimal, suppression, décisions), administration, abonnement. +### 3.3 Temps +- Chaque `Location` porte un fuseau IANA (défaut `Europe/Paris`). +- Instants stockés en `timestamptz`. Une colonne `localDate` dénormalisée sert **uniquement** au regroupement dans la grille. +- **Toute durée se calcule depuis les instants.** Un shift 22 h–06 h la nuit du changement d'heure dure 7 h ou 9 h, jamais 8 h. +- Les nuits traversantes appartiennent à la `localDate` de leur **début**. +- Tout module de date passe par `src/lib/datetime.ts`. Aucun appel direct à `new Date()` dans le domaine. -### Temps, fuseaux, nuits traversantes -Chaque `Location` porte un fuseau IANA — l'application auditée affiche d'ailleurs un bandeau quand le fuseau du compte diffère de celui du poste. Shifts stockés en **`timestamptz`** plus une colonne dénormalisée `localDate` servant **uniquement** au regroupement dans la grille. Toute durée se calcule depuis les instants : un shift 22h–06h la nuit du changement d'heure dure 7 h ou 9 h, jamais 8 h. +### 3.4 Concurrence +- Verrou optimiste `version` sur `WeeklySchedule`, `Shift`, `UserContract`, `PayPeriod`. Un `UPDATE` qui ne matche pas la version lève un conflit rendu à l'utilisateur. +- Recalcul des compteurs **dans la même transaction** que la mutation qui les affecte. +- Toute action de masse et tout export portent une **clé d'idempotence** ; un rejeu retourne le résultat initial sans réexécuter. -### Concurrence -Le planning est édité à plusieurs simultanément. **Verrou optimiste** (`version`) sur `WeeklySchedule` et `PayPeriod`, détection de conflit rendue à l'utilisateur, et **recalcul atomique des compteurs** dans la même transaction que la mutation de shift. Les actions de masse portent une **clé d'idempotence**. +### 3.5 Immutabilité et audit +- `LedgerOperation`, `AuditLog` et `PayrollExport` sont **append-only**. Une correction est une écriture inverse suivie d'une nouvelle écriture, jamais un `UPDATE` ni un `DELETE`. +- `AuditLog` capture auteur, horodatage, entité, avant/après et justification pour : contrat, avenant, absence et décision, publication et dépublication de planning, validation d'heures, ouverture et verrouillage de période, export, changement de rôle ou de permission, ajustement de compteur, accès à une donnée sensible. + +### 3.6 Données sensibles +- NIR, IBAN, BIC et pièces jointes de santé **chiffrés au repos** (chiffrement applicatif par colonne, clé hors base). +- Fichiers servis par **URL signée à durée courte**, jamais par chemin public. +- Toute lecture d'une donnée de catégorie particulière est journalisée. + +### 3.7 États d'écran +Chaque écran implémente **chargement, vide, erreur, interdit**. Accessibilité clavier et contraste conformes WCAG 2.2 AA. Les bandeaux transverses sont dismissibles et non bloquants — l'audit relève explicitement que des surcouches masquaient des contrôles dans le produit observé ; ne pas reproduire ce défaut. --- ## 4. Modèle de données -Nommage repris de l'audit — ce sont les agrégats observés, pas une copie de schéma. +Schéma Prisma normatif. Les noms d'agrégats reprennent ceux de l'audit ; ce n'est pas une copie de schéma propriétaire, dont l'audit ne dispose pas. -**Identité et organisation** -`Account` · `Location` (fuseau, IDCC, jours fériés, configuration) · `Team` · `User` (identité globale, peut appartenir à plusieurs comptes) · `Membership` (User↔Account, rôle, scope, responsable hiérarchique) · `Role` · `Permission` · `Scope` +### 4.1 Tenancy et identité -**Personnel** -`EmployeeProfile` — état civil, contact, données administratives, **bancaires et sociales chiffrées au repos** -`UserContract` — type (CDI, CDD, saisonnier, apprentissage, stage, intérim — populations confirmées par l'écran de paie), dates, temps de travail, modulation, rémunération, établissement -`Amendment` — avenant rattaché au contrat -`WorkPermit` — titre de séjour et échéance (`/dashboard-rh/residence-permit`) -`Dpae` · `PersonnelRegisterEntry` (RUP) -`Document` · `DocumentTemplate` · `SignatureRequest` +```prisma +model Account { + id String @id @default(cuid()) + name String + siren String? + apeCode String? + collectiveAgreementId String + createdAt DateTime @default(now()) +} -**Planning et temps** -`WeeklySchedule` — semaine × périmètre, état **brouillon / validé / publié**, `version` -`Shift` — `membershipId` **nullable** (les shifts non-assignés sont une ligne à part entière dans la grille), début/fin, **heures prévues et réelles**, `labelId`, repas, indicateur de validation -`Rest` — pause/repos, durée théorique, extension -`DailyNote` — la ligne « Notes et événements » de la grille -`Label` — étiquette/poste, **couleur** (la vue `/plannings/labels` groupe par étiquette) -`Clocking` / `ActualHours` · `SignableTimesheet` (par équipe et période, signable et téléchargeable) -`PlanningAlert` · `PlanningAnalysis` +model Location { + id String @id @default(cuid()) + accountId String + name String + siret String? + addressLine1 String? + postalCode String? + city String? + timezone String @default("Europe/Paris") + employerContributionRate Decimal @db.Decimal(5,2) // cotisations patronales, % + addPaidLeaveUplift Boolean @default(false) // majoration forfaitaire du coût + productivityTarget Decimal? @db.Decimal(10,2) + silaeDossier String? + archivedAt DateTime? +} -**Absences et compteurs** -`TimeOffPolicy` · `RttPolicy` (créée une fois, **renouvellement annuel automatique**, archivable — texte de l'écran) -`TimeOff` — contrat, période, politique, statut `pending | accepted | declined | deleted | expired` -`Counter` · `LedgerOperation` — écritures, ajustements protégés, prévision +model Team { + id String @id @default(cuid()) + locationId String + name String + position Int + archivedAt DateTime? +} -**Paie et pilotage** -`PayPeriod` — bornes, population, **verrouillage**, périodes alternatives · `PayrollReport` · `Export` · `ActivityLog` +model User { + id String @id @default(cuid()) + email String @unique + passwordHash String? + firstName String + lastName String + locale String @default("fr") +} -**Communication et commerce** -`Article` · `Conversation` · `Notification` · `Subscription` · `Invoice` · `Integration` +model Membership { + id String @id @default(cuid()) + accountId String + userId String? // null = salarié sans accès applicatif + roleId String + lineManagerId String? + employeeNumber String // matricule interne, unique par compte + silaeMatricule String? // requis avant export Silae + status MembershipStatus @default(INVITED) + invitedAt DateTime? + archivedAt DateTime? + @@unique([accountId, employeeNumber]) +} -### Relations structurantes +enum MembershipStatus { INVITED ACTIVE ARCHIVED } + +model MembershipScope { + id String @id @default(cuid()) + membershipId String + allLocations Boolean @default(false) + locationId String? + teamId String? +} ``` -Account 1─N Location 1─N Team -User N─N Account via Membership -Membership N─1 Role ; Membership N─N Scope(Location|Team) -Membership 1─N UserContract 1─N Amendment -WeeklySchedule 1─N Shift ; Shift N─1 Membership? ; WeeklySchedule 1─N Rest -UserContract 1─N TimeOff ; UserContract 1─N Counter 1─N LedgerOperation -Location 1─N PayPeriod 1─N PayrollReport|Export -Membership 1─N Document ; Document N─1 DocumentTemplate? + +**Invariant** — un `Membership` sans `userId` est un salarié géré mais non connecté. Il doit rester plannifiable, contractualisable et exportable. Ne jamais présupposer l'existence d'un `User`. + +### 4.2 Autorisation + +```prisma +model Role { + id String @id @default(cuid()) + accountId String + key String // stable, référencée par le code + name String // libellé modifiable par le client + isSystem Boolean @default(false) // rôles fournis, non supprimables + @@unique([accountId, key]) +} + +model Permission { + id String @id @default(cuid()) + code String @unique // "ressource.action.qualificatif" + category String +} + +model RolePermission { roleId String; permissionId String; @@id([roleId, permissionId]) } +``` + +Catalogue de permissions à semer : §5. + +### 4.3 Personnel + +```prisma +model EmployeeProfile { + membershipId String @id + birthDate DateTime? + birthPlace String? + nationality String? + addressLine1 String? + postalCode String? + city String? + country String? + phone String? + personalEmail String? + socialSecurityNumberEnc Bytes? // chiffré + ibanEnc Bytes? // chiffré + bicEnc Bytes? // chiffré + emergencyContactName String? + emergencyContactPhone String? +} + +model WorkPermit { + id String @id @default(cuid()) + membershipId String + permitType String + reference String + issuedAt DateTime? + expiresAt DateTime + documentId String? +} + +model UserContract { + id String @id @default(cuid()) + membershipId String + locationId String + contractType ContractType + startDate DateTime @db.Date + endDate DateTime? @db.Date + trialEndDate DateTime? @db.Date + weeklyHours Decimal @db.Decimal(5,2) + isModulated Boolean @default(false) + hourlyRate Decimal? @db.Decimal(10,4) + monthlySalary Decimal? @db.Decimal(10,2) + jobTitleId String? + classification String? + coefficient String? + status ContractStatus @default(ACTIVE) + endReason String? + version Int @default(0) +} + +enum ContractType { CDI CDD SAISONNIER APPRENTISSAGE PROFESSIONNALISATION STAGE INTERIM EXTRA } +enum ContractStatus { DRAFT ACTIVE ENDED } + +model Amendment { + id String @id @default(cuid()) + userContractId String + effectiveDate DateTime @db.Date + changes Json // { champ: { before, after } } + reason String? + documentId String? + createdBy String + createdAt DateTime @default(now()) +} + +model JobTitle { id String @id @default(cuid()); accountId String; name String; archivedAt DateTime? } +model Label { id String @id @default(cuid()); accountId String; name String; color String; position Int; archivedAt DateTime? } +``` + +**Invariant contrats** — les périodes de deux contrats actifs d'un même `Membership` ne se chevauchent pas. Contrainte vérifiée en transaction, testée. + +### 4.4 Planning + +```prisma +model WeeklySchedule { + id String @id @default(cuid()) + locationId String + teamId String + isoYear Int + isoWeek Int + status ScheduleStatus @default(DRAFT) + publishedAt DateTime? + publishedBy String? + version Int @default(0) + @@unique([teamId, isoYear, isoWeek]) +} + +enum ScheduleStatus { DRAFT VALIDATED PUBLISHED } + +model Shift { + id String @id @default(cuid()) + weeklyScheduleId String + membershipId String? // null = shift non assigné + localDate DateTime @db.Date + startAt DateTime @db.Timestamptz + endAt DateTime @db.Timestamptz + breakMinutes Int @default(0) + actualStartAt DateTime? @db.Timestamptz + actualEndAt DateTime? @db.Timestamptz + actualBreakMinutes Int? + labelId String? + mealCount Int @default(0) + isValidated Boolean @default(false) + validatedAt DateTime? + validatedBy String? + note String? + version Int @default(0) +} + +model Rest { + id String @id @default(cuid()) + weeklyScheduleId String + membershipId String + localDate DateTime @db.Date + restType RestType + minutes Int? +} + +enum RestType { WEEKLY_REST COMPENSATORY_REST } + +model DailyNote { + id String @id @default(cuid()) + weeklyScheduleId String + localDate DateTime @db.Date + content String +} + +model Holiday { + id String @id @default(cuid()) + accountId String + locationId String? // null = tous établissements + date DateTime @db.Date + name String + isWorked Boolean @default(false) +} +``` + +**`À VALIDER`** — l'audit décrit `Rest` comme « pause/repos, durée théorique, extension », ce qui est ambigu. Ce plan tranche : les pauses **dans** un shift sont `Shift.breakMinutes` ; `Rest` marque un repos **de journée** (hebdomadaire ou compensateur). Faire confirmer avant le lot 2. + +### 4.5 Absences et compteurs + +```prisma +model AbsenceType { + id String @id @default(cuid()) + accountId String + code String + name String + color String + isPaid Boolean + countsAsWorkTime Boolean + affectsPaidLeaveAccrual Boolean + silaeCode String? // partie de AB- + requiresJustification Boolean @default(false) + minNoticeDays Int? + @@unique([accountId, code]) +} + +model TimeOff { + id String @id @default(cuid()) + userContractId String + absenceTypeId String + startDate DateTime @db.Date + startHalfDay Boolean @default(false) + endDate DateTime @db.Date // dernier jour d'absence + endHalfDay Boolean @default(false) + status TimeOffStatus @default(PENDING) + requestedBy String + requestedAt DateTime @default(now()) + decidedBy String? + decidedAt DateTime? + decisionComment String? + justificationDocumentId String? +} + +enum TimeOffStatus { PENDING ACCEPTED DECLINED CANCELLED EXPIRED } + +model Counter { + id String @id @default(cuid()) + userContractId String + counterType CounterType + acquisitionPeriodStart DateTime @db.Date + acquisitionPeriodEnd DateTime @db.Date + @@unique([userContractId, counterType, acquisitionPeriodStart]) +} + +enum CounterType { PAID_LEAVE RTT COMPENSATORY_REST MODULATION OVERTIME } + +model LedgerOperation { + id String @id @default(cuid()) + counterId String + kind LedgerKind + quantity Decimal @db.Decimal(10,4) // signé + unit LedgerUnit + effectiveDate DateTime @db.Date + sourceType LedgerSource + sourceId String? + reason String? + reversesId String? @unique // écriture inverse + createdBy String + createdAt DateTime @default(now()) +} + +enum LedgerKind { ACCRUAL TAKEN ADJUSTMENT CARRY_OVER EXPIRY REGULARISATION } +enum LedgerUnit { DAY HOUR } +enum LedgerSource { TIMEOFF PAY_PERIOD MANUAL SYSTEM } + +model RttPolicy { + id String @id @default(cuid()) + accountId String + name String + daysPerYear Decimal @db.Decimal(5,2) + periodStart String // "MM-DD" + autoRenew Boolean @default(true) + status PolicyStatus @default(ACTIVE) +} + +enum PolicyStatus { ACTIVE ARCHIVED } + +model RttPolicyAssignment { id String @id @default(cuid()); rttPolicyId String; userContractId String } +``` + +**Invariant ledger** — aucune ligne n'est jamais modifiée ni supprimée. Une correction crée une écriture portant `reversesId`, puis la nouvelle écriture. Le solde est `SUM(quantity)`. Contrainte applicative testée, plus un trigger PostgreSQL interdisant `UPDATE`/`DELETE`. + +### 4.6 Paie + +```prisma +model PayPeriod { + id String @id @default(cuid()) + locationId String + label String + startDate DateTime @db.Date + endDate DateTime @db.Date + kind PayPeriodKind @default(MAIN) + populations ContractType[] + status PayPeriodStatus @default(OPEN) + lockedAt DateTime? + lockedBy String? + version Int @default(0) + @@unique([locationId, startDate, endDate, kind]) +} + +enum PayPeriodKind { MAIN ALTERNATIVE } +enum PayPeriodStatus { OPEN LOCKED } + +model PayPeriodSnapshot { + id String @id @default(cuid()) + payPeriodId String + membershipId String + plannedMinutes Int + actualMinutes Int + absenceMinutes Int + overtimeByBracket Json // [{ fromHour, toHour, rate, minutes }] + absenceBreakdown Json // [{ absenceTypeId, days, minutes }] + variables Json // [{ code, quantity, unit }] + computedAt DateTime @default(now()) + @@unique([payPeriodId, membershipId]) +} + +model PayrollExport { + id String @id @default(cuid()) + payPeriodId String + format ExportFormat + fileKey String + checksum String + rowCount Int + idempotencyKey String @unique + generatedBy String + generatedAt DateTime @default(now()) +} + +enum ExportFormat { SILAE GENERIC_CSV RAW } +``` + +**Invariant période** — le verrouillage écrit les `PayPeriodSnapshot` et interdit toute mutation de `Shift`, `TimeOff` ou heures réelles dont la `localDate` tombe dans la période. Une correction postérieure passe par une **régularisation sur la période ouverte suivante**, jamais par réécriture. + +### 4.7 Documents, conformité, transverse + +```prisma +model DocumentTemplate { id String @id @default(cuid()); accountId String; name String; bodyHtml String; availableFields Json; archivedAt DateTime? } + +model Document { + id String @id @default(cuid()) + accountId String + membershipId String? + category DocumentCategory + name String + fileKey String + mimeType String + sizeBytes Int + checksum String + isSensitive Boolean @default(false) // santé : journalisation de lecture + retentionUntil DateTime? + templateId String? + uploadedBy String + uploadedAt DateTime @default(now()) +} + +enum DocumentCategory { IDENTITY BANK CONTRACT AMENDMENT SICK_NOTE WORK_PERMIT REGISTER OTHER } + +model CollectiveAgreement { + id String @id @default(cuid()) + idcc String + name String + parameters Json // §6 + version Int + effectiveFrom DateTime @db.Date +} + +model ComplianceViolation { + id String @id @default(cuid()) + weeklyScheduleId String + membershipId String? + ruleCode String + severity Severity + localDate DateTime? @db.Date + message String + context Json + detectedAt DateTime @default(now()) + acknowledgedBy String? + acknowledgedAt DateTime? + acknowledgementReason String? +} + +enum Severity { INFO WARNING BLOCKING } + +model AuditLog { + id String @id @default(cuid()) + accountId String + actorMembershipId String? + action String + entityType String + entityId String + before Json? + after Json? + reason String? + ip String? + userAgent String? + occurredAt DateTime @default(now()) + @@index([accountId, entityType, entityId]) +} + +model Integration { id String @id @default(cuid()); accountId String; provider String; credentialsEnc Bytes?; config Json; status String } +model SilaeCodeMapping { id String @id @default(cuid()); accountId String; kind SilaeCodeKind; internalRef String; silaeCode String } +enum SilaeCodeKind { HOURS ABSENCE VARIABLE } + +model FeatureFlag { id String @id @default(cuid()); accountId String?; key String; enabled Boolean @default(false) } +model Notification { id String @id @default(cuid()); membershipId String; notificationType String; payload Json; readAt DateTime? } ``` --- -## 5. Moteur de règles de convention +## 5. Catalogue des permissions -Piloté par les données, jamais en dur. Un `CollectiveAgreement` porte un IDCC et un jeu de paramètres versionné ; chaque règle est une **fonction pure** `(contexte) => Violation[]`, testable en table. +À semer en base au lot 0. Les codes sont **stables** : le code les référence, jamais les libellés. -**Amorce IDCC 1517** — les deux règles visibles dans le libellé de configuration du compte sont les majorations **jour férié 50 %** et **dimanche 100 %**. S'y ajoutent les règles d'ordre public : durées maximales quotidienne et hebdomadaire, repos quotidien et hebdomadaire, jours consécutifs, pause après 6 h, tranches d'heures supplémentaires. +**Planning** — `planning.view`, `planning.view_unpublished`, `planning.create`, `planning.create_on_published`, `planning.edit`, `planning.edit_published`, `planning.delete`, `planning.duplicate`, `planning.publish`, `planning.unpublish`, `planning.validate`, `planning.invalidate`, `planning.bulk_actions`, `planning.unassigned.view`, `planning.alerts.view`, `planning.alerts.acknowledge`, `planning.counters.view`, `planning.labels.manage`, `planning.notes.manage`, `planning.print` -> **Aucune valeur numérique n'est fixée dans ce plan.** L'audit est muet sur les paramètres réels, et les sources publiques se contredisent d'une convention à l'autre. Les valeurs doivent être saisies depuis le texte de l'IDCC 1517 et **validées par un expert paie** avant mise en production. C'est précisément la raison du choix paramétrable. +**Personnel** — `members.view`, `members.create`, `members.edit`, `members.archive`, `members.delete`, `members.salary.view`, `members.contract.create`, `members.contract.edit`, `members.contract.delete_past`, `members.documents.view`, `members.documents.manage`, `members.register.export`, `members.dpae.check` -**Exécution** — revalidation ciblée des semaines-employés impactées à chaque mutation (une modification touche la semaine courante et ses voisines par le repos quotidien), validation complète à la publication. +**Heures** — `hours.view`, `hours.edit_actual`, `hours.validate` -**Restitution non bloquante** — badge sur la cellule, panneau listant les violations, publication possible après confirmation explicite, **tracée dans le journal d'audit**. Un manager doit pouvoir passer outre en connaissance de cause. +**Absences et compteurs** — `timeoff.view_own`, `timeoff.view_others`, `timeoff.request`, `timeoff.decide`, `timeoff.delete`, `timeoff.bypass_notice`, `timeoff.forecast.view`, `counters.view_own`, `counters.view_others`, `counters.adjust` + +**Paie** — `payroll.access`, `payroll.period.create`, `payroll.period.update`, `payroll.period.delete`, `payroll.period.alternative`, `payroll.period.lock`, `payroll.period.unlock`, `payroll.export`, `payroll.export.silae`, `payroll.export.raw` + +**Administration** — `settings.access`, `settings.locations.manage`, `settings.teams.manage`, `settings.agreement.manage`, `settings.jobtitles.manage`, `settings.templates.manage`, `settings.integrations.manage`, `settings.notifications.manage`, `settings.roles.manage`, `role_config.assign_owner_level`, `audit.view` + +**Communication (lot 5)** — `articles.view`, `articles.manage`, `conversations.access` + +### Rôles semés +`employee`, `team_manager`, `hr_manager`, `payroll_manager`, `admin`, `owner`. Ces jeux de permissions sont un **point de départ modifiable par le client** ; ils ne doivent jamais être codés en dur dans les écrans. Seul `owner` détient `role_config.assign_owner_level`. --- -## 6. Compteurs +## 6. Moteur de règles de convention -**Congés : registre d'écritures.** `LedgerOperation` est la source de vérité (acquisition, prise, ajustement manuel, régularisation). Le solde est le cumul ; la prévision projette les acquisitions à venir. Tout ajustement manuel porte auteur, date et justification. +### 6.1 Conception +`src/domain/compliance/`. Une `CollectiveAgreement` porte un `parameters: Json` **versionné**. Chaque règle est une **fonction pure** : -**Heures : calcul à la lecture, figé à la clôture.** Période ouverte → recalcul depuis `Shift`, `ActualHours` et `TimeOff`. Période close → instantané immuable écrit au verrouillage de la `PayPeriod`, pour qu'un export de mars ne bouge pas si un shift de mars est corrigé en juin. Les corrections rétroactives passent par une écriture de régularisation sur la période ouverte, jamais par réécriture du passé. +```ts +type Rule = (ctx: ComplianceContext) => Violation[] +``` -**Bandeau par employé de la grille** — l'écran affiche cinq valeurs par ligne : contrat, planifié, absences, écart, repos compensateur. Le même calcul doit alimenter la grille, le rapport de pointage et l'export de paie ; une seule implémentation, trois consommateurs. +`ComplianceContext` fournit : les shifts de la semaine évaluée **et des semaines adjacentes** (nécessaires au repos quotidien et hebdomadaire), les absences, le contrat, l'âge du salarié, le calendrier des jours fériés de l'établissement, et les paramètres de la convention. -**Coût du planning** — `settings/wage-ratio` expose un taux moyen de cotisations patronales en pourcentage et des options d'ajustement. Le coût prévisionnel s'appuie dessus et se compare à un objectif de productivité défini par établissement. +Aucun `Date` natif : tout passe par `src/lib/datetime.ts`. + +### 6.2 Codes de règles à implémenter +`MAX_DAILY_WORK` · `MAX_DAILY_AMPLITUDE` · `MIN_DAILY_REST` · `MIN_WEEKLY_REST` · `MAX_WEEKLY_WORK_ABSOLUTE` · `MAX_WEEKLY_WORK_AVERAGED` · `MAX_CONSECUTIVE_WORK_DAYS` · `MIN_BREAK_AFTER_THRESHOLD` · `PART_TIME_MIN_WEEKLY_HOURS` · `CONTRACT_HOURS_DEVIATION` · `OVERLAPPING_SHIFTS` · `SHIFT_DURING_ABSENCE` · `SUNDAY_WORK` · `HOLIDAY_WORK` · `MINOR_MAX_DAILY_WORK` · `MINOR_MIN_DAILY_REST` · `MINOR_NIGHT_WORK` + +`OVERLAPPING_SHIFTS` et `SHIFT_DURING_ABSENCE` sont de sévérité **`BLOCKING`** : ce sont des incohérences de données, pas des arbitrages. Toutes les autres sont **`WARNING`**. + +### 6.3 Paramètres — signal d'arrêt + +> **Ce plan ne fixe aucune valeur numérique de convention.** L'audit est muet sur les paramètres réels et les sources publiques se contredisent d'une convention à l'autre. Les seules règles lisibles dans la configuration du compte audité sont les majorations **jour férié 50 %** et **dimanche 100 %**. +> +> **L'orchestrateur doit :** implémenter les règles et leurs tests avec des paramètres **de test explicitement fictifs**, livrer un écran de saisie des paramètres, et **demander les valeurs réelles de l'IDCC 1517 à un expert paie** avant toute mise en production. Ne jamais déduire une valeur d'une recherche web. + +### 6.4 Exécution et restitution +- À chaque mutation de shift : revalidation **ciblée** des couples (salarié, semaine) impactés, y compris les semaines adjacentes. +- À la publication : validation complète du périmètre publié. +- Restitution **non bloquante** pour les `WARNING` : badge sur la cellule, panneau latéral listant les violations. La publication reste possible **après confirmation explicite**, qui écrit `acknowledgedBy`, `acknowledgedAt`, `acknowledgementReason` et une entrée `AuditLog`. +- Les `BLOCKING` empêchent l'enregistrement. --- -## 7. Phasage +## 7. Compteurs et heures -Reprend le découpage en lots proposé par l'audit, qui est cohérent. +### 7.1 Congés — registre +`LedgerOperation` est la source de vérité. Le solde est `SUM(quantity)` sur le `Counter`. La prévision projette les acquisitions restantes de la période en cours. -| Lot | Contenu | Livrable | +Écritures automatiques : acquisition à la clôture de chaque période de paie, prise à l'acceptation d'un `TimeOff`, contre-passation à l'annulation, report et expiration en fin de période d'acquisition. Ajustement manuel réservé à `counters.adjust`, **justification obligatoire**. + +### 7.2 Décompte des jours d'absence +Règles normatives : +- Une absence court du premier jour **jusqu'au dernier jour d'absence inclus** — soit la veille de la reprise. Le champ `endDate` porte ce dernier jour, pas la date de retour. +- Les demi-journées sont portées par `startHalfDay` / `endHalfDay`. +- Le décompte exclut les jours non travaillés selon le calendrier de l'établissement et le rythme du contrat. +- **Un jour férié dans une période de congé n'est pas décompté** en congé payé. L'implémentation le retire du décompte ; elle n'oblige pas l'utilisateur à scinder sa demande. +- Deux absences acceptées ne peuvent pas se chevaucher pour un même contrat. Contrainte vérifiée en transaction. + +### 7.3 Heures — sans pointeuse +Il n'y a **pas de pointage**. Chaque `Shift` porte des heures **prévues** (`startAt`, `endAt`, `breakMinutes`) et, optionnellement, des heures **réelles** (`actualStartAt`, `actualEndAt`, `actualBreakMinutes`). + +- Heures réelles absentes → **les heures prévues font foi**. +- Un manager disposant de `hours.edit_actual` saisit un écart ; `hours.validate` valide la ligne, ce qui écrit auteur, date et écart à l'audit. +- L'écran `/reports/hours` liste par salarié : prévu, réel, écart, statut ; sélection multiple et validation groupée. + +### 7.4 Bandeau de compteurs de la grille +Chaque ligne salarié affiche cinq valeurs : **heures contractuelles · planifié · absences · écart · repos compensateur**. + +**Une seule implémentation** dans `src/domain/counters/` alimente la grille, le rapport d'heures et l'export de paie. Trois calculs divergents seraient un défaut majeur — c'est la première cause d'écart entre un planning et un bulletin. + +### 7.5 Coût du planning +Coût prévisionnel = somme des heures × taux horaire du contrat × (1 + `employerContributionRate`), plus la majoration forfaitaire si `addPaidLeaveUplift`. Comparé à l'objectif de productivité de l'établissement. Visible sous `planning.counters.view`. + +--- + +## 8. Export Silae + +Seule intégration de paie du périmètre v1. `src/domain/payroll/adapters/silae.ts`, derrière une interface `PayrollExportAdapter` qui laisse la place à d'autres formats. + +### 8.1 Format +- **CSV, UTF-8, séparateur `;`** +- En-tête : `matricule;code paie;décompte;date début;date fin;` +- Une ligne par couple (salarié, code de paie) sur la période. + +### 8.2 Codes +Trois familles, préfixées : +| Famille | Préfixe | Source du code | |---|---|---| -| **0 — Fondations** | Tenancy `Account/Location/Team`, identité, `Membership` ; RBAC capacités + scopes ; journal d'audit ; feature flags ; fichiers privés à URL signée courte ; jobs asynchrones ; design system original | Une organisation, des utilisateurs, des droits vérifiables | -| **1 — Cœur opérationnel** | Annuaire et profil/contrat minimal ; grille semaine, shifts, pauses, labels, notes, non-assignés ; brouillon → validé → publié **par équipe** ; absence simple avec approbation ; exports basiques | L'établissement peut planifier et publier | -| **2 — RH et conformité** | Dossier complet, DPAE, RUP, titres de séjour ; modèles de documents et signature ; `Counter`/`LedgerOperation` et politiques RTT ; entrées, sorties, fins d'essai, profils incomplets, modifications de contrat | Le dossier salarié est tenu et conforme | -| **3 — Paie et temps avancé** | `PayPeriod`, verrouillage, revue, exports ; pointage, heures réelles, feuilles signables ; connecteurs paie | La paie sort du produit | -| **4 — Extensions** | Analyses RH, planning prédictif et auto-assignation ; articles, conversations ; abonnement, marketplace et connecteurs de caisse | Confort et pilotage | +| Heures | `HS-` | `SilaeCodeMapping` kind `HOURS` | +| Absences | `AB-` | `AbsenceType.silaeCode` | +| Éléments variables | `EV-` | `SilaeCodeMapping` kind `VARIABLE` | -Le moteur de règles (§5) démarre au lot 1 avec les règles d'ordre public et s'enrichit au lot 2. +> **Signal d'arrêt.** Les codes réels appartiennent au dossier Silae du client et se lisent dans « Saisie des éléments variables ». **Ne pas les inventer.** Livrer l'écran de correspondance (`/settings/integrations/silae`) et une table vide ; demander les codes avant la première mise en production. + +### 8.3 Règles d'export +- **Pré-contrôle bloquant** : tout salarié inclus doit avoir un `silaeMatricule` et tout élément exporté un code mappé. À défaut, l'export échoue en listant précisément les manques — il ne produit jamais un fichier partiel silencieux. +- L'export ne porte que sur une `PayPeriod` **verrouillée**, et lit exclusivement les `PayPeriodSnapshot`. +- **Idempotence** : un réexport de la même période produit le même fichier et le même `checksum`. L'import Silae écrase les données de la même période pour les mêmes salariés ; l'export doit donc être rejouable sans effet de bord. +- Chaque génération écrit un `PayrollExport` et une entrée d'audit. --- -## 8. Arborescence proposée +## 9. Routes et écrans -``` -planflow/ -├── docker-compose.yml -├── prisma/schema.prisma # + migrations/, seed.ts -├── src/ -│ ├── app/(auth)/ # connexion, invitation, mot de passe -│ │ (app)/planning/ # vues semaine | jour | labels, impression -│ │ (app)/members/ # annuaire, dossier, contrats, compteurs -│ │ (app)/timeoffs/ # à traiter | calendrier | traitées | expirées -│ │ (app)/reports/ # paies, historique, activité, pointage -│ │ (app)/hr/ # suivi employé, documents, analyses -│ │ (app)/settings/ # société, planification, gestion, intégrations, RGPD -│ │ kiosk/ # pointeuse (PWA hors-ligne) -│ │ api/ # webhooks caisses, API ouverte -│ ├── domain/ -│ │ ├── access/ # capacités, scopes, can() -│ │ ├── compliance/ # moteur de règles — rules/, engine.ts, agreements/idcc1517.ts -│ │ ├── counters/ # ledger congés, heures, régularisations -│ │ └── payroll/ # variables et adaptateurs d'export -│ ├── server/ # auth, db (extension multi-tenant), actions/, audit/ -│ ├── components/ # ui/, planning/ (îlot de grille) -│ └── lib/ # datetime (fuseaux, DST, nuits traversantes), money -└── tests/ # unit/ (Vitest), e2e/ (Playwright) -``` +Structure de navigation cible. Les routes sont propres à PlanFlow ; l'audit sert d'inventaire de capacités, pas de plan d'URL à copier. -**Performance de la grille** — Server Component pour le chargement, **îlot client** pour l'interaction (glisser-déposer, redimensionnement, actions de masse), mises à jour optimistes, lignes virtualisées. L'audit exige aussi accessibilité clavier et états chargement / vide / erreur / interdit sur chaque écran. +| Zone | Routes | +|---|---| +| **Auth** | `/connexion`, `/mot-de-passe/oubli`, `/mot-de-passe/reinitialisation`, `/invitation/:token` | +| **Accueil** | `/` — suivi hebdomadaire, raccourcis, alertes | +| **Planning** | `/planning/semaine`, `/planning/jour`, `/planning/etiquettes`, `/planning/impression` | +| **Équipe** | `/equipe`, `/equipe/:id/profil`, `/contrats`, `/documents`, `/absences`, `/temps`, `/acces`, `/compteurs/:counterId` | +| **Absences** | `/absences/a-traiter`, `/calendrier`, `/traitees`, `/expirees` | +| **Rapports** | `/rapports/paies`, `/rapports/historique`, `/rapports/heures`, `/rapports/activite` | +| **RH** | `/rh` (aperçu), `/rh/entrees`, `/sorties`, `/fins-essai`, `/profils-incomplets`, `/compteurs-conges`, `/journal-absences`, `/titres-sejour`, `/modifications-contrat`, `/rh/analyses/{effectifs,heures,absences}` | +| **Réglages** | `/reglages/compte`, `/etablissements`, `/equipes`, `/convention`, `/emplois`, `/etiquettes`, `/types-absence`, `/politiques-rtt`, `/modeles-documents`, `/roles`, `/roles/:roleKey`, `/integrations/silae`, `/preferences`, `/impression`, `/productivite`, `/rgpd` | + +**Écrans du produit audité volontairement absents** : pointeuse et ses réglages, bulletins de paie et distribution, signature électronique, abonnement et facturation, marketplace et connecteurs de caisse, ADP. + +### Grille de planning — comportement normatif +Reproduire ces comportements observés à l'écran, avec une interface originale : +- Colonnes lundi→dimanche, numéro de semaine, jour courant distingué. +- **Regroupement par équipe**, chaque équipe portant son propre état de publication et son action publier/dépublier. La publication est **par équipe**, pas par établissement. +- Chaque équipe expose une ligne **« Notes et événements »** et une ligne **« Shifts non assignés »**. +- Shift : bloc coloré par étiquette, libellé, plage horaire, durée de pause, badge d'écart, marque de validation. +- Absences : barres continues sur plusieurs jours, portant le libellé du type et la durée en jours. +- Bandeau de cinq compteurs par ligne salarié (§7.4). +- Navigation de semaine, filtres, sélection des vues, actions de masse. + +**Performance** — Server Component pour le chargement, **îlot client** pour l'interaction, mises à jour optimistes, lignes virtualisées au-delà de 50 lignes. Cible : rendu initial d'une semaine de 80 salariés sous 1,5 s, interaction de glisser-déposer sous 100 ms. --- -## 9. Vérification +## 10. Lots de travail -**Jeu de données de départ** — une organisation, deux établissements, des équipes distinctes, une population mêlant CDI, CDD, temps partiels et un contrat en apprentissage, quatre semaines de plannings publiés, des absences longues chevauchant des semaines, des pointages en écart. C'est la base de toute vérification manuelle. +Ordre imposé. Chaque lot est livrable, testé et mergeable seul. -**Vitest** — le domaine, en tests de table : -- Chaque règle de convention à ses bornes (la valeur limite exacte passe, un cran en dessous échoue) -- Ledger de congés : acquisition, prise, ajustement, prévision, régularisation rétroactive -- Heures : tranches d'heures supplémentaires, **shift traversant un changement d'heure**, nuit traversante -- Autorisation : une capacité absente doit refuser la **mutation**, pas seulement masquer le bouton +### WP-00 — Socle +**Dépend de :** rien +**Livre :** `docker-compose.yml` (app + Postgres 16), projet Next.js 15 TypeScript strict, Prisma, Tailwind + shadcn/ui, Vitest, Playwright, CI GitHub Actions (lint, typecheck, test, build), `.env.example`, `README.md`. +**Critères d'acceptation** +- `docker compose up` démarre l'application et la base, migrations appliquées. +- CI verte sur un dépôt propre. +- `pnpm typecheck` sans erreur en mode strict. -**Playwright** — les parcours qui traversent les agrégats : -1. Construire une semaine, déclencher une alerte de convention, publier malgré tout → la trace apparaît au journal d'audit et le salarié voit son planning -2. Demander une absence → approuver → l'absence s'affiche sur la grille, le ledger est écrit, le solde et la prévision bougent -3. Pointer avec un écart → valider → l'écart remonte dans la période de paie → verrouiller → l'export est figé et une correction ultérieure produit une régularisation -4. Deux sessions éditant la même semaine → conflit détecté, pas de perte silencieuse +### WP-01 — Tenancy, identité, autorisation +**Dépend de :** WP-00 +**Livre :** modèles §4.1 et §4.2 ; extension Prisma de scoping ; RLS PostgreSQL ; Auth.js (mot de passe + invitation par e-mail) ; `can()` ; catalogue de permissions §5 semé ; rôles semés ; `AuditLog` ; chiffrement de colonnes ; stockage de fichiers à URL signée. +**Critères d'acceptation** +- Un membership scopé sur l'établissement A ne peut **lire ni écrire** une donnée de l'établissement B — testé au niveau requête, pas seulement UI. +- Une Server Action appelée sans la permission requise échoue **avant** tout effet de bord. +- Désactiver le scoping applicatif laisse la RLS bloquer l'accès (test d'intégration dédié). +- Toute mutation produit une entrée `AuditLog` avec avant/après. +- Un rôle personnalisé créé par un client modifie effectivement l'accès, sans changement de code. -**Manuel** — `docker compose up`, puis dérouler le jeu de données ; vérifier la pointeuse hors-ligne (couper le réseau, pointer, rétablir, contrôler la synchro) et l'installation PWA. +### WP-02 — Organisation et référentiels +**Dépend de :** WP-01 +**Livre :** établissements, équipes, jours fériés, emplois, étiquettes, types d'absence, convention collective et écran de paramètres ; réglages compte, préférences, productivité, taux de cotisations. +**Critères d'acceptation** +- Création d'un compte à deux établissements et plusieurs équipes de bout en bout. +- Fuseau par établissement effectif sur l'affichage et les calculs. +- Les paramètres de convention sont éditables et versionnés. + +### WP-03 — Personnel et contrats +**Dépend de :** WP-02 +**Livre :** `Membership`, `EmployeeProfile`, `UserContract`, `Amendment`, `WorkPermit` ; annuaire filtrable et trié ; dossier salarié ; invitation ; salarié sans compte ; export du registre unique du personnel. +**Critères d'acceptation** +- Un salarié **sans `userId`** est créable, plannifiable et contractualisable. +- Deux contrats actifs chevauchants sont refusés. +- Un avenant conserve l'historique et n'écrase pas le contrat. +- `members.salary.view` absente masque la rémunération **et** la refuse côté API. +- L'export du registre contient les mentions légales attendues. + +### WP-04 — Planning +**Dépend de :** WP-03 +**Livre :** `WeeklySchedule`, `Shift`, `Rest`, `DailyNote`, `Label` ; vues semaine, jour, étiquettes ; création, déplacement, redimensionnement, duplication, actions de masse ; shifts non assignés ; brouillon → validé → publié **par équipe** ; notifications de publication ; impression PDF. +**Critères d'acceptation** +- Grille conforme à §9, y compris lignes notes et non-assignés. +- La publication est par équipe et notifie les salariés concernés. +- `planning.view_unpublished` absente masque les semaines non publiées côté serveur. +- Deux sessions modifiant la même semaine → conflit détecté, aucune perte silencieuse. +- Un shift 22 h–06 h la nuit du changement d'heure est compté 7 h ou 9 h selon le sens. +- Semaine de 80 salariés sous les cibles de §9. + +### WP-05 — Moteur de règles +**Dépend de :** WP-04 +**Livre :** moteur §6, les 17 règles, `ComplianceViolation`, revalidation ciblée, panneau d'alertes, confirmation tracée à la publication, écran de paramètres. +**Critères d'acceptation** +- Chaque règle a un test aux bornes : la valeur limite exacte passe, un cran au-delà déclenche. +- Modifier un shift revalide la semaine **et ses voisines**. +- Publier malgré un `WARNING` exige une confirmation et écrit l'acquittement et l'audit. +- Un `BLOCKING` empêche l'enregistrement. +- Aucune valeur de convention n'est codée en dur — vérifié par un test qui charge deux jeux de paramètres différents. + +### WP-06 — Absences et compteurs +**Dépend de :** WP-05 +**Livre :** `TimeOff`, `AbsenceType`, `Counter`, `LedgerOperation`, `RttPolicy` ; demande, file à traiter, décision, calendrier, traitées, expirées ; affichage sur la grille ; soldes et prévision ; ajustement manuel ; trigger d'immutabilité du ledger. +**Critères d'acceptation** +- `endDate` porte le dernier jour d'absence ; un test couvre explicitement la confusion avec la date de reprise. +- Un jour férié dans un congé n'est pas décompté, **sans** scission manuelle. +- Deux absences acceptées chevauchantes sont refusées. +- Accepter écrit au ledger ; annuler contre-passe ; le solde reste juste après un aller-retour. +- `UPDATE` et `DELETE` sur `LedgerOperation` sont rejetés par la base. +- Un ajustement manuel sans justification est refusé. +- Une politique RTT se reconduit automatiquement ; archivée, elle cesse de le faire. + +### WP-07 — Heures et périodes de paie +**Dépend de :** WP-06 +**Livre :** heures réelles et validation (§7.3) ; `PayPeriod`, populations, période alternative, verrouillage ; `PayPeriodSnapshot` ; rapport d'heures ; journal d'activité. +**Critères d'acceptation** +- Sans heures réelles, le prévu fait foi partout. +- Le verrouillage fige les instantanés et **refuse** toute mutation dans la période. +- Une correction après verrouillage produit une régularisation sur la période suivante, sans réécrire le passé. +- Grille, rapport d'heures et instantané donnent des chiffres **identiques** sur un même jeu de données — test croisé obligatoire. + +### WP-08 — Export Silae +**Dépend de :** WP-07 +**Livre :** adaptateur §8, écran de correspondance des codes, `silaeMatricule` sur le membership, export générique CSV, historique des exports. +**Critères d'acceptation** +- Fichier conforme : UTF-8, `;`, en-tête exact, préfixes `HS-`/`AB-`/`EV-`. +- Un matricule ou un code manquant fait **échouer** l'export avec la liste des manques ; aucun fichier partiel. +- Réexport de la même période → `checksum` identique. +- Export refusé sur une période non verrouillée. + +### WP-09 — Tableau de bord RH +**Dépend de :** WP-08 +**Livre :** aperçu et indicateurs ; entrées, sorties, fins de période d'essai, profils incomplets, compteurs de congés, journal des absences, titres de séjour, modifications de contrat ; analyses effectifs, heures, absences. +**Critères d'acceptation** +- Chaque indicateur est **explicable** : un clic mène aux lignes sources. +- Les échéances de titres de séjour et de périodes d'essai remontent dans les fenêtres attendues. +- Les agrégats respectent le périmètre du membership. + +### WP-10 — Documents +**Dépend de :** WP-09 +**Livre :** GED par salarié, modèles de documents et génération, catégories, rétention, journalisation des lectures sensibles, brouillon DPAE. +**Critères d'acceptation** +- Les fichiers ne sont accessibles que par URL signée expirante. +- La lecture d'un document de santé est journalisée. +- Un modèle génère un document avec les champs du dossier. +- La DPAE est **générée**, jamais transmise (§12). + +### WP-11 — Communication *(optionnel)* +**Dépend de :** WP-10 +**Livre :** articles et conversations internes. + +### Transverse — RGPD +À traiter **dans chaque lot**, pas en fin de projet : minimisation, chiffrement, journalisation des accès, rétention, export et suppression sur demande. --- -## 10. Risques et conformité +## 11. Stratégie de test -- **RGPD** — le dossier contient état civil, NIR, coordonnées bancaires, titres de séjour et arrêts de travail. Les arrêts sont des **données de santé, catégorie particulière**. Exigences : minimisation, chiffrement au repos, journalisation des accès, durées de conservation, masquage, export et suppression. À traiter au lot 0, pas après. L'application auditée expose d'ailleurs un contrat de sous-traitance RGPD dédié. -- **Matrice de conformité manquante** — le document juridique référencé par l'audit est absent du dépôt ; cette section reste incomplète tant qu'il n'est pas fourni. -- **DPAE / URSSAF** — la transmission automatisée exige un raccordement déclaratif. Générer le formulaire au lot 2 ; la transmission passe par un tiers. -- **Signature électronique** — impose un prestataire de confiance qualifié. Brancher un service existant, ne pas l'implémenter. -- **Paie et DSN** — responsabilité réglementaire majeure et veille législative permanente. Motif du choix « export uniquement ». -- **Paramètres de convention** — voir §5 : à saisir depuis le texte de l'IDCC 1517 et faire valider. -- **Auditabilité** — auteur, horodatage, avant/après et justification sur contrat, absence, planning publié, période de paie et changement de permission. Les agrégats analytiques doivent être explicables depuis leurs sources. -- **Résilience des intégrations** — reprises bornées, idempotence, file d'échecs et état de synchronisation visible. +**Vitest — domaine pur, en tables** +- Chaque règle de convention, aux bornes. +- Ledger : acquisition, prise, ajustement, contre-passation, report, expiration, régularisation rétroactive. +- Heures : tranches d'heures supplémentaires, **changement d'heure**, **nuit traversante**, absence de valeurs réelles. +- Décompte d'absence : dernier jour vs reprise, demi-journées, jour férié inclus, chevauchement. +- Autorisation : chaque permission absente doit refuser la **mutation**. + +**Tests d'intégration — base réelle** +- Scoping multi-tenant au niveau requête. +- RLS active indépendamment du code applicatif. +- Immutabilité du ledger imposée par la base. +- Conflits de version sur écriture concurrente. + +**Playwright — parcours traversants** +1. Construire une semaine, déclencher une alerte, publier après confirmation → acquittement et audit écrits, salarié notifié. +2. Demander un congé → accepter → barre visible sur la grille, ledger écrit, solde et prévision à jour → annuler → contre-passation, solde restauré. +3. Saisir un écart d'heures → valider → période de paie → verrouiller → export Silae → réexport identique. +4. Deux sessions sur la même semaine → conflit rendu, pas de perte. +5. Un manager de l'établissement A tente d'atteindre une ressource de B → refus serveur. + +**Jeu de données de départ** — un compte, **deux établissements**, plusieurs équipes, une trentaine de salariés mêlant CDI, CDD, temps partiels, un apprenti et **un mineur** (règles dédiées), dont au moins un salarié sans compte utilisateur et un rattaché à deux établissements ; quatre semaines publiées, des absences longues chevauchant des semaines, un jour férié en milieu de congé, des écarts d'heures, une semaine incluant un changement d'heure. **Données entièrement fictives.** --- -## 11. Règles clean room +## 12. Conformité et risques -Contraintes reprises de l'audit, à tenir pendant toute la construction : +- **RGPD** — le dossier contient état civil, NIR, coordonnées bancaires, titres de séjour et arrêts de travail. Les arrêts sont des **données de santé, catégorie particulière**. Minimisation, chiffrement au repos, journalisation des accès, rétention, masquage, export et suppression. Traité dès WP-01. +- **Matrice de conformité manquante** — `À VALIDER`, voir §1. Cette section reste incomplète tant que le document n'est pas fourni. +- **Paramètres de convention** — signal d'arrêt §6.3. +- **Codes de paie Silae** — signal d'arrêt §8.2. +- **DPAE** — générée seulement. La transmission à l'URSSAF exige un raccordement déclaratif, hors périmètre. +- **Signature électronique** — exige un prestataire de confiance qualifié. Hors périmètre ; ne pas implémenter de substitut maison, qui n'aurait aucune valeur probante. +- **Paie et DSN** — hors périmètre. Responsabilité réglementaire majeure et veille législative permanente. +- **Conservation des bulletins** — hors périmètre : PlanFlow ne détient pas de bulletins. +- **Auditabilité** — auteur, horodatage, avant/après et justification sur toutes les opérations sensibles ; agrégats explicables depuis leurs sources. -- Ce document et l'audit servent de **liste de capacités et d'invariants**, pas de modèle de code ou de présentation. -- API, schéma de données, textes et interface **originaux**. +--- + +## 13. Règles clean room + +- Ce document et l'audit sont une **liste de capacités et d'invariants**, pas un modèle de code ou de présentation. +- API, schéma, textes et interface **originaux**. - Ne pas reprendre noms de classes, CSS, icônes, illustrations, ni wording propriétaire au-delà des libellés métier nécessaires. -- Traçabilité séparée : exigence observée → spécification interne → test d'acceptation. -- Ne pas supposer les règles cachées ; les valider avant de les coder. +- Traçabilité : exigence observée → spécification interne → test d'acceptation. +- Ne pas supposer les règles cachées ; les faire valider avant de les coder.