Versione OK Pour l'onglet événements.
This commit is contained in:
428
unionflow-mobile-apps/lib/core/services/payment_service.dart
Normal file
428
unionflow-mobile-apps/lib/core/services/payment_service.dart
Normal file
@@ -0,0 +1,428 @@
|
||||
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<PaymentModel> 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<PaymentModel> 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<bool> 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<PaymentModel> 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<List<PaymentModel>> 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<PaymentMethod> 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<void> _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<PaymentModel?> _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';
|
||||
}
|
||||
Reference in New Issue
Block a user