ia_dev/projects/lecoffreio/docs/DATABASE_COMPLETE.md
Nicolas Cantu 61cec6f430 Sync ia_dev: token resolution via .secrets/<env>/ia_token, doc updates
**Motivations:**
- Align master with current codebase (token from projects/<id>/.secrets/<env>/ia_token)
- Id resolution by mail To or by API token; no slug

**Root causes:**
- Token moved from conf.json to .secrets/<env>/ia_token; env from directory name

**Correctifs:**
- Server and scripts resolve project+env by scanning all projects and envs

**Evolutions:**
- tickets-fetch-inbox routes by To address; notary-ai agents and API doc updated

**Pages affectées:**
- ai_working_help/server.js, docs, project_config.py, lib/project_config.sh
- projects/README.md, lecoffreio/docs/API.md, gitea-issues/tickets-fetch-inbox.py
2026-03-16 15:00:23 +01:00

48 KiB

Documentation Complète de la Base de Données LeCoffre.io

Date : 30 octobre 2025 Version : 2.0.1 Auteur : Équipe 4NK

Référence unique (checks de déploiement) : docs/DEPLOYMENT.md#cartographie-des-checks-de-déploiement-source-unique


📋 Table des matières

  1. Vue d'ensemble
  2. Architecture et structure
  3. Schéma Prisma complet
  4. Gestion des migrations
  5. Configuration système dynamique
  6. Soft Delete (Suppression logique)
  7. Reset et import de base de données
  8. Commandes et outils
  9. Liens et références

Vue d'ensemble

Base de données

  • Type : PostgreSQL 14+
  • Port : 5442 (non-standard, spécifique au projet)
  • ORM : Prisma 4.16
  • Schéma : lecoffre-back-main/src/common/databases/schema.prisma
  • Migrations : lecoffre-back-main/prisma/migrations/

Caractéristiques principales

  • Configuration dynamique : Tous les paramètres système stockés en BDD (system_configuration)
  • Soft delete : Suppression logique pour dossiers, documents, clients
  • Multi-offices : Gestion multi-études avec affiliations
  • Ancrage blockchain : Bitcoin Signet pour documents et dossiers
  • Chiffrement : AES-256-GCM pour données sensibles (RIB)
  • Synchronisation : Tables dédiées pour IdNot et API Annuaire
  • Audit trail : Logs d'audit et historique des modifications

Connexion

Format URL :

postgresql://USERNAME:PASSWORD@HOST:PORT/DATABASE?schema=public

Variables d'environnement :

DATABASE_HOST=<IP_POSTGRESQL>
DATABASE_PORT=<PORT_POSTGRESQL>  # 5442
DATABASE_USERNAME=lecoffre-user-${ENV}
DATABASE_PASSWORD=<MOT_DE_PASSE>
DATABASE_NAME=bdd-${ENV}
DATABASE_URL=postgresql://${DATABASE_USERNAME}:${DATABASE_PASSWORD}@${DATABASE_HOST}:${DATABASE_PORT}/${DATABASE_NAME}?schema=public

Architecture et structure

Organisation des modèles

1. Entités principales

  • Users : Utilisateurs notaires (authentification IdNot)
  • Offices : Études notariales
  • OfficeFolders : Dossiers clients
  • Documents : Documents clients
  • Customers : Clients (authentification email/2FA)
  • Contacts : Informations de contact (partagées)
  • Addresses : Adresses (partagées)

2. Gestion des rôles et permissions

  • Roles : Rôles globaux (admin, super-admin, notary, default)
  • OfficeRoles : Rôles spécifiques à un office (Notaire, Collaborateur)
  • Rules : Règles d'accès (CRUD par ressource)
  • RolePermissionsMatrix : Matrice de permissions (143 lignes)

3. Abonnements et facturation

  • SubscriptionPlans : Plans d'abonnement (Standard, Unlimited)
  • Subscriptions : Abonnements actifs par office
  • Seats : Sièges utilisateurs par abonnement

4. Ancrage blockchain

  • DocumentAnchors : Ancres de documents individuels
  • OfficeFolderAnchors : Ancres de dossiers (agrégées)
  • DocumentNotaryAnchors : Ancres de documents notaires
  • AggregatedCertificates : Certificats agrégés par dossier

5. Synchronisation externe

  • SyncOffices : Synchronisation offices IdNot
  • SyncPersons : Synchronisation personnes IdNot
  • SyncAffiliations : Synchronisation affiliations IdNot
  • SyncStripeSubscriptions : Synchronisation abonnements Stripe
  • SyncDailyMetrics : Métriques quotidiennes
  • AnnuOffices : Offices API Annuaire
  • AnnuPersons : Personnes API Annuaire
  • AnnuAffiliations : Affiliations API Annuaire

6. Configuration et textes

  • SystemConfiguration : Configuration système dynamique (109 configs)
  • SiteTexts : Textes éditoriaux multi-langue

7. Sécurité et audit

  • RevokedTokens : Tokens JWT révoqués
  • AuditLogs : Logs d'audit des actions
  • Whitelist : Liste blanche emails
  • UserWhitelist : Liste blanche IdNot

8. Tiers et partage

  • FolderThirdParties : Tiers (courtiers, agents) associés aux dossiers
  • ThirdPartyTotpCodes : Codes OTP pour tiers
  • FolderSharing : Partage de dossiers entre offices

Schéma Prisma complet

Modèles principaux

Users (Utilisateurs notaires)

model Users {
  uid                 String                   @id @unique @default(uuid())
  idNot               String                   @unique @db.VarChar(255)
  contact             Contacts                 @relation(...)
  contact_uid         String                   @unique @db.VarChar(255)
  role                Roles                    @relation(...)
  roles_uid           String                   @db.VarChar(255)
  is_super_admin      Boolean                  @default(false)
  office_role         OfficeRoles?             @relation(...)
  office_role_uid     String?                  @db.VarChar(255)
  office_membership   Offices                  @relation(...)
  office_uid          String                   @db.VarChar(255)
  // Relations
  office_folders      OfficeFolders[]
  created_folders     OfficeFolders[]
  documents_notary    DocumentsNotary[]
  office_affiliations UserOfficeAffiliations[]
  shared_folders      FolderSharing[]
  third_parties_added FolderThirdParties[]
}

