====== Schéma PostgreSQL détaillé ====== ===== Introduction ===== Ce document constitue la fondation du système d'information. Objectifs : * Normalisation des données * Historisation des opérations * Audit complet * Compatibilité PostgreSQL 16+ * Compatible ORM (TypeORM / Prisma / Hibernate) Conventions : * Toutes les tables possèdent un UUID primaire. * Toutes les tables métiers sont historisées. * Toutes les modifications sont auditables. * Toutes les FK sont indexées. * Soft delete systématique. ---- ====== Types PostgreSQL ====== ===== Extensions ===== CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; CREATE EXTENSION IF NOT EXISTS pgcrypto; ---- ===== Types ENUM ===== CREATE TYPE user_status AS ENUM ( 'ACTIVE', 'INACTIVE', 'SUSPENDED', 'DELETED' ); CREATE TYPE property_status AS ENUM ( 'DRAFT', 'PUBLISHED', 'RENTED', 'MAINTENANCE', 'ARCHIVED' ); CREATE TYPE reservation_status AS ENUM ( 'PENDING', 'CONFIRMED', 'SIGNED', 'CANCELLED', 'COMPLETED' ); CREATE TYPE payment_status AS ENUM ( 'PENDING', 'AUTHORIZED', 'PAID', 'REFUNDED', 'FAILED' ); ---- ====== Colonnes standard ====== Toutes les tables métiers héritent logiquement des colonnes suivantes : id UUID PRIMARY KEY created_at TIMESTAMPTZ NOT NULL updated_at TIMESTAMPTZ NOT NULL created_by UUID NULL updated_by UUID NULL deleted_at TIMESTAMPTZ NULL version INTEGER DEFAULT 1 ---- ====== Domaine Sécurité ====== ===== users ===== CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) NOT NULL UNIQUE, password_hash VARCHAR(500) NOT NULL, status user_status NOT NULL DEFAULT 'ACTIVE', email_verified BOOLEAN DEFAULT FALSE, last_login_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), deleted_at TIMESTAMPTZ ); CREATE INDEX idx_users_email ON users(email); CREATE INDEX idx_users_status ON users(status); ---- ===== roles ===== CREATE TABLE roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code VARCHAR(50) UNIQUE NOT NULL, name VARCHAR(100) NOT NULL, description TEXT, created_at TIMESTAMPTZ DEFAULT now() ); ---- ===== permissions ===== CREATE TABLE permissions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code VARCHAR(100) UNIQUE NOT NULL, description TEXT ); ---- ===== role_permissions ===== CREATE TABLE role_permissions ( role_id UUID NOT NULL REFERENCES roles(id), permission_id UUID NOT NULL REFERENCES permissions(id), PRIMARY KEY(role_id, permission_id) ); ---- ===== user_roles ===== CREATE TABLE user_roles ( user_id UUID NOT NULL REFERENCES users(id), role_id UUID NOT NULL REFERENCES roles(id), PRIMARY KEY(user_id, role_id) ); ---- ====== Domaine Utilisateurs ====== ===== user_profiles ===== CREATE TABLE user_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL UNIQUE REFERENCES users(id), first_name VARCHAR(100), last_name VARCHAR(100), phone VARCHAR(50), birth_date DATE, nationality VARCHAR(100), created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); ---- ===== user_addresses ===== CREATE TABLE user_addresses ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id), address_1 VARCHAR(255), address_2 VARCHAR(255), postal_code VARCHAR(20), city VARCHAR(150), country VARCHAR(150), is_default BOOLEAN DEFAULT TRUE ); ---- ===== user_documents ===== CREATE TABLE user_documents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id), document_type VARCHAR(50), file_name VARCHAR(255), storage_path TEXT, uploaded_at TIMESTAMPTZ DEFAULT now() ); ---- ====== Domaine Agence ====== ===== agencies ===== CREATE TABLE agencies ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code VARCHAR(50) UNIQUE, name VARCHAR(255) NOT NULL, registration_number VARCHAR(100), vat_number VARCHAR(100), email VARCHAR(255), phone VARCHAR(50), website VARCHAR(255), created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); ---- ===== agency_branches ===== CREATE TABLE agency_branches ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), agency_id UUID NOT NULL REFERENCES agencies(id), name VARCHAR(255), address TEXT, city VARCHAR(150), postal_code VARCHAR(20), phone VARCHAR(50) ); ---- ====== Domaine Propriétaires ====== ===== owners ===== CREATE TABLE owners ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL UNIQUE REFERENCES users(id), agency_id UUID REFERENCES agencies(id), owner_code VARCHAR(50) UNIQUE, active BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT now() ); ---- ===== owner_bank_accounts ===== CREATE TABLE owner_bank_accounts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), owner_id UUID NOT NULL REFERENCES owners(id), iban VARCHAR(50), bic VARCHAR(20), account_holder VARCHAR(255), active BOOLEAN DEFAULT TRUE ); ---- ====== Domaine Biens ====== ===== properties ===== CREATE TABLE properties ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), owner_id UUID NOT NULL REFERENCES owners(id), property_code VARCHAR(50) UNIQUE, title VARCHAR(255) NOT NULL, slug VARCHAR(255) UNIQUE, status property_status NOT NULL, property_type VARCHAR(50), category VARCHAR(50), surface NUMERIC(10,2), room_count INTEGER, bedroom_count INTEGER, bathroom_count INTEGER, max_capacity INTEGER, description_short TEXT, description_long TEXT, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(), deleted_at TIMESTAMPTZ ); CREATE INDEX idx_properties_owner ON properties(owner_id); CREATE INDEX idx_properties_status ON properties(status); ---- ===== property_addresses ===== CREATE TABLE property_addresses ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), property_id UUID UNIQUE REFERENCES properties(id), address_1 VARCHAR(255), address_2 VARCHAR(255), city VARCHAR(150), postal_code VARCHAR(20), country VARCHAR(100), latitude NUMERIC(10,7), longitude NUMERIC(10,7) ); ---- ===== property_photos ===== CREATE TABLE property_photos ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), property_id UUID NOT NULL REFERENCES properties(id), file_name VARCHAR(255), storage_path TEXT, display_order INTEGER, is_cover BOOLEAN DEFAULT FALSE ); CREATE INDEX idx_property_photos_property ON property_photos(property_id); ---- ===== features ===== CREATE TABLE features ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code VARCHAR(50) UNIQUE, label VARCHAR(255) ); ---- ===== property_features ===== CREATE TABLE property_features ( property_id UUID NOT NULL REFERENCES properties(id), feature_id UUID NOT NULL REFERENCES features(id), PRIMARY KEY(property_id, feature_id) ); ---- ===== property_availability ===== CREATE TABLE property_availability ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), property_id UUID NOT NULL REFERENCES properties(id), start_date DATE NOT NULL, end_date DATE NOT NULL, status VARCHAR(30) NOT NULL, CHECK(end_date >= start_date) ); CREATE INDEX idx_property_availability ON property_availability(property_id,start_date,end_date); ---- ====== Domaine Réservations ====== ===== reservations ===== CREATE TABLE reservations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), property_id UUID NOT NULL REFERENCES properties(id), tenant_id UUID NOT NULL REFERENCES users(id), reservation_number VARCHAR(50) UNIQUE, start_date DATE NOT NULL, end_date DATE NOT NULL, guest_count INTEGER, total_amount NUMERIC(12,2), status reservation_status NOT NULL, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(), CHECK(end_date > start_date) ); CREATE INDEX idx_reservation_property ON reservations(property_id); CREATE INDEX idx_reservation_tenant ON reservations(tenant_id); CREATE INDEX idx_reservation_status ON reservations(status); ---- ====== Domaine Contrats ====== ===== contracts ===== CREATE TABLE contracts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), reservation_id UUID UNIQUE REFERENCES reservations(id), contract_number VARCHAR(50) UNIQUE, version INTEGER DEFAULT 1, generated_at TIMESTAMPTZ, signed_at TIMESTAMPTZ, file_path TEXT ); ---- ====== Domaine Paiement ====== ===== payments ===== CREATE TABLE payments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), reservation_id UUID NOT NULL REFERENCES reservations(id), amount NUMERIC(12,2) NOT NULL, payment_date TIMESTAMPTZ, payment_method VARCHAR(50), status payment_status NOT NULL, transaction_reference VARCHAR(255) ); CREATE INDEX idx_payments_reservation ON payments(reservation_id); ---- ===== invoices ===== CREATE TABLE invoices ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), reservation_id UUID REFERENCES reservations(id), invoice_number VARCHAR(50) UNIQUE, invoice_date DATE, due_date DATE, total_ht NUMERIC(12,2), total_tva NUMERIC(12,2), total_ttc NUMERIC(12,2) ); ---- ====== Domaine Messagerie ====== ===== conversations ===== CREATE TABLE conversations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), subject VARCHAR(255), created_at TIMESTAMPTZ DEFAULT now() ); ---- ===== messages ===== CREATE TABLE messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), conversation_id UUID NOT NULL REFERENCES conversations(id), sender_id UUID NOT NULL REFERENCES users(id), message_body TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT now() ); CREATE INDEX idx_messages_conversation ON messages(conversation_id); ---- ====== Historisation ====== ===== audit_logs ===== CREATE TABLE audit_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), entity_name VARCHAR(100) NOT NULL, entity_id UUID NOT NULL, action VARCHAR(20) NOT NULL, old_values JSONB, new_values JSONB, performed_by UUID REFERENCES users(id), performed_at TIMESTAMPTZ NOT NULL DEFAULT now(), ip_address VARCHAR(100), user_agent TEXT ); CREATE INDEX idx_audit_entity ON audit_logs(entity_name, entity_id); CREATE INDEX idx_audit_date ON audit_logs(performed_at); ---- ===== entity_history ===== Historisation générique. CREATE TABLE entity_history ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), entity_name VARCHAR(100), entity_id UUID, version INTEGER, snapshot JSONB, created_at TIMESTAMPTZ DEFAULT now() ); ---- ====== Stratégie d'indexation ====== Créer systématiquement : * Index sur toutes les FK * Index sur les colonnes de recherche * Index composites sur les calendriers * Index sur les statuts Exemples : CREATE INDEX idx_property_city ON property_addresses(city); CREATE INDEX idx_property_geo ON property_addresses(latitude, longitude); CREATE INDEX idx_property_dates ON property_availability( property_id, start_date, end_date ); ---- ====== Volume cible ====== ^ Domaine ^ Tables ^ | Sécurité | 12 | | Utilisateurs | 8 | | Agence | 7 | | Propriétaires | 6 | | Biens | 18 | | Réservations | 7 | | Contrats | 5 | | Paiements | 8 | | CRM | 6 | | Messagerie | 5 | | Reporting | 5 | | Audit | 2 | **Total estimé : 80 à 90 tables** ---- ====== Étape suivante ====== À partir de ce schéma, nous pouvons désormais produire : - Modèle UML complet - Diagrammes de classes détaillés - Diagrammes de séquence - Diagrammes d'états - Diagrammes de composants - Architecture hexagonale du backend NestJS - Spécification OpenAPI 3.1 complète - Backlog Agile détaillé (300+ User Stories) Le prochain livrable recommandé est le **modèle UML complet**, car il servira directement à générer les API et l'architecture logicielle.