Metadata-Version: 2.4
Name: welloo-payment-sdk
Version: 0.1.3
Summary: SDK Python pour authentifier un marchand et créer un paiement via redirection (checkout hébergé) avec le prestataire Welloo.
Author: Welloo
License: MIT
Keywords: payment,sdk,checkout,merchant,welloo,mobile-money
Requires-Python: >=3.9
Description-Content-Type: text/markdown

# welloo-payment-sdk

SDK Python pour :
- accepter un paiement (page de sélection ou lien direct pour un opérateur) et obtenir une **URL de paiement hébergée** vers laquelle rediriger le client,
- transfert Mobile Money via Welloo
- liste des transactions
- consultation du solde
- vérifier la signature d'un webhook de confirmation.

## ⚠️ Sécurité — à lire avant tout

Ce SDK manipule `api_key` et `service_token`, des **credentials secrets**.
**Il ne doit jamais être exécuté dans du code accessible côté client**
(JS embarqué dans une page publique, etc.). Utilise-le uniquement côté
serveur : Flask, Django, FastAPI, script Python, etc. Ne mets jamais ces
valeurs en dur dans ton code ou ta documentation — passe-les toujours par
des variables d'environnement.

Le frontend doit appeler **ton propre backend**, qui utilise ce SDK pour
parler au prestataire de paiement.

```
[Frontend] --(HTTP, sans secret)--> [Ton backend Flask/Django/FastAPI] --(SDK, avec secrets)--> [Welloo]
```

## Procédure d'installation

### 1. Prérequis

- Python >= 3.9

### 2. Installer le package

```bash
pip install welloo-payment-sdk
```

### 3. Configurer les identifiants

Récupère `api_key` et `service_token` auprès de Welloo, puis expose-les en
variables d'environnement (ex: fichier `.env`, jamais commité) :

```bash
API_KEY=...
SERVICE_TOKEN=...
WEBHOOK_SECRET=...
```

### 4. Instancier le SDK

```python
import os

from welloo_payment_sdk import PaymentSDK, PaymentSDKConfig

# Le SDK est instancié UNE FOIS, côté serveur, avec les secrets pris depuis .env
sdk = PaymentSDK(PaymentSDKConfig(
    api_key=os.environ.get("API_KEY", ""),
    service_token=os.environ.get("SERVICE_TOKEN", ""),
    webhook_secret=os.environ.get("WEBHOOK_SECRET") or None,
))
```

`PaymentSDK` lève une `PaymentSDKError` si `api_key` ou `service_token` est
manquant — instancie-le une seule fois au démarrage de l'application, pas à
chaque requête.

## Utilisation du SDK avec `init_payment`

`init_payment` crée une session de paiement auprès de Welloo et retourne
`checkout_url` : l'URL vers laquelle rediriger le navigateur du client.
Son comportement dépend du champ optionnel `operator` :

- **`operator` absent** : retourne la page de paiement **hébergée par
  Welloo**, où le client choisit lui-même son opérateur Mobile Money et son
  numéro.
- **`operator` fourni** (`"welloo"` ou `"wave"`) : génère directement un
  lien de paiement pour cet opérateur (endpoint `POST /api/v1/payment-link-sdk`),
  sans passer par l'écran de sélection.

```python
# Sans opérateur -> page de sélection hébergée par Welloo
@app.get("/api/v1/init-payment")
def init_payment():
    try:
        payment = sdk.init_payment(InitPaymentPayload(
            amount=5,                              # montant en unité mineure du prestataire
            operator=request.args.get("operator"),   # optionnel : "welloo" | "wave"
            return_url="https://monsite.com/panier",  # lien de retour
            metadata={},
        ))
        return Response(status=302, headers={"Location": payment.checkout_url})
    except PaymentSDKError as e:
        app.logger.error("Impossible de lancer le paiement : %s", e)
        return {"error": "Impossible de lancer le paiement"}, 500
```

> Exemple complet : [`examples/python-backend/app.py`](../../examples/python-backend/app.py).

### Champs de `InitPaymentPayload`

| Champ | Requis | Description |
| --- | --- | --- |
| `amount` | oui | Montant en unité mineure (ex: centimes) selon le prestataire |
| `return_url` | oui | Cible du bouton "Retour" sur la page de paiement |
| `operator` | non | `"welloo"` ou `"wave"` — génère un lien direct pour cet opérateur au lieu de la page de sélection |
| `description` | non | Description du paiement (usage actuellement informatif côté Welloo) |
| `metadata` | non | Dict transmis au prestataire. **Toutes les valeurs doivent être des chaînes** : un entier est rejeté par un `400 { "field": "metadata_<clé>", "message": "Doit etre une chaine de caracteres." }`. Ignoré si `operator` est fourni. |

`init_payment` retourne un `InitPaymentResult` :

| Champ | Description |
| --- | --- |
| `checkout_url` | URL vers laquelle rediriger (page de sélection ou lien direct selon `operator`) |
| `status` | Statut initial de la session (ex: `"active"`, `"pending"`) |

## Utilisation du SDK avec `init_transfer`

`init_transfer` initie un transfert et retourne l'URL de paiement hébergée
vers laquelle rediriger le client (endpoint `POST /api/v1/init`).

```python
@app.get("/api/v1/init-transfer")
def init_transfer():
    try:
        transfer = sdk.init_transfer(InitTransferPayload(
            return_url="https://merchant.example.com/success",
        ))
        return Response(status=302, headers={"Location": transfer.checkout_url})
    except PaymentSDKError as e:
        app.logger.error("Impossible d'initier le transfert : %s", e)
        return {"error": "Impossible d'initier le transfert"}, 500
```