Caractéristiques :

  • Authentification via IdNot OAuth
  • Rôles globaux et rôles d'office
  • Support multi-offices via UserOfficeAffiliations
  • Flag is_super_admin pour accès super-admin

Offices (Études notariales)

model Offices {
  uid                 String                    @id @unique @default(uuid())
  idNot               String                    @unique @db.VarChar(255)
  name                String                    @db.VarChar(255)
  crpcen              String                    @unique @db.VarChar(255)
  address             Addresses                 @relation(...)
  address_uid         String                    @unique @db.VarChar(255)
  office_status       EOfficeStatus             @default(DESACTIVATED)
  // RIB chiffré
  rib_encrypted       String?                   @db.Text
  rib_iv              String?                   @db.VarChar(255)
  rib_auth_tag        String?                   @db.VarChar(255)
  rib_key_version     String?                   @db.VarChar(50)
  rib_encrypted_at    DateTime?
  // Ancrage RIB
  rib_anchor_hash     String?                   @db.VarChar(255)
  rib_anchor_tx_id    String?                   @db.VarChar(255)
  rib_anchor_tx_link  String?                   @db.VarChar(255)
  rib_anchor_tx_hash  String?                   @db.VarChar(255)
  rib_anchor_block_height Int?
  rib_anchor_anchored_at DateTime?
  rib_anchor_proof_data Json?
  // Clé de chiffrement office
  office_encryption_key String?                  @db.VarChar(255)
  office_encryption_key_created_at DateTime?
  // Relations
  users               Users[]
  office_folders      OfficeFolders[]
  subscriptions       Subscriptions[]
  customers           Customers[]
  user_affiliations   UserOfficeAffiliations[]
}

Caractéristiques :

  • Chiffrement AES-256-GCM pour RIB
  • Ancrage blockchain Bitcoin Signet pour RIB
  • Clé de chiffrement unique par office
  • Statut activation/désactivation

OfficeFolders (Dossiers)

model OfficeFolders {
  uid                  String            @id @unique @default(uuid())
  folder_number        String            @db.VarChar(255)
  name                 String            @db.VarChar(255)
  description          String?           @db.VarChar(1000)
  archived_description String?           @db.VarChar(255)
  status               EFolderStatus     @default(LIVE)
  deed                 Deeds             @relation(...)
  deed_uid             String            @unique @db.VarChar(255)
  office               Offices           @relation(...)
  office_uid           String            @db.VarChar(255)
  created_by           Users?            @relation(...)
  created_by_uid       String?           @db.VarChar(255)
  // Soft delete
  deleted_at           DateTime?
  deleted_by_uid       String?           @db.VarChar(255)
  // Ancrage
  folder_anchor        OfficeFolderAnchors? @relation(...)
  folder_anchor_uid    String?              @unique @db.VarChar(255)
  // Relations
  stakeholders         Users[]
  customers            Customers[]
  documents            Documents[]
  documents_notary     DocumentsNotary[]
  folder_sharings      FolderSharing[]
  third_parties        FolderThirdParties[]
  aggregated_certificates AggregatedCertificates[]
}

Statuts :

  • LIVE : Dossier actif
  • ARCHIVED : Dossier archivé
  • DELETED : Dossier supprimé (soft delete)

Documents

model Documents {
  uid                       String              @id @unique @default(uuid())
  document_status           EDocumentStatus     @default(ASKED)
  document_type             DocumentTypes       @relation(...)
  document_type_uid         String              @db.VarChar(255)
  folder                    OfficeFolders       @relation(...)
  folder_uid                String              @db.VarChar(255)
  depositor                 Customers?          @relation(...)
  depositor_uid             String?             @db.VarChar(255)
  third_party_depositor     FolderThirdParties? @relation(...)
  third_party_depositor_uid String?             @db.VarChar(255)
  shared_to_office          Offices?            @relation(...)
  shared_to_office_uid      String?             @db.VarChar(255)
  // Soft delete
  deleted_at                DateTime?
  deleted_by_uid            String?             @db.VarChar(255)
  // Relations
  files                     Files[]
  document_history          DocumentHistory[]
  reminders                 DocumentsReminder[]
  document_anchor           DocumentAnchors?
}

Statuts :

  • ASKED : Document demandé
  • DEPOSITED : Document déposé
  • VALIDATED : Document validé
  • REFUSED : Document refusé

Files (Fichiers)

model Files {
  uid                String    @id @unique @default(uuid())
  document           Documents @relation(...)
  document_uid       String    @db.VarChar(255)
  file_path          String    @unique @db.VarChar(255)
  file_name          String    @db.VarChar(255)
  original_file_name String?   @db.VarChar(510) // Nom du fichier original (avant normalisation)
  display_name       String?   @db.VarChar(510) // Nom final généré par le backend
  mimetype           String    @db.VarChar(255)
  hash               String    @db.VarChar(255) // Hash filigrané (ancré)
  original_hash      String?   @db.VarChar(255) // Hash original (avant filigrane)
  size               Int
  archived_at        DateTime?
  key                String?   @db.VarChar(255)
  watermarked_s3_key String?   @db.VarChar(1000)
  watermarked_at     DateTime?
}

Caractéristiques :

  • original_file_name : Nom du fichier original (avant normalisation) - stocké lors de la création pour traçabilité
  • display_name : Nom final généré par le backend selon les conventions de nommage (préfixe dépositaire N/C/T/NI, segments FirstName.LastName, suffixe .aplc sans index).
  • file_name : Nom normalisé utilisé pour le stockage (IPFS, S3)
  • Hash SHA-256 du fichier filigrané (ancré)
  • Hash SHA-256 du fichier original (stocké pour vérification)
  • Filigrane automatique avant chiffrement
  • Stockage IPFS (Pinata) via S3

DocumentAnchors (Ancres de documents)

model DocumentAnchors {
  uid String @id @unique @default(uuid())

  document     Documents @relation(...)
  document_uid String    @unique @db.VarChar(255)

  file_hash String @db.VarChar(255) // Hash filigrané ancré

  blockchain    EBlockchainName @default(BITCOIN_SIGNET)
  anchor_nb_try Int             @default(0)

  anchored_at     DateTime?
  anchored_by_uid String?   @db.VarChar(255)
  tx_id           String?   @db.VarChar(255)
  tx_link         String?   @db.VarChar(255)
  tx_hash         String?   @db.VarChar(255)
  anchor_job_id   String?   @db.VarChar(255)
  block_height    Int?
  block_time      DateTime?
  confirmations   Int?
  proof_data      Json? // Inclut original_hash
}

