# Schéma de base de données — Xpeditis

PostgreSQL 15 — 21 tables.

**Extensions requises** : `uuid-ossp`, `pg_trgm`

---

## Vue d'ensemble des tables

| Table | Description |
|-------|-------------|
| organizations | Organisations (transitaires, transporteurs) |
| users | Comptes utilisateurs |
| carriers | Transporteurs maritimes |
| ports | Base de données des ports (UN/LOCODE) |
| rate_quotes | Cotations de tarifs carriers |
| bookings | Réservations standard |
| containers | Conteneurs liés aux réservations |
| csv_bookings | Réservations créées via CSV |
| csv_rate_configs | Configuration des carriers CSV |
| carrier_profiles | Profils portail carrier |
| carrier_activities | Activités du portail carrier |
| audit_logs | Journal d'audit des actions |
| notifications | Notifications push (WebSocket) |
| webhooks | Configuration des webhooks tiers |
| subscriptions | Abonnements Stripe |
| licenses | Licences d'utilisation |
| api_keys | Clés API (auth alternative au JWT) |
| invitation_tokens | Tokens d'invitation utilisateur |
| password_reset_tokens | Tokens de réinitialisation mot de passe |
| cookie_consents | Consentements RGPD cookies |
| blog_posts | Articles de blog |

---

## Tables détaillées

### organizations

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| name | VARCHAR(255) | NOT NULL, UNIQUE | Nom de l'organisation |
| type | VARCHAR(50) | NOT NULL | FREIGHT_FORWARDER, CARRIER, SHIPPER |
| scac | CHAR(4) | UNIQUE, NULLABLE | Standard Carrier Alpha Code |
| address_street | VARCHAR(255) | NOT NULL | Adresse |
| address_city | VARCHAR(100) | NOT NULL | Ville |
| address_state | VARCHAR(100) | NULLABLE | État/Province |
| address_postal_code | VARCHAR(20) | NOT NULL | Code postal |
| address_country | CHAR(2) | NOT NULL | Code pays ISO 3166-1 |
| logo_url | TEXT | NULLABLE | URL du logo |
| documents | JSONB | DEFAULT '[]' | Documents compliance |
| siret | VARCHAR(20) | NULLABLE | SIRET (entreprises françaises) |
| status_badge | VARCHAR(50) | NULLABLE | Badge de statut Pappers |
| is_carrier | BOOLEAN | DEFAULT FALSE | Est un carrier (portail) |
| is_active | BOOLEAN | DEFAULT TRUE | Compte actif |
| created_at | TIMESTAMP | DEFAULT NOW() | Création |
| updated_at | TIMESTAMP | DEFAULT NOW() | Mise à jour |

---

### users

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| organization_id | UUID | FK organizations | Organisation |
| email | VARCHAR(255) | NOT NULL, UNIQUE | Email (minuscules) |
| password_hash | VARCHAR(255) | NOT NULL | Hash Argon2 |
| role | VARCHAR(50) | NOT NULL | ADMIN, MANAGER, USER, VIEWER, CARRIER |
| first_name | VARCHAR(100) | NOT NULL | Prénom |
| last_name | VARCHAR(100) | NOT NULL | Nom |
| phone_number | VARCHAR(20) | NULLABLE | Téléphone |
| is_email_verified | BOOLEAN | DEFAULT FALSE | Email vérifié |
| is_active | BOOLEAN | DEFAULT TRUE | Compte actif |
| preferred_language | VARCHAR(10) | DEFAULT 'fr' | Langue préférée (fr, en) |
| last_login_at | TIMESTAMP | NULLABLE | Dernière connexion |
| created_at | TIMESTAMP | DEFAULT NOW() | Création |
| updated_at | TIMESTAMP | DEFAULT NOW() | Mise à jour |

Index : `idx_users_email`, `idx_users_organization`, `idx_users_role`

---

