Files
backend/documentation/CDC_API_Backend.md
T
2026-07-04 22:47:55 +02:00

668 lines
23 KiB
Markdown

# Cahier des Charges — API & Base de Données
## Projet : Laverie Connectée — Backend
**Version :** 1.0
**Date :** 2026-06-27
**Stack principale :** Laravel 11, MySQL 8, Redis, Docker
---
## 1. Contexte & Objectifs
### 1.1 Contexte
Développement d'une API REST centrale servant :
- Une application mobile multi-plateforme (Flutter : Android / iOS / Web)
- Un back-office d'administration (Vue.js)
- Des automates de laverie connectés via passerelle LLDP
L'API est le **seul point d'entrée** pour toute la logique métier. Aucun client ne communique directement avec la base de données.
### 1.2 Objectifs
- Exposer des endpoints REST sécurisés (JWT) pour tous les clients
- Gérer les utilisateurs, l'authentification, les porte-monnaie et les paiements
- Orchestrer les communications avec les automates (commandes machines)
- Automatiser les notifications (cron, push, événementiel)
- Garantir la conformité RGPD sur toutes les données personnelles
---
## 2. Stack Technique
| Composant | Technologie | Version | Justification |
|-----------|-------------|---------|---------------|
| Framework | Laravel | 11.x | Robustesse, écosystème, ORM Eloquent |
| Base de données principale | MySQL | 8.0 | Transactions ACID, maturité |
| Cache & Queues | Redis | 7.x | Queues asynchrones, cache sessions |
| Auth | Laravel Sanctum | 4.x | JWT stateless multi-guard |
| ORM | Eloquent | (Laravel) | Relations, scopes, casting |
| Queues | Laravel Horizon | 5.x | Monitoring des workers Redis |
| Scheduler | Laravel Scheduler | (Laravel) | Cron jobs natifs |
| Notifications Push | Firebase FCM | HTTP v1 | Android + Web |
| Notifications Push | APNs (Apple) | HTTP/2 | iOS |
| Paiement | Abstraction PaymentProvider | — | Multi-partenaire |
| Tests | PHPUnit + Pest | — | TDD sur logique métier critique |
| Documentation API | Scramble (L5-Swagger) | — | OpenAPI 3.0 auto-générée |
| Conteneurisation | Docker + Docker Compose | — | Dev/staging/prod reproductibles |
| CI/CD | GitHub Actions | — | Tests + déploiement automatisé |
---
## 3. Architecture
### 3.1 Structure du projet Laravel
```
app/
├── Http/
│ ├── Controllers/
│ │ ├── Auth/
│ │ │ ├── UserAuthController.php
│ │ │ └── SupervisorAuthController.php
│ │ ├── User/
│ │ │ ├── WalletController.php
│ │ │ ├── BookingController.php
│ │ │ └── WashController.php
│ │ ├── Supervisor/
│ │ │ ├── DashboardController.php
│ │ │ ├── MachineController.php
│ │ │ └── PricingController.php
│ │ └── Webhook/
│ │ └── PaymentWebhookController.php
│ ├── Middleware/
│ │ ├── EnsureEmailVerified.php
│ │ ├── CheckWalletBalance.php
│ │ └── RateLimitPayment.php
│ └── Requests/ # Form Requests (validation)
├── Models/
│ ├── User.php
│ ├── Supervisor.php
│ ├── Establishment.php
│ ├── Machine.php
│ ├── Booking.php
│ ├── Wash.php
│ ├── WalletTransaction.php
│ ├── PricingRule.php
│ ├── Promotion.php
│ ├── Notification.php
│ └── GdprConsent.php
├── Services/
│ ├── WalletService.php # Logique porte-monnaie (atomique)
│ ├── BookingService.php # Réservation + débit auto no-show
│ ├── MachineCommandService.php # Abstraction commandes LLDP
│ ├── NotificationService.php # FCM + APNs
│ ├── Payment/
│ │ ├── PaymentProviderInterface.php
│ │ ├── StripeProvider.php
│ │ └── LydiProvider.php # Exemple partenaire alternatif
│ └── PricingService.php # Calcul tarif dynamique
├── Jobs/
│ ├── SendPushNotificationJob.php
│ ├── DebitNoShowBookingJob.php
│ └── SyncMachineStatusJob.php
├── Events/ & Listeners/
├── Policies/ # Autorisation par modèle
└── Console/Commands/ # Artisan custom
```
### 3.2 Guards d'authentification
```
sanctum guards :
├── user → token scope [user] → app mobile
├── supervisor → token scope [supervisor] → back-office
└── machine → token scope [machine] → automates (optionnel)
```
### 3.3 Flux de données principal
```
App Flutter → HTTPS → API Laravel → MySQL
Redis Queue → Job Workers
FCM / APNs / LLDP Gateway
```
---
## 4. Base de Données — Schéma Complet
### 4.1 Table `users`
```sql
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
uuid CHAR(36) UNIQUE NOT NULL, -- Identifiant public exposé dans l'API
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
phone VARCHAR(20) UNIQUE,
birthdate DATE,
email_verified_at TIMESTAMP NULL,
password VARCHAR(255) NOT NULL,
wallet_balance DECIMAL(10,2) NOT NULL DEFAULT 0.00,
referral_code VARCHAR(20) UNIQUE,
referred_by BIGINT UNSIGNED NULL REFERENCES users(id),
loyalty_points INT UNSIGNED NOT NULL DEFAULT 0,
fcm_token VARCHAR(255) NULL, -- Token push Android/Web
apns_token VARCHAR(255) NULL, -- Token push iOS
locale VARCHAR(10) DEFAULT 'fr',
is_active BOOLEAN DEFAULT TRUE,
anonymized_at TIMESTAMP NULL, -- RGPD : suppression logique
created_at TIMESTAMP,
updated_at TIMESTAMP,
deleted_at TIMESTAMP NULL -- Soft delete
);
```
### 4.2 Table `gdpr_consents`
```sql
CREATE TABLE gdpr_consents (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL REFERENCES users(id),
type ENUM('data_processing','marketing','analytics') NOT NULL,
accepted BOOLEAN NOT NULL,
ip_address VARCHAR(45),
user_agent TEXT,
accepted_at TIMESTAMP NOT NULL,
revoked_at TIMESTAMP NULL
);
```
### 4.3 Table `supervisors`
```sql
CREATE TABLE supervisors (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
establishment_id BIGINT UNSIGNED NOT NULL REFERENCES establishments(id),
name VARCHAR(200) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
role ENUM('admin','manager','viewer') DEFAULT 'manager',
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
```
### 4.4 Table `establishments`
```sql
CREATE TABLE establishments (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
uuid CHAR(36) UNIQUE NOT NULL,
name VARCHAR(200) NOT NULL,
address TEXT NOT NULL,
city VARCHAR(100),
zip_code VARCHAR(10),
latitude DECIMAL(10,8),
longitude DECIMAL(11,8),
timezone VARCHAR(50) DEFAULT 'Europe/Paris',
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
```
### 4.5 Table `machines`
```sql
CREATE TABLE machines (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
uuid CHAR(36) UNIQUE NOT NULL,
establishment_id BIGINT UNSIGNED NOT NULL REFERENCES establishments(id),
name VARCHAR(100) NOT NULL,
type ENUM('washer_small','washer_large','dryer_small','dryer_large') NOT NULL,
qr_code VARCHAR(255) UNIQUE NOT NULL,
lldp_device_id VARCHAR(100), -- Identifiant côté passerelle LLDP
status ENUM('available','running','reserved','maintenance','offline') DEFAULT 'available',
current_user_id BIGINT UNSIGNED NULL REFERENCES users(id),
cycle_started_at TIMESTAMP NULL,
cycle_ends_at TIMESTAMP NULL,
last_heartbeat TIMESTAMP NULL, -- Dernier ping de l'automate
created_at TIMESTAMP,
updated_at TIMESTAMP
);
```
### 4.6 Table `pricing_rules`
```sql
CREATE TABLE pricing_rules (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
machine_id BIGINT UNSIGNED NOT NULL REFERENCES machines(id),
day_type ENUM('weekday','weekend','holiday','all') DEFAULT 'all',
slot_start TIME NOT NULL,
slot_end TIME NOT NULL,
price DECIMAL(6,2) NOT NULL,
label VARCHAR(100), -- Ex: "Heure creuse", "Heure pleine"
requires_app BOOLEAN DEFAULT FALSE, -- Tarif exclusif app
created_at TIMESTAMP,
updated_at TIMESTAMP
);
```
### 4.7 Table `promotions`
```sql
CREATE TABLE promotions (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
establishment_id BIGINT UNSIGNED NOT NULL REFERENCES establishments(id),
machine_type ENUM('washer_small','washer_large','dryer_small','dryer_large','all') DEFAULT 'all',
discount_type ENUM('percent','fixed') NOT NULL,
discount_value DECIMAL(6,2) NOT NULL,
starts_at TIMESTAMP NOT NULL,
ends_at TIMESTAMP NOT NULL,
description TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
```
### 4.8 Table `bookings`
```sql
CREATE TABLE bookings (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
uuid CHAR(36) UNIQUE NOT NULL,
user_id BIGINT UNSIGNED NOT NULL REFERENCES users(id),
machine_id BIGINT UNSIGNED NOT NULL REFERENCES machines(id),
slot_start TIMESTAMP NOT NULL,
slot_end TIMESTAMP NOT NULL,
amount_reserved DECIMAL(6,2) NOT NULL, -- Montant débité au moment de la réservation
amount_penalty DECIMAL(6,2) DEFAULT 0.00, -- Pénalité no-show
status ENUM('pending','confirmed','active','completed','cancelled','no_show') DEFAULT 'pending',
cancelled_at TIMESTAMP NULL,
penalty_applied_at TIMESTAMP NULL,
created_at TIMESTAMP,
updated_at TIMESTAMP
);
```
### 4.9 Table `washes`
```sql
CREATE TABLE washes (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
uuid CHAR(36) UNIQUE NOT NULL,
user_id BIGINT UNSIGNED NOT NULL REFERENCES users(id),
machine_id BIGINT UNSIGNED NOT NULL REFERENCES machines(id),
booking_id BIGINT UNSIGNED NULL REFERENCES bookings(id),
trigger_method ENUM('qr_code','booking','terminal') NOT NULL,
started_at TIMESTAMP NOT NULL,
ended_at TIMESTAMP NULL,
duration_minutes INT UNSIGNED,
cost DECIMAL(6,2) NOT NULL,
pricing_rule_id BIGINT UNSIGNED NULL REFERENCES pricing_rules(id),
created_at TIMESTAMP,
updated_at TIMESTAMP
);
```
### 4.10 Table `wallet_transactions`
```sql
CREATE TABLE wallet_transactions (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
uuid CHAR(36) UNIQUE NOT NULL,
user_id BIGINT UNSIGNED NOT NULL REFERENCES users(id),
type ENUM('credit','debit') NOT NULL,
amount DECIMAL(10,2) NOT NULL,
balance_after DECIMAL(10,2) NOT NULL,
source ENUM('top_up','wash','booking','penalty','refund','referral_bonus') NOT NULL,
reference_id BIGINT UNSIGNED NULL, -- ID du wash ou booking associé
reference_type VARCHAR(100) NULL, -- Morph polymorphique
payment_provider VARCHAR(50) NULL,
payment_ref VARCHAR(255) NULL, -- Référence transaction externe
created_at TIMESTAMP
);
```
### 4.11 Table `push_notifications`
```sql
CREATE TABLE push_notifications (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NULL REFERENCES users(id), -- NULL = broadcast
type VARCHAR(100) NOT NULL,
title VARCHAR(255) NOT NULL,
body TEXT NOT NULL,
data JSON NULL,
channel ENUM('fcm','apns','both') DEFAULT 'both',
status ENUM('pending','sent','failed') DEFAULT 'pending',
sent_at TIMESTAMP NULL,
created_at TIMESTAMP
);
```
---
## 5. Endpoints API — Spécification Complète
### 5.1 Authentification utilisateur
| Méthode | Endpoint | Description | Auth |
|---------|----------|-------------|------|
| POST | `/api/v1/auth/register` | Inscription + collecte consentements RGPD | Public |
| POST | `/api/v1/auth/login` | Connexion, retourne access_token + refresh_token | Public |
| POST | `/api/v1/auth/refresh` | Renouvelle le token | Public |
| POST | `/api/v1/auth/logout` | Révoque le token | User |
| POST | `/api/v1/auth/forgot-password` | Envoi email reset | Public |
| POST | `/api/v1/auth/reset-password` | Réinitialisation | Public |
| GET | `/api/v1/auth/verify-email/{token}` | Vérification email | Public |
**Payload register :**
```json
{
"first_name": "Jean",
"last_name": "Dupont",
"email": "jean@example.com",
"phone": "+33612345678",
"password": "••••••••",
"birthdate": "1990-01-15",
"referral_code": "ABC123",
"consents": {
"data_processing": true,
"marketing": false,
"analytics": true
}
}
```
**Réponse login :**
```json
{
"access_token": "eyJ...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "def50200...",
"user": {
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"first_name": "Jean",
"wallet_balance": "12.50"
}
}
```
### 5.2 Porte-monnaie
| Méthode | Endpoint | Description | Auth |
|---------|----------|-------------|------|
| GET | `/api/v1/wallet` | Solde actuel | User |
| GET | `/api/v1/wallet/transactions` | Historique paginé | User |
| POST | `/api/v1/wallet/top-up/initiate` | Initier rechargement | User |
| POST | `/api/v1/wallet/top-up/confirm` | Confirmer après paiement | User |
| GET | `/api/v1/wallet/top-up/providers` | Liste partenaires paiement disponibles | User |
**Payload top-up initiate :**
```json
{
"amount": 20.00,
"provider": "stripe",
"return_url": "laverie://wallet/topup-result"
}
```
**Réponse :**
```json
{
"payment_intent_id": "pi_xxx",
"redirect_url": "https://checkout.stripe.com/...",
"expires_at": "2026-06-27T15:30:00Z"
}
```
### 5.3 Établissements & Machines
| Méthode | Endpoint | Description | Auth |
|---------|----------|-------------|------|
| GET | `/api/v1/establishments` | Liste avec coordonnées GPS | User |
| GET | `/api/v1/establishments/{uuid}` | Détail + machines + disponibilités | User |
| GET | `/api/v1/machines/{uuid}` | Détail machine + tarif actuel | User |
| GET | `/api/v1/machines/{uuid}/availability` | Créneaux disponibles sur N jours | User |
| GET | `/api/v1/machines/{uuid}/pricing` | Grille tarifaire active | User |
| POST | `/api/v1/machines/{uuid}/heartbeat` | Ping de l'automate (machine scope) | Machine |
### 5.4 Réservations
| Méthode | Endpoint | Description | Auth |
|---------|----------|-------------|------|
| POST | `/api/v1/bookings` | Créer une réservation | User |
| GET | `/api/v1/bookings` | Mes réservations | User |
| GET | `/api/v1/bookings/{uuid}` | Détail réservation | User |
| PATCH | `/api/v1/bookings/{uuid}/cancel` | Annuler | User |
| PATCH | `/api/v1/bookings/{uuid}/move` | Déplacer le créneau | User |
**Payload POST /bookings :**
```json
{
"machine_uuid": "550e8400-...",
"slot_start": "2026-06-28T09:00:00Z",
"slot_end": "2026-06-28T10:00:00Z"
}
```
**Règles métier :**
- Vérification atomique du solde (mutex Redis sur `wallet:user:{id}`)
- Débit immédiat du montant + supplément réservation (1€)
- Si annulation > 2h avant : remboursement intégral
- Si annulation < 2h ou no-show : pénalité définie par `machine.no_show_penalty`
- Cron toutes les 5 minutes : détection no-show → `DebitNoShowBookingJob`
### 5.5 Lavages
| Méthode | Endpoint | Description | Auth |
|---------|----------|-------------|------|
| POST | `/api/v1/washes/start` | Démarrer via QR code | User |
| GET | `/api/v1/washes/{uuid}` | Statut en cours | User |
| GET | `/api/v1/washes` | Historique | User |
**Payload POST /washes/start :**
```json
{
"qr_code": "MACHINE_QR_ABC123",
"program": "60C_standard"
}
```
**Flux :**
1. Valider le QR → identifier la machine
2. Vérifier statut machine (`available`)
3. Vérifier solde suffisant
4. Calculer prix (tarif actuel + promo éventuelle)
5. Débit atomique du porte-monnaie
6. Envoyer commande à la passerelle LLDP via `MachineCommandService`
7. Mettre à jour le statut machine (`running`)
8. Retourner confirmation + durée estimée
### 5.6 Authentification superviseur
| Méthode | Endpoint | Description | Auth |
|---------|----------|-------------|------|
| POST | `/api/v1/supervisor/auth/login` | Connexion superviseur | Public |
| POST | `/api/v1/supervisor/auth/logout` | Déconnexion | Supervisor |
| GET | `/api/v1/supervisor/dashboard` | Statistiques + alertes | Supervisor |
| GET | `/api/v1/supervisor/machines` | Parc machines de l'établissement | Supervisor |
| PATCH | `/api/v1/supervisor/machines/{uuid}` | Mise à jour machine | Supervisor |
| GET | `/api/v1/supervisor/pricing` | Grille tarifaire | Supervisor |
| POST | `/api/v1/supervisor/pricing` | Créer règle tarifaire | Supervisor |
| PUT | `/api/v1/supervisor/pricing/{id}` | Modifier règle | Supervisor |
| DELETE | `/api/v1/supervisor/pricing/{id}` | Supprimer règle | Supervisor |
| GET | `/api/v1/supervisor/promotions` | Promotions actives | Supervisor |
| POST | `/api/v1/supervisor/promotions` | Créer promotion | Supervisor |
---
## 6. Services Métier Critiques
### 6.1 WalletService — Opérations atomiques
Toute opération sur le porte-monnaie passe par ce service. Les débits/crédits sont encapsulés dans des transactions MySQL + verrou Redis pour éviter les race conditions.
```
WalletService::credit(user, amount, source, referenceId)
WalletService::debit(user, amount, source, referenceId) ← lance WalletInsufficientFundsException si insuffisant
WalletService::reserve(user, amount, bookingId) ← débit préventif réservation
WalletService::refund(user, walletTransactionId)
```
**Règle absolue :** Aucun controller ne doit modifier `users.wallet_balance` directement. Tout passe par `WalletService`.
### 6.2 PricingService — Tarif dynamique
```
PricingService::getCurrentPrice(machine, datetime)
→ Cherche la PricingRule active pour (machine_id, day_type, heure)
→ Applique Promotion active si existante
→ Retourne { base_price, discount, final_price, label, requires_app }
```
**Priorité des règles :** Règle spécifique machine > Règle par type machine > Tarif défaut établissement.
### 6.3 MachineCommandService — Abstraction LLDP
```
MachineCommandService::startWash(machine, program)
MachineCommandService::stopWash(machine)
MachineCommandService::getStatus(machine)
```
L'implémentation concrète (`LldpGatewayAdapter`) communique avec la passerelle LLDP via HTTP interne. En cas d'indisponibilité de la passerelle, la commande est mise en queue Redis et retentée 3 fois (backoff exponentiel).
### 6.4 PaymentProviderInterface
```php
interface PaymentProviderInterface {
public function initiateTopUp(User $user, float $amount, string $returnUrl): TopUpIntent;
public function confirmTopUp(string $paymentRef): TopUpConfirmation;
public function handleWebhook(Request $request): void;
}
```
Implémentations : `StripeProvider`, `LydiProvider`, `SumUpProvider` (extensible).
---
## 7. Tâches Planifiées (Cron)
| Fréquence | Job | Description |
|-----------|-----|-------------|
| Toutes les 5 min | `CheckNoShowBookings` | Détecter et débiter les no-shows |
| Toutes les 5 min | `SyncMachineStatuses` | Interroger la passerelle LLDP |
| Toutes les heures | `SendScheduledNotifications` | Notifications planifiées en attente |
| Chaque jour 02:00 | `GenerateDailyStats` | Agrégation statistiques pour dashboard |
| Chaque jour 03:00 | `CleanExpiredTokens` | Purge tokens révoqués |
| Chaque semaine | `AnonymizeInactiveUsers` | RGPD : users inactifs > 3 ans |
---
## 8. Notifications — Architecture
### 8.1 Déclencheurs
| Événement | Canal | Timing |
|-----------|-------|--------|
| Rechargement confirmé | Push | Immédiat |
| Réservation confirmée | Push | Immédiat |
| Rappel créneau | Push | 30 min avant |
| Lavage terminé | Push | Immédiat |
| Solde faible (< 5€) | Push | À chaque débit |
| Promotion disponible | Push | Planifié par superviseur |
| No-show détecté | Push | À la détection |
### 8.2 Implémentation
- Les notifications sont toujours créées en base (`push_notifications`) avant envoi
- L'envoi passe par un Job Redis (`SendPushNotificationJob`) → résilience
- FCM HTTP v1 pour Android/Web, APNs HTTP/2 pour iOS
- En cas d'échec, 3 retries avec backoff (1min, 5min, 15min)
---
## 9. Sécurité
### 9.1 Authentification
- Tokens JWT via Laravel Sanctum (stateless)
- Refresh token rotation (invalidation à chaque renouvellement)
- Rate limiting : 5 tentatives de login / 10 minutes / IP
- Rate limiting paiements : 3 initiations / minute / user
### 9.2 Données
- Mots de passe hashés : `bcrypt` (cost 12)
- Données sensibles (IBAN, tokens paiement) : chiffrées `AES-256-CBC` en base
- HTTPS obligatoire (TLS 1.3 minimum)
- UUID exposés dans l'API (jamais les auto-increment IDs)
- Soft deletes sur `users` — anonymisation RGPD séparée
### 9.3 API
- Validation stricte de tous les inputs (`FormRequest`)
- Politique CORS restrictive (origines whitelist)
- Headers sécurité : `Strict-Transport-Security`, `X-Frame-Options`, `Content-Security-Policy`
---
## 10. Tests
| Type | Outil | Couverture cible |
|------|-------|-----------------|
| Tests unitaires | Pest | WalletService, PricingService, BookingService |
| Tests d'intégration | Pest + SQLite in-memory | Tous les endpoints API |
| Tests de contrats | — | Webhooks paiement |
| Tests de charge | k6 | Endpoints critiques (start wash, top-up) |
**Cas critiques à tester :**
- Race condition débit simultané du porte-monnaie
- Timeout passerelle LLDP → comportement de la queue
- Webhook paiement reçu en doublon (idempotence)
- No-show avec annulation simultanée
---
## 11. Variables d'environnement requises
```env
APP_ENV=production
APP_KEY=base64:...
DB_HOST=
DB_DATABASE=laverie
DB_USERNAME=
DB_PASSWORD=
REDIS_HOST=
REDIS_PASSWORD=
# Paiement
STRIPE_SECRET_KEY=
STRIPE_WEBHOOK_SECRET=
# Notifications
FIREBASE_PROJECT_ID=
FIREBASE_PRIVATE_KEY=
FIREBASE_CLIENT_EMAIL=
APNS_KEY_ID=
APNS_TEAM_ID=
APNS_PRIVATE_KEY_PATH=
# LLDP Gateway
LLDP_GATEWAY_URL=
LLDP_GATEWAY_TOKEN=
# URLs
FRONTEND_URL=
APP_URL=
```
---
## 12. Livrables attendus
- [ ] Code source Laravel complet (GitHub)
- [ ] Migrations SQL versionnées
- [ ] Seeders (données de démo)
- [ ] Documentation OpenAPI 3.0 auto-générée (`/api/documentation`)
- [ ] Collection Postman exportée
- [ ] `docker-compose.yml` pour dev local
- [ ] `.env.example` documenté
- [ ] README avec instructions d'installation
- [ ] Suite de tests (couverture > 80% sur les services critiques)