first commit
This commit is contained in:
433
unionflow-mobile-apps/src/services/WavePaymentService.ts
Normal file
433
unionflow-mobile-apps/src/services/WavePaymentService.ts
Normal file
@@ -0,0 +1,433 @@
|
||||
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<WavePaymentResult> {
|
||||
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<WaveTransactionStatus> {
|
||||
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<WaveFees> {
|
||||
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<void> {
|
||||
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<any[]> {
|
||||
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<void> {
|
||||
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<any[]> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<WaveFees | null> {
|
||||
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<void> {
|
||||
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();
|
||||
Reference in New Issue
Block a user