shwary-python 1.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,10 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
@@ -0,0 +1,170 @@
1
+ Metadata-Version: 2.4
2
+ Name: shwary-python
3
+ Version: 1.0.1
4
+ Summary: SDK Python moderne (Async/Sync) pour l'API de paiement Shwary.
5
+ Author-email: Josué Luis Panzu <josuepanzu8@gmail.com>
6
+ License: MIT
7
+ Keywords: africa,fintech,kenya,mobile-money,payment,rdc,shwary,uganda
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
12
+ Requires-Python: >=3.11
13
+ Requires-Dist: httpx>=0.28.1
14
+ Requires-Dist: phonenumbers>=9.0.22
15
+ Requires-Dist: pydantic>=2.12.5
16
+ Description-Content-Type: text/markdown
17
+
18
+ # Shwary Python SDK
19
+
20
+ [![PyPI version](https://img.shields.io/pypi/v/shwary-python.svg)](https://pypi.org/project/shwary-python/)
21
+ [![Python versions](https://img.shields.io/pypi/pyversions/shwary-python.svg)](https://pypi.org/project/shwary-python/)
22
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
23
+
24
+ **Shwary Python** est une bibliothèque cliente moderne, asynchrone et performante (non officielle) pour l'intégration de l'API [Shwary](https://shwary.com). Elle permet d'initier des paiements Mobile Money en **RDC**, au **Kenya** et en **Ouganda** avec une validation stricte des données avant l'envoi.
25
+
26
+
27
+
28
+ ## Caractéristiques
29
+
30
+ * **Gestion d'erreurs native** : Pas besoin de vérifier les `status_code` manuellement. Le SDK lève des exceptions explicites (`AuthenticationError`, `ValidationError`, etc.).
31
+ * **Async-first** : Construit sur `httpx` pour des performances optimales (Pooling de connexions).
32
+ * **Dual-mode** : Support complet des modes Synchrone et Asynchrone.
33
+ * **Validation Robuste** : Vérification des numéros (E.164) et des montants minimums (ex: 2900 CDF pour la RDC).
34
+ * **Type-safe** : Basé sur Pydantic V2 pour une autocomplétion parfaite dans votre IDE.
35
+ * **Ultra-rapide** : Optimisé avec `uv` et `__slots__` pour minimiser l'empreinte mémoire.
36
+
37
+ ## Installation
38
+
39
+ Avec `uv` (recommandé) :
40
+ ```bash
41
+ uv add shwary-python
42
+ ```
43
+ Ou avec `pip`
44
+ ```bash
45
+ pip install shwary-python
46
+ ```
47
+
48
+ ## Utilisation Rapide
49
+ ### Initier un paiement (Async)
50
+ Le SDK gère les erreurs pour vous. Enveloppez simplement votre appel dans un bloc `try...except`.
51
+
52
+ ```python
53
+ import asyncio
54
+ from shwary import ShwaryAsync
55
+
56
+ async def main():
57
+ async with ShwaryAsync(
58
+ merchant_id="votre-uuid",
59
+ merchant_key="votre-cle-secrete",
60
+ is_sandbox=True
61
+ ) as client:
62
+ try:
63
+ # Le SDK lève une exception si l'API répond avec une erreur
64
+ payment = await client.initiate_payment(
65
+ country="DRC",
66
+ amount=5000,
67
+ phone_number="+243972345678",
68
+ callback_url="[https://votre-site.com/webhooks/shwary](https://votre-site.com/webhooks/shwary)"
69
+ )
70
+ print(f"ID Transaction: {payment['id']}")
71
+ except Exception as e:
72
+ print(f"Le paiement a échoué: {e}")
73
+
74
+ asyncio.run(main())
75
+ ```
76
+
77
+ ### Initier un paiement (Sync)
78
+ ```python
79
+ from shwary import Shwary
80
+
81
+ with Shwary(merchant_id="...", merchant_key="...", is_sandbox=False) as client:
82
+ response = client.initiate_payment(
83
+ country="KE",
84
+ amount=150.5,
85
+ phone_number="+254700000000"
86
+ )
87
+ ```
88
+
89
+ ## Vérifier une transaction
90
+ Si vous n'avez pas reçu de webhook ou souhaitez vérifier le statut actuel :
91
+
92
+ ```python
93
+ # Mode Synchrone
94
+ from shwary import Shwary
95
+
96
+ with Shwary(merchant_id="...", merchant_key="...") as client:
97
+ tx = client.get_transaction("votre-id-transaction")
98
+ print(f"Statut actuel : {tx['status']}")
99
+ ```
100
+
101
+ ## Validation par pays
102
+ Le SDK applique les règles métiers de Shwary localement pour économiser des appels réseau :
103
+
104
+ | Pays | Code | Devise | Montant Min. |
105
+ | :--- | :--- | :--- | :--- |
106
+ | RDC | DRC | CDF | 2900 |
107
+ | Kenya | KE | KES | > 0 |
108
+ | Ouganda | UG | UGX | > 0 |
109
+
110
+ ## Gestion des Webhooks (Callbacks)
111
+ Lorsque le statut d'un paiement change (ex: pending -> completed), Shwary envoie un POST JSON à votre callbackUrl. Voici comment traiter la charge utile avec les modèles du SDK (exemple FastAPI) :
112
+
113
+ ```python
114
+ from fastapi import FastAPI, Request
115
+
116
+ app = FastAPI()
117
+
118
+ @app.post("/webhooks/shwary")
119
+ async def shwary_webhook(data: dict):
120
+ # Shwary envoie le même format que la réponse initiate_payment
121
+ status = data.get("status")
122
+ transaction_id = data.get("id")
123
+
124
+ if status == "completed":
125
+ # Livrez votre service ici
126
+ pass
127
+
128
+ return {"status": "ok"}
129
+ ```
130
+
131
+ ## Gestion des Erreurs
132
+ Le SDK transforme les erreurs HTTP en exceptions Python. Vous n'avez pas besoin de vérifier manuellement les codes de statut, gérez simplement les exceptions :
133
+
134
+ | Exception | Cause |
135
+ | :--- | :--- |
136
+ | **ValidationError** | Données invalides (ex: montant < 2900 CDF en RDC, numéro mal formé). |
137
+ | **AuthenticationError** | Identifiants `merchant_id` ou `merchant_key` incorrects. |
138
+ | **ShwaryAPIError** | Erreur côté serveur Shwary ou problème réseau. |
139
+ | **ShwaryError** | Classe de base pour toutes les exceptions du SDK. |
140
+
141
+
142
+ ```python
143
+ from shwary.exceptions import ValidationError, AuthenticationError, ShwaryAPIError
144
+
145
+ try:
146
+ client.initiate_payment(...)
147
+ except ValidationError as e:
148
+ # Erreur de format téléphone ou montant insuffisant
149
+ print(f"Données invalides : {e}")
150
+ except AuthenticationError:
151
+ # Identifiants merchant_id / merchant_key invalides
152
+ print("Erreur d'authentification Shwary")
153
+ except ShwaryAPIError as e:
154
+ # Autres erreurs API (404, 500, etc.)
155
+ print(f"Erreur API {e.status_code}: {e.message}")
156
+ ```
157
+
158
+ ## Développement
159
+
160
+ Pour contribuer au SDK :
161
+
162
+ 1. Installez uv : curl -LsSf https://astral.sh/uv/install.sh | sh
163
+
164
+ 2. Installez les dépendances : uv sync
165
+
166
+ 3. Lancez les tests : uv run pytest
167
+
168
+ ### Licence
169
+
170
+ Distribué sous la licence MIT. Voir [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) pour plus d'informations.
@@ -0,0 +1,153 @@
1
+ # Shwary Python SDK
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/shwary-python.svg)](https://pypi.org/project/shwary-python/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/shwary-python.svg)](https://pypi.org/project/shwary-python/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ **Shwary Python** est une bibliothèque cliente moderne, asynchrone et performante (non officielle) pour l'intégration de l'API [Shwary](https://shwary.com). Elle permet d'initier des paiements Mobile Money en **RDC**, au **Kenya** et en **Ouganda** avec une validation stricte des données avant l'envoi.
8
+
9
+
10
+
11
+ ## Caractéristiques
12
+
13
+ * **Gestion d'erreurs native** : Pas besoin de vérifier les `status_code` manuellement. Le SDK lève des exceptions explicites (`AuthenticationError`, `ValidationError`, etc.).
14
+ * **Async-first** : Construit sur `httpx` pour des performances optimales (Pooling de connexions).
15
+ * **Dual-mode** : Support complet des modes Synchrone et Asynchrone.
16
+ * **Validation Robuste** : Vérification des numéros (E.164) et des montants minimums (ex: 2900 CDF pour la RDC).
17
+ * **Type-safe** : Basé sur Pydantic V2 pour une autocomplétion parfaite dans votre IDE.
18
+ * **Ultra-rapide** : Optimisé avec `uv` et `__slots__` pour minimiser l'empreinte mémoire.
19
+
20
+ ## Installation
21
+
22
+ Avec `uv` (recommandé) :
23
+ ```bash
24
+ uv add shwary-python
25
+ ```
26
+ Ou avec `pip`
27
+ ```bash
28
+ pip install shwary-python
29
+ ```
30
+
31
+ ## Utilisation Rapide
32
+ ### Initier un paiement (Async)
33
+ Le SDK gère les erreurs pour vous. Enveloppez simplement votre appel dans un bloc `try...except`.
34
+
35
+ ```python
36
+ import asyncio
37
+ from shwary import ShwaryAsync
38
+
39
+ async def main():
40
+ async with ShwaryAsync(
41
+ merchant_id="votre-uuid",
42
+ merchant_key="votre-cle-secrete",
43
+ is_sandbox=True
44
+ ) as client:
45
+ try:
46
+ # Le SDK lève une exception si l'API répond avec une erreur
47
+ payment = await client.initiate_payment(
48
+ country="DRC",
49
+ amount=5000,
50
+ phone_number="+243972345678",
51
+ callback_url="[https://votre-site.com/webhooks/shwary](https://votre-site.com/webhooks/shwary)"
52
+ )
53
+ print(f"ID Transaction: {payment['id']}")
54
+ except Exception as e:
55
+ print(f"Le paiement a échoué: {e}")
56
+
57
+ asyncio.run(main())
58
+ ```
59
+
60
+ ### Initier un paiement (Sync)
61
+ ```python
62
+ from shwary import Shwary
63
+
64
+ with Shwary(merchant_id="...", merchant_key="...", is_sandbox=False) as client:
65
+ response = client.initiate_payment(
66
+ country="KE",
67
+ amount=150.5,
68
+ phone_number="+254700000000"
69
+ )
70
+ ```
71
+
72
+ ## Vérifier une transaction
73
+ Si vous n'avez pas reçu de webhook ou souhaitez vérifier le statut actuel :
74
+
75
+ ```python
76
+ # Mode Synchrone
77
+ from shwary import Shwary
78
+
79
+ with Shwary(merchant_id="...", merchant_key="...") as client:
80
+ tx = client.get_transaction("votre-id-transaction")
81
+ print(f"Statut actuel : {tx['status']}")
82
+ ```
83
+
84
+ ## Validation par pays
85
+ Le SDK applique les règles métiers de Shwary localement pour économiser des appels réseau :
86
+
87
+ | Pays | Code | Devise | Montant Min. |
88
+ | :--- | :--- | :--- | :--- |
89
+ | RDC | DRC | CDF | 2900 |
90
+ | Kenya | KE | KES | > 0 |
91
+ | Ouganda | UG | UGX | > 0 |
92
+
93
+ ## Gestion des Webhooks (Callbacks)
94
+ Lorsque le statut d'un paiement change (ex: pending -> completed), Shwary envoie un POST JSON à votre callbackUrl. Voici comment traiter la charge utile avec les modèles du SDK (exemple FastAPI) :
95
+
96
+ ```python
97
+ from fastapi import FastAPI, Request
98
+
99
+ app = FastAPI()
100
+
101
+ @app.post("/webhooks/shwary")
102
+ async def shwary_webhook(data: dict):
103
+ # Shwary envoie le même format que la réponse initiate_payment
104
+ status = data.get("status")
105
+ transaction_id = data.get("id")
106
+
107
+ if status == "completed":
108
+ # Livrez votre service ici
109
+ pass
110
+
111
+ return {"status": "ok"}
112
+ ```
113
+
114
+ ## Gestion des Erreurs
115
+ Le SDK transforme les erreurs HTTP en exceptions Python. Vous n'avez pas besoin de vérifier manuellement les codes de statut, gérez simplement les exceptions :
116
+
117
+ | Exception | Cause |
118
+ | :--- | :--- |
119
+ | **ValidationError** | Données invalides (ex: montant < 2900 CDF en RDC, numéro mal formé). |
120
+ | **AuthenticationError** | Identifiants `merchant_id` ou `merchant_key` incorrects. |
121
+ | **ShwaryAPIError** | Erreur côté serveur Shwary ou problème réseau. |
122
+ | **ShwaryError** | Classe de base pour toutes les exceptions du SDK. |
123
+
124
+
125
+ ```python
126
+ from shwary.exceptions import ValidationError, AuthenticationError, ShwaryAPIError
127
+
128
+ try:
129
+ client.initiate_payment(...)
130
+ except ValidationError as e:
131
+ # Erreur de format téléphone ou montant insuffisant
132
+ print(f"Données invalides : {e}")
133
+ except AuthenticationError:
134
+ # Identifiants merchant_id / merchant_key invalides
135
+ print("Erreur d'authentification Shwary")
136
+ except ShwaryAPIError as e:
137
+ # Autres erreurs API (404, 500, etc.)
138
+ print(f"Erreur API {e.status_code}: {e.message}")
139
+ ```
140
+
141
+ ## Développement
142
+
143
+ Pour contribuer au SDK :
144
+
145
+ 1. Installez uv : curl -LsSf https://astral.sh/uv/install.sh | sh
146
+
147
+ 2. Installez les dépendances : uv sync
148
+
149
+ 3. Lancez les tests : uv run pytest
150
+
151
+ ### Licence
152
+
153
+ Distribué sous la licence MIT. Voir [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) pour plus d'informations.
@@ -0,0 +1,49 @@
1
+ [project]
2
+ name = "shwary-python"
3
+ version = "1.0.1"
4
+ description = "SDK Python moderne (Async/Sync) pour l'API de paiement Shwary."
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Josué Luis Panzu", email = "josuepanzu8@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.11"
10
+ dependencies = [
11
+ "httpx>=0.28.1",
12
+ "phonenumbers>=9.0.22",
13
+ "pydantic>=2.12.5",
14
+ ]
15
+ license = { text = "MIT" }
16
+ keywords = ["shwary", "fintech", "payment", "mobile-money", "africa", "rdc", "kenya", "uganda"]
17
+ classifiers = [
18
+ "Programming Language :: Python :: 3",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ "Topic :: Software Development :: Libraries :: Python Modules",
22
+ ]
23
+
24
+ [build-system]
25
+ requires = ["hatchling"]
26
+ build-backend = "hatchling.build"
27
+
28
+ [dependency-groups]
29
+ dev = [
30
+ "pytest>=9.0.2",
31
+ "pytest-asyncio>=1.3.0",
32
+ "pytest-mock>=3.15.1",
33
+ "respx>=0.22.0",
34
+ "ruff>=0.14.14",
35
+ ]
36
+
37
+ [tool.pytest.ini_options]
38
+ testpaths = ["tests"]
39
+ asyncio_mode = "auto"
40
+
41
+ [tool.ruff]
42
+ line-length = 88
43
+ select = ["E", "F", "I"] # Erreurs, Flakes, et tris d'Imports
44
+
45
+ [tool.hatch.build.targets.sdist]
46
+ packages = ["src/shwary"]
47
+
48
+ [tool.hatch.build.targets.wheel]
49
+ packages = ["src/shwary"]
@@ -0,0 +1,4 @@
1
+ from .clients import Shwary, ShwaryAsync
2
+ from .exceptions import ShwaryError, ValidationError, AuthenticationError, ShwaryAPIError
3
+
4
+ __all__ = ["Shwary", "ShwaryAsync", "ShwaryError", "ValidationError", "AuthenticationError", "ShwaryAPIError"]
@@ -0,0 +1,4 @@
1
+ from .async_client import ShwaryAsync
2
+ from .sync import Shwary
3
+
4
+ __all__ = ("Shwary", "ShwaryAsync")
@@ -0,0 +1,59 @@
1
+ import httpx
2
+ from ..core import prepare_payment_request
3
+ from ..exceptions import raise_from_response
4
+
5
+
6
+ class ShwaryAsync:
7
+ """
8
+ Client asynchrone haute performance pour l'API Shwary.
9
+ Doit être utilisé comme context manager ou fermé manuellement.
10
+ """
11
+ __slots__ = ('merchant_id', 'merchant_key', 'is_sandbox', '_client', '_base_url')
12
+
13
+ def __init__(self, merchant_id: str, merchant_key: str, is_sandbox: bool = False, timeout: float = 30.0):
14
+ self.is_sandbox = is_sandbox
15
+ self.merchant_id = merchant_id
16
+ self.merchant_key = merchant_key
17
+ self._base_url = "https://api.shwary.com/api/v1/merchants"
18
+ self._client = httpx.AsyncClient(
19
+ base_url=self._base_url,
20
+ timeout=timeout,
21
+ headers={
22
+ "x-merchant-id": merchant_id,
23
+ "x-merchant-key": merchant_key,
24
+ "Content-Type": "application/json",
25
+ "User-Agent": "Shwary-Python-SDK/0.1.0"
26
+ }
27
+ )
28
+
29
+ async def close(self):
30
+ await self._client.aclose()
31
+
32
+ async def __aenter__(self):
33
+ return self
34
+
35
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
36
+ await self.close()
37
+
38
+ async def initiate_payment(self, country: str, amount: float, phone_number: str, callback_url: str = None) -> dict:
39
+ """Initie une demande de paiement."""
40
+
41
+ endpoint, json_data = prepare_payment_request(country, amount, phone_number, callback_url, self.is_sandbox)
42
+
43
+ response = await self._client.post(endpoint, json=json_data)
44
+
45
+ # Gestion centralisée des erreurs
46
+ raise_from_response(response)
47
+
48
+ return response.json()
49
+
50
+ async def get_transaction(self, transaction_id: str) -> dict:
51
+ """
52
+ Récupère les détails d'une transaction unique à partir de son ID.
53
+ """
54
+ endpoint: str = f"/transactions/{transaction_id}"
55
+ response = await self._client.get(endpoint)
56
+
57
+ raise_from_response(response)
58
+
59
+ return response.json()
@@ -0,0 +1,59 @@
1
+ import httpx
2
+ from ..core import prepare_payment_request
3
+ from ..exceptions import raise_from_response
4
+
5
+
6
+ class Shwary:
7
+ """
8
+ Client synchrone pour l'API Shwary.
9
+ À utiliser pour des scripts `standard, Django, Flask,` etc.
10
+ Doit être utilisé comme context manager ou fermé manuellement.
11
+ """
12
+ __slots__ = ('merchant_id', 'merchant_key', 'is_sandbox', '_client', '_base_url')
13
+
14
+ def __init__(self, merchant_id: str, merchant_key: str, is_sandbox: bool = False, timeout: float = 30.0):
15
+ self.is_sandbox = is_sandbox
16
+ self.merchant_id = merchant_id
17
+ self.merchant_key = merchant_key
18
+ self._base_url = "https://api.shwary.com/api/v1/merchants"
19
+ self._client = httpx.Client(
20
+ base_url=self._base_url,
21
+ timeout=timeout,
22
+ headers={
23
+ "x-merchant-id": merchant_id,
24
+ "x-merchant-key": merchant_key,
25
+ "Content-Type": "application/json",
26
+ "User-Agent": "Shwary-Python-SDK/0.1.0"
27
+ }
28
+ )
29
+
30
+ def close(self):
31
+ self._client.close()
32
+
33
+ def __enter__(self):
34
+ return self
35
+
36
+ def __exit__(self, exc_type, exc_val, exc_tb):
37
+ self.close()
38
+
39
+ def initiate_payment(self, country: str, amount: float, phone_number: str, callback_url: str = None) -> dict:
40
+ """Initie une demande de paiement."""
41
+
42
+ endpoint, json_data = prepare_payment_request(country, amount, phone_number, callback_url, self.is_sandbox)
43
+
44
+ response = self._client.post(endpoint, json=json_data)
45
+
46
+ raise_from_response(response)
47
+
48
+ return response.json()
49
+
50
+ def get_transaction(self, transaction_id: str) -> dict:
51
+ """
52
+ Récupère les détails d'une transaction unique à partir de son ID.
53
+ """
54
+ endpoint: str = f"/transactions/{transaction_id}"
55
+ response = self._client.get(endpoint)
56
+
57
+ raise_from_response(response)
58
+
59
+ return response.json()
@@ -0,0 +1,32 @@
1
+ from typing import Any
2
+ from .schemas import PaymentPayload, CountryCode
3
+ from .exceptions import ValidationError
4
+
5
+ def prepare_payment_request(
6
+ country: CountryCode | str,
7
+ amount: float,
8
+ phone: str,
9
+ callback: str,
10
+ is_sandbox: bool
11
+ ) -> tuple[str, dict[str, Any]]:
12
+ """
13
+ Prépare l'URL et le Payload validé.
14
+ Partagé par les clients Sync et Async pour éviter la duplication de logique métier.
15
+ """
16
+ # Validation & Conversion
17
+ try:
18
+ country_enum: CountryCode = CountryCode(country) if isinstance(country, str) else country
19
+ payload: PaymentPayload = PaymentPayload(
20
+ amount=amount,
21
+ clientPhoneNumber=phone,
22
+ callbackUrl=callback,
23
+ country_target=country_enum
24
+ )
25
+ except ValueError as e:
26
+ # On capture les erreurs Pydantic pour les relancer en ValidationError du SDK
27
+ raise ValidationError(str(e)) from e
28
+
29
+ env_path: str = "sandbox/" if is_sandbox else ""
30
+ endpoint: str = f"/payment/{env_path}{country_enum.value}"
31
+
32
+ return endpoint, payload.model_dump(exclude={'country_target'}, exclude_none=True)
@@ -0,0 +1,41 @@
1
+ class ShwaryError(Exception):
2
+ """Exception de base pour toutes les erreurs Shwary."""
3
+ pass
4
+
5
+ class ValidationError(ShwaryError):
6
+ """Levée avant même l'envoi de la requête (ex: mauvais numéro)."""
7
+ pass
8
+
9
+ class AuthenticationError(ShwaryError):
10
+ """Levée sur une erreur 401 (Clé API invalide)."""
11
+ pass
12
+
13
+ class InsufficientFundsError(ShwaryError):
14
+ """Levée si le solde marchand est insuffisant ou règles métiers non respectées."""
15
+ pass
16
+
17
+ class ShwaryAPIError(ShwaryError):
18
+ """Erreur générique retournée par l'API (400, 500, etc.)."""
19
+ def __init__(self, status_code: int, message: str, raw_response: dict = None):
20
+ self.status_code = status_code
21
+ self.raw_response = raw_response
22
+ super().__init__(f"Shwary Error {status_code}: {message}")
23
+
24
+ def raise_from_response(response):
25
+ """Helper pour convertir une réponse HTTP en exception Python."""
26
+ if response.is_success:
27
+ return
28
+
29
+ status = response.status_code
30
+ try:
31
+ data = response.json()
32
+ message = data.get("message", response.text)
33
+ except Exception:
34
+ message = response.text
35
+
36
+ if status == 401:
37
+ raise AuthenticationError(f"Échec d'authentification : {message}")
38
+ elif status == 400 and "balance" in message.lower():
39
+ raise InsufficientFundsError(message)
40
+ else:
41
+ raise ShwaryAPIError(status, message, raw_response=data if 'data' in locals() else None)
File without changes
@@ -0,0 +1,56 @@
1
+ from enum import Enum
2
+ from typing import Optional
3
+ import phonenumbers
4
+ from phonenumbers import NumberParseException
5
+ from pydantic import BaseModel, Field, field_validator, model_validator
6
+
7
+ class CountryCode(str, Enum):
8
+ DRC = "DRC"
9
+ KENYA = "KE"
10
+ UGANDA = "UG"
11
+
12
+ class PaymentPayload(BaseModel):
13
+ amount: float = Field(..., gt=0, description="Montant de la transaction")
14
+ clientPhoneNumber: str = Field(..., description="Numéro au format international")
15
+ callbackUrl: Optional[str] = None
16
+ country_target: CountryCode = Field(..., exclude=True)
17
+
18
+ @field_validator("clientPhoneNumber")
19
+ @classmethod
20
+ def validate_phone(cls, v: str) -> str:
21
+ try:
22
+ # LazyParsing pour accepter +243... ou 243...
23
+ parsed = phonenumbers.parse(v, None)
24
+ if not phonenumbers.is_valid_number(parsed):
25
+ raise ValueError("Numéro de téléphone invalide.")
26
+
27
+ # Formatage strict E.164 (ex: +24381...) requis par Shwary
28
+ return phonenumbers.format_number(parsed, phonenumbers.PhoneNumberFormat.E164)
29
+ except NumberParseException:
30
+ raise ValueError("Format de numéro impossible à analyser.")
31
+
32
+ @model_validator(mode='after')
33
+ def validate_country_consistency(self):
34
+ """
35
+ Fait une validation croisée :
36
+ On ne peut pas envoyer un numéro +254 (Kenya) vers l'endpoint DRC
37
+ """
38
+
39
+ phone: str = self.clientPhoneNumber
40
+ country: CountryCode = self.country_target
41
+
42
+ prefixes: dict[CountryCode, str] = {
43
+ CountryCode.DRC: "+243",
44
+ CountryCode.KENYA: "+254",
45
+ CountryCode.UGANDA: "+256"
46
+ }
47
+
48
+ expected_prefix = prefixes.get(country)
49
+ if expected_prefix and not phone.startswith(expected_prefix):
50
+ raise ValueError(f"Le numéro {phone} ne correspond pas au pays ciblé ({country.value}).")
51
+
52
+ # Règle métier Shwary : Montant min pour RDC
53
+ if country == CountryCode.DRC and self.amount < 2900:
54
+ raise ValueError("Le montant minimum pour la RDC est de 2900 CDF.")
55
+
56
+ return self
@@ -0,0 +1,90 @@
1
+ import pytest
2
+ import respx
3
+ from httpx import Response
4
+ from shwary import Shwary
5
+ from shwary.exceptions import ValidationError, AuthenticationError
6
+
7
+ # --- FIXTURES (Données de test) ---
8
+ @pytest.fixture
9
+ def client() -> Shwary:
10
+ """Crée un client synchrone pour les tests."""
11
+ return Shwary(
12
+ merchant_id="merchant-test-id",
13
+ merchant_key="merchant-test-key",
14
+ is_sandbox=True
15
+ )
16
+
17
+
18
+ def test_validation_fail_fast(client):
19
+ """Vérifie que le SDK bloque les requêtes invalides localement."""
20
+ # Test numéro invalide
21
+ with pytest.raises(ValidationError) as exc:
22
+ client.initiate_payment("DRC", 5000, "ceci n'est pas un numéro")
23
+
24
+ # Vérification de la présence d'un mot clé réellement présent dans le message d'erreur
25
+ error_msg = str(exc.value)
26
+ assert "impossible à analyser" in error_msg or "invalide" in error_msg
27
+
28
+ # Test montant trop bas pour la RDC
29
+ with pytest.raises(ValidationError) as exc:
30
+ client.initiate_payment("DRC", 100, "+243840000000")
31
+ assert "2900" in str(exc.value)
32
+
33
+ @respx.mock
34
+ def test_initiate_payment_success(client):
35
+ """Simule une réponse 200 OK de l'API Shwary."""
36
+
37
+ # On définit ce que l'API est censée répondre
38
+ mock_response: dict[str, bool | str] = {
39
+ "id": "trans-123",
40
+ "status": "pending",
41
+ "isSandbox": True
42
+ }
43
+
44
+ # Interception de la requête POST vers l'URL sandbox
45
+ route = respx.post("https://api.shwary.com/api/v1/merchants/payment/sandbox/DRC").mock(
46
+ return_value=Response(200, json=mock_response)
47
+ )
48
+
49
+ # On initialise la demande de paiement
50
+ response = client.initiate_payment("DRC", 5000, "+243972345678")
51
+
52
+ # Vérifications
53
+ assert route.called
54
+ assert response["id"] == "trans-123"
55
+ assert response["status"] == "pending"
56
+
57
+ @respx.mock
58
+ def test_authentication_error(client):
59
+ """Simule une erreur 401 (Mauvaise clé API)."""
60
+
61
+ respx.post("https://api.shwary.com/api/v1/merchants/payment/sandbox/KE").mock(
62
+ return_value=Response(401, json={"message": "Invalid merchant key"})
63
+ )
64
+
65
+ with pytest.raises(AuthenticationError) as exc:
66
+ client.initiate_payment("KE", 1000, "+254700000000")
67
+
68
+ assert "Invalid merchant key" in str(exc.value)
69
+
70
+ @respx.mock
71
+ def test_get_transaction_success(client):
72
+ """Vérifie la récupération d'une transaction par ID."""
73
+ transaction_id = "c0fdfe50-24be-4de1-9f66-84608fd45a5f"
74
+
75
+ mock_response: dict[str, int | str] = {
76
+ "id": transaction_id,
77
+ "status": "completed",
78
+ "amount": 5000
79
+ }
80
+
81
+ # On intercepte le GET
82
+ route = respx.get(f"https://api.shwary.com/api/v1/merchants/transactions/{transaction_id}").mock(
83
+ return_value=Response(200, json=mock_response)
84
+ )
85
+
86
+ response = client.get_transaction(transaction_id)
87
+
88
+ assert route.called
89
+ assert response["id"] == transaction_id
90
+ assert response["status"] == "completed"
@@ -0,0 +1,30 @@
1
+ import pytest
2
+ import respx
3
+ from httpx import Response
4
+ from shwary import ShwaryAsync
5
+
6
+ @pytest.fixture
7
+ def async_client() -> ShwaryAsync:
8
+ """Crée un client asynchrone pour les tests."""
9
+ return ShwaryAsync(
10
+ merchant_id="merchant-test-id",
11
+ merchant_key="merchant-test-key",
12
+ is_sandbox=True
13
+ )
14
+
15
+ @pytest.mark.asyncio
16
+ @respx.mock
17
+ async def test_async_payment_success(async_client):
18
+ """Vérifie que le client Async fonctionne aussi."""
19
+
20
+ mock_response: dict[str, str] = {"id": "async-123", "status": "completed"}
21
+
22
+ respx.post("https://api.shwary.com/api/v1/merchants/payment/sandbox/UG").mock(
23
+ return_value=Response(200, json=mock_response)
24
+ )
25
+
26
+ # Notez l'utilisation de 'async with' et 'await'
27
+ async with async_client as c:
28
+ response = await c.initiate_payment("UG", 5000, "+256700000000")
29
+
30
+ assert response["id"] == "async-123"