### carriers

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| name | VARCHAR(255) | NOT NULL | Nom (ex: "Maersk") |
| code | VARCHAR(50) | NOT NULL, UNIQUE | Code (ex: "MAERSK") |
| scac | CHAR(4) | NOT NULL, UNIQUE | Standard Carrier Alpha Code |
| logo_url | TEXT | NULLABLE | URL du logo |
| website | TEXT | NULLABLE | Site web |
| api_config | JSONB | NULLABLE | Config API (credentials, timeout) |
| is_active | BOOLEAN | DEFAULT TRUE | Actif |
| supports_api | BOOLEAN | DEFAULT FALSE | Intégration API disponible |
| created_at | TIMESTAMP | DEFAULT NOW() | Création |
| updated_at | TIMESTAMP | DEFAULT NOW() | Mise à jour |

Carriers seedés : Maersk (MAEU), MSC (MSCU), CMA CGM (CMDU), Hapag-Lloyd (HLCU), ONE (ONEY)

---

### ports

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| code | CHAR(5) | NOT NULL, UNIQUE | UN/LOCODE (ex: "NLRTM") |
| name | VARCHAR(255) | NOT NULL | Nom du port |
| city | VARCHAR(255) | NOT NULL | Ville |
| country | CHAR(2) | NOT NULL | Code pays |
| country_name | VARCHAR(100) | NOT NULL | Nom du pays |
| latitude | DECIMAL(9,6) | NOT NULL | Latitude |
| longitude | DECIMAL(9,6) | NOT NULL | Longitude |
| timezone | VARCHAR(50) | NULLABLE | Fuseau horaire IANA |
| is_active | BOOLEAN | DEFAULT TRUE | Actif |

Index GIN pour recherche floue : `idx_ports_name_trgm`, `idx_ports_city_trgm`

---

### rate_quotes

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| carrier_id | UUID | FK carriers | Carrier |
| carrier_name | VARCHAR(255) | NOT NULL | Nom carrier (dénormalisé) |
| carrier_code | VARCHAR(50) | NOT NULL | Code carrier (dénormalisé) |
| origin_code | CHAR(5) | NOT NULL | Port d'origine |
| destination_code | CHAR(5) | NOT NULL | Port de destination |
| base_freight | DECIMAL(10,2) | NOT NULL | Fret de base |
| surcharges | JSONB | DEFAULT '[]' | Surcharges (BAF, CAF, etc.) |
| total_amount | DECIMAL(10,2) | NOT NULL | Prix total |
| currency | CHAR(3) | NOT NULL | Devise ISO 4217 |
| container_type | VARCHAR(20) | NOT NULL | Type conteneur (20GP, 40HC, etc.) |
| mode | VARCHAR(10) | NOT NULL | FCL ou LCL |
| etd | TIMESTAMP | NOT NULL | Départ estimé |
| eta | TIMESTAMP | NOT NULL | Arrivée estimée |
| transit_days | INTEGER | NOT NULL | Jours de transit |
| route | JSONB | NOT NULL | Escales |
| availability | INTEGER | NOT NULL | Disponibilité conteneurs |
| valid_until | TIMESTAMP | NOT NULL | Expiration (created_at + 15 min) |

---

### bookings

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| booking_number | VARCHAR(20) | NOT NULL, UNIQUE | Format WCM-YYYY-XXXXXX |
| user_id | UUID | FK users | Utilisateur créateur |
| organization_id | UUID | FK organizations | Organisation |
| rate_quote_id | UUID | FK rate_quotes, NULLABLE | Cotation source |
| carrier_id | UUID | FK carriers | Carrier |
| status | VARCHAR(50) | NOT NULL | draft, confirmed, shipped, delivered, cancelled |
| shipper | JSONB | NOT NULL | Infos expéditeur |
| consignee | JSONB | NOT NULL | Infos destinataire |
| cargo | JSONB | NULLABLE | Détails marchandise |
| notes | TEXT | NULLABLE | Notes internes |
| created_at | TIMESTAMP | DEFAULT NOW() | Création |
| updated_at | TIMESTAMP | DEFAULT NOW() | Mise à jour |

