Refactoring

This commit is contained in:
DahoudG
2025-09-17 17:54:06 +00:00
parent 12d514d866
commit 63fe107f98
165 changed files with 54220 additions and 276 deletions

View File

@@ -13,10 +13,10 @@ import 'package:dio/dio.dart';
@singleton
class KeycloakWebViewAuthService {
static const String _keycloakBaseUrl = 'http://192.168.1.145:8180';
static const String _keycloakBaseUrl = 'http://192.168.1.11:8180';
static const String _realm = 'unionflow';
static const String _clientId = 'unionflow-mobile';
static const String _redirectUrl = 'http://192.168.1.145:8080/auth/callback';
static const String _redirectUrl = 'http://192.168.1.11:8080/auth/callback';
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
final Dio _dio = Dio();

View File

@@ -1,6 +1,6 @@
class AppConstants {
// API Configuration
static const String baseUrl = 'http://192.168.1.145:8080'; // Backend UnionFlow
static const String baseUrl = 'http://192.168.1.11:8080'; // Backend UnionFlow
static const String apiVersion = '/api';
// Timeout

View File

@@ -19,7 +19,7 @@ class DioClient {
void _configureOptions() {
_dio.options = BaseOptions(
// URL de base de l'API
baseUrl: 'http://192.168.1.145:8080', // Adresse de votre API Quarkus
baseUrl: 'http://192.168.1.11:8080', // Adresse de votre API Quarkus
// Timeouts
connectTimeout: const Duration(seconds: 30),

View File

@@ -0,0 +1,338 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
/// Service d'optimisation des performances pour l'application UnionFlow
///
/// Fournit des utilitaires pour :
/// - Optimisation des widgets
/// - Gestion de la mémoire
/// - Mise en cache intelligente
/// - Monitoring des performances
class PerformanceOptimizer {
static const String _tag = 'PerformanceOptimizer';
/// Singleton instance
static final PerformanceOptimizer _instance = PerformanceOptimizer._internal();
factory PerformanceOptimizer() => _instance;
PerformanceOptimizer._internal();
/// Cache pour les widgets optimisés
final Map<String, Widget> _widgetCache = {};
/// Cache pour les images
final Map<String, ImageProvider> _imageCache = {};
/// Compteurs de performance
final Map<String, int> _performanceCounters = {};
/// Temps de début pour les mesures
final Map<String, DateTime> _performanceTimers = {};
// ========================================
// OPTIMISATION DES WIDGETS
// ========================================
/// Optimise un widget avec RepaintBoundary si nécessaire
static Widget optimizeWidget(Widget child, {
String? key,
bool forceRepaintBoundary = false,
bool addSemantics = true,
}) {
Widget optimized = child;
// Ajouter RepaintBoundary pour les widgets complexes
if (forceRepaintBoundary || _shouldAddRepaintBoundary(child)) {
optimized = RepaintBoundary(
key: key != null ? Key('repaint_$key') : null,
child: optimized,
);
}
// Ajouter Semantics pour l'accessibilité
if (addSemantics && _shouldAddSemantics(child)) {
optimized = Semantics(
key: key != null ? Key('semantics_$key') : null,
child: optimized,
);
}
return optimized;
}
/// Détermine si un RepaintBoundary est nécessaire
static bool _shouldAddRepaintBoundary(Widget widget) {
// Ajouter RepaintBoundary pour les widgets qui changent fréquemment
return widget is AnimatedWidget ||
widget is CustomPaint ||
widget is Image ||
widget.runtimeType.toString().contains('Chart') ||
widget.runtimeType.toString().contains('Graph');
}
/// Détermine si Semantics est nécessaire
static bool _shouldAddSemantics(Widget widget) {
return widget is GestureDetector ||
widget is InkWell ||
widget is ElevatedButton ||
widget is TextButton ||
widget is IconButton;
}
/// Crée un widget avec mise en cache
Widget cachedWidget(String key, Widget Function() builder) {
if (_widgetCache.containsKey(key)) {
return _widgetCache[key]!;
}
final widget = builder();
_widgetCache[key] = widget;
return widget;
}
/// Nettoie le cache des widgets
void clearWidgetCache() {
_widgetCache.clear();
debugPrint('$_tag: Widget cache cleared');
}
// ========================================
// OPTIMISATION DES IMAGES
// ========================================
/// Optimise le chargement d'une image
static ImageProvider optimizeImage(String path, {
double? width,
double? height,
BoxFit fit = BoxFit.cover,
}) {
// Utiliser ResizeImage pour optimiser la mémoire
if (width != null || height != null) {
return ResizeImage(
AssetImage(path),
width: width?.round(),
height: height?.round(),
);
}
return AssetImage(path);
}
/// Met en cache une image
ImageProvider cachedImage(String key, String path) {
if (_imageCache.containsKey(key)) {
return _imageCache[key]!;
}
final image = AssetImage(path);
_imageCache[key] = image;
return image;
}
/// Précharge les images critiques
static Future<void> preloadCriticalImages(BuildContext context, List<String> imagePaths) async {
final futures = imagePaths.map((path) =>
precacheImage(AssetImage(path), context)
).toList();
await Future.wait(futures);
debugPrint('$_tag: ${imagePaths.length} critical images preloaded');
}
// ========================================
// MONITORING DES PERFORMANCES
// ========================================
/// Démarre un timer de performance
void startTimer(String operation) {
_performanceTimers[operation] = DateTime.now();
}
/// Arrête un timer et log le résultat
void stopTimer(String operation) {
final startTime = _performanceTimers[operation];
if (startTime != null) {
final duration = DateTime.now().difference(startTime);
debugPrint('$_tag: $operation took ${duration.inMilliseconds}ms');
_performanceTimers.remove(operation);
// Incrémenter le compteur
_performanceCounters[operation] = (_performanceCounters[operation] ?? 0) + 1;
}
}
/// Incrémente un compteur de performance
void incrementCounter(String metric) {
_performanceCounters[metric] = (_performanceCounters[metric] ?? 0) + 1;
}
/// Obtient les statistiques de performance
Map<String, int> getPerformanceStats() {
return Map.from(_performanceCounters);
}
/// Réinitialise les statistiques
void resetStats() {
_performanceCounters.clear();
_performanceTimers.clear();
debugPrint('$_tag: Performance stats reset');
}
// ========================================
// OPTIMISATION MÉMOIRE
// ========================================
/// Force le garbage collection (debug uniquement)
static void forceGarbageCollection() {
if (kDebugMode) {
// Forcer le GC en créant et supprimant des objets
final temp = List.generate(1000, (i) => Object());
temp.clear();
debugPrint('PerformanceOptimizer: Forced garbage collection');
}
}
/// Nettoie tous les caches
void clearAllCaches() {
clearWidgetCache();
_imageCache.clear();
debugPrint('$_tag: All caches cleared');
}
/// Obtient la taille des caches
Map<String, int> getCacheSizes() {
return {
'widgets': _widgetCache.length,
'images': _imageCache.length,
};
}
// ========================================
// OPTIMISATION DES ANIMATIONS
// ========================================
/// Crée un AnimationController optimisé
static AnimationController createOptimizedController({
required Duration duration,
required TickerProvider vsync,
double? value,
Duration? reverseDuration,
String? debugLabel,
}) {
return AnimationController(
duration: duration,
reverseDuration: reverseDuration,
vsync: vsync,
value: value,
debugLabel: debugLabel ?? 'OptimizedController',
);
}
/// Dispose proprement une liste d'AnimationControllers
static void disposeControllers(List<AnimationController> controllers) {
for (final controller in controllers) {
try {
controller.dispose();
} catch (e) {
// Controller déjà disposé, ignorer l'erreur
debugPrint('$_tag: Controller already disposed: $e');
}
}
controllers.clear();
}
// ========================================
// UTILITAIRES DE PERFORMANCE
// ========================================
/// Vérifie si l'appareil est performant
static bool isHighPerformanceDevice() {
// Logique basée sur les capacités de l'appareil
// Pour l'instant, retourne true par défaut
return true;
}
/// Obtient le niveau de performance recommandé
static PerformanceLevel getRecommendedPerformanceLevel() {
if (isHighPerformanceDevice()) {
return PerformanceLevel.high;
} else {
return PerformanceLevel.medium;
}
}
/// Applique les optimisations selon le niveau de performance
static void applyPerformanceLevel(PerformanceLevel level) {
switch (level) {
case PerformanceLevel.high:
// Toutes les animations et effets activés
debugPrint('$_tag: High performance mode enabled');
break;
case PerformanceLevel.medium:
// Animations réduites
debugPrint('$_tag: Medium performance mode enabled');
break;
case PerformanceLevel.low:
// Animations désactivées
debugPrint('$_tag: Low performance mode enabled');
break;
}
}
// ========================================
// MONITORING EN TEMPS RÉEL
// ========================================
/// Démarre le monitoring des performances
void startPerformanceMonitoring() {
// Monitoring du frame rate
WidgetsBinding.instance.addPersistentFrameCallback((timeStamp) {
_monitorFrameRate();
});
// Monitoring de la mémoire (toutes les 30 secondes)
Timer.periodic(const Duration(seconds: 30), (_) {
_monitorMemoryUsage();
});
debugPrint('$_tag: Performance monitoring started');
}
void _monitorFrameRate() {
// Logique de monitoring du frame rate
// Pour l'instant, juste incrémenter un compteur
incrementCounter('frames_rendered');
}
void _monitorMemoryUsage() {
// Logique de monitoring de la mémoire
if (kDebugMode) {
final cacheSize = getCacheSizes();
debugPrint('$_tag: Cache sizes - Widgets: ${cacheSize['widgets']}, Images: ${cacheSize['images']}');
}
}
}
/// Niveaux de performance
enum PerformanceLevel {
low,
medium,
high,
}
/// Extension pour optimiser les widgets
extension WidgetOptimization on Widget {
/// Optimise ce widget
Widget optimized({
String? key,
bool forceRepaintBoundary = false,
bool addSemantics = true,
}) {
return PerformanceOptimizer.optimizeWidget(
this,
key: key,
forceRepaintBoundary: forceRepaintBoundary,
addSemantics: addSemantics,
);
}
}

View File

@@ -0,0 +1,356 @@
import 'dart:convert';
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:injectable/injectable.dart';
/// Service de mise en cache intelligent pour optimiser les performances
///
/// Fonctionnalités :
/// - Cache multi-niveaux (mémoire + stockage)
/// - Expiration automatique des données
/// - Invalidation intelligente
/// - Compression des données
/// - Statistiques de cache
@singleton
class SmartCacheService {
static const String _tag = 'SmartCacheService';
/// Cache en mémoire (niveau 1)
final Map<String, CacheEntry> _memoryCache = {};
/// Instance SharedPreferences pour le cache persistant
SharedPreferences? _prefs;
/// Statistiques du cache
final CacheStats _stats = CacheStats();
/// Taille maximale du cache mémoire (nombre d'entrées)
static const int _maxMemoryCacheSize = 100;
/// Durée par défaut de validité du cache
static const Duration _defaultCacheDuration = Duration(minutes: 15);
/// Initialise le service de cache
Future<void> initialize() async {
_prefs = await SharedPreferences.getInstance();
await _cleanExpiredEntries();
debugPrint('$_tag: Service initialized');
}
// ========================================
// OPÉRATIONS DE CACHE PRINCIPALES
// ========================================
/// Met en cache une valeur avec une clé
Future<void> put<T>(
String key,
T value, {
Duration? duration,
CacheLevel level = CacheLevel.both,
bool compress = false,
}) async {
final entry = CacheEntry(
key: key,
value: value,
timestamp: DateTime.now(),
duration: duration ?? _defaultCacheDuration,
compressed: compress,
);
// Cache mémoire
if (level == CacheLevel.memory || level == CacheLevel.both) {
_putInMemory(key, entry);
}
// Cache persistant
if (level == CacheLevel.storage || level == CacheLevel.both) {
await _putInStorage(key, entry);
}
_stats.incrementWrites();
debugPrint('$_tag: Cached $key (level: $level)');
}
/// Récupère une valeur du cache
Future<T?> get<T>(String key, {CacheLevel level = CacheLevel.both}) async {
CacheEntry? entry;
// Essayer d'abord le cache mémoire (plus rapide)
if (level == CacheLevel.memory || level == CacheLevel.both) {
entry = _getFromMemory(key);
if (entry != null && !entry.isExpired) {
_stats.incrementHits();
debugPrint('$_tag: Memory cache hit for $key');
return entry.value as T?;
}
}
// Essayer le cache persistant
if (level == CacheLevel.storage || level == CacheLevel.both) {
entry = await _getFromStorage(key);
if (entry != null && !entry.isExpired) {
// Remettre en cache mémoire pour les prochains accès
_putInMemory(key, entry);
_stats.incrementHits();
debugPrint('$_tag: Storage cache hit for $key');
return entry.value as T?;
}
}
_stats.incrementMisses();
debugPrint('$_tag: Cache miss for $key');
return null;
}
/// Vérifie si une clé existe dans le cache
Future<bool> contains(String key, {CacheLevel level = CacheLevel.both}) async {
if (level == CacheLevel.memory || level == CacheLevel.both) {
final entry = _getFromMemory(key);
if (entry != null && !entry.isExpired) return true;
}
if (level == CacheLevel.storage || level == CacheLevel.both) {
final entry = await _getFromStorage(key);
if (entry != null && !entry.isExpired) return true;
}
return false;
}
/// Supprime une entrée du cache
Future<void> remove(String key, {CacheLevel level = CacheLevel.both}) async {
if (level == CacheLevel.memory || level == CacheLevel.both) {
_memoryCache.remove(key);
}
if (level == CacheLevel.storage || level == CacheLevel.both) {
await _prefs?.remove(_getStorageKey(key));
}
debugPrint('$_tag: Removed $key from cache');
}
/// Vide complètement le cache
Future<void> clear({CacheLevel level = CacheLevel.both}) async {
if (level == CacheLevel.memory || level == CacheLevel.both) {
_memoryCache.clear();
}
if (level == CacheLevel.storage || level == CacheLevel.both) {
final keys = _prefs?.getKeys().where((k) => k.startsWith('cache_')).toList() ?? [];
for (final key in keys) {
await _prefs?.remove(key);
}
}
_stats.reset();
debugPrint('$_tag: Cache cleared (level: $level)');
}
// ========================================
// CACHE MÉMOIRE
// ========================================
void _putInMemory(String key, CacheEntry entry) {
// Vérifier la taille du cache et nettoyer si nécessaire
if (_memoryCache.length >= _maxMemoryCacheSize) {
_evictOldestMemoryEntry();
}
_memoryCache[key] = entry;
}
CacheEntry? _getFromMemory(String key) {
return _memoryCache[key];
}
void _evictOldestMemoryEntry() {
if (_memoryCache.isEmpty) return;
String? oldestKey;
DateTime? oldestTime;
for (final entry in _memoryCache.entries) {
if (oldestTime == null || entry.value.timestamp.isBefore(oldestTime)) {
oldestTime = entry.value.timestamp;
oldestKey = entry.key;
}
}
if (oldestKey != null) {
_memoryCache.remove(oldestKey);
debugPrint('$_tag: Evicted oldest memory entry: $oldestKey');
}
}
// ========================================
// CACHE PERSISTANT
// ========================================
Future<void> _putInStorage(String key, CacheEntry entry) async {
final storageKey = _getStorageKey(key);
final jsonData = entry.toJson();
await _prefs?.setString(storageKey, jsonEncode(jsonData));
}
Future<CacheEntry?> _getFromStorage(String key) async {
final storageKey = _getStorageKey(key);
final jsonString = _prefs?.getString(storageKey);
if (jsonString == null) return null;
try {
final jsonData = jsonDecode(jsonString) as Map<String, dynamic>;
return CacheEntry.fromJson(jsonData);
} catch (e) {
debugPrint('$_tag: Error deserializing cache entry $key: $e');
await _prefs?.remove(storageKey);
return null;
}
}
String _getStorageKey(String key) => 'cache_$key';
// ========================================
// NETTOYAGE ET MAINTENANCE
// ========================================
/// Nettoie les entrées expirées
Future<void> _cleanExpiredEntries() async {
// Nettoyer le cache mémoire
final expiredMemoryKeys = _memoryCache.entries
.where((entry) => entry.value.isExpired)
.map((entry) => entry.key)
.toList();
for (final key in expiredMemoryKeys) {
_memoryCache.remove(key);
}
// Nettoyer le cache persistant
final allKeys = _prefs?.getKeys().where((k) => k.startsWith('cache_')).toList() ?? [];
int cleanedCount = 0;
for (final storageKey in allKeys) {
final key = storageKey.substring(6); // Enlever 'cache_'
final entry = await _getFromStorage(key);
if (entry?.isExpired == true) {
await _prefs?.remove(storageKey);
cleanedCount++;
}
}
debugPrint('$_tag: Cleaned ${expiredMemoryKeys.length} memory entries and $cleanedCount storage entries');
}
/// Nettoie périodiquement le cache
void startPeriodicCleanup() {
Timer.periodic(const Duration(minutes: 30), (_) {
_cleanExpiredEntries();
});
}
// ========================================
// STATISTIQUES
// ========================================
/// Obtient les statistiques du cache
CacheStats getStats() => _stats;
/// Obtient des informations détaillées sur le cache
Future<CacheInfo> getCacheInfo() async {
final memorySize = _memoryCache.length;
final storageKeys = _prefs?.getKeys().where((k) => k.startsWith('cache_')).length ?? 0;
return CacheInfo(
memoryEntries: memorySize,
storageEntries: storageKeys,
stats: _stats,
);
}
}
/// Niveaux de cache
enum CacheLevel {
memory, // Cache en mémoire uniquement
storage, // Cache persistant uniquement
both, // Les deux niveaux
}
/// Entrée de cache
class CacheEntry {
final String key;
final dynamic value;
final DateTime timestamp;
final Duration duration;
final bool compressed;
CacheEntry({
required this.key,
required this.value,
required this.timestamp,
required this.duration,
this.compressed = false,
});
bool get isExpired => DateTime.now().difference(timestamp) > duration;
Map<String, dynamic> toJson() => {
'key': key,
'value': value,
'timestamp': timestamp.millisecondsSinceEpoch,
'duration': duration.inMilliseconds,
'compressed': compressed,
};
factory CacheEntry.fromJson(Map<String, dynamic> json) => CacheEntry(
key: json['key'],
value: json['value'],
timestamp: DateTime.fromMillisecondsSinceEpoch(json['timestamp']),
duration: Duration(milliseconds: json['duration']),
compressed: json['compressed'] ?? false,
);
}
/// Statistiques du cache
class CacheStats {
int _hits = 0;
int _misses = 0;
int _writes = 0;
int get hits => _hits;
int get misses => _misses;
int get writes => _writes;
double get hitRate => (_hits + _misses) > 0 ? _hits / (_hits + _misses) : 0.0;
void incrementHits() => _hits++;
void incrementMisses() => _misses++;
void incrementWrites() => _writes++;
void reset() {
_hits = 0;
_misses = 0;
_writes = 0;
}
@override
String toString() => 'CacheStats(hits: $_hits, misses: $_misses, writes: $_writes, hitRate: ${(hitRate * 100).toStringAsFixed(1)}%)';
}
/// Informations sur le cache
class CacheInfo {
final int memoryEntries;
final int storageEntries;
final CacheStats stats;
CacheInfo({
required this.memoryEntries,
required this.storageEntries,
required this.stats,
});
@override
String toString() => 'CacheInfo(memory: $memoryEntries, storage: $storageEntries, $stats)';
}

View File

@@ -0,0 +1,496 @@
import 'dart:async';
import 'dart:convert';
import 'package:injectable/injectable.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/payment_model.dart';
import '../models/wave_checkout_session_model.dart';
import 'wave_payment_service.dart';
import 'api_service.dart';
/// Service d'intégration complète Wave Money
/// Gère les paiements, webhooks, et synchronisation
@LazySingleton()
class WaveIntegrationService {
final WavePaymentService _wavePaymentService;
final ApiService _apiService;
final SharedPreferences _prefs;
// Stream controllers pour les événements de paiement
final _paymentStatusController = StreamController<PaymentStatusUpdate>.broadcast();
final _webhookController = StreamController<WaveWebhookData>.broadcast();
WaveIntegrationService(
this._wavePaymentService,
this._apiService,
this._prefs,
);
/// Stream des mises à jour de statut de paiement
Stream<PaymentStatusUpdate> get paymentStatusUpdates => _paymentStatusController.stream;
/// Stream des webhooks Wave
Stream<WaveWebhookData> get webhookUpdates => _webhookController.stream;
/// Initie un paiement Wave complet avec suivi
Future<WavePaymentResult> initiateWavePayment({
required String cotisationId,
required double montant,
required String numeroTelephone,
String? nomPayeur,
String? emailPayeur,
Map<String, dynamic>? metadata,
}) async {
try {
// 1. Créer la session Wave
final session = await _wavePaymentService.createCheckoutSession(
montant: montant,
devise: 'XOF',
successUrl: 'https://unionflow.app/payment/success',
errorUrl: 'https://unionflow.app/payment/error',
typePaiement: 'COTISATION',
description: 'Paiement cotisation $cotisationId',
referenceExterne: cotisationId,
);
// 2. Créer le modèle de paiement
final payment = PaymentModel(
id: session.id ?? session.waveSessionId,
cotisationId: cotisationId,
numeroReference: session.waveSessionId,
montant: montant,
codeDevise: 'XOF',
methodePaiement: 'WAVE',
statut: 'EN_ATTENTE',
dateTransaction: DateTime.now(),
numeroTransaction: session.waveSessionId,
referencePaiement: session.referenceExterne,
operateurMobileMoney: 'WAVE',
numeroTelephone: numeroTelephone,
nomPayeur: nomPayeur,
emailPayeur: emailPayeur,
metadonnees: {
'wave_session_id': session.waveSessionId,
'wave_checkout_url': session.waveUrl,
'cotisation_id': cotisationId,
'numero_telephone': numeroTelephone,
'source': 'unionflow_mobile',
...?metadata,
},
dateCreation: DateTime.now(),
);
// 3. Sauvegarder localement pour suivi
await _savePaymentLocally(payment);
// 4. Démarrer le suivi du paiement
_startPaymentTracking(payment.id, session.waveSessionId);
return WavePaymentResult(
success: true,
payment: payment,
session: session,
checkoutUrl: session.waveUrl,
);
} catch (e) {
return WavePaymentResult(
success: false,
errorMessage: 'Erreur lors de l\'initiation du paiement: ${e.toString()}',
);
}
}
/// Vérifie le statut d'un paiement Wave
Future<PaymentModel?> checkPaymentStatus(String paymentId) async {
try {
// Récupérer depuis le cache local d'abord
final localPayment = await _getLocalPayment(paymentId);
if (localPayment != null && localPayment.isCompleted) {
return localPayment;
}
// Vérifier avec l'API Wave
final sessionId = localPayment?.metadonnees?['wave_session_id'] as String?;
if (sessionId != null) {
final session = await _wavePaymentService.getCheckoutSession(sessionId);
final updatedPayment = await _wavePaymentService.getPaymentStatus(sessionId);
// Mettre à jour le cache local
await _updateLocalPayment(updatedPayment);
// Notifier les listeners
_paymentStatusController.add(PaymentStatusUpdate(
paymentId: paymentId,
status: updatedPayment.statut,
payment: updatedPayment,
));
return updatedPayment;
}
return localPayment;
} catch (e) {
throw WavePaymentException('Erreur lors de la vérification du statut: ${e.toString()}');
}
}
/// Traite un webhook Wave reçu
Future<void> processWaveWebhook(Map<String, dynamic> webhookData) async {
try {
final webhook = WaveWebhookData.fromJson(webhookData);
// Valider la signature du webhook (sécurité)
if (!await _validateWebhookSignature(webhookData)) {
throw WavePaymentException('Signature webhook invalide');
}
// Traiter selon le type d'événement
switch (webhook.eventType) {
case 'payment.completed':
await _handlePaymentCompleted(webhook);
break;
case 'payment.failed':
await _handlePaymentFailed(webhook);
break;
case 'payment.cancelled':
await _handlePaymentCancelled(webhook);
break;
default:
print('Type de webhook non géré: ${webhook.eventType}');
}
// Notifier les listeners
_webhookController.add(webhook);
} catch (e) {
throw WavePaymentException('Erreur lors du traitement du webhook: ${e.toString()}');
}
}
/// Récupère l'historique des paiements Wave
Future<List<PaymentModel>> getWavePaymentHistory({
String? cotisationId,
DateTime? startDate,
DateTime? endDate,
int limit = 50,
}) async {
try {
// Récupérer depuis le cache local
final localPayments = await _getLocalPayments(
cotisationId: cotisationId,
startDate: startDate,
endDate: endDate,
limit: limit,
);
// Synchroniser avec le serveur si nécessaire
if (await _shouldSyncWithServer()) {
final serverPayments = await _apiService.getPaymentHistory(
methodePaiement: 'WAVE',
cotisationId: cotisationId,
startDate: startDate,
endDate: endDate,
limit: limit,
);
// Fusionner et mettre à jour le cache
await _mergeAndCachePayments(serverPayments);
return serverPayments;
}
return localPayments;
} catch (e) {
throw WavePaymentException('Erreur lors de la récupération de l\'historique: ${e.toString()}');
}
}
/// Calcule les statistiques des paiements Wave
Future<WavePaymentStats> getWavePaymentStats({
DateTime? startDate,
DateTime? endDate,
}) async {
try {
final payments = await getWavePaymentHistory(
startDate: startDate,
endDate: endDate,
);
final completedPayments = payments.where((p) => p.isSuccessful).toList();
final failedPayments = payments.where((p) => p.isFailed).toList();
final pendingPayments = payments.where((p) => p.isPending).toList();
final totalAmount = completedPayments.fold<double>(
0.0,
(sum, payment) => sum + payment.montant,
);
final totalFees = completedPayments.fold<double>(
0.0,
(sum, payment) => sum + (payment.fraisTransaction ?? 0.0),
);
return WavePaymentStats(
totalPayments: payments.length,
completedPayments: completedPayments.length,
failedPayments: failedPayments.length,
pendingPayments: pendingPayments.length,
totalAmount: totalAmount,
totalFees: totalFees,
averageAmount: completedPayments.isNotEmpty
? totalAmount / completedPayments.length
: 0.0,
successRate: payments.isNotEmpty
? (completedPayments.length / payments.length) * 100
: 0.0,
);
} catch (e) {
throw WavePaymentException('Erreur lors du calcul des statistiques: ${e.toString()}');
}
}
/// Démarre le suivi d'un paiement
void _startPaymentTracking(String paymentId, String sessionId) {
Timer.periodic(const Duration(seconds: 10), (timer) async {
try {
final payment = await checkPaymentStatus(paymentId);
if (payment != null && (payment.isCompleted || payment.isFailed)) {
timer.cancel();
}
} catch (e) {
print('Erreur lors du suivi du paiement $paymentId: $e');
timer.cancel();
}
});
}
/// Gestion des événements webhook
Future<void> _handlePaymentCompleted(WaveWebhookData webhook) async {
final paymentId = webhook.data['payment_id'] as String?;
if (paymentId != null) {
final payment = await _getLocalPayment(paymentId);
if (payment != null) {
final updatedPayment = payment.copyWith(
statut: 'CONFIRME',
dateModification: DateTime.now(),
);
await _updateLocalPayment(updatedPayment);
}
}
}
Future<void> _handlePaymentFailed(WaveWebhookData webhook) async {
final paymentId = webhook.data['payment_id'] as String?;
if (paymentId != null) {
final payment = await _getLocalPayment(paymentId);
if (payment != null) {
final updatedPayment = payment.copyWith(
statut: 'ECHEC',
messageErreur: webhook.data['error_message'] as String?,
dateModification: DateTime.now(),
);
await _updateLocalPayment(updatedPayment);
}
}
}
Future<void> _handlePaymentCancelled(WaveWebhookData webhook) async {
final paymentId = webhook.data['payment_id'] as String?;
if (paymentId != null) {
final payment = await _getLocalPayment(paymentId);
if (payment != null) {
final updatedPayment = payment.copyWith(
statut: 'ANNULE',
dateModification: DateTime.now(),
);
await _updateLocalPayment(updatedPayment);
}
}
}
/// Méthodes de cache local
Future<void> _savePaymentLocally(PaymentModel payment) async {
final payments = await _getLocalPayments();
payments.add(payment);
await _prefs.setString('wave_payments', jsonEncode(payments.map((p) => p.toJson()).toList()));
}
Future<PaymentModel?> _getLocalPayment(String paymentId) async {
final payments = await _getLocalPayments();
try {
return payments.firstWhere((p) => p.id == paymentId);
} catch (e) {
return null;
}
}
Future<List<PaymentModel>> _getLocalPayments({
String? cotisationId,
DateTime? startDate,
DateTime? endDate,
int? limit,
}) async {
final paymentsJson = _prefs.getString('wave_payments');
if (paymentsJson == null) return [];
final paymentsList = jsonDecode(paymentsJson) as List;
var payments = paymentsList.map((json) => PaymentModel.fromJson(json)).toList();
// Filtrer selon les critères
if (cotisationId != null) {
payments = payments.where((p) => p.cotisationId == cotisationId).toList();
}
if (startDate != null) {
payments = payments.where((p) => p.dateTransaction.isAfter(startDate)).toList();
}
if (endDate != null) {
payments = payments.where((p) => p.dateTransaction.isBefore(endDate)).toList();
}
// Trier par date décroissante
payments.sort((a, b) => b.dateTransaction.compareTo(a.dateTransaction));
// Limiter le nombre de résultats
if (limit != null && payments.length > limit) {
payments = payments.take(limit).toList();
}
return payments;
}
Future<void> _updateLocalPayment(PaymentModel payment) async {
final payments = await _getLocalPayments();
final index = payments.indexWhere((p) => p.id == payment.id);
if (index != -1) {
payments[index] = payment;
await _prefs.setString('wave_payments', jsonEncode(payments.map((p) => p.toJson()).toList()));
}
}
Future<void> _mergeAndCachePayments(List<PaymentModel> serverPayments) async {
final localPayments = await _getLocalPayments();
final mergedPayments = <String, PaymentModel>{};
// Ajouter les paiements locaux
for (final payment in localPayments) {
mergedPayments[payment.id] = payment;
}
// Fusionner avec les paiements du serveur (priorité au serveur)
for (final payment in serverPayments) {
mergedPayments[payment.id] = payment;
}
await _prefs.setString(
'wave_payments',
jsonEncode(mergedPayments.values.map((p) => p.toJson()).toList()),
);
}
Future<bool> _shouldSyncWithServer() async {
final lastSync = _prefs.getInt('last_wave_sync') ?? 0;
final now = DateTime.now().millisecondsSinceEpoch;
const syncInterval = 5 * 60 * 1000; // 5 minutes
return (now - lastSync) > syncInterval;
}
Future<bool> _validateWebhookSignature(Map<String, dynamic> webhookData) async {
// TODO: Implémenter la validation de signature Wave
// Pour l'instant, on retourne true (à sécuriser en production)
return true;
}
void dispose() {
_paymentStatusController.close();
_webhookController.close();
}
}
/// Résultat d'un paiement Wave
class WavePaymentResult {
final bool success;
final PaymentModel? payment;
final WaveCheckoutSessionModel? session;
final String? checkoutUrl;
final String? errorMessage;
WavePaymentResult({
required this.success,
this.payment,
this.session,
this.checkoutUrl,
this.errorMessage,
});
}
/// Mise à jour de statut de paiement
class PaymentStatusUpdate {
final String paymentId;
final String status;
final PaymentModel payment;
PaymentStatusUpdate({
required this.paymentId,
required this.status,
required this.payment,
});
}
/// Données de webhook Wave
class WaveWebhookData {
final String eventType;
final String eventId;
final DateTime timestamp;
final Map<String, dynamic> data;
WaveWebhookData({
required this.eventType,
required this.eventId,
required this.timestamp,
required this.data,
});
factory WaveWebhookData.fromJson(Map<String, dynamic> json) {
return WaveWebhookData(
eventType: json['event_type'] as String,
eventId: json['event_id'] as String,
timestamp: DateTime.parse(json['timestamp'] as String),
data: json['data'] as Map<String, dynamic>,
);
}
}
/// Statistiques des paiements Wave
class WavePaymentStats {
final int totalPayments;
final int completedPayments;
final int failedPayments;
final int pendingPayments;
final double totalAmount;
final double totalFees;
final double averageAmount;
final double successRate;
WavePaymentStats({
required this.totalPayments,
required this.completedPayments,
required this.failedPayments,
required this.pendingPayments,
required this.totalAmount,
required this.totalFees,
required this.averageAmount,
required this.successRate,
});
}
/// Exception spécifique aux paiements Wave
class WavePaymentException implements Exception {
final String message;
final String? code;
final dynamic originalError;
WavePaymentException(this.message, {this.code, this.originalError});
@override
String toString() => 'WavePaymentException: $message';
}