# Architecture multi-tenant — YimaShop → Plateforme POS multi-boutiques

> Rapport d'architecture. Trajectoire : chaque client (boutique) disposera de **sa propre base de données**, d'une **application PWA (Ionic)** consommant une **API**, et à terme d'un **site e-commerce Symfony** bâti sur cette même base.
>
> **Statut** : Phase 1 livrée (informations de connexion par boutique + test + chiffrement). Le reste est une feuille de route.

---

## 1. Vision & objectifs

Le portail actuel gère des boutiques, des utilisateurs, des médias et la sécurité dans **une base unique**. L'objectif est d'évoluer vers une **plateforme POS SaaS multi-tenant** où :

- chaque **boutique cliente** possède **sa propre base de données** (isolation forte des données) ;
- les employés/managers utilisent une **PWA Ionic** qui consomme une **API REST** (auth JWT, liaison appareil) ;
- chaque client peut disposer d'un **site e-commerce Symfony** (même socle de code), branché sur sa base ;
- le **portail** reste le **plan de contrôle** (gestion des boutiques, provisioning, sécurité, facturation à venir).

Vocabulaire : **base centrale** = base du portail, **système de référence** (toutes les données : boutiques, utilisateurs, RBAC, connexions, **et les données métier POS**) ; **bases clients** = une base par client contenant une **projection en lecture des données POS** destinée à son futur site e-commerce.

## 2. État actuel (audité)

- **Connexion Doctrine unique** via `DATABASE_URL` (`config/packages/doctrine.yaml`). Un seul EntityManager, un seul DBAL. Aucune machinerie multi-connexion, aucun `DriverManager` dynamique avant la phase 1.
- **Credentials en clair** dans le code existant (mots de passe SMTP, clés SMS). → **corrigé pour la DB des boutiques** : `EncryptionService` (libsodium) introduit en phase 1.
- **API** : firewall `api` stateless JWT sur `^/api` (`lexik/jwt` 3.1), réponses **JSON manuelles** normalisées par `ApiResponseFormatter` (enveloppe `{code, success, message, data, meta}`). Pas d'API Platform. Serializer présent mais non utilisé par l'API.
- **CORS** (`nelmio/cors`) déjà configuré pour `^/api/`, origine par env, en-têtes Ionic-friendly (`ngsw-bypass`), expose `X-JWT-Expiration`. `allow_credentials: true`.
- **Surface `/api/mobile/*`** déclarée publique en sécurité + `DeviceUidValidationListener` (en-tête `X-Device-UID`, entité `DeviceApp`, services `DeviceApp*`/`DeviceManagement*`/`DeviceApproval*`) — **scaffoldée mais sans contrôleurs**.
- **RBAC par boutique** en place (entités `Role`/`Permission`, `PermissionResolver`, `UserCompany.boutiqueRole`) — voir la fondation RBAC. Les rôles globaux restent inchangés.
- **Provisioning/backup** : `DatabaseBackupService` sait lire les params d'une connexion (`getParams()`) et exécuter `CREATE/DROP DATABASE` + `mysqldump/mysql` — réutilisable pour le data plane, mais aujourd'hui lié à l'EM par défaut.

## 3. Modèle retenu : base centrale autoritaire + projection POS par client

**Décision (confirmée)** : la **base centrale** (`yima-shop`) est **l'unique système de référence** — elle contient **toutes** les données (boutiques, utilisateurs, RBAC, sécurité, **et les données métier POS** : catalogue, stock, ventes…). Chaque **base client** (`amansa`, …) ne contient qu'une **duplication en lecture d'un sous-ensemble des données POS** (catalogue publiable, prix, stock, commandes visibles), destinée à alimenter le **futur site e-commerce** du client. L'écriture métier reste **centralisée** ; la base client est une **projection dérivée**, jamais autoritaire.

| Critère | **Central autoritaire + projection POS** (retenu) | Base par tenant autoritaire | Schéma partagé (`tenant_id`) |
|---|---|---|---|
| Source de vérité | Centrale (unique) | Éclatée par client | Centrale |
| Données dans la base client | Sous-ensemble POS en lecture | Tout, autoritaire | — |
| Analytics/gestion transverse | Simple (tout au centre) | Complexe (agrégation × N) | Simple |
| Isolation du site e-commerce | Forte (lit sa propre base) | Forte | Faible |
| Complexité | **Synchronisation** central→client | Provisioning + migrations × N | Fuite applicative possible |