---

### containers

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| booking_id | UUID | FK bookings, NULLABLE | Réservation |
| type | VARCHAR(20) | NOT NULL | 20GP, 40HC, 40RF, etc. |
| category | VARCHAR(20) | NOT NULL | DRY, REEFER, OPEN_TOP, FLAT_RACK, TANK |
| container_number | VARCHAR(11) | NULLABLE, UNIQUE | ISO 6346 |
| seal_number | VARCHAR(50) | NULLABLE | Numéro de plomb |
| vgm | INTEGER | NULLABLE | Masse Brute Vérifiée (kg) |
| is_hazmat | BOOLEAN | DEFAULT FALSE | Marchandise dangereuse |
| imo_class | VARCHAR(10) | NULLABLE | Classe IMO |
| cargo_description | TEXT | NULLABLE | Description de la marchandise |

---

### csv_bookings

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| organization_id | UUID | FK organizations | Organisation |
| carrier_id | UUID | FK carriers, NULLABLE | Carrier assigné |
| booking_number | VARCHAR(50) | NOT NULL, UNIQUE | Numéro de réservation |
| shipper_name | VARCHAR(255) | NOT NULL | Expéditeur |
| consignee_name | VARCHAR(255) | NOT NULL | Destinataire |
| origin_port | CHAR(5) | NOT NULL | Port d'origine (UN/LOCODE) |
| destination_port | CHAR(5) | NOT NULL | Port de destination |
| container_type | VARCHAR(20) | NOT NULL | Type conteneur |
| commodity | TEXT | NULLABLE | Marchandise |
| weight_kg | DECIMAL(10,2) | NULLABLE | Poids |
| status | VARCHAR(50) | NOT NULL | pending, accepted, rejected, in_transit, delivered |
| carrier_magic_link_token | VARCHAR(255) | NULLABLE | Token lien magique carrier |
| carrier_magic_link_expires_at | TIMESTAMP | NULLABLE | Expiration du token |
| carrier_password_hash | VARCHAR(255) | NULLABLE | Hash mdp portail carrier |
| notes | TEXT | NULLABLE | Notes |
| documents | JSONB | DEFAULT '[]' | Documents joints |
| created_at | TIMESTAMP | DEFAULT NOW() | Création |
| updated_at | TIMESTAMP | DEFAULT NOW() | Mise à jour |

---

### csv_rate_configs

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| company_name | VARCHAR(255) | NOT NULL, UNIQUE | Nom du carrier CSV |
| csv_file_path | VARCHAR(500) | NOT NULL | Chemin fichier CSV |
| type | VARCHAR(50) | DEFAULT 'CSV_ONLY' | CSV_ONLY ou CSV_AND_API |
| has_api | BOOLEAN | DEFAULT FALSE | A une API |
| api_connector | VARCHAR(100) | NULLABLE | Identifiant du connecteur API |
| is_active | BOOLEAN | DEFAULT TRUE | Actif |
| uploaded_at | TIMESTAMP | NOT NULL | Upload |
| uploaded_by | UUID | FK users, NULLABLE | Uploadé par |
| row_count | INTEGER | NULLABLE | Nombre de lignes |

---

### carrier_profiles

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| organization_id | UUID | FK organizations, UNIQUE | Organisation carrier |
| carrier_code | VARCHAR(50) | NOT NULL | Code carrier |
| contact_email | VARCHAR(255) | NOT NULL | Email de contact |
| contact_name | VARCHAR(255) | NULLABLE | Nom du contact |
| is_active | BOOLEAN | DEFAULT TRUE | Actif |
| created_at | TIMESTAMP | DEFAULT NOW() | Création |

---

### carrier_activities

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| carrier_profile_id | UUID | FK carrier_profiles | Profil carrier |
| csv_booking_id | UUID | FK csv_bookings | Réservation CSV |
| action | VARCHAR(50) | NOT NULL | ACCEPTED, REJECTED, VIEWED, DOCUMENT_UPLOADED |
| notes | TEXT | NULLABLE | Notes de l'action |
| created_at | TIMESTAMP | DEFAULT NOW() | Création |

