Refactoring
This commit is contained in:
@@ -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';
|
||||
}
|
||||
Reference in New Issue
Block a user