Caractéristiques :

  • Ancrage Bitcoin Signet
  • Preuve complète dans proof_data (inclut original_hash)
  • Lien explorateur blockchain dans tx_link

SystemConfiguration (Configuration système)

model SystemConfiguration {
  uid String @id @unique @default(uuid())

  key         String          @unique @db.VarChar(255)
  value       String?         @db.Text
  description String?         @db.Text
  category    EConfigCategory

  value_type   EConfigValueType @default(STRING)
  is_sensitive Boolean          @default(false)
  is_required  Boolean          @default(false)
  default_value String?         @db.Text

  created_at DateTime @default(now())
  updated_at DateTime @updatedAt
  created_by String?  @db.VarChar(255)
  updated_by String?  @db.VarChar(255)
  change_history Json?
}

Catégories :

  • FRONTEND : Paramètres Next.js
  • BACKEND : Paramètres serveur
  • INTEGRATION_IDNOT : ID.NOT (OpenID, API Annuaire)
  • INTEGRATION_BITCOIN : Bitcoin Signet
  • INTEGRATION_STRIPE : Stripe (abonnements)
  • INTEGRATION_MAILCHIMP : Mailchimp (emails)
  • INTEGRATION_PINATA : IPFS/Pinata
  • INTEGRATION_DOCAPOST : Docapost (OCR)
  • INTEGRATION_OVH : OVH (SMS)
  • SECURITY : Secrets, tokens
  • PERFORMANCE : Limites, cache
  • FEATURE_FLAGS : Activation fonctionnalités
  • SYSTEM : Logs, monitoring

Enums

Statuts

enum EFolderStatus {
  LIVE      // En cours
  ARCHIVED  // Archivé
  DELETED   // Supprimé (soft delete)
}

enum EDocumentStatus {
  ASKED     // Demandé
  DEPOSITED // Déposé
  VALIDATED // Validé
  REFUSED   // Refusé
}

enum ECustomerStatus {
  VALIDATED // Validé
  PENDING   // En attente
  ERRONED   // Erroné
}

enum EOfficeStatus {
  ACTIVATED   // Activé
  DESACTIVATED // Désactivé
}

enum ESubscriptionStatus {
  ACTIVE   // Actif
  INACTIVE // Inactif
}

enum ESubscriptionType {
  STANDARD  // Standard
  UNLIMITED // Illimité
}

Blockchain

enum EBlockchainName {
  TEZOS
  BITCOIN_SIGNET
}

Partage

enum EShareRole {
  OWNER         // Propriétaire
  CONTRIBUTOR   // Contributeur
  GUEST_NOTARY  // Notaire invité
  VIEWER        // Lecteur
}

enum EShareStatus {
  ACTIVE   // Actif
  REVOKED  // Révoqué
  EXPIRED  // Expiré
}

Tiers

enum EThirdPartyRole {
  COURTIER
  AGENT_IMMOBILIER
  TUTELLE
  SYNDIC
  EXPERT
  AUTRE
}

enum EAuthMethod {
  EMAIL_CODE
  SMS_CODE
}

Gestion des migrations

Structure des migrations

Répertoire : lecoffre-back-main/prisma/migrations/

Format : YYYYMMDDHHMMSS_nom_migration/

Exemples :

  • 20251020150254_add_system_configuration/
  • 20251029160000_add_watermark_and_proof_columns/
  • 20251201140000_add_original_hash_to_files/

Commandes Prisma

Générer le client Prisma

cd lecoffre-back-main
npx prisma generate

Créer une migration

# Développement (crée et applique)
npx prisma migrate dev --name nom_migration

# Production (crée uniquement)
npx prisma migrate dev --create-only --name nom_migration

Appliquer les migrations

# Production (via npm script)
npm run migrate

# Directement
npx prisma migrate deploy --schema=src/common/databases/schema.prisma

Ouvrir Prisma Studio

npx prisma studio

Reset la base de données (⚠️ danger)

npx prisma migrate reset

Gestion du schéma

Fichier : lecoffre-back-main/src/common/databases/schema.prisma

Configuration : lecoffre-back-main/package.json

{
  "prisma": {
    "schema": "src/common/databases/schema.prisma"
  }
}

Lien symbolique migrations

Le script de déploiement crée automatiquement un lien symbolique pour que Prisma trouve les migrations :

# Création automatique lors du déploiement
src/common/databases/migrations → prisma/migrations

Vérification :

ls -la lecoffre-back-main/src/common/databases/

Détection migrations baselinées

Le script de déploiement détecte automatiquement les migrations marquées comme appliquées sans être exécutées et les réinitialise si nécessaire.

Détection :

  • Vérifie si _prisma_migrations existe avec des entrées
  • Vérifie si system_configuration existe (créée par une migration)
  • Si migrations marquées mais system_configuration absent → réinitialisation automatique

Configuration système dynamique

Vue d'ensemble

Tous les paramètres système sont stockés dans la table system_configuration au lieu de fichiers .env. Cela permet :

  • Modification en temps réel via interface web
  • Historique complet des modifications
  • Masquage automatique des valeurs sensibles
  • Catégorisation par type d'intégration
  • Validation des valeurs requises

Import depuis fichiers

Script : npm run config:import-env

Fichiers : .secrets/<env>/env-full-<env>-for-bdd-injection.txt

Exécution :

cd /home/debian/sites/<env>-lecoffreio.4nkweb.com
set -a && source .secrets/<env>/.env.<env> && set +a
# Note: scripts_v2 utilise systemd. Sur le serveur, dans le répertoire backend :
cd lecoffre-back-main && npm run config:import-env -- --env <env>

Ce que fait le script :

  • Lit .secrets/<env>/env-full-<env>-for-bdd-injection.txt
  • Exclut les variables DATABASE_*
  • Crée ou met à jour chaque variable dans la BDD
  • Catégorise automatiquement selon le mapping
  • Conserve un historique des modifications

Interface web

URL : /super-admin/system-config

Accès : Super-admin uniquement

Fonctionnalités :

  • Vue d'ensemble de toutes les configurations
  • Filtrage par catégorie
  • Affichage/masquage valeurs sensibles
  • Édition avec historique
  • Validation des valeurs requises