Ce modèle privilégie une **gouvernance centralisée** (une seule vérité, analytics et administration faciles) tout en **découplant** chaque site e-commerce, qui lit une base dédiée réduite (surface d'exposition minimale). Le point de conception central devient la **synchronisation** (§4bis), pas le provisioning d'un store autoritaire.

**Base centrale** : `companies`, `company_database` (connexion chiffrée), `user`, `user_company` + RBAC, sécurité/audit, **+ tout le domaine POS**. **Base client** : tables de projection en lecture (catalogue, prix, stock, commandes exposées, clients finaux du site).

## 4bis. Synchronisation central → client

Le cœur du modèle : propager vers chaque base client le sous-ensemble POS dont son site a besoin. Mécanismes candidats :

- **(a) Événements de domaine via Messenger — recommandé.** À chaque changement POS pertinent au centre (produit créé/modifié, prix, stock, commande), publier un message asynchrone qui écrit la projection dans la base client (via `TenantConnectionService`). Avantages : temps quasi réel, découplé, rejouable ; le mode `MESSENGER_MODE` (sync/async) existe déjà.
- **(b) Export/ETL planifié.** Commande `app:tenant:project {boutique?}` en cron qui reconstruit/rafraîchit la projection. Plus simple, mais latence et charge par batch.
- **(c) Réplication SQL native.** Lourde, couple les schémas central et client, peu adaptée à une projection partielle et transformée.

Principes : **idempotence** (upsert par clé métier), **le central gagne** en cas de conflit, **cohérence éventuelle** assumée côté site, projection **en lecture seule** côté client (le site n'écrit que ses propres données de navigation/panier si besoin). Cible d'écriture = la `CompanyDatabase` du client.

## 4. Gestion des connexions

- **Phase 1 (livrée)** : `TenantConnectionService::createConnection(CompanyDatabase)` construit une connexion DBAL à la volée via `DriverManager::getConnection()`, mot de passe **déchiffré en mémoire** uniquement au moment de l'ouverture. `testConnection()` exécute `SELECT 1` + `SELECT VERSION()` et met à jour le statut (`connection_status`, `last_tested_at`, `server_version`).
- **Phase 2 (cible)** : **registre de connexions** (cache par requête) pour réutiliser une connexion tenant ouverte ; **second EntityManager dynamique** si l'on veut mapper des entités métier sur la base client (via `DriverManager` + une `Configuration` ORM dédiée, ou un bundle type `hakam/multi-tenancy-bundle`). Résolution du tenant courant par ordre de priorité :
  1. **sous-domaine / domaine** (`acme.pos.exemple.com`, ou domaine e-commerce propre) ;
  2. **en-tête API** (`X-Boutique-Id` / claim JWT) pour la PWA ;
  3. **boutique de l'utilisateur** authentifié (`UserCompany`) en repli.
- **Cycle de vie** : ouverture paresseuse, fermeture en fin de requête (`kernel.terminate`), pas de connexion tenant sur les routes du control plane.

## 5. Provisioning & migrations

- **Provisioning** (phase 2) : à la création d'une boutique, option « créer la base » → `CREATE DATABASE` + application d'un **schéma client initial** (jeu d'entités data-plane). Réutiliser/généraliser `DatabaseBackupService` (aujourd'hui lié à l'EM par défaut) pour cibler une `CompanyDatabase`.
- **Migrations par tenant** : un jeu de migrations Doctrine **dédié au data plane**, appliqué à chaque base client via une commande `app:tenant:migrate {boutique?}` itérant sur les `CompanyDatabase` actives. Suivi d'une **version de schéma par tenant** (`CompanyDatabase.schemaVersion`) pour détecter les bases en retard.
- **Idempotence & sécurité** : dry-run, verrou par tenant, journalisation `FileLogger`, jamais de mot de passe dans les logs.

## 6. Sécurité

- **Chiffrement des credentials (livré)** : `EncryptionService` libsodium `secretbox` (authentifié), clé 32 o via `APP_ENCRYPTION_KEY` (hors dépôt, `.env.local`/secrets). Le mot de passe DB n'est **jamais** stocké ni journalisé en clair ; à l'édition, idiome « laisser vide pour conserver ».
- **Moindre privilège** : chaque base client devrait avoir **son propre utilisateur MySQL/MariaDB** restreint à sa base. Éviter un compte `root` partagé.
- **Isolation réseau** : bases clients idéalement sur hôtes/instances distincts, non exposés publiquement ; liste blanche d'hôtes autorisés côté portail.
- **SSRF du test de connexion** : l'action « Tester la connexion » ouvre une connexion vers un hôte/port fourni par un administrateur → réservée à `COMPANY_EDIT` (admin/manager). À durcir en phase 2 (liste blanche d'hôtes, interdiction des IP internes) si des rôles moins privilégiés y accèdent.
- **Rotation de clé** : prévoir une commande de re-chiffrement (`app:crypto:rotate`) lisant l'ancienne clé et ré-écrivant avec la nouvelle.

## 7. API pour la PWA

- **Compléter `/api/mobile/*`** : implémenter les contrôleurs (`Controller/Api/Mobile/*`) pour les routes déjà déclarées (login, 2fa, refresh, telemetry, logs, diagnostics), en s'appuyant sur l'infra device-bound existante (`X-Device-UID`, `DeviceApp`).
- **Endpoints métier POS/e-commerce** : catalogue, panier, ventes, sessions de caisse, rapports — **scopés au tenant** résolu (§4). Autorisation via les permissions RBAC (`pos.sell`, `pos.refund`, `report.view`, …) déjà seedées.
- **Contrat & qualité** : versionnement (`/api/v1`), pagination/tri normalisés, enveloppe `ApiResponseFormatter`, documentation OpenAPI (`nelmio/api-doc` déjà présent, `/api/doc`).

## 8. PWA Ionic

- Consomme l'API JWT ; stockage du token + refresh (rotation déjà gérée côté serveur). CORS déjà prêt (`ngsw-bypass` pour le service worker Angular/Ionic, en-têtes d'expiration exposés).
- **Offline-first** : cache service-worker des lectures, file d'attente des écritures (ventes hors-ligne) synchronisée à la reconnexion.
- **Liaison appareil** : `X-Device-UID` + flux d'approbation d'appareil existants.

## 9. Sites e-commerce Symfony par client (plus tard)

- **Même socle de code**, bootstrap par tenant (résolution par domaine), branché sur la **base projetée du client** (lecture des données POS synchronisées, §4bis). Le site e-commerce ne lit **jamais** la base centrale : il consomme uniquement sa projection (surface réduite, isolation).
- Le site peut écrire ses propres données (panier, comptes clients du site) dans sa base ; les données POS y sont en lecture (le central reste autoritaire).

## 10. Backups par tenant

- Généraliser `DatabaseBackupService` pour accepter une `CompanyDatabase` (au lieu de l'EM par défaut) : `mysqldump` par client, rétention, restauration, planification. Sauvegardes **isolées par client** (conformité, restauration granulaire).

## 11. Feuille de route

| Phase | Contenu | Statut |
|---|---|---|
| **1** | Champs de connexion DB par boutique + **chiffrement** + **test de connexion** | ✅ Livré |
| **1.5** | Client de test **AmansaShop** (fixture + base `amansa`) + **tests fonctionnels de l'auth API** (login → JWT → refresh → `/api/me`, erreurs, sécurité) | ✅ Livré |
| **2** | **Schéma de projection POS** (tables lues par le site) + **synchronisation central→client** (Messenger, §4bis) ; provisioning `CREATE DATABASE`, migrations par tenant, registre de connexions | À faire |
| **3** | API PWA : contrôleurs `/api/mobile/*`, endpoints POS/e-commerce, OpenAPI | À faire |
| **4** | PWA Ionic (offline, sync) puis sites e-commerce Symfony par client (lecture de sa base projetée), backups par tenant | À faire |

**Décisions ouvertes** : périmètre exact de la projection POS (quelles tables/colonnes exposées au site) ; mécanisme de synchro retenu (Messenger événementiel recommandé vs ETL) ; bases clients même serveur vs instances dédiées ; rotation de clé/secrets par client.

---

### Annexe — artefacts des phases 1 et 1.5

Phase 1 :
- `src/Entity/CompanyDatabase.php` (1:1 `Company`), `src/Enum/DatabaseDriver.php`
- `src/Service/Security/EncryptionService.php` (libsodium), `src/Service/Database/TenantConnectionService.php` (`DriverManager` + test)
- `src/Form/CompanyDatabaseFormType.php` + intégration `CompanyFormType`, section `_database_section.html.twig`, action `admin_companies_database_test`
- Config : `APP_ENCRYPTION_KEY` (`.env` placeholder, `.env.local` réel), câblage `config/services.yaml`

Phase 1.5 :
- Fixture `src/DataFixtures/AppFixtures.php::createAmansaShop` (boutique AmansaShop + manager `yalya96@gmail.com` + `CompanyDatabase`→`amansa`, mot de passe chiffré) — idempotente.
- Tests fonctionnels `tests/Functional/Api/AuthApiTest.php` (login/JWT/refresh/`/api/me` + erreurs), clés JWT de test dédiées + `ENABLE_TOKEN_SECURITY_VALIDATION=false` en `.env.test`.
- Prérequis base de test : `doctrine:database:create --env=test --if-not-exists` puis `doctrine:schema:update --force --env=test`.