---

### audit_logs

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| user_id | UUID | FK users, NULLABLE | Utilisateur |
| organization_id | UUID | FK organizations, NULLABLE | Organisation |
| action | VARCHAR(100) | NOT NULL | Type d'action (26 types) |
| resource_type | VARCHAR(50) | NULLABLE | Type de ressource |
| resource_id | VARCHAR(255) | NULLABLE | ID de la ressource |
| status | VARCHAR(20) | NOT NULL | SUCCESS, FAILURE, WARNING |
| metadata | JSONB | NULLABLE | Données supplémentaires |
| ip_address | VARCHAR(45) | NULLABLE | Adresse IP |
| user_agent | TEXT | NULLABLE | User agent |
| created_at | TIMESTAMP | DEFAULT NOW() | Création |

Index : `idx_audit_logs_user_id`, `idx_audit_logs_action`, `idx_audit_logs_created_at`

---

### notifications

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| user_id | UUID | FK users | Destinataire |
| type | VARCHAR(50) | NOT NULL | BOOKING_CREATED, DOCUMENT_UPLOADED, etc. (9 types) |
| title | VARCHAR(255) | NOT NULL | Titre |
| message | TEXT | NOT NULL | Message |
| data | JSONB | NULLABLE | Données associées |
| priority | VARCHAR(20) | NOT NULL | LOW, MEDIUM, HIGH, URGENT |
| read | BOOLEAN | DEFAULT FALSE | Lu |
| read_at | TIMESTAMP | NULLABLE | Date de lecture |
| created_at | TIMESTAMP | DEFAULT NOW() | Création |

---

### webhooks

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| organization_id | UUID | FK organizations | Organisation |
| url | TEXT | NOT NULL | URL cible |
| secret | VARCHAR(255) | NOT NULL | Secret HMAC SHA-256 |
| events | JSONB | NOT NULL | Événements souscrits |
| is_active | BOOLEAN | DEFAULT TRUE | Actif |
| last_triggered_at | TIMESTAMP | NULLABLE | Dernier déclenchement |
| failure_count | INTEGER | DEFAULT 0 | Compteur d'échecs |
| created_at | TIMESTAMP | DEFAULT NOW() | Création |

---

### subscriptions

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| organization_id | UUID | FK organizations, UNIQUE | Organisation |
| stripe_customer_id | VARCHAR(255) | NULLABLE | ID client Stripe |
| stripe_subscription_id | VARCHAR(255) | NULLABLE | ID abonnement Stripe |
| plan | VARCHAR(50) | NOT NULL | FREE, BRONZE, SILVER, GOLD, PLATINIUM |
| status | VARCHAR(50) | NOT NULL | active, past_due, cancelled, trialing, pending_payment, pending_bank_transfer |
| current_period_start | TIMESTAMP | NULLABLE | Début de période |
| current_period_end | TIMESTAMP | NULLABLE | Fin de période |
| billing_interval | VARCHAR(20) | NULLABLE | monthly, yearly |
| commission_rate | DECIMAL(5,4) | DEFAULT 0 | Taux de commission Xpeditis |
| created_at | TIMESTAMP | DEFAULT NOW() | Création |
| updated_at | TIMESTAMP | DEFAULT NOW() | Mise à jour |

---

### licenses

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| organization_id | UUID | FK organizations | Organisation |
| feature | VARCHAR(100) | NOT NULL | Fonctionnalité activée |
| is_active | BOOLEAN | DEFAULT TRUE | Actif |
| valid_until | TIMESTAMP | NULLABLE | Expiration |
| created_at | TIMESTAMP | DEFAULT NOW() | Création |

---