API REST

  • GET /api/v1/super-admin/system-config : Liste toutes les configurations
  • GET /api/v1/super-admin/system-config/category/:category : Filtre par catégorie
  • GET /api/v1/super-admin/system-config/:key : Récupère une configuration
  • PUT /api/v1/super-admin/system-config/:uid : Met à jour une configuration
  • POST /api/v1/super-admin/system-config : Crée une configuration
  • DELETE /api/v1/super-admin/system-config/:uid : Supprime une configuration
  • GET /api/v1/super-admin/system-config/validate : Valide les configurations requises

Variables sensibles

Automatiquement identifiées par :

  • is_sensitive: true dans le mapping
  • Clés contenant : SECRET, KEY, PASSWORD, TOKEN

Exemples :

  • ACCESS_TOKEN_SECRET
  • IDNOT_CLIENT_SECRET
  • RIB_ENCRYPTION_MASTER_KEY
  • STRIPE_SECRET_KEY

Exclusions

Variables JAMAIS stockées en BDD :

  • DATABASE_URL
  • DATABASE_*
  • POSTGRES_*

Ces variables restent exclusivement dans les fichiers .env.

Documentation complète : Voir README.md et API.md.


Soft Delete (Suppression logique)

Principe

Au lieu de supprimer physiquement (DELETE FROM), on marque l'enregistrement comme supprimé avec deleted_at et deleted_by_uid.

Tables concernées

  • OfficeFolders : deleted_at, deleted_by_uid
  • Documents : deleted_at, deleted_by_uid
  • Customers : deleted_at, deleted_by_uid

Statut DELETED

Pour les dossiers, le statut passe à DELETED :

enum EFolderStatus {
  LIVE      // En cours
  ARCHIVED  // Archivé
  DELETED   // Supprimé (soft delete)
}

Filtrage par défaut

Les repositories filtrent automatiquement les enregistrements supprimés :

// Par défaut, exclure les supprimés
query.where = {
  ...query.where,
  deleted_at: null,
};

Méthodes standardisées

// Soft delete
async softDelete(uid: string, deletedByUid: string): Promise<Entity> {
  return this.repository.update(uid, {
    deleted_at: new Date(),
    deleted_by_uid: deletedByUid,
    status: EFolderStatus.DELETED, // Pour les dossiers
  });
}

// Restauration
async restore(uid: string): Promise<Entity> {
  return this.repository.update(uid, {
    deleted_at: null,
    deleted_by_uid: null,
    status: EFolderStatus.LIVE, // Pour les dossiers
  });
}

Endpoints API

  • DELETE /api/v1/notary/folders/:uidsoftDelete()
  • POST /api/v1/notary/folders/:uid/restorerestore()
  • GET /api/v1/notary/folders/deleted → Liste corbeille

Avantages

  1. Traçabilité : On sait qui a supprimé et quand
  2. Récupération : Possibilité de restaurer facilement
  3. Audit : Historique complet des suppressions
  4. Conformité RGPD : Les données peuvent être anonymisées plus tard
  5. Sécurité : Évite les suppressions accidentelles irréversibles

Documentation complète : Voir README.md et cette section.


Reset et import de base de données

Vue d'ensemble

LeCoffre.io dispose d'un système pour recréer et importer une base de données depuis un dump SQL.

⚠️ ATTENTION : Cette opération est DESTRUCTIVE et supprime toutes les données existantes.

Script standalone

Fichier : deploy/scripts/reset-and-import-database.sh

Usage :

./deploy/scripts/reset-and-import-database.sh <ENV> [SQL_FILE]

Arguments :

  • ENV : Environnement cible (test | pprod | prod)
  • SQL_FILE : (Optionnel) Chemin vers le dump SQL. Si non fourni, utilise .secrets/<ENV>/bdd.<ENV>

Exemples :

# Utiliser le fichier par défaut (deploy/bdd.test)
./deploy/scripts/reset-and-import-database.sh test

# Spécifier un dump SQL
./deploy/scripts/reset-and-import-database.sh test deploy/backups/bdd-test_20251103_120000.sql

# Importer un dump depuis un autre environnement
./deploy/scripts/reset-and-import-database.sh test /path/to/production-dump.sql

Ce que fait le script :

  1. Crée un backup de sécurité de la base actuelle
  2. Supprime la base de données
  3. Recrée la base de données vide
  4. Importe les données depuis le dump SQL
  5. Génère le client Prisma
  6. Applique les migrations Prisma (via npm run migrate)
  7. Synchronise le schéma (prisma db push) sans reseed

Via script de déploiement

Option : --resetDatabase [SQL_FILE]

Usage :

# Reset avec le fichier par défaut (.secrets/test/bdd.test)
./deploy/scripts/build-and-deploy.sh test --resetDatabase

# Reset avec un dump spécifique
./deploy/scripts/build-and-deploy.sh test --resetDatabase deploy/backups/bdd-test_20251103_120000.sql

# Combiner avec d'autres options
./deploy/scripts/build-and-deploy.sh test --deploy --resetDatabase --setSettings

Étapes post-import indispensables

Après chaque reset/import, exécuter immédiatement :

1. Réinjecter la configuration dynamique

cd /home/debian/sites/<env>-lecoffreio.4nkweb.com
set -a && source .secrets/<env>/.env.<env> && set +a
# Note: scripts_v2 utilise systemd. Sur le serveur, dans le répertoire backend :
cd lecoffre-back-main && npm run config:import-env -- --env <env>

2. Re-seeder la matrice des permissions

# Sur le serveur, avec accès PostgreSQL (psql) :
psql -h ... -U ... -d ... -f ... \
  psql -U "$DATABASE_USERNAME" -d "$DATABASE_NAME" \
  -f /repo/lecoffre-back-main/prisma/migrations/20251107_role_permissions_matrix/migration.sql

Vérification : SELECT COUNT(*) FROM role_permissions_matrix; → 143 lignes attendues

3. Contrôles rapides

  • SELECT COUNT(*) FROM system_configuration; → ~109 entrées
  • /api/v1/public/health doit répondre 200
  • Connexion Id.Not : badge Visiteur affiché si pas d'abonnement

Gestion des backups

Emplacement des fichiers SQL :

