Claude 2e9a4c5efe WP-07 : heures prévu/réalisé/payé et périodes de paie verrouillables
Les trois grandeurs
Sans pointeuse, le réalisé est saisi par le manager. La matrice impose de
distinguer trois grandeurs et non deux, avec deux règles qui ne se négocient
pas :
- **Sans heures réelles, le prévu fait foi.** Attendre une saisie qui ne
  viendra pas ne produirait aucune paie. Une saisie partielle — un début sans
  fin — ne suffit pas : elle donnerait une durée fantaisiste.
- **Le paiement ne dépend jamais de la validation.** Une ligne non validée part
  en paie sur la base du réalisé. Bloquer le paiement d'heures accomplies faute
  de validation est précisément ce que la matrice interdit ; l'écran l'énonce
  pour qu'aucun manager ne croie l'inverse.

Toute correction d'heures déjà saisies exige un motif — sans lui, une
correction est indistinguable d'une erreur de saisie. Une première saisie n'en
demande pas : il n'y a rien à corriger, et exiger un motif à chaque ligne le
ferait remplir machinalement.

Le verrou de période
- Verrouiller fige les instantanés **et** ferme le mois : plus aucun créneau,
  aucune absence, aucune heure réelle ne peut être écrit sur ces dates. Le
  contrôle passe avant l'écriture, dans planning, absences et heures.
- Déverrouiller exige une justification. Rouvrir périme les fichiers déjà
  transmis au cabinet, et six mois plus tard personne ne saurait pourquoi.
- La **péremption d'un export est déduite** de `generatedAt < unlockedAt`,
  jamais stockée : la stocker obligerait à réécrire une trace qui doit rester
  append-only, et une trace réécrite ne prouve plus rien.
- Supprimer une période demande de **retaper son libellé** : un « êtes-vous
  sûr » se clique sans lire, et la suppression efface des instantanés sur
  lesquels un export a pu être bâti.

Un seul calcul pour trois écrans
Les instantanés viennent de `buildPayrollPeriod`, la même fonction que le
rapport de paie et l'export. C'est ce qui satisfait le critère croisé : trois
calculs séparés divergeraient, et l'écart ne se verrait qu'au bulletin. Un test
d'intégration compare instantané et rapport sur le même jeu de données.

Tests enfin idempotents
La suite e2e échouait au second passage : les absences acceptées d'une
exécution bloquaient les demandes de la suivante. Trois corrections, dans
l'ordre d'importance — chaque test travaille sur **son** salarié (le
chevauchement se juge par personne), chaque test libère ce qu'il a créé, et les
fenêtres de dates sont écartées d'une exécution à l'autre. Trois passages
consécutifs passent désormais.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
2026-08-08 12:25:02 +00:00
2026-08-07 17:54:11 +00:00
2026-08-07 17:54:11 +00:00
2026-08-07 17:54:11 +00:00
2026-08-07 17:54:11 +00:00
2026-08-07 17:54:11 +00:00
2026-08-07 17:54:11 +00:00
2026-08-07 17:54:11 +00:00
2026-08-07 17:54:11 +00:00
2026-08-07 17:54:11 +00:00

PlanFlow

Gestion du personnel, des plannings et des temps, multi-établissements, auto-hébergée. La paie est exportée vers Silae ; PlanFlow ne produit ni bulletin ni DSN.

La spécification de construction est PLAN.md. Elle est normative : en cas d'écart entre le code et le plan, c'est le plan qui a raison, ou le plan qui doit être corrigé — jamais l'écart qui s'installe.

Document Rôle
PLAN.md Spécification : périmètre, modèle de données, règles, lots de travail
matrice-conformite-rh-france-2026.md Exigences réglementaires françaises, cotées P0/P1/P2
Audit Combo/ Audit fonctionnel du produit de référence

État

WP-00 — socle et implémentation du design : six écrans sur données de démonstration.

WP-01 — tenancy, identité, autorisation : en cours. Modèle de données, RLS, journal d'audit append-only, catalogue de 70 capacités, cinq rôles, sessions en base. Les écrans lisent encore src/lib/demo.

Démarrer

Avec Docker

cp .env.example .env
# Renseigner POSTGRES_PASSWORD et ENCRYPTION_KEY (voir ci-dessous)
docker compose up --build

