# 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)