Bonnes pratiques

Ce guide présente les meilleures pratiques pour utiliser la bibliothèque Faso Arzeka Payment de manière sûre, performante et maintenable.

Sécurité

1. Protéger les credentials

Ne jamais hard-coder les credentials

# ❌ MAUVAIS
client = ArzekaPayment()
client.authenticate("username", "password")

Utiliser des variables d’environnement

# ✅ BON
import os

client = ArzekaPayment()
client.authenticate(
    os.getenv('ARZEKA_USERNAME'),
    os.getenv('ARZEKA_PASSWORD')
)

2. Ne jamais logger les secrets

# ❌ MAUVAIS
logger.info(f"Authenticating with password: {password}")

# ✅ BON
logger.info("Authenticating...")

# ❌ MAUVAIS
logger.debug(f"Hash secret: {hash_secret}")

# ✅ BON
logger.debug("Hash secret: ***")

3. Utiliser HTTPS en production

# ✅ BON - Production
client = ArzekaPayment(
    base_url="https://pwg.fasoarzeka.com/",
    verify_ssl=True
)

# ⚠️ Acceptable seulement en test
client = ArzekaPayment(
    base_url="https://pwg-test.fasoarzeka.com/",
    verify_ssl=False  # Uniquement en test !
)

4. Valider les webhooks

@app.route('/webhook', methods=['POST'])
def webhook():
    data = request.json
    order_id = data.get('mappedOrderId')

    # ✅ Toujours vérifier auprès d'Arzeka
    verified_status = check_payment(order_id)

    if verified_status['status'] == 'COMPLETED':
        # Traiter le paiement
        process_order(order_id)

    return jsonify({'status': 'received'}), 200

Performance

1. Réutiliser les instances client

Créer un nouveau client à chaque requête

# ❌ MAUVAIS - Crée une nouvelle session à chaque fois
def make_payment():
    client = ArzekaPayment()
    client.authenticate("username", "password")
    response = client.initiate_payment(...)
    client.close()
    return response

Réutiliser le même client

# ✅ BON - Réutilise la même session
client = ArzekaPayment()
client.authenticate("username", "password")

def make_payment():
    return client.initiate_payment(...)

# Faire plusieurs paiements
payment1 = make_payment()
payment2 = make_payment()

# Fermer à la fin
client.close()

2. Utiliser le Context Manager

# ✅ OPTIMAL
with ArzekaPayment() as client:
    client.authenticate("username", "password")

    # Faire plusieurs opérations
    for order in orders:
        response = client.initiate_payment(...)

3. Configurer un timeout approprié

# ⚠️ Trop court - risque de timeout
client = ArzekaPayment(timeout=5)

# ✅ BON - Équilibré
client = ArzekaPayment(timeout=30)

# ⚠️ Trop long - bloque l'application
client = ArzekaPayment(timeout=300)

4. Activer le retry automatique

# ✅ BON
client = ArzekaPayment(
    max_retries=3,
    retry_delay=2
)

Fiabilité

1. Toujours gérer les erreurs

from fasoarzeka import (
    ArzekaPayment,
    ArzekaValidationError,
    ArzekaAPIError,
    ArzekaConnectionError
)

# ✅ BON
try:
    response = client.initiate_payment(...)
except ArzekaValidationError as e:
    logger.error(f"Validation error: {e}")
    return None
except ArzekaAPIError as e:
    logger.error(f"API error: {e}")
    return None
except ArzekaConnectionError as e:
    logger.error(f"Connection error: {e}")
    # Réessayer plus tard
    queue_for_retry(payment_data)
    return None

2. Utiliser la réauthentification automatique

# ✅ BON - Pas besoin de gérer l'expiration du token
client = ArzekaPayment()
client.authenticate("username", "password")

# Le client se réauthentifie automatiquement si nécessaire
response = client.initiate_payment(...)

3. Valider les données avant envoi

from fasoarzeka.utils import validate_phone_number, format_msisdn

def process_payment(amount, phone, ...):
    # ✅ Validation en amont
    if amount < 100:
        raise ValueError("Montant minimum : 100 FCFA")

    if not validate_phone_number(phone):
        raise ValueError("Numéro de téléphone invalide")

    # Formatage
    phone = format_msisdn(phone)

    # Envoyer à l'API
    response = client.initiate_payment(
        amount=amount,
        additional_info={
            "mobile": phone,
            # ...
        },
        # ...
    )

4. Implémenter l’idempotence

# ✅ BON - Vérifier si déjà traité
@app.route('/webhook', methods=['POST'])
def webhook():
    data = request.json
    order_id = data.get('mappedOrderId')

    # Vérifier si déjà traité
    if is_already_processed(order_id):
        logger.info(f"Order {order_id} already processed")
        return jsonify({'status': 'already_processed'}), 200

    # Traiter le paiement
    process_payment(order_id)
    mark_as_processed(order_id)

    return jsonify({'status': 'success'}), 200

Structure du code

1. Organiser les configurations

# config.py
import os
from dataclasses import dataclass

@dataclass
class ArzekaConfig:
    username: str = os.getenv('ARZEKA_USERNAME', '')
    password: str = os.getenv('ARZEKA_PASSWORD', '')
    merchant_id: str = os.getenv('ARZEKA_MERCHANT_ID', '')
    hash_secret: str = os.getenv('ARZEKA_HASH_SECRET', '')
    base_url: str = os.getenv('ARZEKA_BASE_URL', 'https://pwg-test.fasoarzeka.com/')
    webhook_url: str = os.getenv('ARZEKA_WEBHOOK_URL', '')
    return_url: str = os.getenv('ARZEKA_RETURN_URL', '')