### Champs de `InitTransferPayload`

| Champ | Description |
| --- | --- |
| `return_url` | URL de retour après paiement |

`init_transfer` retourne un `InitTransferResult` avec un seul champ :
`checkout_url` (URL vers laquelle rediriger).

> `init_payment` rejette un `amount` inférieur à 5 (`PaymentSDKError`), avant
> même d'appeler l'API.

## Utilisation du SDK avec `get_transactions` et `get_payment_status`

`get_transactions` liste les transactions du service authentifié (endpoint
`GET /api/v1/payments`). `get_payment_status(reference)` vérifie le statut
réel d'**une** transaction précise (endpoint `GET /api/v1/payments/{reference}`) —
la `reference` est le `session_reference` renvoyé dans chaque transaction.

```python
# Sans ?reference= -> liste complète. Avec ?reference= -> statut d'une transaction.
@app.get("/api/v1/transactions")
def transactions():
    try:
        reference = request.args.get("reference")
        if reference:
            status = sdk.get_payment_status(reference)
            return jsonify({"reference": status.reference, "status": status.status, "raw": status.raw})

        return jsonify(sdk.get_transactions())
    except PaymentSDKError as e:
        app.logger.error("Impossible de récupérer les transactions : %s", e)
        return {"error": "Impossible de récupérer les transactions"}, 500
```

`get_transactions` retourne une liste des transactions brutes renvoyées par
Welloo (montant, devise, statut, opérateur, dates, etc.). `get_payment_status`
retourne un `GetPaymentStatusResult` :

| Champ | Description |
| --- | --- |
| `reference` | La référence passée en paramètre |
| `status` | Statut réel de la transaction (ex: `"active"`, `"completed"`, `"expire"`) |
| `raw` | Réponse brute complète du prestataire |

**Ne te fie jamais uniquement à une URL de retour** (`return_url`) pour
valider un paiement : revérifie toujours via `get_payment_status()` ou un
webhook signé.

## Utilisation du SDK avec `get_solde`

`get_solde` récupère le solde du compte Welloo (endpoint `GET /api/v1/solde`).

```python
@app.get("/api/v1/solde")
def solde():
    try:
        result = sdk.get_solde()
        return jsonify({"currency": result.currency, "solde": result.solde})
    except PaymentSDKError as e:
        app.logger.error("Impossible de récupérer le solde : %s", e)
        return {"error": "Impossible de récupérer le solde"}, 500
```

`get_solde` retourne un `GetSoldeResult` :

| Champ | Description |
| --- | --- |
| `currency` | Devise du solde (ex: `"XOF"`) |
| `solde` | Montant disponible sur le compte |
| `raw` | Réponse brute complète du prestataire |

## Gérer les erreurs

Toutes les méthodes qui appellent l'API lèvent une `PaymentSDKError` en cas
d'échec, avec deux attributs utiles pour distinguer une erreur actionnable
par l'appelant (ex: opérateur invalide) d'une vraie panne :

| Attribut | Description |
| --- | --- |
| `status_code` | Code HTTP renvoyé par Welloo (ex: `400`, `401`) — `None` pour une erreur réseau |
| `details` | Corps JSON brut de la réponse d'erreur Welloo (`{"message": ..., "errors": [...]}`) |

Un pattern courant : exposer le vrai message pour les erreurs 4xx
(actionnables), et rester générique pour les 5xx (pour ne pas fuiter de
détail interne que l'appelant ne peut de toute façon pas corriger) :

```python
def send_sdk_error(e: PaymentSDKError, fallback_message: str):
    app.logger.error(e)  # détail complet toujours loggé côté serveur

    if e.status_code and e.status_code < 500:
        details = isinstance(e.details, dict) and e.details.get("errors")
        message = (isinstance(e.details, dict) and e.details.get("message")) or str(e)
        return {"error": message, "details": details}, e.status_code

    return {"error": fallback_message}, 500
```

## Vérifier un webhook

> Contrairement à Express, Flask ne parse pas le corps de la requête tant
> que `request.get_json()` n'est pas appelé — `request.get_data()` renvoie
> donc toujours le corps brut exact, nécessaire au calcul HMAC, sans
> précaution d'ordre de middleware particulière.

```python
@app.post("/api/v1/webhook")
def webhook():
    signature = request.headers.get("X-Webhook-Signature", "")
    raw_body = request.get_data()  # corps brut, nécessaire au calcul HMAC

    if not signature or not sdk.verify_webhook_signature(raw_body, signature):
        return {"error": "Invalid signature"}, 401

    event = request.get_json(silent=True) or {}
    # event["status"] == "paid" -> mettre à jour la commande en base
    return "", 200
```

## API complète

| Méthode | Description |
| --- | --- |
| `init_payment(payload)` | Crée une session de paiement et retourne `checkout_url` (page de sélection, ou lien direct si `operator` est fourni). |
| `init_transfer(payload)` | Initie un transfert et retourne `checkout_url`. |
| `get_transactions()` | Liste les transactions du service authentifié. |
| `get_payment_status(reference)` | Vérifie le statut réel d'une transaction précise. |
| `get_solde()` | Récupère le solde du compte Welloo. |
| `verify_webhook_signature(raw_body, signature)` | Vérifie la signature HMAC-SHA256 d'un webhook. |

## Build

```bash
py -m build
```
