# Documentation Technique — Module Application

> Module : `Core\v1\Application`
> Chemin : `app/v1/Application/`
> Architecture : Multi-tenant (Stancl Tenancy), API REST, Laravel

---

## Table des matières

1. [Installation & Configuration](#1-installation--configuration)
2. [Vue d'ensemble](#2-vue-densemble)
3. [Structure du module](#3-structure-du-module)
4. [Schéma de base de données](#4-schéma-de-base-de-données)
5. [Modèles et relations](#5-modèles-et-relations)
6. [Services](#6-services)
7. [API REST — Endpoints](#7-api-rest--endpoints)
8. [Enums](#8-enums)
9. [Système de permissions](#9-système-de-permissions)
10. [Événements et Listeners](#10-événements-et-listeners)
11. [Tests](#11-tests)
12. [Conventions et patterns](#12-conventions-et-patterns)

---

## 1. Installation & Configuration

### 1.1 Migrations

Les migrations sont toutes dans `database/migrations/tenant/` et s'exécutent **uniquement en contexte tenant** via Stancl Tenancy.

Pour migrer tous les tenants :
```bash
php artisan tenants:migrate
```

Pour migrer un tenant spécifique :
```bash
php artisan tenants:migrate --tenants=<tenant_id>
```

> ⚠️ Ne pas utiliser `php artisan migrate` seul : les tables de ce module n'existent que dans les bases tenant, pas dans la base centrale.

**Ordre de migration (chronologique) :**

| Date | Fichier | Description |
|---|---|---|
| 2025-01-15 | `create_applications_table` | Table `applications` |
| 2025-01-16 | `create_application_permissions_table` | Table `application_permissions` |
| 2025-01-16 | `create_application_permission_assignments_table` | Pivot permissions ↔ applications |
| 2026-02-17 | `create_application_modules_table` | Table `application_modules` (v1) |
| 2026-02-17 | `create_application_module_permissions_table` | Pivot permissions ↔ modules |
| 2026-02-17 | `create_application_packs_table` | Table `application_packs` (v1) |
| 2026-02-17 | `create_application_pack_modules_table` | Table `application_pack_modules` (v1) |
| 2026-04-16 | `create_app_modules_table` | Table `app_modules` (types génériques) |
| 2026-04-16 | `create_application_packs_table` | Refonte table `application_packs` |
| 2026-04-16 | `create_application_configs_table` | Table `application_configs` |
| 2026-04-16 | `create_application_modules_table` | Refonte table `application_modules` |
| 2026-04-16 | `create_application_module_prices_table` | Table `application_module_prices` |
| 2026-04-16 | `create_offer_application_modules_table` | Table `offer_application_modules` |
| 2026-04-17 | `create_pack_modules_table` | Pivot packs ↔ modules |
| 2026-04-17 | `add_permissions_in_offer_application_modules_table` | Colonne `permissions` sur offer modules |
| 2026-04-17 | `add_permissions_in_application_modules_table` | Colonne `permissions` sur modules |
| 2026-04-21 | `rename_offer_id_in_offer_application_modules_table` | Renommage colonne `offer_id` |
| 2026-04-28 | `create_storage_offers_table` | Table `storage_offers` |
| 2026-04-29 | `add_column_key_in_applications_table` | Colonne `key` sur applications |
| 2026-05-07 | `create_application_pack_modules_table` | Refonte table `application_pack_modules` |
| 2026-05-07 | `remove_application_pack_id_in_application_modules_table` | Suppression colonne obsolète |
| 2026-05-09 | `create_storage_subscriptions_table` | Table `storage_subscriptions` |
| 2026-05-15 | `create_application_module_dependencies_table` | Table des dépendances entre modules |

---

### 1.2 Seeders

Les seeders sont dans `database/seeders/`. Ils doivent être exécutés **dans l'ordre** car certains dépendent des données créées par les précédents.

#### Ordre recommandé

```bash
# 1. Types de modules génériques (requis par tous les autres seeders)
php artisan db:seed --class="Core\v1\Application\Database\Seeders\AppModuleSeeder"

# 2. Modules liés à une application de test (requiert AppModuleSeeder)
php artisan db:seed --class="Core\v1\Application\Database\Seeders\ApplicationModuleSeeder"

# 3. Données complètes de démonstration : applications, permissions, modules, packs
php artisan db:seed --class="Core\v1\Application\Database\Seeders\ApplicationPermissionSeeder"
```

> ⚠️ `ApplicationModuleSeeder` dépend de `AppModuleSeeder` : il affiche un warning si aucun `AppModule` n'est trouvé.
>
> ⚠️ `ApplicationPermissionSeeder` s'exécute **pour tous les tenants** via `tenancy()->runForMultiple(null, ...)`.

#### Ce que chaque seeder crée

| Seeder | Données créées |
|---|---|
| `AppModuleSeeder` | 11 modules génériques : Pipeline, RDV, Satisfaction client, Devis, Dépenses, Comptes et Caisses, Congés, Dossiers collaborateurs, Fiches de paie, Gestion de projets, Gestion documentaire |
| `ApplicationModuleSeeder` | Crée ou récupère une application "Test Application" et lui associe tous les `AppModule` existants avec les permissions VIEW et CREATE par défaut |
| `ApplicationPermissionSeeder` | 2 applications (App Budget, App CRM) + 37 permissions réparties en 9 catégories + 9 modules + 6 packs (Starter, Premium, Enterprise pour Budget ; Essentiel, Business, Marketing Pro pour CRM) |

---

### 1.3 Service Provider

Le module est enregistré via `ApplicationServiceProvider`. Vérifiez qu'il est bien déclaré dans `config/app.php` ou dans le mécanisme d'auto-découverte du projet.

```php
// config/app.php ou équivalent
Core\v1\Application\ApplicationServiceProvider::class,
```

---

### 1.4 Variables d'environnement

Ce module ne nécessite pas de variables d'environnement spécifiques. Il hérite de la configuration Laravel standard et de Stancl Tenancy.

---

### 1.5 Prérequis inter-modules

Ce module dépend des modules suivants qui doivent être migrés et configurés en premier :

| Module | Raison |
|---|---|
| `Core\v1\Contact` | Relation `contacts` sur `Application` |
| `Core\v1\License` | `ServiceOffer`, `ContactApplication`, `LicenseOffer` référencent les modèles Application |
| `Core\v1\Setting` | `Currency` utilisé dans `StorageOffer` |
| `Core\v1\ExternalApplications` | Type polymorphique dans `StorageSubscription` |

---

## 2. Vue d'ensemble

Le module **Application** est le référentiel central qui modélise les logiciels/services proposés sur la plateforme. Il gère :

- Le catalogue d'applications et leurs métadonnées
- La décomposition en modules fonctionnels avec dépendances transitives
- Le regroupement de modules en packs tarifaires
- Le système de permissions granulaires (code + catégorie)
- Les offres de stockage et le cycle de vie des abonnements (pending → approved/rejected)

Le module tourne exclusivement en contexte **tenant** : toutes les routes passent par `InitializeTenancyByDomain` et `PreventAccessFromCentralDomains`.

---

## 2. Structure du module

```
app/v1/Application/
├── ApplicationServiceProvider.php
├── database/
│   ├── factories/
│   ├── migrations/tenant/
│   └── seeders/
├── Enums/
│   ├── ApplicationStatusEnum.php
│   ├── ApplicationConfigOptionEnum.php
│   ├── ApplicationModulePermissionEnum.php
│   ├── ApplicationPackPermissionEnum.php
│   ├── ApplicationPermissionDefinitionEnum.php
│   ├── PermissionApplicationModuleEnum.php
│   ├── StorageOfferPermissionEnum.php
│   ├── StorageOfferStatusEnum.php
│   ├── StorageSubscriptionPermissionEnum.php
│   ├── StorageSubscriptionStatusEnum.php
│   ├── StorageUnitEnum.php
│   └── Permissions/
│       ├── ECU/   (Account, Advice, Budget, Credit, Debt, Heritage, Saving, Transaction, Transfert)
│       └── Shop/  (EntryShop, EntryStore, ExitStore, Invoice, Lot, SalesSession, CashBox, Shop, Product, Sale, Store, User, Statistic, StockStatus, StoreProduct, StoreUser, Sync, TransferShop)
├── Events/
│   ├── StorageSubscriptionApproved.php
│   └── StorageSubscriptionRejected.php
├── Http/
│   ├── Controllers/
│   ├── Requests/
│   └── Resources/
├── Listeners/
│   └── NotifyExternalAppOnStorageApproved.php
├── Models/
├── routes/
│   ├── api.php       (central — vide actuellement)
│   └── tenant.php    (toutes les routes actives)
├── Services/
│   ├── ApplicationService.php
│   ├── ApplicationConfigService.php
│   ├── ApplicationModuleService.php
│   ├── ApplicationPackService.php
│   ├── ApplicationPackModuleService.php
│   ├── ApplicationPermissionService.php
│   ├── ModuleDependencyResolver.php
│   ├── PermissionApplicationModuleCollection.php
│   └── StorageSubscriptionService.php
└── tests/Feature/
```

---

## 3. Schéma de base de données

Toutes les tables sont créées dans le contexte **tenant**.

### `applications`
| Colonne | Type | Description |
|---|---|---|
| `id` | UUID (PK) | Généré automatiquement via `Str::uuid()` |
| `name` | string | Nom de l'application |
| `key` | string | Slug unique auto-généré depuis le nom (`Str::slug($name, '_')`) avec suffixe numérique si collision |
| `description` | text (nullable) | Description |
| `status` | string | `active` / `suspended` |
| `metadata` | JSON | Données additionnelles libres |
| `created_at` / `updated_at` | timestamp | — |

### `application_permissions`
| Colonne | Type | Description |
|---|---|---|
| `id` | UUID (PK) | — |
| `code` | string | Identifiant de permission (ex. `shop.sale.view`) |
| `description` | text | Description lisible |
| `category` | string | Domaine fonctionnel (ex. `Shop`, `ECU`) |
| `metadata` | JSON | — |

### `application_permission_assignments` *(pivot)*
| Colonne | Type |
|---|---|
| `application_id` | UUID (FK → applications) |
| `permission_id` | UUID (FK → application_permissions) |
| `created_at` / `updated_at` | timestamp |

### `app_modules`
| Colonne | Type | Description |
|---|---|---|
| `id` | UUID (PK) | — |
| `name` | string | Nom du type de module générique |
| *(autres colonnes)* | — | Non contraintes (`$guarded = []`) |

### `application_modules`
| Colonne | Type | Description |
|---|---|---|
| `id` | UUID (PK) | — |
| `application_id` | UUID (FK → applications) | — |
| `app_module_id` | UUID (FK → app_modules) | Type de module |
| `description` | text (nullable) | — |
| `permissions` | JSON | Permissions spécifiques à ce module |

### `application_module_dependencies` *(pivot auto-référentielle)*
| Colonne | Type | Description |
|---|---|---|
| `application_module_id` | UUID | Module qui dépend d'un autre |
| `required_application_module_id` | UUID | Module requis |

### `application_module_prices`
| Colonne | Type | Description |
|---|---|---|
| `id` | UUID (PK) | — |
| `application_module_id` | UUID (FK) | — |
| *(autres colonnes prix)* | — | — |

### `application_module_permissions` *(pivot)*
| Colonne | Type |
|---|---|
| `application_module_id` | UUID |
| `application_permission_id` | UUID |

### `application_packs`
| Colonne | Type | Description |
|---|---|---|
| `id` | UUID (PK) | — |
| `application_id` | UUID (FK → applications) | — |
| `pack_id` | UUID (FK → packs) | Lien avec le pack tarifaire (module License) |
| `description` | text (nullable) | — |

### `application_pack_modules`
| Colonne | Type | Description |
|---|---|---|
| `id` | UUID (PK) | — |
| `application_id` | UUID (FK → applications) | — |
| `application_pack_id` | UUID (FK → application_packs) | — |
| *(autres colonnes)* | — | — |

### `application_configs`
| Colonne | Type | Description |
|---|---|---|
| `id` | UUID (PK) | — |
| `application_id` | UUID (FK → applications) | Relation `hasOne` |
| *(options de config)* | — | Pilotées par `ApplicationConfigOptionEnum` |

### `storage_offers`
| Colonne | Type | Description |
|---|---|---|
| `id` | UUID (PK) | — |
| `application_id` | UUID (FK → applications) | — |
| `name` | string | Nom de l'offre |
| `description` | text (nullable) | — |
| `capacity` | int | Valeur numérique de la capacité |
| `value` | string | Unité : `KB`, `MB`, `GB`, `TB` |
| `price` | decimal | Tarif |
| `currency_id` | UUID (FK → currencies) | Devise |
| `status` | string | `active` / `inactive` (défaut : `inactive` à la création) |

### `storage_subscriptions`
| Colonne | Type | Description |
|---|---|---|
| `id` | UUID (PK) | — |
| `contact_id` | UUID | ID du contact (polymorphique) |
| `contact_type` | string | Classe du contact (ex. `ExternalApplication`) |
| `storage_offer_id` | UUID (FK → storage_offers) | — |
| `contact_application_id` | UUID (nullable, FK → contact_applications) | — |
| `status` | string | `pending` / `approved` / `rejected` |
| `notes` | text (nullable) | Commentaire admin |
| `requested_at` | timestamp | Date de soumission |
| `approved_at` | timestamp (nullable) | Date d'approbation |
| `rejected_at` | timestamp (nullable) | Date de rejet |

---

## 4. Modèles et relations

### `Application`
```
Application
 ├── hasOne    → ApplicationConfig
 ├── hasMany   → ApplicationModule
 ├── hasMany   → ApplicationPack
 ├── hasMany   → ApplicationPackModule
 ├── hasMany   → ServiceOffer (module License)
 ├── belongsToMany → ApplicationPermission  (via application_permission_assignments)
 └── belongsToMany → Contact               (via contact_applications)
```

**Scopes disponibles :** `scopeActive`, `scopeSuspended`, `scopeSearch`, `scopeApplyFilters`

**Auto-boot :**
- `id` → UUID généré
- `key` → `Str::slug($name, '_')` + suffixe numérique si collision sur la colonne `key`

---

### `ApplicationModule`
```
ApplicationModule
 ├── belongsTo → Application
 ├── belongsTo → AppModule
 ├── hasMany   → ApplicationModulePrice
 ├── belongsToMany → ApplicationModule (self, via application_module_dependencies) [dependencies]
 └── belongsToMany → ApplicationModule (self, inverse)                             [requiredBy]
```

Le champ `permissions` est casté en `array` (JSON).

---

### `ApplicationPack`
```
ApplicationPack
 ├── belongsTo → Application
 ├── belongsTo → Pack (module License)
 └── hasMany   → ApplicationPackModule
```

**Auto-delete** : la suppression d'un pack déclenche `$model->applicationModules()->delete()` via le boot.

---

### `ApplicationPermission`
```
ApplicationPermission
 ├── belongsToMany → ApplicationModule (via application_module_permissions)
 └── belongsToMany → License          (via license_app_permissions, module License)
```

**Scopes :** `scopeByCategory`, `scopeSearch`

---

### `StorageOffer` *(namespace `App\v1\Application\Models`)*
```
StorageOffer
 ├── belongsTo → Application
 └── belongsTo → Currency (module Setting)
```

Attribut calculé `statusFormatted` via `Attribute::make()` → retourne l'option formatée de `StorageOfferStatusEnum`.

Méthodes statiques de factory : `createFromRequest($data)` et `updateFromRequest($data)` encapsulent la logique de création/mise à jour avec `DB::transaction`.

---

### `StorageSubscription` *(namespace `App\v1\Application\Models`)*
```
StorageSubscription
 ├── morphTo       → contact (ExternalApplication)
 ├── belongsTo     → StorageOffer
 └── belongsTo     → ContactApplication (module License)
```

Champs `requested_at`, `approved_at`, `rejected_at` castés en `datetime`.

---

## 5. Services

### `ApplicationService`

| Méthode | Description |
|---|---|
| `list(filters, perPage, page)` | Listing paginé avec counts (modules, packs, contacts, offers) |
| `collection(filters)` | Liste complète non paginée pour les selects |
| `create(data)` | Création d'une Application |
| `update(application, data)` | Mise à jour partielle (seules les clés présentes sont modifiées) |
| `delete(application)` | Suppression en cascade : packs → offer modules → prices → offers → application |
| `findApplication(id)` | Retourne `null` si non trouvé |
| `findOrFail(id)` | Lève une exception si non trouvé |
| `getStatus()` | Retourne `ApplicationStatusEnum::options()` |
| `getDataTableData(search, offset)` | Format DataTables (draw, recordsTotal, recordsFiltered, data) |
| `verifyLicenseOffer(application)` | Retourne `true` si l'application est liée à une `LicenseOffer` active (bloque la suppression) |

---

### `StorageSubscriptionService`

| Méthode | Description |
|---|---|
| `request(data)` | Crée une souscription en statut `pending`. Lève `RuntimeException` si une souscription `pending` existe déjà pour ce `contact_id` |
| `approve(subscription, notes)` | Passe en `approved`, set `approved_at`, fire `StorageSubscriptionApproved` |
| `reject(subscription, notes)` | Passe en `rejected`, set `rejected_at`, fire `StorageSubscriptionRejected` |
| `findPending(contactId)` | Retourne la souscription `pending` active d'un contact |
| `collection(filters)` | Retourne toutes les souscriptions avec eager loading (`storageOffer.application`, `currency`, `contactApplication`) |
| `toCapacityBytes(offer)` | *(privé)* Convertit capacity + unit en bytes (KB/MB/GB/TB) |

---

### `ModuleDependencyResolver`

Résout les dépendances transitives de modules via un **BFS** (Breadth-First Search) avec protection anti-cycles.

| Méthode | Description |
|---|---|
| `resolve(applicationModuleIds[])` | Retourne tous les IDs de modules requis (transitivement), sans doublons |
| `enrichModules(modules[], offer)` | Enrichit un snapshot de modules d'une `ServiceOffer` en ajoutant les dépendances manquantes avec `is_dependency: true` |

---

### `PermissionApplicationModuleCollection`

Service utilitaire qui aggrège les options de permissions disponibles pour tous les modules d'application (utilisé par la route `/module/permission/collection`).

---

## 6. API REST — Endpoints

Toutes les routes sont préfixées par le domaine tenant et protégées par `auth:api`.

### Applications

| Méthode | URL | Action | Description |
|---|---|---|---|
| `GET` | `/applications` | `index` | Liste paginée (params : `status`, `search`, `per_page`, `page`) |
| `GET` | `/applications/collection` | `collection` | Liste complète non paginée |
| `GET` | `/applications/datatables` | `getApplicationsDatatable` | Format DataTables |
| `GET` | `/applications/status` | `getStatus` | Enum des statuts disponibles |
| `POST` | `/applications` | `store` | Créer une application |
| `GET` | `/applications/{application}/show` | `show` | Détail d'une application (avec counts) |
| `PATCH` | `/applications/{application}/update` | `update` | Modifier une application |
| `DELETE` | `/applications/{application}/delete` | `destroy` | Supprimer (bloqué si LicenseOffer liée, HTTP 409) |
| `PUT` | `/applications/{application}/permissions/sync` | `syncPermissions` | Remplace toutes les permissions |
| `POST` | `/applications/{application}/permissions/attach` | `attachPermissions` | Ajoute des permissions |
| `POST` | `/applications/{application}/permissions/detach` | `detachPermissions` | Retire des permissions |

---

### Configuration d'application

| Méthode | URL | Action |
|---|---|---|
| `GET` | `/applications/get/{application}/configuration` | `showApplicationConfig` |
| `POST` | `/applications/store/{application}/configuration` | `storeApplicationConfig` |
| `PUT` | `/applications/update/{application}/configuration` | `updateApplicationConfig` |
| `DELETE` | `/applications/delete/{application}/configuration` | `destroyApplicationConfig` |
| `GET` | `/applications/get/configuration/collection` | `collectionApplicationConfig` |
| `GET` | `/applications/get/configuration/datatables` | `datatableApplicationConfig` |

---

### Modules d'application

| Méthode | URL | Action |
|---|---|---|
| `GET` | `/applications/get/{application}/modules/collection` | `collectionApplicationModule` |
| `GET` | `/applications/get/{application}/modules/datatables` | `datatableApplicationModule` |
| `POST` | `/applications/{application}/modules` | `storeApplicationModule` |
| `GET` | `/applications/{application}/modules/{module}` | `showApplicationModule` |
| `PUT` | `/applications/{application}/modules/{module}` | `updateApplicationModule` |
| `DELETE` | `/applications/{application}/modules/{module}` | `destroyApplicationModule` |

---

### Dépendances de modules

| Méthode | URL | Action |
|---|---|---|
| `GET` | `/applications/{application}/modules/{module}/dependencies` | Liste des dépendances |
| `POST` | `/applications/{application}/modules/{module}/dependencies` | Ajouter une dépendance |
| `DELETE` | `/applications/{application}/modules/{module}/dependencies/{required}` | Supprimer une dépendance |

---

### Packs d'application

| Méthode | URL | Action |
|---|---|---|
| `GET` | `/applications/get/{application}/packs/collection` | `collectionApplicationPack` |
| `GET` | `/applications/get/{application}/packs/datatables` | `datatableApplicationPack` |
| `POST` | `/applications/{application}/packs` | `storeApplicationPack` |
| `GET` | `/applications/{application}/packs/{pack}` | `showApplicationPack` |
| `PUT` | `/applications/{application}/packs/{pack}` | `updateApplicationPack` |
| `DELETE` | `/applications/{application}/packs/{pack}` | `destroyApplicationPack` |

---

### Modules dans les packs

| Méthode | URL | Action |
|---|---|---|
| `GET` | `/applications/get/{application}/pack/modules/collection` | `collectionApplicationPackModule` |
| `GET` | `/applications/get/{application}/pack/modules/datatables` | `datatableApplicationPackModule` |
| `POST` | `/applications/{application}/pack/modules` | `storeApplicationPackModule` |
| `GET` | `/applications/{application}/pack/modules/{module}` | `showApplicationPackModule` |
| `PUT` | `/applications/{application}/pack/modules/{module}` | `updateApplicationPackModule` |
| `DELETE` | `/applications/{application}/pack/modules/{module}` | `destroyApplicationPackModule` |

---

### Permissions d'application

| Méthode | URL | Action |
|---|---|---|
| `GET` | `/apps-permissions` | `index` |
| `GET` | `/apps-permissions/collection` | `collection` |
| `GET` | `/apps-permissions/datatables` | `getDatatable` |
| `POST` | `/apps-permissions` | `store` |
| `GET` | `/apps-permissions/{permission}/show` | `show` |
| `PUT` | `/apps-permissions/{permission}/update` | `update` |
| `DELETE` | `/apps-permissions/{permission}/delete` | `destroy` |
| `GET` | `/apps-permissions/categories` | `categories` |

---

### Offres de modules (OfferApplicationModule)

| Méthode | URL | Action |
|---|---|---|
| `GET` | `/get/{offer}/offer/application/modules` | `getOfferApplicationModules` |
| `POST` | `/store/offer/application/modules` | `storeOfferApplicationModules` |
| `PATCH` | `/update/{offerApplicationModule}/offer/application/modules` | `updateOfferApplicationModules` |
| `DELETE` | `/delete/{offerApplicationModule}/offer/application/modules` | `deleteOfferApplicationModules` |

---

### AppModules (types génériques)

| Méthode | URL | Action |
|---|---|---|
| `GET` | `/app_modules` | `index` |
| `GET` | `/app_modules/datatables` | `datatables` |
| `GET` | `/app_modules/{appModule}/show` | `show` |
| `POST` | `/app_modules/store` | `store` |
| `PUT` | `/app_modules/{appModule}/update` | `update` |
| `DELETE` | `/app_modules/{appModule}/delete` | `destroy` |

---

### Offres de stockage

| Méthode | URL | Description |
|---|---|---|
| `GET` | `/get/storage/offers/collection` | Liste complète (filtre : `application_id`) |
| `GET` | `/get/storage/offers/datatable` | Format DataTables (filtres : `status`, `application_id`) |
| `GET` | `/get/{offer}/storage/offer/detail` | Détail d'une offre |
| `POST` | `/store/storage/offer` | Créer une offre (statut initial : `inactive`) |
| `PATCH` | `/update/{offer}/storage/offer` | Modifier une offre |
| `PATCH` | `/update/status/{offer}/storage/offer` | Basculer le statut `active` ↔ `inactive` |
| `DELETE` | `/delete/{offer}/storage/offer` | Supprimer une offre |
| `GET` | `/get/storage/offers` | Liste publique par `key` d'application (sans auth stricte) |

**Corps de création (`POST /store/storage/offer`) :**
```json
{
  "application_id": "uuid",
  "name": "Pack 5 Go",
  "description": "...",
  "capacity": 5,
  "value": "GB",
  "price": 9.99,
  "currency_id": "uuid"
}
```

---

### Abonnements de stockage — Admin

| Méthode | URL | Description |
|---|---|---|
| `GET` | `/get/storage/subscriptions/collection` | Liste (filtres : `status`, `application_id`, `search`) |
| `GET` | `/get/{subscription}/storage/subscription/detail` | Détail |
| `POST` | `/store/storage/subscription` | Créer manuellement une souscription |
| `PATCH` | `/approve/{subscription}/storage/subscription` | Approuver (body : `notes`) |
| `PATCH` | `/reject/{subscription}/storage/subscription` | Rejeter (body : `notes`) |
| `DELETE` | `/delete/{subscription}/storage/subscription` | Supprimer (seulement si `pending`) |

---

### Abonnements de stockage — Application externe (M2M)

Routes accessibles via authentification clé publique/secrète (`auth:api`), préfixe `/storage-subscriptions`.

| Méthode | URL | Description |
|---|---|---|
| `POST` | `/storage-subscriptions/request` | Soumettre une demande de souscription |
| `GET` | `/storage-subscriptions/mine` | Lister ses propres souscriptions (params : `contact_id`, `contact_type`, `app_key`) |

**Corps de la demande (`POST /storage-subscriptions/request`) :**
```json
{
  "storage_offer_id": "uuid",
  "contact_id": "uuid",
  "contact_type": "...",
  "contact_application_id": "uuid (optionnel)",
  "notes": "... (optionnel)"
}
```

> Erreur 422 si une souscription `pending` existe déjà pour ce `contact_id`, ou si l'offre est inactive.

---

### Routes utilitaires (Enums)

| Méthode | URL | Retourne |
|---|---|---|
| `GET` | `/module/permission/collection` | Collection des permissions de modules |
| `GET` | `/module/permission/enum` | `PermissionApplicationModuleEnum::options()` |
| `GET` | `/application/option/enum` | `ApplicationConfigOptionEnum::options()` |
| `GET` | `/storage/unit/collection` | `StorageUnitEnum::options()` (KB, MB, GB, TB) |
| `GET` | `/storage/subscription/status/collection` | `StorageSubscriptionStatusEnum::options()` |

---

## 7. Enums

Tous les enums implémentent une interface commune avec les méthodes : `options()`, `optionsInArray()`, `getOption()`, `label()`, `color()`, `toLocaleFr()`, `toLocaleEn()`.

| Enum | Valeurs |
|---|---|
| `ApplicationStatusEnum` | `active`, `suspended` |
| `StorageOfferStatusEnum` | `active`, `inactive` |
| `StorageSubscriptionStatusEnum` | `pending`, `approved`, `rejected` |
| `StorageUnitEnum` | `KB`, `MB`, `GB`, `TB` |
| `ApplicationConfigOptionEnum` | Options de configuration d'application |
| `PermissionApplicationModuleEnum` | Codes de permissions des modules |

---

## 8. Système de permissions

### Architecture

Les permissions sont organisées en deux niveaux :
1. **`ApplicationPermission`** (table) : permissions déclarées avec un `code`, une `category` et une `description`
2. **Enums de permissions** (fichiers) : définitions statiques des codes par domaine fonctionnel

### Domaines définis

**ECU (Espace Client Utilisateur) :**
- `AccountPermissionEnum` — Gestion des comptes
- `AdvicePermissionEnum` — Conseils
- `BudgetPermissionEnum` — Budgets
- `CreditPermissionEnum` — Crédits
- `DebtPermissionEnum` — Dettes
- `HeritagePermissionEnum` — Patrimoine
- `SavingPermissionEnum` — Épargne
- `TransactionPermissionEnum` — Transactions
- `TransfertPermissionEnum` — Virements

**Shop :**
- `ShopPermissionEnum`, `ShopProductPermissionEnum`, `ShopSalePermissionEnum`
- `ShopStorePermissionEnum`, `ShopUserPermissionEnum`, `ShopCashBoxPermissionEnum`
- `StorePermissionEnum`, `StoreProductPermissionEnum`, `StoreUserPermissionEnum`
- `EntryShopPermissionEnum`, `EntryStorePermissionEnum`, `ExitStorePermissionEnum`
- `InvoicePermissionEnum`, `LotPermissionEnum`, `SalesSessionPermissionEnum`
- `StatisticPermissionEnum`, `StockStatusPermissionEnum`, `SyncPermissionEnum`
- `TransferShopPermissionEnum`

### Association des permissions

```
Application ←→ ApplicationPermission    (pivot application_permission_assignments)
ApplicationModule ←→ ApplicationPermission  (pivot application_module_permissions)
License ←→ ApplicationPermission           (pivot license_app_permissions, module License)
```

### Vérification de permission dans les controllers

Les controllers `StorageOfferController` et `StorageSubscriptionController` utilisent le trait `ManagePermissionTrait` :
```php
if (! $this->verifyUserPermission(StorageOfferPermissionEnum::VIEW->value)) {
    return response()->json(['message' => trans('auth.forbidden')], 403);
}
```

---

## 9. Événements et Listeners

### `StorageSubscriptionApproved`

Déclenché par `StorageSubscriptionService::approve()` après mise à jour en BDD.

**Payload :**
```php
public function __construct(
    public readonly string $userId,
    public readonly int    $capacityBytes,
    public readonly string $subscriptionId,
)
```

La capacité est convertie en bytes via `toCapacityBytes()` (KB × 1 024, MB × 1 048 576, GB × 1 073 741 824, TB × 1 099 511 627 776).

---

### `StorageSubscriptionRejected`

Déclenché par `StorageSubscriptionService::reject()`.

**Payload :** instance `StorageSubscription` avec relations `storageOffer` et `contact` chargées.

---

### `NotifyExternalAppOnStorageApproved` *(Listener)*

Écoute `StorageSubscriptionApproved` et notifie l'application externe concernée.

---

## 10. Tests

Les tests sont des tests de fonctionnalité (Feature Tests) qui utilisent des factories.

| Fichier | Couverture |
|---|---|
| `ApplicationConfigTest.php` | CRUD de la configuration d'application |
| `ApplicationModuleTest.php` | CRUD des modules |
| `ApplicationPackTest.php` | CRUD des packs |
| `ApplicationPackModuleTest.php` | CRUD des modules dans les packs |
| `AppModuleTest.php` | CRUD des AppModules génériques |
| `OfferApplicationModuleTest.php` | Gestion des modules d'offres |
| `StorageOfferTest.php` | CRUD + statut des offres de stockage |

**Factories disponibles :**
`ApplicationFactory`, `ApplicationConfigFactory`, `ApplicationModuleFactory`, `ApplicationModulePriceFactory`, `ApplicationPackFactory`, `ApplicationPackModuleFactory`, `AppModuleFactory`, `OfferApplicationModuleFactory`, `PermissionFactory`, `StorageOfferFactory`

---

## 11. Conventions et patterns

### UUIDs

Tous les modèles utilisent des UUIDs comme clé primaire :
```php
protected $keyType = 'string';
public $incrementing = false;

public static function boot() {
    parent::boot();
    static::creating(fn($model) => $model->id = (string) Str::uuid());
}
```

### Réponses API

Le module utilise `ApiResponse` (helper global) pour les réponses standardisées :
```php
ApiResponse::success($data, $message)
ApiResponse::created($data, $message)
ApiResponse::notFound($entity, $statusCode)
ApiResponse::error($type, $code, $message, $statusCode)
```

### Multi-tenancy

Toutes les routes passent par :
```php
InitializeTenancyByDomain::class
PreventAccessFromCentralDomains::class
```

Le service provider (`ApplicationServiceProvider`) enregistre les routes tenant dans le contexte approprié.

### Suppression en cascade (Application)

La suppression d'une `Application` est gérée manuellement dans `ApplicationService::delete()` :
```
application.packs()         → delete()
application.offers          → offerModules() + prices() + offer → delete()
application                 → delete()
```

La suppression est bloquée (HTTP 409) si une `LicenseOffer` référence l'application.

### Format DataTables

Les méthodes `*Datatable` retournent le format standard :
```json
{
  "draw": 1,
  "recordsTotal": 100,
  "recordsFiltered": 10,
  "data": [...]
}
```

### Transactions DB

Les opérations critiques (création d'offres, approbation/rejet d'abonnements) utilisent `DB::transaction()` ou `DB::beginTransaction()` / `DB::commit()` / `DB::rollBack()`.