# Utilisation
from config import ArzekaConfig

config = ArzekaConfig()
client = ArzekaPayment(base_url=config.base_url)
client.authenticate(config.username, config.password)

2. Créer des wrappers métier

# payment_service.py
from fasoarzeka import ArzekaPayment
from config import ArzekaConfig
import logging

logger = logging.getLogger(__name__)

class PaymentService:
    def __init__(self):
        self.config = ArzekaConfig()
        self.client = ArzekaPayment(base_url=self.config.base_url)
        self.client.authenticate(
            self.config.username,
            self.config.password
        )

    def create_payment(self, amount: int, customer_phone: str, order_id: str):
        """Créer un paiement avec validation et logging"""
        try:
            logger.info(f"Creating payment for order {order_id}")

            response = self.client.initiate_payment(
                amount=amount,
                merchant_id=self.config.merchant_id,
                additional_info={
                    "mobile": customer_phone,
                    # ...
                },
                mapped_order_id=order_id,
                hash_secret=self.config.hash_secret,
                link_for_update_status=self.config.webhook_url,
                link_back_to_calling_website=self.config.return_url
            )

            logger.info(f"Payment created: {response['mappedOrderId']}")
            return response

        except Exception as e:
            logger.error(f"Payment creation failed: {e}")
            raise

    def get_payment_status(self, order_id: str):
        """Récupérer le statut d'un paiement"""
        try:
            return self.client.check_payment(order_id)
        except Exception as e:
            logger.error(f"Status check failed for {order_id}: {e}")
            raise

    def close(self):
        """Fermer le client"""
        self.client.close()

3. Séparer les responsabilités

# validators.py
def validate_payment_data(data):
    """Valider les données de paiement"""
    if data['amount'] < 100:
        raise ValueError("Montant minimum : 100 FCFA")
    # ...

# formatters.py
def format_payment_response(response):
    """Formater la réponse pour l'API publique"""
    return {
        'order_id': response['mappedOrderId'],
        'payment_url': response['url'],
        'status': response['status']
    }

# services.py
def process_payment(payment_data):
    """Traiter un paiement"""
    validate_payment_data(payment_data)

    response = payment_service.create_payment(
        amount=payment_data['amount'],
        customer_phone=payment_data['phone'],
        order_id=payment_data['order_id']
    )

    return format_payment_response(response)

Logging

1. Configurer le logging correctement

import logging

# Configuration de base
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('arzeka.log'),
        logging.StreamHandler()
    ]
)

2. Logger les événements importants

logger = logging.getLogger(__name__)

# ✅ BON
logger.info(f"Payment initiated for order {order_id}")
logger.info(f"Payment completed: {order_id}")
logger.warning(f"Token will expire in 5 minutes")
logger.error(f"Payment failed: {order_id}", exc_info=True)

3. Éviter le logging excessif

# ❌ MAUVAIS - Trop verbeux
logger.debug("Entering function")
logger.debug("Validating data")
logger.debug("Data validated")
logger.debug("Calling API")
logger.debug("API called")

# ✅ BON - Pertinent
logger.info(f"Processing payment for order {order_id}")
logger.info(f"Payment successful: {order_id}")

Tests

1. Tester avec mocks

import unittest
from unittest.mock import patch, MagicMock
from fasoarzeka import ArzekaPayment

class TestPaymentService(unittest.TestCase):
    @patch('fasoarzeka.ArzekaPayment')
    def test_create_payment(self, mock_client):
        # Configurer le mock
        mock_client.return_value.initiate_payment.return_value = {
            'mappedOrderId': 'ORDER-001',
            'url': 'https://...',
            'status': 'PENDING'
        }

        # Tester
        service = PaymentService()
        response = service.create_payment(1000, "22670123456", "ORDER-001")

        # Vérifier
        self.assertEqual(response['mappedOrderId'], 'ORDER-001')

2. Utiliser l’environnement de test

# tests/conftest.py
import os

os.environ['ARZEKA_BASE_URL'] = 'https://pwg-test.fasoarzeka.com/'
os.environ['ARZEKA_USERNAME'] = 'test_user'
os.environ['ARZEKA_PASSWORD'] = 'test_pass'

Production

Checklist de déploiement

Configuration

  • [ ] Variables d’environnement configurées

  • [ ] URL de production utilisée

  • [ ] Credentials de production configurés

  • [ ] SSL activé

Logging

  • [ ] Niveau de log approprié (INFO ou WARNING)

  • [ ] Rotation des logs configurée

  • [ ] Logs centralisés (Sentry, CloudWatch, etc.)

Monitoring

  • [ ] Métriques collectées

  • [ ] Alertes configurées

  • [ ] Dashboard créé

Sécurité

  • [ ] Secrets jamais loggés

  • [ ] HTTPS utilisé

  • [ ] Webhooks validés

  • [ ] Rate limiting implémenté

Performance

  • [ ] Connection pooling activé

  • [ ] Timeout configuré

  • [ ] Retry activé

  • [ ] Cache implémenté si applicable

Tests

  • [ ] Tests unitaires passent

  • [ ] Tests d’intégration passent

  • [ ] Tests en environnement de pré-production

Voir aussi