**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
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
- Vue d'ensemble
- Architecture et structure
- Schéma Prisma complet
- Gestion des migrations
- Configuration système dynamique
- Soft Delete (Suppression logique)
- Reset et import de base de données
- Commandes et outils
- 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_adminpour 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 actifARCHIVED: 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épositaireN/C/T/NI, segmentsFirstName.LastName, suffixe.aplcsans 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(inclutoriginal_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.jsBACKEND: Paramètres serveurINTEGRATION_IDNOT: ID.NOT (OpenID, API Annuaire)INTEGRATION_BITCOIN: Bitcoin SignetINTEGRATION_STRIPE: Stripe (abonnements)INTEGRATION_MAILCHIMP: Mailchimp (emails)INTEGRATION_PINATA: IPFS/PinataINTEGRATION_DOCAPOST: Docapost (OCR)INTEGRATION_OVH: OVH (SMS)SECURITY: Secrets, tokensPERFORMANCE: Limites, cacheFEATURE_FLAGS: Activation fonctionnalitésSYSTEM: 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_migrationsexiste avec des entrées - Vérifie si
system_configurationexiste (créée par une migration) - Si migrations marquées mais
system_configurationabsent → 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 configurationsGET /api/v1/super-admin/system-config/category/:category: Filtre par catégorieGET /api/v1/super-admin/system-config/:key: Récupère une configurationPUT /api/v1/super-admin/system-config/:uid: Met à jour une configurationPOST /api/v1/super-admin/system-config: Crée une configurationDELETE /api/v1/super-admin/system-config/:uid: Supprime une configurationGET /api/v1/super-admin/system-config/validate: Valide les configurations requises
Variables sensibles
Automatiquement identifiées par :
is_sensitive: truedans le mapping- Clés contenant :
SECRET,KEY,PASSWORD,TOKEN
Exemples :
ACCESS_TOKEN_SECRETIDNOT_CLIENT_SECRETRIB_ENCRYPTION_MASTER_KEYSTRIPE_SECRET_KEY
Exclusions
Variables JAMAIS stockées en BDD :
DATABASE_URLDATABASE_*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/:uid→softDelete()POST /api/v1/notary/folders/:uid/restore→restore()GET /api/v1/notary/folders/deleted→ Liste corbeille
Avantages
- Traçabilité : On sait qui a supprimé et quand
- Récupération : Possibilité de restaurer facilement
- Audit : Historique complet des suppressions
- Conformité RGPD : Les données peuvent être anonymisées plus tard
- 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 :
- ✅ Crée un backup de sécurité de la base actuelle
- ✅ Supprime la base de données
- ✅ Recrée la base de données vide
- ✅ Importe les données depuis le dump SQL
- ✅ Génère le client Prisma
- ✅ Applique les migrations Prisma (via
npm run migrate) - ✅ 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/healthdoit répondre200- Connexion Id.Not : badge
Visiteuraffiché 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éebdd-<ENV>_<timestamp>.sql - Reset (
--resetDatabase) : Créesafety-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
- DATABASE_RESET_GUIDE.md : Guide complet de réinitialisation et import
- README.md : consolidation opérationnelle
- DEPLOYMENT.md : procédures d'exploitation
- ARCHITECTURE.md : Architecture rôles et utilisateurs
- 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 : https://www.prisma.io/docs/
- PostgreSQL : https://www.postgresql.org/docs/
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 :
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
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 entreUsersetOffices(nouveau système multi-office)Users.office_role→OfficeRoles: Many-to-One (optionnel)Users.role→Roles: 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.deed→Deeds: Many-to-One (obligatoire)OfficeFolders.folder_anchor→OfficeFolderAnchors: One-to-One (optionnel)OfficeFolders.stakeholders→Users: Many-to-ManyOfficeFolders.customers→Customers: 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.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
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 :
Roles→Rules: Many-to-Many (viaRolesHasRules)OfficeRoles→Rules: Many-to-Many (viaOfficeRolesHasRules)Rules→RulesGroups: Many-to-Many (viaRulesGroupsHasRules)
7. Abonnements et sièges
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
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é parinvited_notary_idnot(IdNot) etinvited_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. Voirdocs/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 :
DeedTypes→DocumentTypes: Many-to-Many (viaDeedTypeHasDocumentTypes)Deeds→DocumentTypes: Many-to-Many (viaDeedHasDocumentTypes)- 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_uidpour 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.jtietRevokedTokens.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ésultatsdefaultFetchRows = 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: UniqueOffices.crpcen: Unique (code CRPCEN)Whitelist.email: UniqueUserWhitelist.idNot: UniqueSubscriptionPlans.stripe_price_id: UniqueRevokedTokens.jti: Unique (JWT ID)SystemConfiguration.key: UniqueAnnuOffices.idnot: UniqueAnnuPersons.idnot: Unique
Contraintes composites
OfficeFolders:@@unique([folder_number, office_uid])- Numéro unique par officeDocumentTypes:@@unique([name, office_uid])- Nom unique par officeDeedTypes:@@unique([name, office_uid])- Nom unique par officeFolderSharing:@@unique([folder_uid, shared_to_office_uid])- Partage unique par officeDocumentRemindersConfig:@@unique([document_type_uid, office_uid])UserOfficeAffiliations:@@unique([user_uid, office_uid])- Affiliation uniqueAppointments:@@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 tokensRevokedTokens.user_uid: Index pour recherche par utilisateurSyncOffices.office_uid: Index pour synchronisationSyncOffices.idnot: Index pour recherche IdNotSyncPersons.user_uid: Index pour synchronisationSyncPersons.idnot: Index pour recherche IdNotSyncAffiliations.person_idnot: Index pour synchronisationSyncAffiliations.office_idnot: Index pour synchronisationSyncStripeSubscriptions.office_uid: Index pour synchronisationSyncStripeSubscriptions.stripe_subscription_id: Index pour recherche StripeSyncDailyMetrics.date: Index pour requêtes temporellesFolderThirdParties.folder_uid: Index pour recherche par dossierFolderThirdParties.email: Index pour recherche par emailThirdPartyTotpCodes.third_party_uid: Index pour recherche par tiersSystemConfiguration.category: Index pour filtrage par catégorieSystemConfiguration.key: Index pour recherche rapideSiteTexts.scope: Index pour filtrage par scopeSiteTexts.text_key: Index pour recherche par cléAnnuOffices.idnot: Index pour recherche IdNotAnnuOffices.crpcen: Index pour recherche CRPCENAnnuOffices.synced_at: Index pour requêtes temporellesAnnuPersons.idnot: Index pour recherche IdNotAnnuPersons.email: Index pour recherche par emailAnnuPersons.synced_at: Index pour requêtes temporellesAnnuAffiliations.person_idnot: Index pour rechercheAnnuAffiliations.office_idnot: Index pour rechercheAnnuAffiliations.synced_at: Index pour requêtes temporelles
Foreign Keys et cascades
Toutes les relations utilisent onDelete: Cascade sauf exceptions :
- Relations optionnelles peuvent être
nullsans 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