668 lines
23 KiB
Markdown
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)
|