L'application écoute sur http://localhost:3000. Les migrations s'appliquent au démarrage du conteneur.

En local

Nécessite Node 22, pnpm 10 et un PostgreSQL 16 accessible.

pnpm install
cp .env.example .env          # renseigner DATABASE_URL et ENCRYPTION_KEY
pnpm db:generate
pnpm db:deploy
pnpm dev

Clé de chiffrement

ENCRYPTION_KEY chiffre au repos les colonnes sensibles exigées par le plan (§3.6) : NIR, IBAN, BIC.

openssl rand -base64 32

Elle vit hors de la base : une sauvegarde volée ne doit pas suffire à lire ces colonnes. La perdre rend ces données irrécupérables — la sauvegarder séparément et documenter sa rotation.

Vérifier

pnpm verify      # typecheck + lint + tests unitaires
pnpm test:e2e    # build, serveur standalone, tests de bout en bout

pnpm verify est ce que la CI exécute sur chaque pull request, suivi du build et des tests end-to-end.

Configuration de la base — à ne pas rater

L'application ne doit pas se connecter en superutilisateur PostgreSQL.

Un superutilisateur contourne la row-level security, y compris déclarée en FORCE. Connecter PlanFlow avec un tel compte désactive silencieusement la seconde couche d'isolation multi-tenant : les requêtes fonctionnent, les tests applicatifs passent, et rien n'indique que la protection a disparu — jusqu'au jour où quelqu'un lit les données d'un autre établissement.

CREATE ROLE planflow_app LOGIN PASSWORD '…' NOSUPERUSER NOBYPASSRLS;
GRANT USAGE ON SCHEMA public TO planflow_app;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO planflow_app;

Les migrations, elles, s'appliquent avec un compte propriétaire distinct.

L'application vérifie ce point au démarrage : elle refuse de démarrer en production sur une base mal configurée, et se contente d'un avertissement en développement. GET /api/sante expose l'état sous tenantIsolation.

Choix structurants

Aucun traceur tiers. L'audit du produit de référence a intercepté 2102 requêtes de traçage — Segment, LinkedIn Ads, Google Ads, DoubleClick, Clarity, Hotjar — et aucune requête métier. Une application RH ne doit pas envoyer un contexte de navigation portant sur des salariés identifiables à des régies publicitaires. Deux garde-fous rendent la règle vérifiable plutôt que déclarative :

  • une Content-Security-Policy qui ne nomme aucune origine externe, posée par requête avec un nonce (src/proxy.ts) ;
  • un test qui échoue si une dépendance de traçage apparaît dans package.json.

Pour de la télémétrie technique, passer par une interface abstraite auto-hébergée.

Le serveur testé est celui qui est déployé. Les tests end-to-end lancent le serveur standalone, celui que l'image Docker exécute — pas next dev, dont la politique de sécurité est volontairement plus permissive.

Structure

src/
├── app/                 écrans (App Router)
├── lib/
│   ├── env.ts           contrat d'environnement, validé à l'import
│   └── security/csp.ts  politique de sécurité, fonction pure et testable
├── server/
│   ├── db.ts            client Prisma — le scoping multi-tenant s'y greffe au WP-01
│   └── health.ts
└── proxy.ts             en-têtes de sécurité par requête
prisma/                  schéma et migrations
tests/
├── unit/                Vitest
└── e2e/                 Playwright

Écarts assumés par rapport au plan

Trois choix diffèrent de ce qu'annonçait PLAN.md §2, et le plan a été mis à jour en conséquence.

Sujet Plan initial Retenu Raison
Next.js 15 16.3 Version stable courante ; démarrer un greenfield une majeure en retard n'a pas de contrepartie.
Authentification Auth.js v5 Sessions maison en base Auth.js v5 est encore en beta. Le besoin se limite à identifiants et invitation, sans OAuth, et la matrice de conformité (n° 23) impose la révocation de session — immédiate avec des sessions en base, malaisée avec des jetons JWT.
Convention Next middleware.ts proxy.ts Next 16 a renommé la convention ; middleware est déprécié.
S
Description
No description provided
Readme
15 MiB
0 Stars 1 Watchers 0 Forks
Languages
TypeScript 97%
CSS 1.2%
Shell 0.7%
JavaScript 0.5%
PLpgSQL 0.4%
Other 0.2%