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

@@ -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';
}