import 'package:injectable/injectable.dart'; import '../models/payment_model.dart'; import '../models/cotisation_model.dart'; import 'api_service.dart'; import 'cache_service.dart'; import 'wave_payment_service.dart'; import 'orange_money_service.dart'; import 'moov_money_service.dart'; /// Service de gestion des paiements /// Gère les transactions de paiement avec différents opérateurs @LazySingleton() class PaymentService { final ApiService _apiService; final CacheService _cacheService; final WavePaymentService _waveService; final OrangeMoneyService _orangeService; final MoovMoneyService _moovService; PaymentService( this._apiService, this._cacheService, this._waveService, this._orangeService, this._moovService, ); /// Initie un paiement pour une cotisation Future initiatePayment({ required String cotisationId, required double montant, required String methodePaiement, required String numeroTelephone, String? nomPayeur, String? emailPayeur, }) async { try { PaymentModel payment; // Déléguer au service spécialisé selon la méthode de paiement switch (methodePaiement) { case 'WAVE': payment = await _waveService.initiatePayment( cotisationId: cotisationId, montant: montant, numeroTelephone: numeroTelephone, nomPayeur: nomPayeur, emailPayeur: emailPayeur, ); break; case 'ORANGE_MONEY': payment = await _orangeService.initiatePayment( cotisationId: cotisationId, montant: montant, numeroTelephone: numeroTelephone, nomPayeur: nomPayeur, emailPayeur: emailPayeur, ); break; case 'MOOV_MONEY': payment = await _moovService.initiatePayment( cotisationId: cotisationId, montant: montant, numeroTelephone: numeroTelephone, nomPayeur: nomPayeur, emailPayeur: emailPayeur, ); break; default: throw PaymentException('Méthode de paiement non supportée: $methodePaiement'); } // Sauvegarder en cache await _cachePayment(payment); return payment; } catch (e) { if (e is PaymentException) rethrow; throw PaymentException('Erreur lors de l\'initiation du paiement: ${e.toString()}'); } } /// Vérifie le statut d'un paiement Future checkPaymentStatus(String paymentId) async { try { // Essayer le cache d'abord final cachedPayment = await _getCachedPayment(paymentId); // Si le paiement est déjà terminé (succès ou échec), retourner le cache if (cachedPayment != null && (cachedPayment.isSuccessful || cachedPayment.isFailed)) { return cachedPayment; } // Déterminer le service à utiliser selon la méthode de paiement PaymentModel payment; if (cachedPayment != null) { switch (cachedPayment.methodePaiement) { case 'WAVE': payment = await _waveService.checkPaymentStatus(paymentId); break; case 'ORANGE_MONEY': payment = await _orangeService.checkPaymentStatus(paymentId); break; case 'MOOV_MONEY': payment = await _moovService.checkPaymentStatus(paymentId); break; default: throw PaymentException('Méthode de paiement inconnue: ${cachedPayment.methodePaiement}'); } } else { // Si pas de cache, essayer tous les services (peu probable) throw PaymentException('Paiement non trouvé en cache'); } // Mettre à jour le cache await _cachePayment(payment); return payment; } catch (e) { // En cas d'erreur réseau, retourner le cache si disponible final cachedPayment = await _getCachedPayment(paymentId); if (cachedPayment != null) { return cachedPayment; } throw PaymentException('Erreur lors de la vérification du paiement: ${e.toString()}'); } } /// Annule un paiement en cours Future cancelPayment(String paymentId) async { try { // Récupérer le paiement en cache pour connaître la méthode final cachedPayment = await _getCachedPayment(paymentId); if (cachedPayment == null) { throw PaymentException('Paiement non trouvé'); } // Déléguer au service approprié bool cancelled = false; switch (cachedPayment.methodePaiement) { case 'WAVE': cancelled = await _waveService.cancelPayment(paymentId); break; case 'ORANGE_MONEY': cancelled = await _orangeService.cancelPayment(paymentId); break; case 'MOOV_MONEY': cancelled = await _moovService.cancelPayment(paymentId); break; default: throw PaymentException('Méthode de paiement non supportée pour l\'annulation'); } return cancelled; } catch (e) { if (e is PaymentException) rethrow; throw PaymentException('Erreur lors de l\'annulation du paiement: ${e.toString()}'); } } /// Retente un paiement échoué Future retryPayment(String paymentId) async { try { // Récupérer le paiement original final originalPayment = await _getCachedPayment(paymentId); if (originalPayment == null) { throw PaymentException('Paiement original non trouvé'); } // Réinitier le paiement avec les mêmes paramètres return await initiatePayment( cotisationId: originalPayment.cotisationId, montant: originalPayment.montant, methodePaiement: originalPayment.methodePaiement, numeroTelephone: originalPayment.numeroTelephone ?? '', nomPayeur: originalPayment.nomPayeur, emailPayeur: originalPayment.emailPayeur, ); } catch (e) { if (e is PaymentException) rethrow; throw PaymentException('Erreur lors de la nouvelle tentative de paiement: ${e.toString()}'); } } /// Récupère l'historique des paiements d'une cotisation Future> getPaymentHistory(String cotisationId) async { try { // Essayer le cache d'abord final cachedPayments = await _cacheService.getPayments(); if (cachedPayments != null) { final filteredPayments = cachedPayments .where((p) => p.cotisationId == cotisationId) .toList(); if (filteredPayments.isNotEmpty) { return filteredPayments; } } // Si pas de cache, retourner une liste vide // En production, on pourrait appeler l'API ici return []; } catch (e) { throw PaymentException('Erreur lors de la récupération de l\'historique: ${e.toString()}'); } } /// Valide les données de paiement avant envoi bool validatePaymentData({ required String cotisationId, required double montant, required String methodePaiement, required String numeroTelephone, }) { // Validation du montant if (montant <= 0) return false; // Validation du numéro de téléphone selon l'opérateur if (!_validatePhoneNumber(numeroTelephone, methodePaiement)) { return false; } // Validation de la méthode de paiement if (!_isValidPaymentMethod(methodePaiement)) { return false; } return true; } /// Calcule les frais de transaction selon la méthode double calculateTransactionFees(double montant, String methodePaiement) { switch (methodePaiement) { case 'ORANGE_MONEY': return _calculateOrangeMoneyFees(montant); case 'WAVE': return _calculateWaveFees(montant); case 'MOOV_MONEY': return _calculateMoovMoneyFees(montant); case 'CARTE_BANCAIRE': return _calculateCardFees(montant); default: return 0.0; } } /// Retourne les méthodes de paiement disponibles List getAvailablePaymentMethods() { return [ PaymentMethod( id: 'ORANGE_MONEY', nom: 'Orange Money', icone: '📱', couleur: '#FF6600', description: 'Paiement via Orange Money', fraisMinimum: 0, fraisMaximum: 1000, montantMinimum: 100, montantMaximum: 1000000, ), PaymentMethod( id: 'WAVE', nom: 'Wave', icone: '🌊', couleur: '#00D4FF', description: 'Paiement via Wave', fraisMinimum: 0, fraisMaximum: 500, montantMinimum: 100, montantMaximum: 2000000, ), PaymentMethod( id: 'MOOV_MONEY', nom: 'Moov Money', icone: '💙', couleur: '#0066CC', description: 'Paiement via Moov Money', fraisMinimum: 0, fraisMaximum: 800, montantMinimum: 100, montantMaximum: 1500000, ), PaymentMethod( id: 'CARTE_BANCAIRE', nom: 'Carte bancaire', icone: '💳', couleur: '#4CAF50', description: 'Paiement par carte bancaire', fraisMinimum: 100, fraisMaximum: 2000, montantMinimum: 500, montantMaximum: 5000000, ), ]; } /// Méthodes privées Future _cachePayment(PaymentModel payment) async { try { // Utiliser le service de cache pour sauvegarder final payments = await _cacheService.getPayments() ?? []; // Remplacer ou ajouter le paiement final index = payments.indexWhere((p) => p.id == payment.id); if (index >= 0) { payments[index] = payment; } else { payments.add(payment); } await _cacheService.savePayments(payments); } catch (e) { // Ignorer les erreurs de cache } } Future _getCachedPayment(String paymentId) async { try { final payments = await _cacheService.getPayments(); if (payments != null) { return payments.firstWhere( (p) => p.id == paymentId, orElse: () => throw StateError('Payment not found'), ); } return null; } catch (e) { return null; } } bool _validatePhoneNumber(String numero, String operateur) { // Supprimer les espaces et caractères spéciaux final cleanNumber = numero.replaceAll(RegExp(r'[^\d]'), ''); switch (operateur) { case 'ORANGE_MONEY': // Orange: 07, 08, 09 (Côte d'Ivoire) return RegExp(r'^(225)?(0[789])\d{8}$').hasMatch(cleanNumber); case 'WAVE': // Wave accepte tous les numéros ivoiriens return RegExp(r'^(225)?(0[1-9])\d{8}$').hasMatch(cleanNumber); case 'MOOV_MONEY': // Moov: 01, 02, 03 return RegExp(r'^(225)?(0[123])\d{8}$').hasMatch(cleanNumber); default: return cleanNumber.length >= 8; } } bool _isValidPaymentMethod(String methode) { const validMethods = [ 'ORANGE_MONEY', 'WAVE', 'MOOV_MONEY', 'CARTE_BANCAIRE', 'VIREMENT', 'ESPECES' ]; return validMethods.contains(methode); } double _calculateOrangeMoneyFees(double montant) { if (montant <= 1000) return 0; if (montant <= 5000) return 25; if (montant <= 10000) return 50; if (montant <= 25000) return 100; if (montant <= 50000) return 200; return montant * 0.005; // 0.5% } double _calculateWaveFees(double montant) { // Wave a généralement des frais plus bas if (montant <= 2000) return 0; if (montant <= 10000) return 25; if (montant <= 50000) return 100; return montant * 0.003; // 0.3% } double _calculateMoovMoneyFees(double montant) { if (montant <= 1000) return 0; if (montant <= 5000) return 30; if (montant <= 15000) return 75; if (montant <= 50000) return 150; return montant * 0.004; // 0.4% } double _calculateCardFees(double montant) { // Frais fixes + pourcentage pour les cartes return 100 + (montant * 0.025); // 100 XOF + 2.5% } } /// Modèle pour les méthodes de paiement disponibles class PaymentMethod { final String id; final String nom; final String icone; final String couleur; final String description; final double fraisMinimum; final double fraisMaximum; final double montantMinimum; final double montantMaximum; PaymentMethod({ required this.id, required this.nom, required this.icone, required this.couleur, required this.description, required this.fraisMinimum, required this.fraisMaximum, required this.montantMinimum, required this.montantMaximum, }); } /// Exception personnalisée pour les erreurs de paiement class PaymentException implements Exception { final String message; PaymentException(this.message); @override String toString() => 'PaymentException: $message'; }