import AsyncStorage from '@react-native-async-storage/async-storage'; import NetInfo from '@react-native-community/netinfo'; import { ApiService } from './ApiService'; import { AnalyticsService } from './AnalyticsService'; /** * Service de paiement Wave Money pour l'application mobile UnionFlow * * Ce service gère toutes les interactions avec l'API Wave Money : * - Initiation des paiements * - Vérification du statut des transactions * - Calcul des frais * - Gestion hors ligne avec synchronisation * - Cache des données pour performance * * @author Lions Dev Team * @version 2.0.0 * @since 2025-01-16 */ export interface WavePaymentRequest { type: 'cotisation' | 'adhesion' | 'aide' | 'evenement'; amount: string; phoneNumber: string; description: string; metadata?: any; } export interface WavePaymentResult { success: boolean; transactionId?: string; waveTransactionId?: string; status?: 'SUCCES' | 'EN_ATTENTE' | 'ECHEC'; message?: string; error?: string; } export interface WaveTransactionStatus { transactionId: string; status: string; message: string; timestamp: string; } export interface WaveFees { base: string; fees: string; total: string; } class WavePaymentServiceClass { private readonly CACHE_KEY = 'wave_payment_cache'; private readonly PENDING_PAYMENTS_KEY = 'pending_wave_payments'; private readonly FEES_CACHE_KEY = 'wave_fees_cache'; private readonly CACHE_DURATION = 5 * 60 * 1000; // 5 minutes /** * Initie un paiement Wave Money */ async initiatePayment(request: WavePaymentRequest): Promise { try { // Vérifier la connectivité const netInfo = await NetInfo.fetch(); if (!netInfo.isConnected) { // Mode hors ligne - sauvegarder pour synchronisation ultérieure await this.savePendingPayment(request); return { success: false, error: 'Pas de connexion internet. Le paiement sera traité dès que la connexion sera rétablie.', }; } // Valider la demande this.validatePaymentRequest(request); // Préparer les données pour l'API const apiRequest = this.prepareApiRequest(request); // Appeler l'API selon le type de paiement let response; switch (request.type) { case 'cotisation': response = await ApiService.post('/api/v1/payments/wave/cotisation', apiRequest); break; case 'adhesion': response = await ApiService.post('/api/v1/payments/wave/adhesion', apiRequest); break; case 'aide': response = await ApiService.post('/api/v1/payments/wave/aide-mutuelle', apiRequest); break; case 'evenement': response = await ApiService.post('/api/v1/payments/wave/evenement', apiRequest); break; default: throw new Error('Type de paiement non supporté'); } // Traiter la réponse const result = this.processPaymentResponse(response); // Sauvegarder en cache pour consultation ultérieure await this.cachePaymentResult(request, result); // Analytics AnalyticsService.trackEvent('wave_payment_initiated', { type: request.type, amount: request.amount, success: result.success, }); return result; } catch (error) { console.error('Erreur initiation paiement Wave:', error); AnalyticsService.trackEvent('wave_payment_error', { type: request.type, amount: request.amount, error: error.message, }); return { success: false, error: this.getErrorMessage(error), }; } } /** * Vérifie le statut d'une transaction Wave */ async checkTransactionStatus(transactionId: string): Promise { try { const response = await ApiService.get(`/api/v1/payments/wave/status/${transactionId}`); return { transactionId: response.transactionId, status: response.status, message: response.message, timestamp: response.timestamp, }; } catch (error) { console.error('Erreur vérification statut:', error); throw new Error('Impossible de vérifier le statut de la transaction'); } } /** * Calcule les frais Wave Money pour un montant donné */ async calculateFees(amount: string): Promise { try { // Vérifier le cache d'abord const cachedFees = await this.getCachedFees(amount); if (cachedFees) { return cachedFees; } const response = await ApiService.get(`/api/v1/payments/wave/fees?montant=${amount}`); const fees: WaveFees = { base: response.montantBase, fees: response.frais, total: response.montantTotal, }; // Mettre en cache await this.cacheFees(amount, fees); return fees; } catch (error) { console.error('Erreur calcul frais:', error); // Calcul local en cas d'erreur API return this.calculateFeesLocally(amount); } } /** * Synchronise les paiements en attente */ async syncPendingPayments(): Promise { try { const pendingPayments = await this.getPendingPayments(); if (pendingPayments.length === 0) { return; } console.log(`Synchronisation de ${pendingPayments.length} paiements en attente`); for (const payment of pendingPayments) { try { const result = await this.initiatePayment(payment.request); if (result.success) { // Supprimer de la liste des paiements en attente await this.removePendingPayment(payment.id); // Notifier l'utilisateur du succès // NotificationService.showLocalNotification({ // title: 'Paiement synchronisé', // body: `Votre paiement de ${payment.request.amount} FCFA a été traité avec succès.`, // }); } } catch (error) { console.error('Erreur synchronisation paiement:', error); } } } catch (error) { console.error('Erreur synchronisation générale:', error); } } /** * Retourne l'historique des paiements Wave */ async getPaymentHistory(): Promise { try { const cached = await AsyncStorage.getItem(this.CACHE_KEY); if (cached) { const data = JSON.parse(cached); return data.payments || []; } return []; } catch (error) { console.error('Erreur récupération historique:', error); return []; } } // ==================== MÉTHODES PRIVÉES ==================== private validatePaymentRequest(request: WavePaymentRequest): void { if (!request.amount || parseFloat(request.amount) <= 0) { throw new Error('Montant invalide'); } if (parseFloat(request.amount) < 100) { throw new Error('Montant minimum : 100 FCFA'); } if (parseFloat(request.amount) > 1000000) { throw new Error('Montant maximum : 1,000,000 FCFA'); } if (!request.phoneNumber || !this.isValidWaveNumber(request.phoneNumber)) { throw new Error('Numéro Wave invalide'); } if (!request.description) { throw new Error('Description obligatoire'); } } private isValidWaveNumber(phoneNumber: string): boolean { // Validation des numéros Wave CI : +225 suivi de 8 chiffres const wavePattern = /^\+225[0-9]{8}$/; return wavePattern.test(phoneNumber); } private prepareApiRequest(request: WavePaymentRequest): any { return { montant: request.amount, numeroTelephone: request.phoneNumber, description: request.description, metadata: { ...request.metadata, source: 'mobile_app', version: '2.0.0', }, }; } private processPaymentResponse(response: any): WavePaymentResult { if (response.transactionId) { return { success: true, transactionId: response.transactionId, waveTransactionId: response.waveTransactionId, status: response.statut, message: response.message, }; } else { return { success: false, error: response.message || 'Erreur lors du paiement', }; } } private async savePendingPayment(request: WavePaymentRequest): Promise { try { const pendingPayments = await this.getPendingPayments(); const newPayment = { id: Date.now().toString(), request, timestamp: new Date().toISOString(), }; pendingPayments.push(newPayment); await AsyncStorage.setItem( this.PENDING_PAYMENTS_KEY, JSON.stringify(pendingPayments) ); } catch (error) { console.error('Erreur sauvegarde paiement en attente:', error); } } private async getPendingPayments(): Promise { try { const stored = await AsyncStorage.getItem(this.PENDING_PAYMENTS_KEY); return stored ? JSON.parse(stored) : []; } catch (error) { console.error('Erreur récupération paiements en attente:', error); return []; } } private async removePendingPayment(paymentId: string): Promise { try { const pendingPayments = await this.getPendingPayments(); const filtered = pendingPayments.filter(p => p.id !== paymentId); await AsyncStorage.setItem( this.PENDING_PAYMENTS_KEY, JSON.stringify(filtered) ); } catch (error) { console.error('Erreur suppression paiement en attente:', error); } } private async cachePaymentResult(request: WavePaymentRequest, result: WavePaymentResult): Promise { try { const cached = await AsyncStorage.getItem(this.CACHE_KEY); const data = cached ? JSON.parse(cached) : { payments: [] }; data.payments.unshift({ request, result, timestamp: new Date().toISOString(), }); // Garder seulement les 50 derniers paiements data.payments = data.payments.slice(0, 50); await AsyncStorage.setItem(this.CACHE_KEY, JSON.stringify(data)); } catch (error) { console.error('Erreur cache paiement:', error); } } private async getCachedFees(amount: string): Promise { try { const cached = await AsyncStorage.getItem(this.FEES_CACHE_KEY); if (!cached) return null; const data = JSON.parse(cached); const entry = data[amount]; if (entry && Date.now() - entry.timestamp < this.CACHE_DURATION) { return entry.fees; } return null; } catch (error) { console.error('Erreur récupération cache frais:', error); return null; } } private async cacheFees(amount: string, fees: WaveFees): Promise { try { const cached = await AsyncStorage.getItem(this.FEES_CACHE_KEY); const data = cached ? JSON.parse(cached) : {}; data[amount] = { fees, timestamp: Date.now(), }; await AsyncStorage.setItem(this.FEES_CACHE_KEY, JSON.stringify(data)); } catch (error) { console.error('Erreur cache frais:', error); } } private calculateFeesLocally(amount: string): WaveFees { const montant = parseFloat(amount); let frais: number; // Barème Wave Money Côte d'Ivoire if (montant <= 1000) { frais = 25; } else if (montant <= 5000) { frais = 50; } else if (montant <= 25000) { frais = montant * 0.01; // 1% } else { frais = 250; // Plafond } const total = montant + frais; return { base: `${montant.toLocaleString()} FCFA`, fees: `${frais.toLocaleString()} FCFA`, total: `${total.toLocaleString()} FCFA`, }; } private getErrorMessage(error: any): string { if (error.response?.data?.message) { return error.response.data.message; } if (error.message) { return error.message; } return 'Une erreur inattendue est survenue'; } } export const WavePaymentService = new WavePaymentServiceClass();