Mobile Money en Afrique
En Afrique de l'Ouest, Mobile Money est LE moyen de paiement dominant :
Pour Keneya, ignorer Mobile Money aurait été ignorer 70% de nos utilisateurs potentiels.
Intégration Orange Money
1. API REST Orange Money
typescript
import axios from 'axios'
class OrangeMoneyService {
private baseURL = 'https://api.orange.com/orange-money-webpay/dev/v1'
private merchantKey: string
private token: string
async getAccessToken() {
const response = await axios.post(`${this.baseURL}/oauth/token`, {
grant_type: 'client_credentials'
}, {
auth: {
username: process.env.ORANGE_CLIENT_ID,
password: process.env.ORANGE_CLIENT_SECRET
}
})
this.token = response.data.access_token
return this.token
}
async initiatePayment(amount: number, phoneNumber: string, orderId: string) {
const payload = {
merchant_key: this.merchantKey,
currency: 'XOF', // Franc CFA
order_id: orderId,
amount: amount,
return_url: 'https://keneya.com/payment/callback',
cancel_url: 'https://keneya.com/payment/cancel',
notif_url: 'https://keneya.com/api/webhooks/orange-money',
lang: 'fr',
reference: `KENEYA-${orderId}`
}
const response = await axios.post(
`${this.baseURL}/webpayment`,
payload,
{
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json'
}
}
)
return {
paymentUrl: response.data.payment_url,
paymentToken: response.data.pay_token
}
}
async checkPaymentStatus(payToken: string) {
const response = await axios.post(
`${this.baseURL}/transactionstatus`,
{ pay_token: payToken },
{
headers: {
'Authorization': `Bearer ${this.token}`
}
}
)
return response.data.status // SUCCESS, PENDING, FAILED
}
}2. Webhook handler
typescript
// webhooks/orange-money.controller.ts
@Controller('webhooks/orange-money')
export class OrangeMoneyWebhookController {
@Post()
async handleWebhook(@Body() payload: any, @Req() req: Request) {
// Vérifier la signature
const signature = req.headers['x-orange-signature']
const isValid = this.verifySignature(payload, signature)
if (!isValid) {
throw new UnauthorizedException('Invalid signature')
}
const { order_id, status, amount } = payload
if (status === 'SUCCESS') {
// Marquer la consultation comme payée
await this.consultationService.markAsPaid(order_id, {
provider: 'orange_money',
amount,
transactionId: payload.txnid
})
// Envoyer notification au patient
await this.notificationService.sendPaymentConfirmation(order_id)
}
return { status: 'ok' }
}
private verifySignature(payload: any, signature: string): boolean {
const secret = process.env.ORANGE_WEBHOOK_SECRET
const computed = crypto
.createHmac('sha256', secret)
.update(JSON.stringify(payload))
.digest('hex')
return computed === signature
}
}Intégration Moov Money
API Moov Money
typescript
class MoovMoneyService {
private baseURL = 'https://api.moov-africa.com/v1'
async initiatePayment(amount: number, phoneNumber: string) {
const payload = {
amount: amount.toString(),
currency: 'XOF',
customer_phone: phoneNumber,
merchant_transaction_id: `KENEYA-${Date.now()}`,
callback_url: 'https://keneya.com/api/webhooks/moov-money'
}
const response = await axios.post(
`${this.baseURL}/payments/ussd-push`,
payload,
{
headers: {
'Authorization': `Bearer ${process.env.MOOV_API_KEY}`,
'Content-Type': 'application/json'
}
}
)
return {
transactionId: response.data.transaction_id,
status: response.data.status
}
}
}Gestion unifiée
typescript
// payment.service.ts
export class PaymentService {
async processPayment(
provider: 'orange' | 'moov',
amount: number,
phoneNumber: string,
orderId: string
) {
let result
switch (provider) {
case 'orange':
result = await this.orangeMoneyService.initiatePayment(
amount, phoneNumber, orderId
)
break
case 'moov':
result = await this.moovMoneyService.initiatePayment(
amount, phoneNumber
)
break
}
// Sauvegarder la transaction
await this.transactionRepo.create({
orderId,
provider,
amount,
status: 'pending',
createdAt: new Date()
})
return result
}
}Sécurité
1. Validation des montants
typescript
function validateAmount(amount: number): boolean {
// Minimum 100 FCFA, maximum 500,000 FCFA
if (amount < 100 || amount > 500000) {
throw new Error('Montant invalide')
}
return true
}2. Idempotence
typescript
async function ensureIdempotent(orderId: string) {
const existing = await this.transactionRepo.findByOrderId(orderId)
if (existing && existing.status === 'success') {
throw new Error('Transaction déjà traitée')
}
}3. Rate limiting
typescript
@UseGuards(ThrottlerGuard)
@Throttle(5, 60) // 5 requêtes par minute
@Post('payment')
async initiatePayment(@Body() dto: PaymentDto) {
// ...
}Expérience utilisateur
typescript
// Frontend - React Native
function PaymentScreen() {
const [provider, setProvider] = useState<'orange' | 'moov'>()
const handlePayment = async () => {
try {
const result = await api.post('/payments', {
provider,
amount: consultationFee,
phoneNumber: user.phone
})
if (provider === 'orange') {
// Rediriger vers la page Orange Money
Linking.openURL(result.paymentUrl)
} else {
// Afficher instructions USSD pour Moov
Alert.alert(
'Composez le code USSD',
`Tapez *155# et suivez les instructions`
)
}
// Polling du statut
pollPaymentStatus(result.transactionId)
} catch (error) {
Alert.alert('Erreur', error.message)
}
}
return (
<View>
<Text>Choisissez votre opérateur</Text>
<TouchableOpacity onPress={() => setProvider('orange')}>
<Text>Orange Money</Text>
</TouchableOpacity>
<TouchableOpacity onPress={() => setProvider('moov')}>
<Text>Moov Money</Text>
</TouchableOpacity>
<Button onPress={handlePayment}>Payer {consultationFee} FCFA</Button>
</View>
)
}Résultats Keneya
Conclusion
L'intégration Mobile Money est essentielle pour toute application africaine impliquant des transactions. Une bonne implémentation combine sécurité, fiabilité et expérience utilisateur fluide.
*Un projet nécessitant Mobile Money ? [Parlons-en](/contact).*