deploy/
├── bdd.test                           # ⭐ Dump SQL par défaut pour test
├── bdd.pprod                          # ⭐ Dump SQL par défaut pour pprod
├── bdd.prod                           # ⭐ Dump SQL par défaut pour prod
└── backups/
    ├── bdd-test_20251103_120000.sql       # Backup automatique lors du déploiement
    ├── safety-backup-test_20251103_150000.sql  # Backup de sécurité avant reset
    └── ...

Backups automatiques :

  • Déploiement (--deploy) : Crée bdd-<ENV>_<timestamp>.sql
  • Reset (--resetDatabase) : Crée safety-backup-<ENV>_<timestamp>.sql

Documentation complète : Voir DATABASE_RESET_GUIDE.md


Commandes et outils

Commandes Prisma

# Générer le client Prisma
npx prisma generate

# Créer une migration
npx prisma migrate dev --name nom_migration

# Appliquer les migrations
npx prisma migrate deploy --schema=src/common/databases/schema.prisma

# Ouvrir Prisma Studio (interface graphique)
npx prisma studio

# Reset la base de données (⚠️ danger)
npx prisma migrate reset

Scripts npm backend

cd lecoffre-back-main

# Migrations
npm run migrate

# Configuration
npm run config:import-env

# Ancrage
npm run anchorage
npm run anchorage:reanchor
npm run anchorage:complete
npm run anchorage:reanchor-all

# Utilitaires
npm run promote:superadmin
npm run activate:subscriptions
npm run token:get

Connexion PostgreSQL

# Connexion directe
psql -h <IP_POSTGRESQL> -p <PORT_POSTGRESQL> -U <DATABASE_USERNAME> -d <DATABASE_NAME>

# Sur le serveur (accès PostgreSQL)
psql -U <DATABASE_USERNAME> -d <DATABASE_NAME>

Vérifications

# Compter les enregistrements
psql -h <IP_POSTGRESQL> -p <PORT_POSTGRESQL> -U <DATABASE_USERNAME> -d <DATABASE_NAME> \
  -c "SELECT 'offices' as table, COUNT(*) FROM offices UNION SELECT 'users', COUNT(*) FROM users;"

# Vérifier les configurations
psql ... -c "SELECT COUNT(*) FROM system_configuration;"
psql ... -c "SELECT category, COUNT(*) FROM system_configuration GROUP BY category;"

# Vérifier les permissions
psql ... -c "SELECT COUNT(*) FROM role_permissions_matrix;"

Liens et références

Documentation principale

Schéma et migrations

  • Schéma Prisma : lecoffre-back-main/src/common/databases/schema.prisma
  • Migrations : lecoffre-back-main/prisma/migrations/
  • Package.json : lecoffre-back-main/package.json (configuration Prisma)

Scripts de déploiement

  • Reset database : deploy/scripts/reset-and-import-database.sh
  • Déploiement : deploy/scripts/build-and-deploy.sh
  • Migrations : deploy/scripts/build-and-deploy-local-migrateResolveDatabase.sh

Documentation externe

Fichiers de configuration

  • Variables d'environnement : .secrets/<env>/.env.<env>
  • Injection BDD : .secrets/<env>/env-full-<env>-for-bdd-injection.txt
  • Dumps SQL : .secrets/<env>/bdd.<env>

Relations complètes entre les tables

Graphe des relations principales

1. Entités de base (Contacts, Addresses)

Addresses (1) ──< (0..1) Contacts
Addresses (1) ──< (0..1) Offices
Contacts (1) ──< (0..1) Users
Contacts (1) ──< (0..1) Customers

Relations :

  • AddressesContacts : One-to-One (optionnel)
  • AddressesOffices : One-to-One (optionnel)
  • ContactsUsers : One-to-One (obligatoire)
  • ContactsCustomers : One-to-One (obligatoire)

2. Utilisateurs et offices

Offices (1) ──< (N) Users (office_membership)
Offices (1) ──< (N) Users (office_affiliations via UserOfficeAffiliations)
Offices (1) ──< (N) OfficeRoles
Offices (1) ──< (N) OfficeFolders
Offices (1) ──< (N) Customers
Offices (1) ──< (N) Subscriptions
Offices (1) ──< (N) DocumentTypes
Offices (1) ──< (N) DeedTypes
Offices (1) ──< (N) FolderSharing (shared_from_office)
Offices (1) ──< (N) FolderSharing (shared_to_office)
Offices (1) ──< (N) Documents (shared_to_office)

Relations :

  • Users.office_membershipOffices : Many-to-One (ancien système, conservé)
  • UserOfficeAffiliations : Many-to-Many entre Users et Offices (nouveau système multi-office)
  • Users.office_roleOfficeRoles : Many-to-One (optionnel)
  • Users.roleRoles : Many-to-One (obligatoire)

3. Dossiers et documents

OfficeFolders (1) ──< (N) Documents
OfficeFolders (1) ──< (N) DocumentsNotary
OfficeFolders (1) ──< (N) Notes
OfficeFolders (1) ──< (N) FolderSharing
OfficeFolders (1) ──< (N) FolderThirdParties
OfficeFolders (1) ──< (N) AggregatedCertificates
OfficeFolders (1) ──< (1) OfficeFolderAnchors
OfficeFolders (1) ──< (1) Deeds
OfficeFolders (N) ──> (1) Offices
OfficeFolders (N) ──> (1) Users (created_by)
OfficeFolders (N) ──> (M) Users (stakeholders - Many-to-Many)
OfficeFolders (N) ──> (M) Customers (Many-to-Many)

Relations :

  • OfficeFolders.deedDeeds : Many-to-One (obligatoire)
  • OfficeFolders.folder_anchorOfficeFolderAnchors : One-to-One (optionnel)
  • OfficeFolders.stakeholdersUsers : Many-to-Many
  • OfficeFolders.customersCustomers : Many-to-Many

4. Documents et fichiers

Documents (1) ──< (N) Files
Documents (1) ──< (N) DocumentHistory
Documents (1) ──< (N) DocumentsReminder
Documents (1) ──< (1) DocumentAnchors
Documents (N) ──> (1) OfficeFolders
Documents (N) ──> (1) DocumentTypes
Documents (N) ──> (1) Customers (depositor)
Documents (N) ──> (1) FolderThirdParties (third_party_depositor)
Documents (N) ──> (1) Offices (shared_to_office)

