**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
1710 lines
48 KiB
Markdown
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
|