Ajout documentation
This commit is contained in:
@@ -0,0 +1,667 @@
|
||||
# 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)
|
||||
Reference in New Issue
Block a user