alphapay 0.1.0__py3-none-any.whl

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.
alphapay/__init__.py ADDED
@@ -0,0 +1,36 @@
1
+ """Official Python SDK for the AlphaPay payment aggregator API."""
2
+
3
+ from .client import AlphaPayClient
4
+ from .exceptions import (
5
+ AlphaPayAuthenticationError,
6
+ AlphaPayConnectionError,
7
+ AlphaPayError,
8
+ AlphaPayIdempotencyError,
9
+ AlphaPayNotFoundError,
10
+ AlphaPayPermissionError,
11
+ AlphaPayRateLimitError,
12
+ AlphaPayServerError,
13
+ AlphaPayValidationError,
14
+ AlphaPayWebhookSignatureError,
15
+ )
16
+ from .pagination import paginate
17
+ from .webhook import verify_signature
18
+
19
+ __version__ = "0.1.0"
20
+
21
+ __all__ = [
22
+ "AlphaPayClient",
23
+ "AlphaPayError",
24
+ "AlphaPayAuthenticationError",
25
+ "AlphaPayPermissionError",
26
+ "AlphaPayNotFoundError",
27
+ "AlphaPayValidationError",
28
+ "AlphaPayRateLimitError",
29
+ "AlphaPayServerError",
30
+ "AlphaPayConnectionError",
31
+ "AlphaPayIdempotencyError",
32
+ "AlphaPayWebhookSignatureError",
33
+ "verify_signature",
34
+ "paginate",
35
+ "__version__",
36
+ ]
alphapay/client.py ADDED
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from .http import DEFAULT_MAX_RETRIES, DEFAULT_TIMEOUT, Http
6
+ from .resources.api_keys import ApiKeysResource
7
+ from .resources.balances import BalancesResource
8
+ from .resources.checkout_sessions import CheckoutSessionsResource
9
+ from .resources.customers import CustomersResource
10
+ from .resources.payment_links import PaymentLinksResource
11
+ from .resources.settlements import SettlementsResource
12
+ from .resources.transactions import TransactionsResource
13
+ from .resources.wallet_transfers import WalletTransfersResource
14
+ from .resources.webhook_endpoints import WebhookEndpointsResource
15
+
16
+
17
+ class AlphaPayClient:
18
+ """Client principal du SDK AlphaPay. Une instance = une clé API = un
19
+ marchand + un environnement (live/sandbox, déduit automatiquement du
20
+ préfixe de la clé -- ``sk_live_...`` ou ``sk_test_...``).
21
+
22
+ :Example:
23
+
24
+ >>> alphapay = AlphaPayClient(os.environ["ALPHAPAY_SECRET_KEY"])
25
+ >>> payment = alphapay.transactions.payin_initialize(
26
+ ... amount=5000,
27
+ ... currency="XOF",
28
+ ... country="BJ",
29
+ ... network="mtn_bj",
30
+ ... customer={"email": "client@exemple.com", "first_name": "Client", "last_name": "Test", "phone": "+22900000000"},
31
+ ... idempotency_key=True,
32
+ ... )
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ api_key: str,
38
+ base_url: Optional[str] = None,
39
+ timeout: float = DEFAULT_TIMEOUT,
40
+ max_retries: int = DEFAULT_MAX_RETRIES,
41
+ ) -> None:
42
+ #: Client HTTP bas niveau -- nécessaire à :func:`alphapay.pagination.paginate`
43
+ #: pour suivre un lien "next" (URL absolue renvoyée par l'API), pas
44
+ #: destiné à un usage direct en dehors de ce cas.
45
+ self.http = Http(api_key, base_url, timeout, max_retries)
46
+ self.environment = self.http.environment
47
+
48
+ self.transactions = TransactionsResource(self.http)
49
+ self.payment_links = PaymentLinksResource(self.http)
50
+ self.checkout_sessions = CheckoutSessionsResource(self.http)
51
+ self.customers = CustomersResource(self.http)
52
+ self.settlements = SettlementsResource(self.http)
53
+ self.wallet_transfers = WalletTransfersResource(self.http)
54
+ self.balances = BalancesResource(self.http)
55
+ self.api_keys = ApiKeysResource(self.http)
56
+ self.webhook_endpoints = WebhookEndpointsResource(self.http)
alphapay/exceptions.py ADDED
@@ -0,0 +1,138 @@
1
+ """
2
+ Toute erreur API passe par apps.core.renderers.StandardJSONRenderer côté
3
+ AlphaPayBack :
4
+ {"success": false, "error": <forme variable>, "code": <statut HTTP>}
5
+ `error` n'a PAS une forme unique :
6
+ - erreurs de validation DRF -> {"champ": ["message", ...], ...}
7
+ - erreurs métier personnalisées -> {"message": "...", "code": "<code_machine>"}
8
+ - erreurs d'authentification/permission -> {"detail": "..."}
9
+ str(exception) normalise ces trois formes en une seule chaîne lisible ;
10
+ `.raw` garde la forme originale pour qui a besoin du détail par champ.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from typing import Any, Dict, List, Optional
16
+
17
+
18
+ class AlphaPayError(Exception):
19
+ """Exception de base -- toutes les erreurs API en sont une sous-classe."""
20
+
21
+ def __init__(
22
+ self,
23
+ message: str,
24
+ *,
25
+ status: int,
26
+ error_code: Optional[str] = None,
27
+ raw: Any = None,
28
+ field_errors: Optional[Dict[str, List[str]]] = None,
29
+ request_id: Optional[str] = None,
30
+ ) -> None:
31
+ super().__init__(message)
32
+ self.status = status
33
+ #: Code machine, quand l'API en fournit un (ex. "dashboard_only", "invalid_file_type") --
34
+ #: absent sur les erreurs de validation par champ.
35
+ self.error_code = error_code
36
+ #: Corps d'erreur brut, tel que renvoyé par l'API.
37
+ self.raw = raw
38
+ #: Présent uniquement sur une erreur de validation DRF ({"champ": [...]}).
39
+ self.field_errors = field_errors
40
+ self.request_id = request_id
41
+
42
+
43
+ class AlphaPayAuthenticationError(AlphaPayError):
44
+ pass
45
+
46
+
47
+ class AlphaPayPermissionError(AlphaPayError):
48
+ pass
49
+
50
+
51
+ class AlphaPayNotFoundError(AlphaPayError):
52
+ pass
53
+
54
+
55
+ class AlphaPayValidationError(AlphaPayError):
56
+ pass
57
+
58
+
59
+ class AlphaPayRateLimitError(AlphaPayError):
60
+ """429 -- `retry_after` (secondes) est renseigné quand l'API précise un header Retry-After."""
61
+
62
+ def __init__(self, message: str, *, retry_after: Optional[int] = None, **kwargs: Any) -> None:
63
+ super().__init__(message, **kwargs)
64
+ self.retry_after = retry_after
65
+
66
+
67
+ class AlphaPayServerError(AlphaPayError):
68
+ pass
69
+
70
+
71
+ class AlphaPayConnectionError(AlphaPayError):
72
+ """Connexion/DNS/timeout -- jamais atteint le serveur AlphaPay, `status` vaut 0."""
73
+
74
+ pass
75
+
76
+
77
+ class AlphaPayIdempotencyError(AlphaPayError):
78
+ """409 -- Idempotency-Key réutilisée avec un payload différent (cf. apps.core.mixins.IdempotencyMixin côté API)."""
79
+
80
+ pass
81
+
82
+
83
+ class AlphaPayWebhookSignatureError(Exception):
84
+ """Levée par alphapay.webhook.verify_signature() -- signature invalide ou timestamp hors fenêtre de tolérance (rejeu)."""
85
+
86
+ pass
87
+
88
+
89
+ def error_from_response(status: int, body: Any, request_id: Optional[str] = None) -> AlphaPayError:
90
+ """Construit l'exception normalisée à partir de la réponse HTTP. `body` est
91
+ déjà le contenu de la clé "error" de l'enveloppe (pas l'enveloppe entière).
92
+ """
93
+ message, error_code, field_errors = _interpret_error_body(body)
94
+
95
+ kwargs = dict(status=status, error_code=error_code, raw=body, field_errors=field_errors, request_id=request_id)
96
+
97
+ if status == 401:
98
+ return AlphaPayAuthenticationError(message, **kwargs)
99
+ if status == 403:
100
+ return AlphaPayPermissionError(message, **kwargs)
101
+ if status == 404:
102
+ return AlphaPayNotFoundError(message, **kwargs)
103
+ if status == 409:
104
+ return AlphaPayIdempotencyError(message, **kwargs)
105
+ if status == 429:
106
+ return AlphaPayRateLimitError(message, **kwargs)
107
+ if status in (400, 422):
108
+ return AlphaPayValidationError(message, **kwargs)
109
+ if status >= 500:
110
+ return AlphaPayServerError(message, **kwargs)
111
+ return AlphaPayError(message, **kwargs)
112
+
113
+
114
+ def _interpret_error_body(body: Any):
115
+ if body is None:
116
+ return "Erreur AlphaPay inconnue.", None, None
117
+ if isinstance(body, str):
118
+ return body, None, None
119
+ if isinstance(body, dict):
120
+ # {"message": "...", "code": "..."} -- erreurs métier personnalisées.
121
+ message = body.get("message")
122
+ if isinstance(message, str):
123
+ code = body.get("code")
124
+ return message, code if isinstance(code, str) else None, None
125
+ # {"detail": "..."} -- erreurs d'auth/permission DRF standard.
126
+ detail = body.get("detail")
127
+ if isinstance(detail, str):
128
+ return detail, None, None
129
+ # {"champ": ["erreur", ...], ...} -- erreurs de validation DRF.
130
+ field_errors = {
131
+ field: value
132
+ for field, value in body.items()
133
+ if isinstance(field, str) and isinstance(value, list) and value and all(isinstance(v, str) for v in value)
134
+ }
135
+ if field_errors:
136
+ summary = " — ".join(f"{field}: {', '.join(errors)}" for field, errors in field_errors.items())
137
+ return summary, None, field_errors
138
+ return "Erreur AlphaPay inconnue.", None, None
alphapay/http.py ADDED
@@ -0,0 +1,198 @@
1
+ """
2
+ Client HTTP bas niveau, partagé par toutes les ressources. Gère
3
+ l'authentification, l'enveloppe standard {success, data, code}, les retries
4
+ avec backoff sur 429/5xx/erreur réseau, et le mapping vers des exceptions
5
+ typées (cf. exceptions.py) -- chaque ressource (Transactions, PaymentLinks,
6
+ ...) n'a plus qu'à décrire le endpoint, jamais la mécanique réseau.
7
+
8
+ Volontairement stdlib uniquement (urllib), pas de dépendance à `requests` --
9
+ cohérent avec les SDK Node/PHP jumeaux (0 dépendance runtime).
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import json
15
+ import random
16
+ import secrets
17
+ import socket
18
+ import time
19
+ import urllib.error
20
+ import urllib.parse
21
+ import urllib.request
22
+ from typing import Any, Dict, Optional, Union
23
+
24
+ from .exceptions import (
25
+ AlphaPayConnectionError,
26
+ AlphaPayError,
27
+ AlphaPayRateLimitError,
28
+ AlphaPayServerError,
29
+ error_from_response,
30
+ )
31
+
32
+ DEFAULT_BASE_URL = "https://api.alphapay.me/api/v1"
33
+ DEFAULT_TIMEOUT = 30.0
34
+ DEFAULT_MAX_RETRIES = 2
35
+
36
+
37
+ class Http:
38
+ def __init__(
39
+ self,
40
+ api_key: str,
41
+ base_url: Optional[str] = None,
42
+ timeout: float = DEFAULT_TIMEOUT,
43
+ max_retries: int = DEFAULT_MAX_RETRIES,
44
+ ) -> None:
45
+ if not api_key:
46
+ raise ValueError("AlphaPayClient: `api_key` est requis.")
47
+ self._api_key = api_key
48
+ self.environment = "live" if api_key.startswith("sk_live_") else "sandbox"
49
+ self.base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
50
+ self._timeout = timeout
51
+ self._max_retries = max_retries
52
+
53
+ def request(
54
+ self,
55
+ method: str,
56
+ path: str,
57
+ *,
58
+ query: Optional[Dict[str, Any]] = None,
59
+ body: Optional[Dict[str, Any]] = None,
60
+ idempotency_key: Union[str, bool, None] = None,
61
+ ) -> Any:
62
+ # `path` est une URL absolue quand on suit un lien "next"/"previous"
63
+ # renvoyé tel quel par l'API (cf. pagination.paginate()) -- déjà
64
+ # complète, avec sa propre query string ; ne jamais la préfixer par
65
+ # base_url ni y rajouter `query` par-dessus.
66
+ if path.startswith("http://") or path.startswith("https://"):
67
+ url = path
68
+ else:
69
+ qs = self._build_query_string(query)
70
+ url = f"{self.base_url}{path}{qs}"
71
+
72
+ headers = {"Authorization": f"Bearer {self._api_key}", "Accept": "application/json"}
73
+ data: Optional[bytes] = None
74
+ if body is not None:
75
+ headers["Content-Type"] = "application/json"
76
+ data = json.dumps(body).encode("utf-8")
77
+ if idempotency_key:
78
+ key = idempotency_key if isinstance(idempotency_key, str) else self._random_idempotency_key()
79
+ headers["Idempotency-Key"] = key
80
+
81
+ attempt = 0
82
+ while True:
83
+ try:
84
+ return self._attempt(method, url, headers, data)
85
+ except (AlphaPayRateLimitError, AlphaPayServerError) as exc:
86
+ if attempt >= self._max_retries:
87
+ raise
88
+ retry_after = getattr(exc, "retry_after", None)
89
+ delay = retry_after if retry_after is not None else self._backoff_seconds(attempt)
90
+ time.sleep(delay)
91
+ attempt += 1
92
+ except AlphaPayConnectionError:
93
+ if attempt >= self._max_retries:
94
+ raise
95
+ time.sleep(self._backoff_seconds(attempt))
96
+ attempt += 1
97
+
98
+ def request_binary(self, path: str) -> Dict[str, Any]:
99
+ """Pour les rares endpoints qui répondent en binaire plutôt qu'en JSON
100
+ (ex. GET /transactions/{id}/invoice/, application/pdf, et
101
+ /transactions/export/, text/csv) -- bypass l'enveloppe standard,
102
+ jamais utilisée par un endpoint qui en renvoie une.
103
+
104
+ Retourne {"data": bytes, "content_type": str|None, "filename": str|None}.
105
+ """
106
+ url = f"{self.base_url}{path}"
107
+ req = urllib.request.Request(
108
+ url,
109
+ headers={"Authorization": f"Bearer {self._api_key}", "Accept": "application/pdf, text/csv, application/octet-stream"},
110
+ method="GET",
111
+ )
112
+ try:
113
+ with urllib.request.urlopen(req, timeout=self._timeout) as response:
114
+ raw = response.read()
115
+ content_type = response.headers.get("Content-Type")
116
+ filename = self._extract_filename(response.headers.get("Content-Disposition"))
117
+ return {"data": raw, "content_type": content_type, "filename": filename}
118
+ except urllib.error.HTTPError as exc:
119
+ # Une erreur sur cet endpoint reste en JSON (cf. InvoiceError côté API) --
120
+ # on retombe sur le mapping JSON normal pour un message exploitable.
121
+ raise self._error_from_http_error(exc) from exc
122
+ except socket.timeout as exc:
123
+ raise AlphaPayConnectionError("Requête AlphaPay expirée.", status=0) from exc
124
+ except urllib.error.URLError as exc:
125
+ raise AlphaPayConnectionError(f"Impossible de joindre l'API AlphaPay : {exc.reason}", status=0) from exc
126
+
127
+ def _attempt(self, method: str, url: str, headers: Dict[str, str], data: Optional[bytes]) -> Any:
128
+ req = urllib.request.Request(url, data=data, headers=headers, method=method)
129
+ try:
130
+ with urllib.request.urlopen(req, timeout=self._timeout) as response:
131
+ body = response.read()
132
+ request_id = response.headers.get("X-Request-Id")
133
+ return self._unwrap_success(body, request_id)
134
+ except urllib.error.HTTPError as exc:
135
+ raise self._error_from_http_error(exc) from exc
136
+ except socket.timeout as exc:
137
+ raise AlphaPayConnectionError("Requête AlphaPay expirée.", status=0) from exc
138
+ except urllib.error.URLError as exc:
139
+ reason = exc.reason
140
+ is_timeout = "timed out" in str(reason).lower()
141
+ message = "Requête AlphaPay expirée." if is_timeout else f"Impossible de joindre l'API AlphaPay : {reason}"
142
+ raise AlphaPayConnectionError(message, status=0) from exc
143
+
144
+ def _error_from_http_error(self, exc: urllib.error.HTTPError) -> AlphaPayError:
145
+ request_id = exc.headers.get("X-Request-Id") if exc.headers else None
146
+ raw_body = exc.read()
147
+ json_body = self._try_parse_json(raw_body)
148
+ error_body = json_body.get("error") if isinstance(json_body, dict) and "error" in json_body else json_body
149
+ error = error_from_response(exc.code, error_body, request_id)
150
+ if isinstance(error, AlphaPayRateLimitError):
151
+ retry_after = exc.headers.get("Retry-After") if exc.headers else None
152
+ error.retry_after = int(retry_after) if retry_after else None
153
+ return error
154
+
155
+ def _unwrap_success(self, raw_body: bytes, request_id: Optional[str]) -> Any:
156
+ json_body = self._try_parse_json(raw_body)
157
+ if isinstance(json_body, dict) and "data" in json_body:
158
+ return json_body["data"]
159
+ return json_body
160
+
161
+ @staticmethod
162
+ def _try_parse_json(raw_body: bytes) -> Any:
163
+ if not raw_body:
164
+ return None
165
+ try:
166
+ return json.loads(raw_body)
167
+ except json.JSONDecodeError:
168
+ # Réponse non-JSON (page d'erreur d'un proxy en amont, ex. 502 Traefik) --
169
+ # traité comme une absence de corps exploitable.
170
+ return None
171
+
172
+ @staticmethod
173
+ def _build_query_string(query: Optional[Dict[str, Any]]) -> str:
174
+ if not query:
175
+ return ""
176
+ filtered = {k: v for k, v in query.items() if v is not None}
177
+ if not filtered:
178
+ return ""
179
+ return "?" + urllib.parse.urlencode(filtered)
180
+
181
+ @staticmethod
182
+ def _backoff_seconds(attempt: int) -> float:
183
+ # Backoff exponentiel + gigue : 0.4-0.6s, 0.8-1.2s, 1.6-2.4s...
184
+ base = 0.4 * (2**attempt)
185
+ return base + random.uniform(0, base * 0.5)
186
+
187
+ @staticmethod
188
+ def _random_idempotency_key() -> str:
189
+ return f"idem_{secrets.token_hex(16)}"
190
+
191
+ @staticmethod
192
+ def _extract_filename(content_disposition: Optional[str]) -> Optional[str]:
193
+ if not content_disposition:
194
+ return None
195
+ import re
196
+
197
+ match = re.search(r'filename="?([^";]+)"?', content_disposition)
198
+ return match.group(1) if match else None
alphapay/pagination.py ADDED
@@ -0,0 +1,36 @@
1
+ """Itération automatique sur les ressources paginées."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, Iterator
6
+
7
+ from .http import Http
8
+
9
+
10
+ def paginate(http: Http, first_page: Dict[str, Any]) -> Iterator[Any]:
11
+ """Parcourt automatiquement toutes les pages d'une ressource paginée,
12
+ qu'elle soit paginée par page (avec "count") ou par curseur (sans
13
+ "count" -- ex. ``client.transactions.list()``).
14
+
15
+ Suit le lien "next" renvoyé TEL QUEL par l'API plutôt que de recalculer
16
+ un numéro de page soi-même : un curseur n'est pas un numéro de page
17
+ (c'est un jeton opaque encodant une position dans le tri), l'incrémenter
18
+ à la main ne ferait qu'interroger indéfiniment la même première page.
19
+
20
+ :param http: ``client.http`` -- nécessaire pour requêter l'URL absolue "next".
21
+ :param first_page: Le premier appel déjà résolu, ex.
22
+ ``client.transactions.list(status="SUCCESS")``.
23
+
24
+ :Example:
25
+
26
+ >>> for tx in paginate(client.http, client.transactions.list(status="SUCCESS")):
27
+ ... print(tx["reference"])
28
+ """
29
+ page = first_page
30
+ while True:
31
+ for item in page["results"]:
32
+ yield item
33
+ next_url = page.get("next")
34
+ if not next_url:
35
+ return
36
+ page = http.request("GET", next_url)
alphapay/py.typed ADDED
File without changes
File without changes
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from ..http import Http
6
+
7
+
8
+ class IpWhitelistResource:
9
+ """Whitelist IP requise pour les payouts -- partagée par toutes les
10
+ clés du marchand (cf. ``apps.api_keys.models.MerchantIpWhitelistEntry``
11
+ côté API). Fonctionne bien via clé API -- vérifié en conditions réelles.
12
+ """
13
+
14
+ def __init__(self, http: Http) -> None:
15
+ self._http = http
16
+
17
+ def list(self, **params: Any) -> Dict[str, Any]:
18
+ return self._http.request("GET", "/merchant-ip-whitelist/", query=params)
19
+
20
+ def create(self, *, ip_address: str, label: Optional[str] = None) -> Dict[str, Any]:
21
+ body = {k: v for k, v in dict(ip_address=ip_address, label=label).items() if v is not None}
22
+ return self._http.request("POST", "/merchant-ip-whitelist/", body=body)
23
+
24
+ def update(self, id: str, **params: Any) -> Dict[str, Any]:
25
+ return self._http.request("PATCH", f"/merchant-ip-whitelist/{id}/", body=params)
26
+
27
+ def delete(self, id: str) -> None:
28
+ self._http.request("DELETE", f"/merchant-ip-whitelist/{id}/")
29
+
30
+
31
+ class ApiKeysResource:
32
+ """Gestion des clés API du marchand, et whitelist IP (via ``.ip_whitelist``).
33
+
34
+ ATTENTION : ``list``/``create``/``get``/``revoke``/``delete``
35
+ ci-dessous sont entièrement inaccessibles via clé API (403,
36
+ ``error_code == "dashboard_only"``, vérifié en conditions réelles) --
37
+ cohérent avec la sécurité attendue : une clé compromise ne doit pas
38
+ pouvoir créer d'autres clés pour elle-même ni lister les clés
39
+ existantes. Seul ``.ip_whitelist`` fonctionne via clé API.
40
+ """
41
+
42
+ def __init__(self, http: Http) -> None:
43
+ self._http = http
44
+ self.ip_whitelist = IpWhitelistResource(http)
45
+
46
+ def list(self, **params: Any) -> Dict[str, Any]:
47
+ return self._http.request("GET", "/merchant-api-keys/", query=params)
48
+
49
+ def create(self, *, environment: str, name: Optional[str] = None, scope: Optional[str] = None, expires_at: Optional[str] = None) -> Dict[str, Any]:
50
+ """``secret`` n'est présent QUE dans cette réponse -- jamais
51
+ récupérable ensuite, à stocker immédiatement côté appelant.
52
+ """
53
+ body = {k: v for k, v in dict(name=name, environment=environment, scope=scope, expires_at=expires_at).items() if v is not None}
54
+ return self._http.request("POST", "/merchant-api-keys/", body=body)
55
+
56
+ def get(self, id: str) -> Dict[str, Any]:
57
+ return self._http.request("GET", f"/merchant-api-keys/{id}/")
58
+
59
+ def revoke(self, id: str) -> Dict[str, Any]:
60
+ return self._http.request("POST", f"/merchant-api-keys/{id}/revoke/")
61
+
62
+ def delete(self, id: str) -> None:
63
+ self._http.request("DELETE", f"/merchant-api-keys/{id}/")
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict
4
+
5
+ from ..http import Http
6
+
7
+
8
+ class BalancesResource:
9
+ """Soldes par pays/devise et grand livre des mouvements -- lecture seule
10
+ (les mouvements naissent des autres ressources : transactions,
11
+ reversements, transferts).
12
+ """
13
+
14
+ def __init__(self, http: Http) -> None:
15
+ self._http = http
16
+
17
+ def list(self, **params: Any) -> Dict[str, Any]:
18
+ return self._http.request("GET", "/merchant-balances/", query=params)
19
+
20
+ def get(self, id: str) -> Dict[str, Any]:
21
+ return self._http.request("GET", f"/merchant-balances/{id}/")
22
+
23
+ def ledger_entries(self, **params: Any) -> Dict[str, Any]:
24
+ """ATTENTION : inaccessible via clé API (contrairement à
25
+ :meth:`list`/:meth:`get`, qui fonctionnent bien avec une clé --
26
+ vérifié en conditions réelles) : lève systématiquement
27
+ :class:`~alphapay.exceptions.AlphaPayPermissionError` (403,
28
+ ``error_code == "dashboard_only"``). Le grand livre détaillé n'est
29
+ consultable que depuis le dashboard (compte utilisateur).
30
+
31
+ Filtres reconnus : ``page``, ``page_size``, ``country``,
32
+ ``created_at__gte``, ``created_at__lte``.
33
+ """
34
+ return self._http.request("GET", "/merchant-ledger-entries/", query=params)
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional, Union
4
+
5
+ from ..http import Http
6
+
7
+
8
+ class CheckoutSessionsResource:
9
+ """Sessions de checkout à usage unique -- chacune génère sa propre page
10
+ de paiement hébergée (``checkout_url``), pré-remplie pour un client
11
+ précis. Contrairement à un lien de paiement réutilisable, une session
12
+ correspond à UNE tentative de paiement.
13
+ """
14
+
15
+ def __init__(self, http: Http) -> None:
16
+ self._http = http
17
+
18
+ def list(self, **params: Any) -> Dict[str, Any]:
19
+ """Filtres reconnus : ``page``, ``page_size``, ``search``, ``status``."""
20
+ return self._http.request("GET", "/checkout-sessions/", query=params)
21
+
22
+ def create(
23
+ self,
24
+ *,
25
+ amount: Union[int, float, str],
26
+ currency: str,
27
+ customer_email: str,
28
+ customer_name: str,
29
+ description: Optional[str] = None,
30
+ country: Optional[str] = None,
31
+ customer_phone: Optional[str] = None,
32
+ return_url: Optional[str] = None,
33
+ metadata: Optional[Dict[str, Any]] = None,
34
+ idempotency_key: Union[str, bool, None] = None,
35
+ ) -> Dict[str, Any]:
36
+ body = {
37
+ k: v
38
+ for k, v in dict(
39
+ amount=amount,
40
+ currency=currency,
41
+ description=description,
42
+ country=country,
43
+ customer_email=customer_email,
44
+ customer_name=customer_name,
45
+ customer_phone=customer_phone,
46
+ return_url=return_url,
47
+ metadata=metadata,
48
+ ).items()
49
+ if v is not None
50
+ }
51
+ return self._http.request("POST", "/checkout-sessions/", body=body, idempotency_key=idempotency_key)
52
+
53
+ def get(self, id: str) -> Dict[str, Any]:
54
+ return self._http.request("GET", f"/checkout-sessions/{id}/")
55
+
56
+ def cancel(self, id: str) -> Dict[str, Any]:
57
+ return self._http.request("POST", f"/checkout-sessions/{id}/cancel/")
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from ..http import Http
6
+
7
+
8
+ class CustomersResource:
9
+ def __init__(self, http: Http) -> None:
10
+ self._http = http
11
+
12
+ def list(self, **params: Any) -> Dict[str, Any]:
13
+ """Filtres reconnus : ``page``, ``page_size``, ``search``, ``ordering``."""
14
+ return self._http.request("GET", "/customers/", query=params)
15
+
16
+ def create(
17
+ self, *, country: str, phone: Optional[str] = None, full_name: Optional[str] = None, email: Optional[str] = None
18
+ ) -> Dict[str, Any]:
19
+ """``country`` -- UUID de ``geo.Country`` (ForeignKey côté API), PAS un
20
+ code ISO2 ("BJ") -- à la différence de ``country`` sur les transactions/
21
+ paiements. Référentiel pas encore couvert par ce SDK.
22
+ """
23
+ body = {k: v for k, v in dict(country=country, phone=phone, full_name=full_name, email=email).items() if v is not None}
24
+ return self._http.request("POST", "/customers/", body=body)
25
+
26
+ def get(self, id: str) -> Dict[str, Any]:
27
+ return self._http.request("GET", f"/customers/{id}/")
28
+
29
+ def update(self, id: str, **params: Any) -> Dict[str, Any]:
30
+ return self._http.request("PATCH", f"/customers/{id}/", body=params)
31
+
32
+ def delete(self, id: str) -> None:
33
+ self._http.request("DELETE", f"/customers/{id}/")
34
+
35
+ def transactions(self, id: str, **params: Any) -> Dict[str, Any]:
36
+ """Contrairement à ``TransactionsResource.list()``, cet endpoint EST
37
+ paginé par page (``page`` fonctionne ici) -- ``CustomerTransactionsView``
38
+ n'a pas de ``pagination_class`` propre, il retombe donc sur le
39
+ défaut global (``StandardResultsPagination``), pas
40
+ ``CreatedAtCursorPagination``.
41
+ """
42
+ return self._http.request("GET", f"/customers/{id}/transactions/", query=params)