Relations :

  • Documents.depositorCustomers : Many-to-One (optionnel, pour clients)
  • Documents.third_party_depositorFolderThirdParties : Many-to-One (optionnel, pour tiers)
  • Documents.shared_to_officeOffices : Many-to-One (optionnel, pour confrères invités)
  • Documents.document_anchorDocumentAnchors : One-to-One (optionnel)

5. Ancrage blockchain

DocumentAnchors (1) ──> (1) Documents
OfficeFolderAnchors (1) ──> (1) OfficeFolders
DocumentNotaryAnchors (1) ──> (1) DocumentsNotary
AggregatedCertificates (N) ──> (1) OfficeFolders

Relations :

  • Tous les ancres sont en relation One-to-One avec leur entité source
  • AggregatedCertificates : Certificats agrégés par dossier (plusieurs certificats possibles)

6. Rôles et permissions

Roles (1) ──< (N) Users
Roles (N) ──> (M) Rules (Many-to-Many via relation implicite)
OfficeRoles (1) ──< (N) Users
OfficeRoles (N) ──> (M) Rules (Many-to-Many via relation implicite)
Rules (N) ──> (M) RulesGroups (Many-to-Many)
RolePermissionsMatrix : Table indépendante (matrice 143 lignes)

Relations :

  • RolesRules : Many-to-Many (via RolesHasRules)
  • OfficeRolesRules : Many-to-Many (via OfficeRolesHasRules)
  • RulesRulesGroups : Many-to-Many (via RulesGroupsHasRules)

7. Abonnements et sièges

Subscriptions (1) ──< (N) Seats
Subscriptions (N) ──> (1) Offices
Subscriptions (N) ──> (1) SubscriptionPlans (optionnel)
Seats (N) ──> (1) Users

Relations :

  • Subscriptions.officeOffices : Many-to-One (obligatoire)
  • Subscriptions.subscription_planSubscriptionPlans : Many-to-One (optionnel)
  • Seats.subscriptionSubscriptions : Many-to-One (obligatoire)
  • Seats.userUsers : Many-to-One (obligatoire)

8. Partage et tiers

FolderSharing (N) ──> (1) OfficeFolders
FolderSharing (N) ──> (1) Offices (shared_from_office)
FolderSharing (N) ──> (1) Offices (shared_to_office)
FolderSharing (N) ──> (1) Users (shared_by_user)
FolderThirdParties (N) ──> (1) OfficeFolders
FolderThirdParties (N) ──> (1) Users (added_by)
FolderThirdParties (1) ──< (N) Documents (third_party_depositor)
FolderThirdParties (1) ──< (N) ThirdPartyTotpCodes

