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

1710 lines
48 KiB
Markdown

# 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=<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)
```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>/env-full-<env>-for-bdd-injection.txt`
**Exécution** :
```bash
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](./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<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
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 <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** :
```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/<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
```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-<ENV>_<timestamp>.sql`
- **Reset** (`--resetDatabase`) : Crée `safety-backup-<ENV>_<timestamp>.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 <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
```bash
# 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](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** : <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)
```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<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** :
```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<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** :
```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<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** :
```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<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** :
```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<boolean> {
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<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
```typescript
// 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** :
```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