Metadata-Version: 2.5
Name: smartdevhub-core
Version: 0.2.0
Summary: SDK Python officiel de Smartdev Core — paiements (Wave, Orange Money), SMS/WhatsApp, vérification d'identité et campagnes pour l'Afrique de l'Ouest
Project-URL: Documentation, https://core.smartdevafrica.com/docs/
License: MIT
License-File: LICENSE
Keywords: africa,kyc,orange-money,payments,smartdev,sms,wave,xof
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: httpx>=0.23.0
Requires-Dist: pydantic>=2.0
Provides-Extra: aiohttp
Requires-Dist: httpx-aiohttp<1,>=0.1.8; extra == 'aiohttp'
Description-Content-Type: text/markdown

# Smartdev Core — SDK Python

SDK officiel de [Smartdev Core](https://core.smartdevafrica.com/docs/) : paiements
(Wave, Orange Money), SMS/WhatsApp/Email, vérification d'identité progressive
et campagnes — une seule API pour opérer en Afrique de l'Ouest.

Généré depuis le contrat TypeSpec de l'API : le SDK ne peut pas dériver du serveur.

## Installation

```bash
pip install smartdevhub-core
```

Le paquet s'installe sous le nom `smartdevhub-core` et s'importe sous le nom
du produit : `smartdev_core`.

Pour le client asynchrone aiohttp (`DefaultAioHttpClient`) :

```bash
pip install "smartdevhub-core[aiohttp]"
```

## Démarrage

```python
import os
from smartdev_core import SmartdevApi

core = SmartdevApi(token=os.environ["SMARTDEV_KEY"])  # sk_test_… ou sk_live_…

# Encaisser 15 000 XOF au Sénégal — le routing choisit le rail (Wave, Orange Money…)
core.pay.payment_intents_create(
    idempotency_key="ord-923-pay",
    country="SN",
    amount=15000,
    currency="XOF",
    reference="ORD-923",
    customer_msisdn="+221771234567",
)

# Notifier — WhatsApp d'abord, SMS en secours
core.relay.messages_send(
    idempotency_key="ord-923-notif",
    to="+221771234567",
    country="SN",
    channels=["whatsapp", "sms"],
    template="app.order.confirmed",
    variables={"ref": "ORD-923"},
)
```

Sous-clients : `pay`, `relay`, `trust`, `campaigns`, `sandbox`, `platform`.
Client asynchrone : `AsyncSmartdevApi`.

## Crédit : vérifier avant une campagne

*Disponible à partir de `smartdevhub-core` **0.2.0**. Portée `billing:read`.*

```python
compte = core.platform.billing_balance()

# `available_minor` est la SEULE valeur à regarder avant d'envoyer.
# `None` = sans limite (offre contractuelle).
if compte.suspended:
    raise RuntimeError("Compte suspendu — aucun envoi facturable ne partira")
if compte.available_minor is not None and int(compte.available_minor) < cout_estime_minor:
    # Rechargez depuis la console : aucune clé d'API ne recharge.
    raise RuntimeError(f"Crédit insuffisant : {compte.available_minor} {compte.currency}")
```

Un envoi non financé est refusé par un **402** (`insufficient_credit`, `credit_limit_reached`,
`billing_account_suspended`) ou un **422** (`billing_currency_mismatch` — le Core refuse de
convertir plutôt que de débiter au taux 1:1). Un 402 n'est pas un incident réseau : le rejouer
en boucle ne changera rien tant que le compte n'a pas été rechargé.

## Webhooks : vérifier la signature

*Disponible à partir de `smartdevhub-core` **0.2.0**.*

**N'écrivez pas cette vérification à la main.** Elle tient en dix lignes, et ces
dix lignes ont trois pièges qui ne se voient jamais en test — ils acceptent un
webhook forgé, ou en rejettent un légitime, le jour où ils se déclenchent.

```python
import os
from flask import Flask, request
from smartdev_core.webhooks import verify_webhook, WebhookVerificationError

app = Flask(__name__)

@app.post("/webhooks/smartdev")
def smartdev_webhook():
    try:
        event = verify_webhook(
            secret=os.environ["SMARTDEV_WEBHOOK_SECRET"],   # whsec_…
            headers=request.headers,
            payload=request.get_data(),                     # BRUT, pas request.json
        )
    except WebhookVerificationError:
        # Ni le détail ni un 500 : la plateforme réessaiera sur un non-2xx.
        return "", 400

    # event["id"] est stable entre les retries : c'est VOTRE clé d'idempotence.
    if event["type"] == "pay.payment.succeeded":
        crediter(event["id"], event["data"])
    return "", 200
```

### ⚠️ Il faut le corps BRUT

`request.json` — ou `json.dumps(request.json)` — ne reproduit pas à l'octet près
ce que Core a signé : l'ordre des clés, les espaces et l'échappement diffèrent. La
vérification échouera **toujours**, et le symptôme (« signature invalide ») fait
soupçonner le secret pendant des heures. Le helper refuse d'ailleurs un objet déjà
parsé avec un `TypeError` explicite plutôt que de vous laisser chercher.

| Framework | Corps brut |
|---|---|
| Flask | `request.get_data()` |
| FastAPI / Starlette | `await request.body()` |
| Django | `request.body` |

### Les erreurs sont typées

Toutes héritent de `WebhookVerificationError` et portent un `code` stable, sûr à
journaliser — aucun message ne contient la signature attendue, qui en ferait un
oracle.

| Classe | `code` |
|---|---|
| `WebhookSignatureMissingError` | `missing_signature` |
| `WebhookTimestampError` | `invalid_timestamp` · `timestamp_out_of_tolerance` |
| `WebhookSignatureInvalidError` | `invalid_signature` |
| `WebhookPayloadError` | `invalid_payload` |

`verify_webhook` **ne rend jamais `False`** : un webhook non vérifié n'est pas un
cas nominal qu'on teste par distraction, c'est une erreur qu'on ne peut pas
ignorer.

### Ce que la plateforme attend en retour

| | |
|---|---|
| Succès | n'importe quel **2xx**, corps vide accepté |
| Échec | tout le reste — **une 3xx compte comme un échec**, les redirections ne sont pas suivies |
| Délai | **10 s** par tentative |
| Retries | 5 tentatives : 5 s, 30 s, 2 min, 10 min, 1 h |
| Horodatage | Le helper refuse hors **±5 min**, dans les deux sens. C'est un défaut **côté récepteur** (`tolerance_seconds`), pas un rejet côté plateforme : vérifiez l'horloge de votre serveur (NTP) |
| Idempotence | `event["id"]` (`evt_…`) est **stable entre les retries** |

Répondez 2xx vite et traitez en tâche de fond : un traitement de plus de 10 s est
compté comme un échec et vous serez rappelé, avec le même `event["id"]`.

## Environnements

| Clé | Base URL |
|---|---|
| `sk_test_…` | `https://sandbox.core.smartdevafrica.com` |
| `sk_live_…` | `https://api.core.smartdevafrica.com` |

Le sandbox simule les providers (paiement accepté/refusé, SMS livré/échoué,
KYC approuvé/rejeté) — aucun vrai débit, aucun vrai SMS.

## Documentation

Guide complet, erreurs, webhooks et idempotence : [core.smartdevafrica.com/docs](https://core.smartdevafrica.com/docs/).

## Licence

[MIT](./LICENSE)
