434 lines
12 KiB
TypeScript
434 lines
12 KiB
TypeScript
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();
|