feat: Version 3.6.2 - Correctifs tâches #17-20

- #17: Amélioration gestion des secteurs et statistiques
- #18: Optimisation services API et logs
- #19: Corrections Flutter widgets et repositories
- #20: Fix création passage - détection automatique ope_users.id vs users.id

Suppression dossier web/ (migration vers app Flutter)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-16 14:11:15 +01:00
parent 7b78037175
commit 232940b1eb
196 changed files with 8483 additions and 7966 deletions

View File

@@ -278,55 +278,102 @@ Log::info('Stripe account activated', [
## 📱 FLOW TAP TO PAY (Application Flutter)
### 🎯 Architecture technique
Le flow Tap to Pay repose sur trois composants principaux :
1. **DeviceInfoService** - Vérification de compatibilité hardware (Android SDK ≥ 28, NFC disponible)
2. **StripeTapToPayService** - Gestion du SDK Stripe Terminal et des paiements
3. **Backend API** - Endpoints PHP pour les tokens de connexion et PaymentIntents
### 🔄 Diagramme de séquence complet
```
┌─────────────┐ ┌─────────────┐ ┌──────────┐ ┌─────────┐
│ App Flutter │ │ API PHP │ │ Stripe │ │ Carte
└──────┬──────┘ └──────┬──────┘ └────┬─────┘ └────┬────┘
│ │ │ │
[1] │ Validation form │ │
│ + montant CB
│ │
[2] │ POST/PUT passage │ │
│──────────────────>│ │ │
│ │
[3] │<──────────────────│ │ │
Passage ID: 456
[4] │ POST create-intent│ │
──────────────────>│ (avec passage_id: 456)
│ │
[5] │ │ Create PaymentIntent
│─────────────────>│
│ │
[6] │ │<─────────────────│
│ pi_xxx + secret │ │
[7] │<──────────────────│ │
PaymentIntent ID
│ │
[8] │ SDK Terminal Init │ │
"Approchez carte"
│ │ │
[9] │<──────────────────────────────────────────────────────│
NFC : Lecture carte sans contact
[10] │ Process Payment │ │ │
───────────────────────────────────>│
│ │
[11] │<───────────────────────────────────│
Payment Success
│ │
[12] │ POST confirm
──────────────────>│ │
│ │
[13] │ PUT passage/456
──────────────────>│ (ajout stripe_payment_id)
│ │
[14] │<──────────────────│ │
Passage updated │ │ │
│ │
┌─────────────┐ ┌─────────────────┐ ┌──────────┐ ┌─────────┐ ┌──────────────┐
│ App Flutter │ │ DeviceInfo │ │ API │ │ Stripe │ │ SDK Terminal
│ │ Service │ │ PHP │ │ │ │ │
└──────┬──────┘ └────────┬────────┘ └────┬─────┘ └────┬─────┘ └──────┬───────┘
│ │ │ │
[1] │ Login utilisateur │
────────────────────>│
│ │ │ │
[2] │ │ checkStripeCertification()
│ • Android SDK ≥ 28
│ │ • NFC disponible
│ │
[3] │<────────────────────│
│ ✅ Compatible │ │ │
│ │ │
[4] │ Validation form
│ + montant CB │ │ │
│ │ │
[5] │ POST/PUT passage
│────────────────────────────────────────>│ │
│ │
[6] │<────────────────────────────────────────
│ Passage ID: 456 │ │ │
│ │
[7] │ initialize()
│────────────────────────────────────────────────────────────────────────────>
│ │
[8] │ │ │ Terminal.initTerminal()
│ │ │ │ │ (fetchToken callback)
│ │ │
[9] │ │ POST /terminal/connection-token
│────────────────────────────────────────>│
{amicale_id, stripe_account, location_id} │
│ │
[10] │ │ │ CreateConnectionToken
│───────────────>│
│ │
[11] │ │<───────────────│
│ │ {secret: "..."}│
│ │
[12] │<────────────────────────────────────────
Connection Token │ │ │
│ │
[13] │────────────────────────────────────────────────────────────────────────────>
Token delivered to SDK │ │ ✅ SDK Ready │
│ │
[14] │ createPaymentIntent() │ │ │
│────────────────────────────────────────>│ │ │
│ {amount, passage_id, amicale_id} │ │ │
│ │ │ │ │
[15] │ │ │ Create PaymentIntent │
│ │ │───────────────>│ │
│ │ │ │ │
[16] │ │ │<───────────────│ │
│ │ │ pi_xxx + secret│ │
│ │ │ │ │
[17] │<────────────────────────────────────────│ │ │
│ PaymentIntent ID │ │ │ │
│ │ │ │ │
[18] │ collectPayment() │ │ │ │
│────────────────────────────────────────────────────────────────────────────>│
│ │ │ │ │
[19] │ │ │ │ discoverReaders()
│ │ │ │ + connectReader()
│ │ │ │ │
[20] │ │ │ │ collectPaymentMethod()
│ 💳 "Présentez la carte" │ │ ⏳ Attente NFC │
│ │ │ │ │
[21] │<──────────────────────────────────────────────────────────── 📱 TAP CARTE │
│ │ │ │ │
[22] │ confirmPayment() │ │ │ │
│────────────────────────────────────────────────────────────────────────────>│
│ │ │ │ │
[23] │ │ │ │ confirmPaymentIntent()
│ │ │ │ │
[24] │ │ │ │ ✅ Succeeded │
│<────────────────────────────────────────────────────────────────────────────│
│ Payment Success │ │ │ │
│ │ │ │ │
[25] │ PUT passage/456 │ │ │ │
│────────────────────────────────────────>│ │ │
│ {stripe_payment_id: "pi_xxx"} │ │ │
│ │ │ │ │
[26] │<────────────────────────────────────────│ │ │
│ ✅ Passage updated │ │ │ │
```
### 🎮 Gestion du Terminal de Paiement
@@ -378,6 +425,59 @@ Le terminal de paiement reste affiché jusqu'à la réponse définitive de Strip
- **Persistence** : Le terminal reste ouvert jusqu'à réponse définitive de Stripe
- **Gestion d'erreur** : Possibilité de réessayer sans perdre le contexte
### 🔑 Connection Token - Flow détaillé
Le Connection Token est crucial pour le SDK Stripe Terminal. Il est récupéré via un callback au moment de l'initialisation.
**Code côté App (stripe_tap_to_pay_service.dart:87-89) :**
```dart
await Terminal.initTerminal(
fetchToken: _fetchConnectionToken, // Callback appelé automatiquement
);
```
**Callback de récupération (lignes 137-161) :**
```dart
Future<String> _fetchConnectionToken() async {
debugPrint('🔑 Récupération du token de connexion Stripe...');
final response = await ApiService.instance.post(
'/stripe/terminal/connection-token',
data: {
'amicale_id': CurrentAmicaleService.instance.amicaleId,
'stripe_account': _stripeAccountId,
'location_id': _locationId,
}
);
final token = response.data['secret'];
if (token == null || token.isEmpty) {
throw Exception('Token de connexion invalide');
}
debugPrint('✅ Token de connexion récupéré');
return token;
}
```
**Backend PHP :**
```php
// POST /stripe/terminal/connection-token
$token = \Stripe\Terminal\ConnectionToken::create([], [
'stripe_account' => $amicale->stripe_id,
]);
return response()->json([
'secret' => $token->secret,
]);
```
**Points importants :**
- ✅ Le token est **temporaire** (valide quelques minutes)
- ✅ Un nouveau token est créé à **chaque initialisation** du SDK
- ✅ Le token est spécifique au **compte Stripe Connect** de l'amicale
- ✅ Utilisé pour **authentifier** le Terminal SDK auprès de Stripe
### 📋 Détail des étapes
#### Étape 1 : VALIDATION DU FORMULAIRE
@@ -1085,28 +1185,168 @@ POST/PUT /api/passages
## 🔄 GESTION DES ERREURS
### 📱 Erreurs Tap to Pay
### 📱 Erreurs Tap to Pay - Messages utilisateur clairs
| Code erreur | Description | Action utilisateur |
|-------------|-------------|-------------------|
| `device_not_compatible` | iPhone non compatible | Afficher message explicatif |
| `nfc_disabled` | NFC désactivé | Demander activation dans réglages |
| `card_declined` | Carte refusée | Essayer autre carte |
| `insufficient_funds` | Solde insuffisant | Essayer autre carte |
| `network_error` | Erreur réseau | Réessayer ou mode offline |
| `timeout` | Timeout lecture carte | Rapprocher carte et réessayer |
L'application détecte automatiquement le type d'erreur et affiche un message adapté :
### 🔄 Flow de retry
#### Gestion intelligente des erreurs (passage_form_dialog.dart)
```dart
catch (e) {
final errorMsg = e.toString().toLowerCase();
String userMessage;
bool shouldCancelPayment = true;
if (errorMsg.contains('canceled') || errorMsg.contains('cancelled')) {
// Annulation volontaire
userMessage = 'Paiement annulé';
} else if (errorMsg.contains('cardreadtimedout') || errorMsg.contains('timed out')) {
// Timeout NFC avec conseils
userMessage = 'Lecture de la carte impossible.\n\n'
'Conseils :\n'
'• Maintenez la carte contre le dos du téléphone\n'
'• Ne bougez pas jusqu\'à confirmation\n'
'• Retirez la coque si nécessaire\n'
'• Essayez différentes positions sur le téléphone';
} else if (errorMsg.contains('already') && errorMsg.contains('payment')) {
// PaymentIntent existe déjà
userMessage = 'Un paiement est déjà en cours pour ce passage.\n'
'Veuillez réessayer dans quelques instants.';
shouldCancelPayment = false;
}
// Annulation automatique du PaymentIntent pour permettre nouvelle tentative
if (shouldCancelPayment && _paymentIntentId != null) {
await StripeTapToPayService.instance.cancelPayment(_paymentIntentId!);
}
}
```
#### Table des erreurs et actions
| Type erreur | Message utilisateur | Action automatique |
|-------------|--------------------|--------------------|
| `canceled` / `cancelled` | "Paiement annulé" | Annulation PaymentIntent ✅ |
| `cardReadTimedOut` | Message avec 4 conseils NFC | Annulation PaymentIntent ✅ |
| `already payment` | "Paiement déjà en cours" | Pas d'annulation ⏳ |
| `device_not_compatible` | "Appareil non compatible" | Annulation PaymentIntent ✅ |
| `nfc_disabled` | "NFC désactivé" | Annulation PaymentIntent ✅ |
| Autre erreur | Message technique complet | Annulation PaymentIntent ✅ |
### ⚠️ Contraintes NFC - Tap to Pay vs Google Pay
**Différence fondamentale :**
- **Google Pay (émission)** : Le téléphone *émet* un signal NFC puissant → fonctionne avec coque
- **Tap to Pay (réception)** : Le téléphone *lit* le signal de la carte → très sensible aux interférences
#### Coques problématiques
-**Kevlar / Carbone** : Fibres conductrices perturbent la réception NFC
-**Métal** : Bloque complètement les ondes NFC
-**Coque épaisse** : Réduit la portée effective
-**TPU / Silicone** : Compatible
#### Bonnes pratiques pour réussite NFC
**Position optimale :**
```
┌─────────────────┐
│ 📱 Téléphone │
│ │
│ [Capteur NFC]│ ← Généralement vers le haut du dos
│ │
│ │
│ 💳 Carte ici │ ← Maintenir immobile 3-5 secondes
└─────────────────┘
```
**Checklist utilisateur :**
1. ✅ Retirer la coque si échec
2. ✅ Carte à plat contre le dos du téléphone
3. ✅ Ne pas bouger pendant toute la lecture
4. ✅ Essayer différentes positions (haut/milieu du téléphone)
5. ✅ Carte sans contact activée (logo sans contact visible)
### 🔄 Flow de retry automatique
```
1. Erreur détectée
2. Message utilisateur explicite
3. Option "Réessayer" proposée
4. Conservation du montant et contexte
5. Nouveau PaymentIntent si nécessaire
6. Maximum 3 tentatives
1. Erreur détectée → Analyse du type
2. Annulation automatique PaymentIntent (si applicable)
3. Message clair avec conseils contextuels
4. Bouton "Réessayer" disponible
5. Nouveau PaymentIntent créé automatiquement
6. Conservation du contexte (montant, passage)
```
**Avantages :**
- ✅ Pas de blocage "PaymentIntent déjà existant"
- ✅ Nombre illimité de tentatives
- ✅ Contexte préservé (pas besoin de tout ressaisir)
- ✅ Messages orientés solution plutôt qu'erreur technique
### 🏗️ Environnement et Build Release
#### Détection automatique de l'environnement
L'application détecte l'environnement via l'URL de l'API (plus fiable que `kDebugMode`) :
```dart
// stripe_tap_to_pay_service.dart (lignes 236-252)
Future<bool> _ensureReaderConnected() async {
// Détection via URL API
final apiUrl = ApiService.instance.baseUrl;
final isProduction = apiUrl.contains('app3.geosector.fr');
final isSimulated = !isProduction;
final config = TapToPayDiscoveryConfiguration(
isSimulated: isSimulated,
);
debugPrint('🔧 Environnement: ${isProduction ? "PRODUCTION (réel)" : "DEV/REC (simulé)"}');
debugPrint('🔧 isSimulated: $isSimulated');
}
```
**Mapping environnement :**
| URL API | Environnement | Reader Stripe | Cartes acceptées |
|---------|---------------|---------------|------------------|
| `dapp.geosector.fr` | DEV | Simulé | Cartes test uniquement |
| `rapp.geosector.fr` | REC | Simulé | Cartes test uniquement |
| `app3.geosector.fr` | PROD | Réel | Cartes réelles uniquement |
#### ⚠️ Restriction Stripe - Build Release obligatoire en PROD
**Erreur si app debuggable en PROD :**
```
Debuggable applications are not supported when using the production
version of the Tap to Pay reader. Please use a simulated version of
the reader by setting TapToPayDiscoveryConfiguration.isSimulated to true.
```
**Solution - Build release :**
```bash
# Build APK optimisé pour production
flutter build apk --release
# Installation sur device
adb install build/app/outputs/flutter-apk/app-release.apk
```
**Différences debug vs release :**
| Aspect | Debug (`flutter run`) | Release (`flutter build`) |
|--------|-----------------------|--------------------|
| **Optimisation** | ❌ Code non optimisé | ✅ R8/ProGuard activé |
| **Taille APK** | ~200 MB | ~30-50 MB |
| **Performance** | Lente (dev mode) | Rapide (optimisée) |
| **Tap to Pay PROD** | ❌ Refusé par Stripe | ✅ Accepté |
| **Débogage** | ✅ Hot reload, logs | ❌ Pas de hot reload |
**Pourquoi Stripe refuse les apps debug :**
- **Sécurité renforcée** : Les apps debuggables peuvent être inspectées
- **Conformité PCI-DSS** : Exigences de sécurité pour paiements réels
- **Protection production** : Éviter utilisation accidentelle de readers réels en dev
---
## 📊 MONITORING ET LOGS