23 KiB
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
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
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
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
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
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
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
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
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
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
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
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 :
{
"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 :
{
"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 :
{
"amount": 20.00,
"provider": "stripe",
"return_url": "laverie://wallet/topup-result"
}
Réponse :
{
"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 :
{
"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 :
{
"qr_code": "MACHINE_QR_ABC123",
"program": "60C_standard"
}
Flux :
- Valider le QR → identifier la machine
- Vérifier statut machine (
available) - Vérifier solde suffisant
- Calculer prix (tarif actuel + promo éventuelle)
- Débit atomique du porte-monnaie
- Envoyer commande à la passerelle LLDP via
MachineCommandService - Mettre à jour le statut machine (
running) - 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
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-CBCen 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
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.ymlpour dev local.env.exampledocumenté- README avec instructions d'installation
- Suite de tests (couverture > 80% sur les services critiques)