Relations :

  • FolderSharing : Partage de dossiers entre offices (contrainte unique sur [folder_uid, shared_to_office_uid]). Notaire invité identifié par invited_notary_idnot (IdNot) et invited_notary_email (email de préférence pour les envois ; seul champ email, par défaut depuis IdNot à l'invitation). Accès aux dossiers invités = croisement sur IdNot ; envois = email en base uniquement. Voir docs/ARCHITECTURE.md § Partage Inter-Études.
  • FolderThirdParties : Tiers associés aux dossiers (courtiers, agents, etc.)

9. Types de documents et actes

DeedTypes (1) ──< (N) Deeds
DeedTypes (N) ──> (M) DocumentTypes (Many-to-Many via DeedTypeHasDocumentTypes)
Deeds (1) ──< (1) OfficeFolders
Deeds (N) ──> (M) DocumentTypes (Many-to-Many via DeedHasDocumentTypes)
DocumentTypes (N) ──> (1) Offices
DocumentTypes (1) ──< (N) Documents
DocumentTypes (N) ──> (M) DeedTypes (Many-to-Many)
DocumentTypes (N) ──> (M) Deeds (Many-to-Many)
DocumentTypes (1) ──< (N) DocumentRemindersConfig

Relations :

  • DeedTypesDocumentTypes : Many-to-Many (via DeedTypeHasDocumentTypes)
  • DeedsDocumentTypes : Many-to-Many (via DeedHasDocumentTypes)
  • Contrainte unique sur DocumentTypes : [name, office_uid]
  • Contrainte unique sur DeedTypes : [name, office_uid]

10. Synchronisation externe

SyncOffices : Table indépendante (snapshot offices IdNot)
SyncPersons : Table indépendante (snapshot personnes IdNot)
SyncAffiliations : Table indépendante (snapshot affiliations IdNot)
SyncStripeSubscriptions : Table indépendante (snapshot abonnements Stripe)
SyncDailyMetrics : Table indépendante (métriques quotidiennes)
AnnuOffices : Table indépendante (API Annuaire - offices)
AnnuPersons : Table indépendante (API Annuaire - personnes)
AnnuAffiliations : Table indépendante (API Annuaire - affiliations)

Relations :

  • Tables de synchronisation indépendantes (pas de foreign keys)
  • Index sur idnot, office_uid, user_uid pour recherche rapide

11. Configuration et textes

SystemConfiguration : Table indépendante (109 configs)
SiteTexts : Table indépendante (textes éditoriaux)

Relations :

  • Tables indépendantes
  • Contrainte unique sur SystemConfiguration.key
  • Contrainte unique sur SiteTexts : [text_key, locale, version]

12. Sécurité et audit

RevokedTokens : Table indépendante (tokens JWT révoqués)
AuditLogs : Table indépendante (logs d'audit)
Whitelist : Table indépendante (emails autorisés)
UserWhitelist : Table indépendante (IdNot autorisés)

Relations :

  • Tables indépendantes
  • Index sur RevokedTokens.jti et RevokedTokens.user_uid

Patterns de requêtes Prisma

Structure de base des repositories

Tous les repositories étendent BaseRepository qui définit :

  • maxFetchRows = 100 : Limite maximale de résultats
  • defaultFetchRows = 50 : Limite par défaut

Patterns de requêtes courants

1. FindMany avec pagination

// Pattern standard dans tous les repositories
public async findMany(query: Prisma.ModelFindManyArgs) {
  query.take = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows);
  return this.model.findMany(query);
}

Exemples :

// Documents avec pagination
await documentsRepository.findMany({
  where: { folder_uid: folderUid },
  include: { document_type: true, files: true },
  orderBy: { created_at: 'desc' },
  take: 20,
  skip: 0
});

// Users avec relations
await usersRepository.findMany({
  where: { office_uid: officeUid },
  include: {
    contact: true,
    role: true,
    office_membership: { include: { address: true } }
  }
});

2. FindUnique avec include optionnel

// Pattern avec include optionnel
public async findOneByUid(uid: string, include?: Prisma.ModelInclude) {
  return this.model.findUnique({
    where: { uid },
    include
  });
}

Exemples :

// Document simple
const doc = await documentsRepository.findOneByUid(documentUid);

// Document avec relations complètes
const doc = await documentsRepository.findOneByUid(documentUid, {
  folder: { include: { office: true } },
  document_type: true,
  files: true,
  depositor: { include: { contact: true } }
});

3. FindFirst pour recherche conditionnelle

// Pattern pour recherche avec conditions
public async findOne(query: Prisma.ModelFindFirstArgs) {
  return this.model.findFirst(query);
}

Exemples :

// Customer par email
const customer = await customersRepository.findOne({
  where: {
    contact: { email: userEmail }
  },
  include: { contact: { include: { address: true } } }
});

// Folder par numéro et office
const folder = await officeFoldersRepository.findFirst({
  where: {
    folder_number: folderNumber,
    office_uid: officeUid
  }
});

4. Create avec relations imbriquées

// Pattern pour création avec relations
public async create(data: Entity): Promise<Model> {
  return this.model.create({
    data: {
      // Champs simples
      field1: data.field1,
      // Relations avec connect
      relation1: {
        connect: { uid: data.relation1.uid }
      },
      // Relations avec connectOrCreate
      relation2: {
        connectOrCreate: {
          where: { uniqueField: data.relation2.uniqueField },
          create: { /* ... */ }
        }
      },
      // Relations Many-to-Many avec connect
      manyRelation: {
        connect: data.manyRelation.map(uid => ({ uid }))
      }
    },
    include: {
      relation1: true,
      relation2: true
    }
  });
}

Exemples :

// User avec contact et office
await usersRepository.create({
  idNot: user.idNot,
  contact: {
    create: {
      first_name: contact.first_name,
      last_name: contact.last_name,
      email: contact.email,
      address: {
        create: {
          address: address.address,
          city: address.city,
          zip_code: address.zip_code
        }
      }
    }
  },
  office_membership: {
    connectOrCreate: {
      where: { idNot: office.idNot },
      create: { /* ... */ }
    }
  }
});

// Folder avec stakeholders
await officeFoldersRepository.create(folder, createdByUid, {
  stakeholders: {
    connect: stakeholderUids.map(uid => ({ uid }))
  }
});

5. Update avec relations

// Pattern pour mise à jour avec relations
public async update(uid: string, data: Partial<Entity>): Promise<Model> {
  const updateData: Prisma.ModelUpdateInput = {};

  // Champs simples
  if (data.field1) updateData.field1 = data.field1;

  // Relations Many-to-Many avec set (remplace tout)
  if (data.manyRelation) {
    updateData.manyRelation = {
      set: data.manyRelation.map(uid => ({ uid }))
    };
  }

  // Relations avec connect/disconnect
  if (data.relation) {
    updateData.relation = {
      connect: { uid: data.relation.uid }
    };
  }

  return this.model.update({
    where: { uid },
    data: updateData,
    include: { /* relations à inclure */ }
  });
}

Exemples :

// Update folder avec stakeholders
await officeFoldersRepository.update(folderUid, {
  name: newName,
  stakeholders: {
    set: newStakeholderUids.map(uid => ({ uid }))
  }
});

// Update document status avec historique
await documentsRepository.update(documentUid, {
  document_status: EDocumentStatus.VALIDATED,
  document_history: {
    create: {
      document_status: EDocumentStatus.VALIDATED
    }
  }
});

6. Soft Delete

// Pattern pour soft delete
public async softDelete(uid: string, deletedByUid: string): Promise<Model> {
  return this.model.update({
    where: { uid },
    data: {
      deleted_at: new Date(),
      deleted_by_uid: deletedByUid,
      status: EStatus.DELETED // Pour les entités avec statut
    }
  });
}

Exemples :

// Soft delete folder
await officeFoldersRepository.updateDirect(folderUid, {
  deleted_at: new Date(),
  deleted_by_uid: userUid,
  status: EFolderStatus.DELETED
});

// Soft delete document
await documentsRepository.update(documentUid, {
  deleted_at: new Date(),
  deleted_by_uid: userUid
});

7. Transactions

// Pattern pour opérations atomiques
public async delete(uid: string): Promise<Model> {
  return this.instanceDb.$transaction(async (transaction) => {
    // Supprimer les relations d'abord
    await transaction.relatedModel.deleteMany({
      where: { foreign_key: uid }
    });

    // Puis supprimer l'entité principale
    return transaction.model.delete({
      where: { uid }
    });
  });
}

Exemples :

// Delete document avec historique
await documentsRepository.delete(documentUid);
// Supprime automatiquement document_history puis documents

// CreateMany avec historique
await documentsRepository.createMany(documents);
// Crée documents puis document_history pour chacun

8. Count pour vérifications

// Pattern pour vérifier existence
public async existsWithUniqueField(field: string, officeUid: string, excludeUid?: string): Promise<boolean> {
  const count = await this.model.count({
    where: {
      unique_field: field,
      office_uid: officeUid,
      ...(excludeUid ? { NOT: { uid: excludeUid } } : {})
    }
  });
  return count > 0;
}

Exemples :

// Vérifier numéro de dossier unique
const exists = await officeFoldersRepository.existsWithFolderNumber(
  folderNumber,
  officeUid,
  excludeUid
);

// Compter documents par statut
const count = await documentsRepository.count({
  where: {
    folder_uid: folderUid,
    document_status: EDocumentStatus.ASKED
  }
});

9. Requêtes SQL brutes ($queryRaw)

// Pattern pour requêtes SQL complexes
const results = await prisma.$queryRaw<ResultType[]>`
  SELECT
    u.idnot,
    u.uid as user_uid,
    c.email,
    c.first_name,
    c.last_name,
    o.name as office_name
  FROM users u
  INNER JOIN contacts c ON u.contact_uid = c.uid
  LEFT JOIN offices o ON u.office_uid = o.uid
  WHERE c.email IS NOT NULL
    AND LOWER(c.email) LIKE LOWER(${searchTerm})
  ORDER BY c.last_name ASC
  LIMIT 50
`;

Exemples :

  • Recherche de notaires : IdNotDirectoryService.searchNotariesInSync() (site base + appel direct API annuaire /persons?q= ou v2 + folder_sharing). Voir docs/API.md § Recherche confrères.
  • Requêtes de synchronisation complexes
  • Agrégations et statistiques

10. Upsert pour création/mise à jour atomique

// Pattern pour upsert (utilise contrainte unique)
public async upsert(
  uniqueField1: string,
  uniqueField2: string,
  createData: Prisma.ModelCreateInput,
  updateData: Prisma.ModelUpdateInput
): Promise<Model> {
  return this.model.upsert({
    where: {
      unique_composite: {
        field1: uniqueField1,
        field2: uniqueField2
      }
    },
    create: createData,
    update: updateData
  });
}

Exemples :

// Upsert folder sharing
await folderSharingRepository.upsert(
  folderUid,
  sharedToOfficeUid,
  createData,
  updateData
);
// Utilise @@unique([folder_uid, shared_to_office_uid])

Filtrage par défaut (Soft Delete)

Les repositories filtrent automatiquement les entités supprimées :

// Pattern standard
query.where = {
  ...query.where,
  deleted_at: null  // Exclure les supprimés
};

Exceptions :

  • Endpoints dédiés pour la corbeille : GET /api/v1/notary/folders/deleted
  • Super-admin peut voir tout

Patterns d'inclusion de relations

Inclusion simple

include: {
  relation: true
}

Inclusion imbriquée

include: {
  relation1: {
    include: {
      nestedRelation: true
    }
  }
}

Inclusion conditionnelle

include: {
  relation: {
    where: {
      status: 'ACTIVE'
    }
  }
}

Exemples courants :

// User complet
include: {
  contact: { include: { address: true } },
  role: { include: { rules: true } },
  office_role: { include: { rules: true } },
  office_membership: { include: { address: true } },
  office_affiliations: {
    where: { status: 'ACTIVE', is_primary: true },
    include: { office: true }
  }
}

// Folder complet
include: {
  office: true,
  deed: { include: { deed_type: true } },
  created_by: { include: { contact: true } },
  stakeholders: { include: { contact: true } },
  customers: { include: { contact: { include: { address: true } } } },
  documents: {
    include: {
      document_type: true,
      files: true
    }
  },
  folder_anchor: true
}

Contraintes et index

Contraintes uniques

Contraintes sur champs simples

  • Users.idNot : Unique (identifiant IdNot)
  • Users.contact_uid : Unique (un contact = un user)
  • Offices.idNot : Unique
  • Offices.crpcen : Unique (code CRPCEN)
  • Whitelist.email : Unique
  • UserWhitelist.idNot : Unique
  • SubscriptionPlans.stripe_price_id : Unique
  • RevokedTokens.jti : Unique (JWT ID)
  • SystemConfiguration.key : Unique
  • AnnuOffices.idnot : Unique
  • AnnuPersons.idnot : Unique

Contraintes composites

  • OfficeFolders : @@unique([folder_number, office_uid]) - Numéro unique par office
  • DocumentTypes : @@unique([name, office_uid]) - Nom unique par office
  • DeedTypes : @@unique([name, office_uid]) - Nom unique par office
  • FolderSharing : @@unique([folder_uid, shared_to_office_uid]) - Partage unique par office
  • DocumentRemindersConfig : @@unique([document_type_uid, office_uid])
  • UserOfficeAffiliations : @@unique([user_uid, office_uid]) - Affiliation unique
  • Appointments : @@unique([user_uid, choice, status])
  • AnnuAffiliations : @@unique([person_idnot, office_idnot])
  • SiteTexts : @@unique([text_key, locale, version])
  • RolePermissionsMatrix : @@unique([resource, scoped_entity_normalized, action, role])

Index

Index simples

  • RevokedTokens.jti : Index pour recherche rapide de tokens
  • RevokedTokens.user_uid : Index pour recherche par utilisateur
  • SyncOffices.office_uid : Index pour synchronisation
  • SyncOffices.idnot : Index pour recherche IdNot
  • SyncPersons.user_uid : Index pour synchronisation
  • SyncPersons.idnot : Index pour recherche IdNot
  • SyncAffiliations.person_idnot : Index pour synchronisation
  • SyncAffiliations.office_idnot : Index pour synchronisation
  • SyncStripeSubscriptions.office_uid : Index pour synchronisation
  • SyncStripeSubscriptions.stripe_subscription_id : Index pour recherche Stripe
  • SyncDailyMetrics.date : Index pour requêtes temporelles
  • FolderThirdParties.folder_uid : Index pour recherche par dossier
  • FolderThirdParties.email : Index pour recherche par email
  • ThirdPartyTotpCodes.third_party_uid : Index pour recherche par tiers
  • SystemConfiguration.category : Index pour filtrage par catégorie
  • SystemConfiguration.key : Index pour recherche rapide
  • SiteTexts.scope : Index pour filtrage par scope
  • SiteTexts.text_key : Index pour recherche par clé
  • AnnuOffices.idnot : Index pour recherche IdNot
  • AnnuOffices.crpcen : Index pour recherche CRPCEN
  • AnnuOffices.synced_at : Index pour requêtes temporelles
  • AnnuPersons.idnot : Index pour recherche IdNot
  • AnnuPersons.email : Index pour recherche par email
  • AnnuPersons.synced_at : Index pour requêtes temporelles
  • AnnuAffiliations.person_idnot : Index pour recherche
  • AnnuAffiliations.office_idnot : Index pour recherche
  • AnnuAffiliations.synced_at : Index pour requêtes temporelles

Foreign Keys et cascades

Toutes les relations utilisent onDelete: Cascade sauf exceptions :

  • Relations optionnelles peuvent être null sans cascade
  • Relations Many-to-Many via tables de jointure implicites (Prisma gère automatiquement)

Exemples de cascades :

  • Users.contactContacts : Cascade (supprimer user supprime contact)
  • Users.office_membershipOffices : Cascade (supprimer office supprime users)
  • Documents.folderOfficeFolders : Cascade (supprimer folder supprime documents)
  • Files.documentDocuments : Cascade (supprimer document supprime files)

Dernière mise à jour : 30 octobre 2025 Maintenu par : Équipe LeCoffre.io Version : 2.0.1