### api_keys

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| organization_id | UUID | FK organizations | Organisation |
| user_id | UUID | FK users | Créateur |
| name | VARCHAR(255) | NOT NULL | Nom de la clé |
| key_hash | VARCHAR(255) | NOT NULL | Hash Argon2 de la clé |
| key_prefix | VARCHAR(10) | NOT NULL | Préfixe visible (ex: xped_) |
| last_used_at | TIMESTAMP | NULLABLE | Dernière utilisation |
| expires_at | TIMESTAMP | NULLABLE | Expiration |
| is_active | BOOLEAN | DEFAULT TRUE | Actif |
| created_at | TIMESTAMP | DEFAULT NOW() | Création |

---

### invitation_tokens

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| organization_id | UUID | FK organizations | Organisation |
| email | VARCHAR(255) | NOT NULL | Email invité |
| token | VARCHAR(255) | NOT NULL, UNIQUE | Token d'invitation |
| role | VARCHAR(50) | NOT NULL | Rôle assigné |
| expires_at | TIMESTAMP | NOT NULL | Expiration (24h) |
| used_at | TIMESTAMP | NULLABLE | Utilisé le |
| created_by | UUID | FK users | Créateur |

---

### password_reset_tokens

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| user_id | UUID | FK users | Utilisateur |
| token | VARCHAR(255) | NOT NULL, UNIQUE | Token de réinitialisation |
| expires_at | TIMESTAMP | NOT NULL | Expiration (1h) |
| used_at | TIMESTAMP | NULLABLE | Utilisé le |
| created_at | TIMESTAMP | DEFAULT NOW() | Création |

---

### cookie_consents

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| user_id | UUID | FK users, NULLABLE | Utilisateur (si connecté) |
| session_id | VARCHAR(255) | NULLABLE | Session anonyme |
| analytics | BOOLEAN | NOT NULL | Consentement analytics |
| marketing | BOOLEAN | NOT NULL | Consentement marketing |
| ip_address | VARCHAR(45) | NULLABLE | IP |
| created_at | TIMESTAMP | DEFAULT NOW() | Création |

---

### blog_posts

| Colonne | Type | Contraintes | Description |
|---------|------|-------------|-------------|
| id | UUID | PK | Identifiant |
| slug | VARCHAR(255) | NOT NULL, UNIQUE | Slug URL |
| title_fr | VARCHAR(500) | NOT NULL | Titre en français |
| title_en | VARCHAR(500) | NULLABLE | Titre en anglais |
| content_fr | TEXT | NOT NULL | Contenu en français |
| content_en | TEXT | NULLABLE | Contenu en anglais |
| excerpt_fr | TEXT | NULLABLE | Extrait français |
| excerpt_en | TEXT | NULLABLE | Extrait anglais |
| cover_image_url | TEXT | NULLABLE | Image de couverture |
| author_id | UUID | FK users | Auteur |
| is_published | BOOLEAN | DEFAULT FALSE | Publié |
| published_at | TIMESTAMP | NULLABLE | Date de publication |
| tags | JSONB | DEFAULT '[]' | Tags |
| created_at | TIMESTAMP | DEFAULT NOW() | Création |
| updated_at | TIMESTAMP | DEFAULT NOW() | Mise à jour |

---

## Relations principales

```
organizations 1──* users
organizations 1──1 subscriptions
organizations 1──* licenses
organizations 1──* api_keys
organizations 1──* webhooks
organizations 1──* bookings
organizations 1──* csv_bookings
organizations 1──1 carrier_profiles

users 1──* bookings
users 1──* audit_logs
users 1──* notifications
users 1──* api_keys

bookings 1──* containers
bookings 1──1 rate_quotes (optionnel)

csv_bookings 1──* carrier_activities (via carrier_profiles)
carrier_profiles 1──* carrier_activities
```

---

## Stratégie de migrations

Les migrations sont dans `apps/backend/src/infrastructure/persistence/typeorm/migrations/`.
Ne jamais modifier une migration déjà appliquée — créer une nouvelle migration.

```bash
cd apps/backend
npm run migration:generate -- src/infrastructure/persistence/typeorm/migrations/NomMigration
npm run migration:run
```

---

*Dernière mise à jour : Mai 2026 — 21 tables*
