Patterns
Immutabilité du registre fiscal en PostgreSQL
INSERT-only avec triggers BEFORE UPDATE/DELETE
Le registre fiscal Scell.io (table `fiscal_entries`) doit être effectivement immuable : aucune mise à jour ni suppression possible une fois validé, conformément aux exigences ISCA de la DGFiP. Trois niveaux de défense en profondeur. (1) Niveau application : ISCA Guard Eloquent qui bloque toute mutation hors statut `draft`. (2) Niveau DB : trigger `BEFORE UPDATE OR DELETE` qui lève une exception si l'entrée est `validated`. (3) Niveau audit : tout tentative est loggée dans `fiscal_audit_logs` avec sévérité `critical`. La chaîne SHA-256 (chaque entrée hash inclut le hash précédent) rend toute falsification rétroactive immédiatement détectable via `GET /api/v1/fiscal/integrity`. Pattern complémentaire : révoquer le `UPDATE`/`DELETE` au niveau GRANT pour le rôle applicatif, ne donner ce droit qu'à un rôle `dba_emergency` utilisé exclusivement pour les correctifs de schéma (jamais pour modifier des données fiscales). Une suppression GDPR (droit à l'oubli) sur des PII non-fiscales se fait par anonymisation (NULL ou hash), pas par DELETE.
À retenir
- 3 niveaux : ISCA Guard app + trigger DB + audit log
- Trigger BEFORE UPDATE/DELETE leve exception sur entrée validated
- GRANT révoqué côté applicatif, rôle dba_emergency séparé
- Chaîne SHA-256 détecte toute falsification rétroactive
- GDPR : anonymisation des PII non-fiscales, jamais DELETE
Exemple de code
-- 1. Trigger d'immutabilité au niveau DB
CREATE OR REPLACE FUNCTION prevent_fiscal_mutation()
RETURNS TRIGGER AS $$
BEGIN
IF OLD.status = 'validated' THEN
RAISE EXCEPTION 'Fiscal entry % is immutable (status=validated)', OLD.id
USING ERRCODE = 'restrict_violation';
END IF;
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER fiscal_entries_immutable
BEFORE UPDATE OR DELETE ON fiscal_entries
FOR EACH ROW EXECUTE FUNCTION prevent_fiscal_mutation();
-- 2. Révocation des droits côté applicatif
REVOKE UPDATE, DELETE ON fiscal_entries FROM scell_app;
GRANT UPDATE, DELETE ON fiscal_entries TO dba_emergency;
-- 3. Vérification d'intégrité de la chaîne SHA-256
SELECT id, prev_hash, hash,
encode(sha256((id || prev_hash || data)::bytea), 'hex') AS computed
FROM fiscal_entries
WHERE encode(sha256((id || prev_hash || data)::bytea), 'hex') <> hash;
-- Doit retourner 0 lignes