Metadata-Version: 2.4
Name: wourapay
Version: 0.1.0
Summary: SDK Python officiel de Wourapay — vérification serveur des paiements et des webhooks
Project-URL: Homepage, https://wourapay.com
Project-URL: Documentation, https://api.wourapay.com/docs
Project-URL: Source, https://github.com/woura-it/wourapay-python
Author: Wourapay
License: MIT
Keywords: afrique,mobile money,paiement,webhook,wourapay
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Office/Business :: Financial
Requires-Python: >=3.9
Requires-Dist: httpx>=0.24
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.25; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Description-Content-Type: text/markdown

# wourapay — SDK Python

SDK officiel de [Wourapay](https://wourapay.com) pour les **backends**.

Deux usages, et rien d'autre :

1. **Vérifier un paiement** avant de livrer une commande.
2. **Vérifier la signature d'un callback** avant de traiter son contenu.

Encaisser, créer des liens de paiement ou gérer des sous-comptes se fait
directement en HTTP — un wrapper y ajouterait une surface à maintenir sans rien
simplifier. Voir [la documentation API](https://api.wourapay.com/docs).

```bash
pip install wourapay
```

---

## 1. Vérifier un paiement

Après que votre front a affiché « paiement réussi », **vérifiez côté serveur**.
Le navigateur peut affirmer n'importe quoi ; seul cet appel fait autorité.

```python
from wourapay import Wourapay

client = Wourapay(api_key="kpy_…")          # ou variable WOURAPAY_API_KEY

result = client.verify(reference="ORDER-2026-001")
if result.verified:
    deliver_order()
```

`verified` est la **seule** valeur sur laquelle livrer : elle vaut `True`
uniquement si la transaction vous appartient **et** qu'elle est réellement
`successful`. Un `status` renseigné ne suffit pas.

Si la transaction n'est pas encore terminale, l'API la revérifie en direct auprès
de l'agrégateur avant de répondre.

<details>
<summary>Recherche par identifiant Wourapay</summary>

```python
result = client.verify(transaction_id="e5f2…")
```
</details>

<details>
<summary>Version asynchrone</summary>

```python
from wourapay import AsyncWourapay

async with AsyncWourapay(api_key="kpy_…") as client:
    result = await client.verify(reference="ORDER-1")
```
</details>

### Le résultat

| Champ | Description |
|---|---|
| `verified` | `True` ⇔ transaction à vous **et** `successful` |
| `status` | `pending` · `processing` · `successful` · `failed` · `expired` · `not_found` |
| `transaction_id`, `display_id`, `reference` | Identifiants |
| `amount`, `currency`, `network_code` | Montant et réseau |
| `client_id` | Sous-compte crédité, le cas échéant |
| `paid_at` | Horodatage du succès |
| `raw` | Réponse brute, pour les champs ajoutés après cette version |

---

## 2. Vérifier un callback

Wourapay signe chaque callback. **Vérifiez la signature avant de traiter le
contenu** : un callback non vérifié n'est qu'une assertion d'un inconnu sur
l'état de votre argent.

```python
from wourapay import verify_webhook_signature

@app.post("/webhooks/wourapay")
async def handle(request):
    body = await request.body()                      # OCTETS BRUTS
    header = request.headers["X-Wourapay-Signature"]

    if not verify_webhook_signature(body, header, secret=WEBHOOK_SECRET):
        return Response(status_code=400)

    event = json.loads(body)
    ...
```

Votre secret (`whsec_…`) est lisible dans votre espace marchand, section
**Webhooks**.

### Deux pièges à éviter

**Passez le corps brut.** Jamais un JSON re-sérialisé : un espace ou un ordre de
clés différent invalide la signature.

```python
verify_webhook_signature(await request.body(), header, secret)      # ✅
verify_webhook_signature(json.dumps(request.json()), header, secret)  # ❌
```

**Ne désactivez pas la tolérance en production.** L'horodatage fait partie de la
signature : c'est ce qui empêche le rejeu d'une capture réseau. `tolerance=0`
est réservé aux tests.

### Rotation de secret

Quand vous régénérez votre secret, Wourapay signe pendant **24 h** avec l'ancien
**et** le nouveau. Déployez votre nouveau secret quand vous voulez dans cette
fenêtre : aucun callback n'est perdu.

### Laisser remonter l'erreur

```python
from wourapay import verify_webhook_signature, InvalidSignature

try:
    verify_webhook_signature(body, header, secret, raise_on_error=True)
except InvalidSignature as exc:
    return Response(str(exc), status_code=400)
```

Le module de signature ne dépend que de la bibliothèque standard : il reste
utilisable dans une fonction serverless qui n'installe rien d'autre.

---

## Erreurs

Toutes dérivent de `WourapayError` : un seul `except` suffit à ne rien laisser
passer.

| Exception | Quand |
|---|---|
| `AuthenticationError` | Clé absente, invalide ou révoquée |
| `APIError` | L'API a répondu une erreur — porte `status_code` et `payload` |
| `NetworkError` | API injoignable : DNS, TLS, timeout, coupure |
| `InvalidSignature` | Signature absente, expirée ou ne correspondant pas |
| `WourapayError` | Base commune |

`NetworkError` mérite une attention particulière : contrairement à `APIError`,
elle veut dire **« on ne sait pas »**, pas « le paiement a échoué ». Sur une
vérification, c'est un signal de réessai, jamais de refus de commande.

---

## Développement

```bash
pip install -e ".[dev]"
pytest
```

Les vecteurs de `tests/test_webhooks.py` font foi : ils doivent rester alignés
sur l'implémentation de référence côté API. Un test qui diverge signifie qu'un
marchand rejettera des callbacks légitimes — ou pire, en acceptera de falsifiés.

## Licence

MIT
