# 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`](./DEPLOYMENT.md#cartographie-des-checks-de-déploiement-source-unique) --- ## 📋 Table des matières 1. [Vue d'ensemble](#vue-densemble) 2. [Architecture et structure](#architecture-et-structure) 3. [Schéma Prisma complet](#schéma-prisma-complet) 4. [Gestion des migrations](#gestion-des-migrations) 5. [Configuration système dynamique](#configuration-système-dynamique) 6. [Soft Delete (Suppression logique)](#soft-delete-suppression-logique) 7. [Reset et import de base de données](#reset-et-import-de-base-de-données) 8. [Commandes et outils](#commandes-et-outils) 9. [Liens et références](#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** : ```text postgresql://USERNAME:PASSWORD@HOST:PORT/DATABASE?schema=public ``` **Variables d'environnement** : ```bash DATABASE_HOST= DATABASE_PORT= # 5442 DATABASE_USERNAME=lecoffre-user-${ENV} DATABASE_PASSWORD= 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) ```prisma 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) ```prisma 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) ```prisma 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 ```prisma 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) ```prisma 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) ```prisma 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) ```prisma 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 ```prisma 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 ```prisma enum EBlockchainName { TEZOS BITCOIN_SIGNET } ``` #### Partage ```prisma enum EShareRole { OWNER // Propriétaire CONTRIBUTOR // Contributeur GUEST_NOTARY // Notaire invité VIEWER // Lecteur } enum EShareStatus { ACTIVE // Actif REVOKED // Révoqué EXPIRED // Expiré } ``` #### Tiers ```prisma 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 ```bash cd lecoffre-back-main npx prisma generate ``` #### Créer une migration ```bash # 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 ```bash # Production (via npm script) npm run migrate # Directement npx prisma migrate deploy --schema=src/common/databases/schema.prisma ``` #### Ouvrir Prisma Studio ```bash npx prisma studio ``` #### Reset la base de données (⚠️ danger) ```bash npx prisma migrate reset ``` ### Gestion du schéma **Fichier** : `lecoffre-back-main/src/common/databases/schema.prisma` **Configuration** : `lecoffre-back-main/package.json` ```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 : ```bash # Création automatique lors du déploiement src/common/databases/migrations → prisma/migrations ``` **Vérification** : ```bash 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-full--for-bdd-injection.txt` **Exécution** : ```bash cd /home/debian/sites/-lecoffreio.4nkweb.com set -a && source .secrets//.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 ``` **Ce que fait le script** : - Lit `.secrets//env-full--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](./README.md#consolidation-operationnelle-ex-operationsmd) et [API.md](./API.md#configuration-systeme). --- ## 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` : ```prisma enum EFolderStatus { LIVE // En cours ARCHIVED // Archivé DELETED // Supprimé (soft delete) } ``` ### Filtrage par défaut Les repositories filtrent automatiquement les enregistrements supprimés : ```typescript // Par défaut, exclure les supprimés query.where = { ...query.where, deleted_at: null, }; ``` ### Méthodes standardisées ```typescript // Soft delete async softDelete(uid: string, deletedByUid: string): Promise { 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 { 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/:uid` → `softDelete()` - `POST /api/v1/notary/folders/:uid/restore` → `restore()` - `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](./README.md#consolidation-operationnelle-ex-operationsmd) 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** : ```bash ./deploy/scripts/reset-and-import-database.sh [SQL_FILE] ``` **Arguments** : - `ENV` : Environnement cible (test | pprod | prod) - `SQL_FILE` : (Optionnel) Chemin vers le dump SQL. Si non fourni, utilise `.secrets//bdd.` **Exemples** : ```bash # 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** : ```bash # 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 ```bash cd /home/debian/sites/-lecoffreio.4nkweb.com set -a && source .secrets//.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 ``` #### 2. Re-seeder la matrice des permissions ```bash # 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** : ```text 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-_.sql` - **Reset** (`--resetDatabase`) : Crée `safety-backup-_.sql` **Documentation complète** : Voir [DATABASE_RESET_GUIDE.md](DATABASE_RESET_GUIDE.md) --- ## Commandes et outils ### Commandes Prisma ```bash # 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 ```bash 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 ```bash # Connexion directe psql -h -p -U -d # Sur le serveur (accès PostgreSQL) psql -U -d ``` ### Vérifications ```bash # Compter les enregistrements psql -h -p -U -d \ -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 - **[DATABASE_RESET_GUIDE.md](DATABASE_RESET_GUIDE.md)** : Guide complet de réinitialisation et import - **[README.md](./README.md#consolidation-operationnelle-ex-operationsmd)** : consolidation opérationnelle - **[DEPLOYMENT.md](./DEPLOYMENT.md)** : procédures d'exploitation - **[ARCHITECTURE.md](ARCHITECTURE.md)** : Architecture rôles et utilisateurs - **[DEPLOYMENT.md](DEPLOYMENT.md)** : Documentation du déploiement ### 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 - **Prisma** : - **PostgreSQL** : ### Fichiers de configuration - **Variables d'environnement** : `.secrets//.env.` - **Injection BDD** : `.secrets//env-full--for-bdd-injection.txt` - **Dumps SQL** : `.secrets//bdd.` --- ## Relations complètes entre les tables ### Graphe des relations principales #### 1. Entités de base (Contacts, Addresses) ```text Addresses (1) ──< (0..1) Contacts Addresses (1) ──< (0..1) Offices Contacts (1) ──< (0..1) Users Contacts (1) ──< (0..1) Customers ``` **Relations** : - `Addresses` → `Contacts` : One-to-One (optionnel) - `Addresses` → `Offices` : One-to-One (optionnel) - `Contacts` → `Users` : One-to-One (obligatoire) - `Contacts` → `Customers` : One-to-One (obligatoire) #### 2. Utilisateurs et offices ```text 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_membership` → `Offices` : Many-to-One (ancien système, conservé) - `UserOfficeAffiliations` : Many-to-Many entre `Users` et `Offices` (nouveau système multi-office) - `Users.office_role` → `OfficeRoles` : Many-to-One (optionnel) - `Users.role` → `Roles` : Many-to-One (obligatoire) #### 3. Dossiers et documents ```text 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.deed` → `Deeds` : Many-to-One (obligatoire) - `OfficeFolders.folder_anchor` → `OfficeFolderAnchors` : One-to-One (optionnel) - `OfficeFolders.stakeholders` → `Users` : Many-to-Many - `OfficeFolders.customers` → `Customers` : Many-to-Many #### 4. Documents et fichiers ```text 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.depositor` → `Customers` : Many-to-One (optionnel, pour clients) - `Documents.third_party_depositor` → `FolderThirdParties` : Many-to-One (optionnel, pour tiers) - `Documents.shared_to_office` → `Offices` : Many-to-One (optionnel, pour confrères invités) - `Documents.document_anchor` → `DocumentAnchors` : One-to-One (optionnel) #### 5. Ancrage blockchain ```text 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 ```text 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** : - `Roles` → `Rules` : Many-to-Many (via `RolesHasRules`) - `OfficeRoles` → `Rules` : Many-to-Many (via `OfficeRolesHasRules`) - `Rules` → `RulesGroups` : Many-to-Many (via `RulesGroupsHasRules`) #### 7. Abonnements et sièges ```text Subscriptions (1) ──< (N) Seats Subscriptions (N) ──> (1) Offices Subscriptions (N) ──> (1) SubscriptionPlans (optionnel) Seats (N) ──> (1) Users ``` **Relations** : - `Subscriptions.office` → `Offices` : Many-to-One (obligatoire) - `Subscriptions.subscription_plan` → `SubscriptionPlans` : Many-to-One (optionnel) - `Seats.subscription` → `Subscriptions` : Many-to-One (obligatoire) - `Seats.user` → `Users` : Many-to-One (obligatoire) #### 8. Partage et tiers ```text 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 ```text 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** : - `DeedTypes` → `DocumentTypes` : Many-to-Many (via `DeedTypeHasDocumentTypes`) - `Deeds` → `DocumentTypes` : Many-to-Many (via `DeedHasDocumentTypes`) - Contrainte unique sur `DocumentTypes` : `[name, office_uid]` - Contrainte unique sur `DeedTypes` : `[name, office_uid]` #### 10. Synchronisation externe ```text 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 ```text 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 ```text 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 ```typescript // 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** : ```typescript // 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 ```typescript // Pattern avec include optionnel public async findOneByUid(uid: string, include?: Prisma.ModelInclude) { return this.model.findUnique({ where: { uid }, include }); } ``` **Exemples** : ```typescript // 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 ```typescript // Pattern pour recherche avec conditions public async findOne(query: Prisma.ModelFindFirstArgs) { return this.model.findFirst(query); } ``` **Exemples** : ```typescript // 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 ```typescript // Pattern pour création avec relations public async create(data: Entity): Promise { 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** : ```typescript // 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 ```typescript // Pattern pour mise à jour avec relations public async update(uid: string, data: Partial): Promise { 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** : ```typescript // 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 ```typescript // Pattern pour soft delete public async softDelete(uid: string, deletedByUid: string): Promise { 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** : ```typescript // 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 ```typescript // Pattern pour opérations atomiques public async delete(uid: string): Promise { 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** : ```typescript // 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 ```typescript // Pattern pour vérifier existence public async existsWithUniqueField(field: string, officeUid: string, excludeUid?: string): Promise { const count = await this.model.count({ where: { unique_field: field, office_uid: officeUid, ...(excludeUid ? { NOT: { uid: excludeUid } } : {}) } }); return count > 0; } ``` **Exemples** : ```typescript // 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) ```typescript // Pattern pour requêtes SQL complexes const results = await prisma.$queryRaw` 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 ```typescript // Pattern pour upsert (utilise contrainte unique) public async upsert( uniqueField1: string, uniqueField2: string, createData: Prisma.ModelCreateInput, updateData: Prisma.ModelUpdateInput ): Promise { return this.model.upsert({ where: { unique_composite: { field1: uniqueField1, field2: uniqueField2 } }, create: createData, update: updateData }); } ``` **Exemples** : ```typescript // 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 : ```typescript // 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 ```typescript include: { relation: true } ``` #### Inclusion imbriquée ```typescript include: { relation1: { include: { nestedRelation: true } } } ``` #### Inclusion conditionnelle ```typescript include: { relation: { where: { status: 'ACTIVE' } } } ``` **Exemples courants** : ```typescript // 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.contact` → `Contacts` : Cascade (supprimer user supprime contact) - `Users.office_membership` → `Offices` : Cascade (supprimer office supprime users) - `Documents.folder` → `OfficeFolders` : Cascade (supprimer folder supprime documents) - `Files.document` → `Documents` : Cascade (supprimer document supprime files) --- **Dernière mise à jour** : 30 octobre 2025 **Maintenu par** : Équipe LeCoffre.io **Version** : 2.0.1