cinetpay-python 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.
Files changed (41) hide show
  1. cinetpay/__init__.py +175 -0
  2. cinetpay/api/__init__.py +17 -0
  3. cinetpay/api/async_balance.py +70 -0
  4. cinetpay/api/async_payment.py +118 -0
  5. cinetpay/api/async_transfer.py +118 -0
  6. cinetpay/api/balance.py +70 -0
  7. cinetpay/api/payment.py +118 -0
  8. cinetpay/api/transfer.py +118 -0
  9. cinetpay/async_client.py +290 -0
  10. cinetpay/auth/__init__.py +11 -0
  11. cinetpay/auth/async_authenticator.py +119 -0
  12. cinetpay/auth/authenticator.py +140 -0
  13. cinetpay/auth/token_store.py +72 -0
  14. cinetpay/client.py +256 -0
  15. cinetpay/constants/__init__.py +15 -0
  16. cinetpay/constants/channels.py +12 -0
  17. cinetpay/constants/currencies.py +14 -0
  18. cinetpay/constants/payment_methods.py +43 -0
  19. cinetpay/constants/statuses.py +44 -0
  20. cinetpay/errors/__init__.py +14 -0
  21. cinetpay/errors/api_error.py +45 -0
  22. cinetpay/errors/auth_error.py +12 -0
  23. cinetpay/errors/base.py +18 -0
  24. cinetpay/errors/network_errors.py +29 -0
  25. cinetpay/http/__init__.py +9 -0
  26. cinetpay/http/async_http_client.py +173 -0
  27. cinetpay/http/http_client.py +173 -0
  28. cinetpay/logger.py +70 -0
  29. cinetpay/py.typed +0 -0
  30. cinetpay/types/__init__.py +59 -0
  31. cinetpay/types/balance.py +35 -0
  32. cinetpay/types/config.py +139 -0
  33. cinetpay/types/payment.py +210 -0
  34. cinetpay/types/transfer.py +163 -0
  35. cinetpay/types/webhook.py +59 -0
  36. cinetpay/validation.py +163 -0
  37. cinetpay/webhooks/__init__.py +8 -0
  38. cinetpay/webhooks/notification.py +79 -0
  39. cinetpay_python-0.1.0.dist-info/METADATA +376 -0
  40. cinetpay_python-0.1.0.dist-info/RECORD +41 -0
  41. cinetpay_python-0.1.0.dist-info/WHEEL +4 -0
cinetpay/__init__.py ADDED
@@ -0,0 +1,175 @@
1
+ """CinetPay Python SDK — payments and mobile money transfers in Africa.
2
+
3
+ Usage::
4
+
5
+ from cinetpay import CinetPayClient, ClientConfig, CountryCredentials
6
+
7
+ client = CinetPayClient(ClientConfig(
8
+ credentials={
9
+ "CI": CountryCredentials(api_key="sk_test_...", api_password="..."),
10
+ },
11
+ ))
12
+
13
+ # Initialize a payment
14
+ payment = client.payment.initialize(request, "CI")
15
+
16
+ # Check payment status
17
+ status = client.payment.get_status(payment.payment_token, "CI")
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ __version__ = "0.1.0"
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Main clients
26
+ # ---------------------------------------------------------------------------
27
+ from cinetpay.async_client import AsyncCinetPayClient
28
+ from cinetpay.client import CinetPayClient
29
+
30
+ # ---------------------------------------------------------------------------
31
+ # Configuration types
32
+ # ---------------------------------------------------------------------------
33
+ from cinetpay.types.config import (
34
+ API_KEY_PREFIX_LIVE,
35
+ API_KEY_PREFIX_TEST,
36
+ AsyncTokenStoreProtocol,
37
+ ClientConfig,
38
+ CountryCredentials,
39
+ DEFAULT_BASE_URL,
40
+ DEFAULT_TIMEOUT,
41
+ DEFAULT_TOKEN_TTL,
42
+ PRODUCTION_BASE_URL,
43
+ TokenStoreProtocol,
44
+ )
45
+
46
+ # ---------------------------------------------------------------------------
47
+ # Domain types
48
+ # ---------------------------------------------------------------------------
49
+ from cinetpay.types.balance import Balance
50
+ from cinetpay.types.payment import (
51
+ PaymentDetails,
52
+ PaymentRequest,
53
+ PaymentResponse,
54
+ PaymentStatus,
55
+ PaymentStatusUser,
56
+ )
57
+ from cinetpay.types.transfer import (
58
+ TransferRequest,
59
+ TransferResponse,
60
+ TransferStatus,
61
+ TransferStatusUser,
62
+ )
63
+ from cinetpay.types.webhook import WebhookPayload, WebhookUser
64
+
65
+ # ---------------------------------------------------------------------------
66
+ # Constants
67
+ # ---------------------------------------------------------------------------
68
+ from cinetpay.constants import (
69
+ API_CODES,
70
+ CHANNELS,
71
+ COUNTRY_CODES,
72
+ CURRENCIES,
73
+ PAYMENT_METHODS,
74
+ PAYMENT_METHODS_BY_COUNTRY,
75
+ TRANSACTION_STATUSES,
76
+ Channel,
77
+ CountryCode,
78
+ Currency,
79
+ PaymentMethod,
80
+ TransactionStatus,
81
+ is_final_status,
82
+ )
83
+
84
+ # ---------------------------------------------------------------------------
85
+ # Errors
86
+ # ---------------------------------------------------------------------------
87
+ from cinetpay.errors import (
88
+ ApiError,
89
+ AuthenticationError,
90
+ CinetPayError,
91
+ NetworkError,
92
+ TimeoutError,
93
+ )
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # Validation
97
+ # ---------------------------------------------------------------------------
98
+ from cinetpay.validation import ValidationError
99
+
100
+ # ---------------------------------------------------------------------------
101
+ # Webhooks
102
+ # ---------------------------------------------------------------------------
103
+ from cinetpay.webhooks.notification import parse_notification, verify_notification
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # Logger
107
+ # ---------------------------------------------------------------------------
108
+ from cinetpay.logger import LoggerProtocol, NoopLogger, StandardLibLogger
109
+
110
+ # ---------------------------------------------------------------------------
111
+ # Token store
112
+ # ---------------------------------------------------------------------------
113
+ from cinetpay.auth.token_store import MemoryTokenStore
114
+
115
+ __all__ = [
116
+ # Version
117
+ "__version__",
118
+ # Clients
119
+ "AsyncCinetPayClient",
120
+ "CinetPayClient",
121
+ # Config types
122
+ "API_KEY_PREFIX_LIVE",
123
+ "API_KEY_PREFIX_TEST",
124
+ "AsyncTokenStoreProtocol",
125
+ "ClientConfig",
126
+ "CountryCredentials",
127
+ "DEFAULT_BASE_URL",
128
+ "DEFAULT_TIMEOUT",
129
+ "DEFAULT_TOKEN_TTL",
130
+ "PRODUCTION_BASE_URL",
131
+ "TokenStoreProtocol",
132
+ # Domain types
133
+ "Balance",
134
+ "PaymentDetails",
135
+ "PaymentRequest",
136
+ "PaymentResponse",
137
+ "PaymentStatus",
138
+ "PaymentStatusUser",
139
+ "TransferRequest",
140
+ "TransferResponse",
141
+ "TransferStatus",
142
+ "TransferStatusUser",
143
+ "WebhookPayload",
144
+ "WebhookUser",
145
+ # Constants
146
+ "API_CODES",
147
+ "CHANNELS",
148
+ "COUNTRY_CODES",
149
+ "CURRENCIES",
150
+ "Channel",
151
+ "CountryCode",
152
+ "Currency",
153
+ "PAYMENT_METHODS",
154
+ "PAYMENT_METHODS_BY_COUNTRY",
155
+ "PaymentMethod",
156
+ "TRANSACTION_STATUSES",
157
+ "TransactionStatus",
158
+ "is_final_status",
159
+ # Errors
160
+ "ApiError",
161
+ "AuthenticationError",
162
+ "CinetPayError",
163
+ "NetworkError",
164
+ "TimeoutError",
165
+ "ValidationError",
166
+ # Webhooks
167
+ "parse_notification",
168
+ "verify_notification",
169
+ # Logger
170
+ "LoggerProtocol",
171
+ "NoopLogger",
172
+ "StandardLibLogger",
173
+ # Token store
174
+ "MemoryTokenStore",
175
+ ]
@@ -0,0 +1,17 @@
1
+ """API modules — payment, transfer, balance (sync and async)."""
2
+
3
+ from cinetpay.api.async_balance import AsyncBalanceApi
4
+ from cinetpay.api.async_payment import AsyncPaymentApi
5
+ from cinetpay.api.async_transfer import AsyncTransferApi
6
+ from cinetpay.api.balance import BalanceApi
7
+ from cinetpay.api.payment import PaymentApi
8
+ from cinetpay.api.transfer import TransferApi
9
+
10
+ __all__ = [
11
+ "AsyncBalanceApi",
12
+ "AsyncPaymentApi",
13
+ "AsyncTransferApi",
14
+ "BalanceApi",
15
+ "PaymentApi",
16
+ "TransferApi",
17
+ ]
@@ -0,0 +1,70 @@
1
+ """Asynchronous balance API — retrieve merchant account balance."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from cinetpay.auth.async_authenticator import AsyncAuthenticator
8
+ from cinetpay.errors.api_error import ApiError
9
+ from cinetpay.http.async_http_client import AsyncHttpClient
10
+ from cinetpay.types.balance import Balance, to_balance
11
+
12
+
13
+ class AsyncBalanceApi:
14
+ """Async CinetPay merchant balance API.
15
+
16
+ Accessible via ``client.balance``.
17
+
18
+ Example::
19
+
20
+ balance = await client.balance.get("CI")
21
+ print(balance.available_balance) # "249711.74"
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ http_client: AsyncHttpClient,
27
+ authenticators: dict[str, AsyncAuthenticator],
28
+ ) -> None:
29
+ self._http_client = http_client
30
+ self._authenticators = authenticators
31
+
32
+ async def get(self, country: str) -> Balance:
33
+ """Retrieve the available balance for a merchant account.
34
+
35
+ Args:
36
+ country: ISO country code (e.g. ``"CI"``, ``"SN"``).
37
+
38
+ Returns:
39
+ Balance with available amount and currency.
40
+
41
+ Raises:
42
+ ApiError: If authentication fails.
43
+ TypeError: If the country is not configured.
44
+ """
45
+ auth = self._resolve_authenticator(country)
46
+
47
+ try:
48
+ token = await auth.get_token()
49
+ raw: dict[str, Any] = await self._http_client.get("/v1/balances", token)
50
+ return to_balance(raw)
51
+ except ApiError as exc:
52
+ if _is_token_expired(exc):
53
+ token = await auth.force_refresh()
54
+ raw = await self._http_client.get("/v1/balances", token)
55
+ return to_balance(raw)
56
+ raise
57
+
58
+ def _resolve_authenticator(self, country: str) -> AsyncAuthenticator:
59
+ key = country.upper()
60
+ auth = self._authenticators.get(key)
61
+ if auth is None:
62
+ available = ", ".join(self._authenticators.keys())
63
+ raise TypeError(
64
+ f'No credentials configured for country "{key}". Available: {available}'
65
+ )
66
+ return auth
67
+
68
+
69
+ def _is_token_expired(error: ApiError) -> bool:
70
+ return error.api_code == 1003 or error.api_status == "EXPIRED_TOKEN"
@@ -0,0 +1,118 @@
1
+ """Asynchronous payment API — initialize payments and check status."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+ from urllib.parse import quote
7
+
8
+ from cinetpay.auth.async_authenticator import AsyncAuthenticator
9
+ from cinetpay.errors.api_error import ApiError
10
+ from cinetpay.http.async_http_client import AsyncHttpClient
11
+ from cinetpay.types.payment import (
12
+ PaymentRequest,
13
+ PaymentResponse,
14
+ PaymentStatus,
15
+ serialize_payment_request,
16
+ to_payment_response,
17
+ to_payment_status,
18
+ )
19
+ from cinetpay.validation import validate_payment_request
20
+
21
+
22
+ class AsyncPaymentApi:
23
+ """Async CinetPay web payment API.
24
+
25
+ Accessible via ``client.payment``.
26
+
27
+ Example::
28
+
29
+ payment = await client.payment.initialize(request, "CI")
30
+ status = await client.payment.get_status(payment.payment_token, "CI")
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ http_client: AsyncHttpClient,
36
+ authenticators: dict[str, AsyncAuthenticator],
37
+ ) -> None:
38
+ self._http_client = http_client
39
+ self._authenticators = authenticators
40
+
41
+ async def initialize(
42
+ self,
43
+ request: PaymentRequest,
44
+ country: str,
45
+ ) -> PaymentResponse:
46
+ """Initialize a web payment transaction.
47
+
48
+ Args:
49
+ request: Payment parameters (amount, currency, URLs, customer, etc.).
50
+ country: ISO country code (e.g. ``"CI"``, ``"SN"``).
51
+
52
+ Returns:
53
+ Response containing ``payment_url`` (redirect) and ``payment_token`` (status check).
54
+
55
+ Raises:
56
+ ApiError: If the API returns an error (e.g. TRANSACTION_EXIST).
57
+ ValidationError: If a request field is invalid.
58
+ TypeError: If the country is not configured.
59
+ """
60
+ validate_payment_request(request)
61
+ auth = self._resolve_authenticator(country)
62
+ body = serialize_payment_request(request)
63
+
64
+ try:
65
+ token = await auth.get_token()
66
+ raw: dict[str, Any] = await self._http_client.post("/v1/payment", body, token)
67
+ return to_payment_response(raw)
68
+ except ApiError as exc:
69
+ if _is_token_expired(exc):
70
+ token = await auth.force_refresh()
71
+ raw = await self._http_client.post("/v1/payment", body, token)
72
+ return to_payment_response(raw)
73
+ raise
74
+
75
+ async def get_status(
76
+ self,
77
+ identifier: str,
78
+ country: str,
79
+ ) -> PaymentStatus:
80
+ """Retrieve the current status of a payment.
81
+
82
+ Args:
83
+ identifier: ``payment_token``, ``transaction_id``, or ``merchant_transaction_id``.
84
+ country: ISO country code (e.g. ``"CI"``, ``"SN"``).
85
+
86
+ Returns:
87
+ Transaction status with user information.
88
+
89
+ Raises:
90
+ ApiError: If the transaction is not found (NOT_FOUND).
91
+ """
92
+ auth = self._resolve_authenticator(country)
93
+ path = f"/v1/payment/{quote(identifier, safe='')}"
94
+
95
+ try:
96
+ token = await auth.get_token()
97
+ raw: dict[str, Any] = await self._http_client.get(path, token)
98
+ return to_payment_status(raw)
99
+ except ApiError as exc:
100
+ if _is_token_expired(exc):
101
+ token = await auth.force_refresh()
102
+ raw = await self._http_client.get(path, token)
103
+ return to_payment_status(raw)
104
+ raise
105
+
106
+ def _resolve_authenticator(self, country: str) -> AsyncAuthenticator:
107
+ key = country.upper()
108
+ auth = self._authenticators.get(key)
109
+ if auth is None:
110
+ available = ", ".join(self._authenticators.keys())
111
+ raise TypeError(
112
+ f'No credentials configured for country "{key}". Available: {available}'
113
+ )
114
+ return auth
115
+
116
+
117
+ def _is_token_expired(error: ApiError) -> bool:
118
+ return error.api_code == 1003 or error.api_status == "EXPIRED_TOKEN"
@@ -0,0 +1,118 @@
1
+ """Asynchronous transfer API — create transfers and check status."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+ from urllib.parse import quote
7
+
8
+ from cinetpay.auth.async_authenticator import AsyncAuthenticator
9
+ from cinetpay.errors.api_error import ApiError
10
+ from cinetpay.http.async_http_client import AsyncHttpClient
11
+ from cinetpay.types.transfer import (
12
+ TransferRequest,
13
+ TransferResponse,
14
+ TransferStatus,
15
+ serialize_transfer_request,
16
+ to_transfer_response,
17
+ to_transfer_status,
18
+ )
19
+ from cinetpay.validation import validate_transfer_request
20
+
21
+
22
+ class AsyncTransferApi:
23
+ """Async CinetPay money transfer API.
24
+
25
+ Accessible via ``client.transfer``.
26
+
27
+ Example::
28
+
29
+ transfer = await client.transfer.create(request, "CI")
30
+ status = await client.transfer.get_status(transfer.transaction_id, "CI")
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ http_client: AsyncHttpClient,
36
+ authenticators: dict[str, AsyncAuthenticator],
37
+ ) -> None:
38
+ self._http_client = http_client
39
+ self._authenticators = authenticators
40
+
41
+ async def create(
42
+ self,
43
+ request: TransferRequest,
44
+ country: str,
45
+ ) -> TransferResponse:
46
+ """Create a money transfer to a mobile money number.
47
+
48
+ Args:
49
+ request: Transfer parameters (amount, currency, phone, operator, etc.).
50
+ country: ISO country code (e.g. ``"CI"``, ``"SN"``).
51
+
52
+ Returns:
53
+ Response containing initial status and transaction identifiers.
54
+
55
+ Raises:
56
+ ApiError: If the API returns an error (e.g. INSUFFICIENT_BALANCE).
57
+ ValidationError: If a request field is invalid.
58
+ TypeError: If the country is not configured.
59
+ """
60
+ validate_transfer_request(request)
61
+ auth = self._resolve_authenticator(country)
62
+ body = serialize_transfer_request(request)
63
+
64
+ try:
65
+ token = await auth.get_token()
66
+ raw: dict[str, Any] = await self._http_client.post("/v1/transfer", body, token)
67
+ return to_transfer_response(raw)
68
+ except ApiError as exc:
69
+ if _is_token_expired(exc):
70
+ token = await auth.force_refresh()
71
+ raw = await self._http_client.post("/v1/transfer", body, token)
72
+ return to_transfer_response(raw)
73
+ raise
74
+
75
+ async def get_status(
76
+ self,
77
+ transaction_id: str,
78
+ country: str,
79
+ ) -> TransferStatus:
80
+ """Retrieve the current status of a transfer.
81
+
82
+ Args:
83
+ transaction_id: CinetPay transaction ID or ``merchant_transaction_id``.
84
+ country: ISO country code (e.g. ``"CI"``, ``"SN"``).
85
+
86
+ Returns:
87
+ Transfer status with recipient information.
88
+
89
+ Raises:
90
+ ApiError: If the transaction is not found (NOT_FOUND).
91
+ """
92
+ auth = self._resolve_authenticator(country)
93
+ path = f"/v1/transfer/{quote(transaction_id, safe='')}"
94
+
95
+ try:
96
+ token = await auth.get_token()
97
+ raw: dict[str, Any] = await self._http_client.get(path, token)
98
+ return to_transfer_status(raw)
99
+ except ApiError as exc:
100
+ if _is_token_expired(exc):
101
+ token = await auth.force_refresh()
102
+ raw = await self._http_client.get(path, token)
103
+ return to_transfer_status(raw)
104
+ raise
105
+
106
+ def _resolve_authenticator(self, country: str) -> AsyncAuthenticator:
107
+ key = country.upper()
108
+ auth = self._authenticators.get(key)
109
+ if auth is None:
110
+ available = ", ".join(self._authenticators.keys())
111
+ raise TypeError(
112
+ f'No credentials configured for country "{key}". Available: {available}'
113
+ )
114
+ return auth
115
+
116
+
117
+ def _is_token_expired(error: ApiError) -> bool:
118
+ return error.api_code == 1003 or error.api_status == "EXPIRED_TOKEN"
@@ -0,0 +1,70 @@
1
+ """Synchronous balance API — retrieve merchant account balance."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from cinetpay.auth.authenticator import Authenticator
8
+ from cinetpay.errors.api_error import ApiError
9
+ from cinetpay.http.http_client import HttpClient
10
+ from cinetpay.types.balance import Balance, to_balance
11
+
12
+
13
+ class BalanceApi:
14
+ """CinetPay merchant balance API.
15
+
16
+ Accessible via ``client.balance``.
17
+
18
+ Example::
19
+
20
+ balance = client.balance.get("CI")
21
+ print(balance.available_balance) # "249711.74"
22
+ """
23
+
24
+ def __init__(
25
+ self,
26
+ http_client: HttpClient,
27
+ authenticators: dict[str, Authenticator],
28
+ ) -> None:
29
+ self._http_client = http_client
30
+ self._authenticators = authenticators
31
+
32
+ def get(self, country: str) -> Balance:
33
+ """Retrieve the available balance for a merchant account.
34
+
35
+ Args:
36
+ country: ISO country code (e.g. ``"CI"``, ``"SN"``).
37
+
38
+ Returns:
39
+ Balance with available amount and currency.
40
+
41
+ Raises:
42
+ ApiError: If authentication fails.
43
+ TypeError: If the country is not configured.
44
+ """
45
+ auth = self._resolve_authenticator(country)
46
+
47
+ try:
48
+ token = auth.get_token()
49
+ raw: dict[str, Any] = self._http_client.get("/v1/balances", token)
50
+ return to_balance(raw)
51
+ except ApiError as exc:
52
+ if _is_token_expired(exc):
53
+ token = auth.force_refresh()
54
+ raw = self._http_client.get("/v1/balances", token)
55
+ return to_balance(raw)
56
+ raise
57
+
58
+ def _resolve_authenticator(self, country: str) -> Authenticator:
59
+ key = country.upper()
60
+ auth = self._authenticators.get(key)
61
+ if auth is None:
62
+ available = ", ".join(self._authenticators.keys())
63
+ raise TypeError(
64
+ f'No credentials configured for country "{key}". Available: {available}'
65
+ )
66
+ return auth
67
+
68
+
69
+ def _is_token_expired(error: ApiError) -> bool:
70
+ return error.api_code == 1003 or error.api_status == "EXPIRED_TOKEN"
@@ -0,0 +1,118 @@
1
+ """Synchronous payment API — initialize payments and check status."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+ from urllib.parse import quote
7
+
8
+ from cinetpay.auth.authenticator import Authenticator
9
+ from cinetpay.errors.api_error import ApiError
10
+ from cinetpay.http.http_client import HttpClient
11
+ from cinetpay.types.payment import (
12
+ PaymentRequest,
13
+ PaymentResponse,
14
+ PaymentStatus,
15
+ serialize_payment_request,
16
+ to_payment_response,
17
+ to_payment_status,
18
+ )
19
+ from cinetpay.validation import validate_payment_request
20
+
21
+
22
+ class PaymentApi:
23
+ """CinetPay web payment API.
24
+
25
+ Accessible via ``client.payment``.
26
+
27
+ Example::
28
+
29
+ payment = client.payment.initialize(request, "CI")
30
+ status = client.payment.get_status(payment.payment_token, "CI")
31
+ """
32
+
33
+ def __init__(
34
+ self,
35
+ http_client: HttpClient,
36
+ authenticators: dict[str, Authenticator],
37
+ ) -> None:
38
+ self._http_client = http_client
39
+ self._authenticators = authenticators
40
+
41
+ def initialize(
42
+ self,
43
+ request: PaymentRequest,
44
+ country: str,
45
+ ) -> PaymentResponse:
46
+ """Initialize a web payment transaction.
47
+
48
+ Args:
49
+ request: Payment parameters (amount, currency, URLs, customer, etc.).
50
+ country: ISO country code (e.g. ``"CI"``, ``"SN"``).
51
+
52
+ Returns:
53
+ Response containing ``payment_url`` (redirect) and ``payment_token`` (status check).
54
+
55
+ Raises:
56
+ ApiError: If the API returns an error (e.g. TRANSACTION_EXIST).
57
+ ValidationError: If a request field is invalid.
58
+ TypeError: If the country is not configured.
59
+ """
60
+ validate_payment_request(request)
61
+ auth = self._resolve_authenticator(country)
62
+ body = serialize_payment_request(request)
63
+
64
+ try:
65
+ token = auth.get_token()
66
+ raw: dict[str, Any] = self._http_client.post("/v1/payment", body, token)
67
+ return to_payment_response(raw)
68
+ except ApiError as exc:
69
+ if _is_token_expired(exc):
70
+ token = auth.force_refresh()
71
+ raw = self._http_client.post("/v1/payment", body, token)
72
+ return to_payment_response(raw)
73
+ raise
74
+
75
+ def get_status(
76
+ self,
77
+ identifier: str,
78
+ country: str,
79
+ ) -> PaymentStatus:
80
+ """Retrieve the current status of a payment.
81
+
82
+ Args:
83
+ identifier: ``payment_token``, ``transaction_id``, or ``merchant_transaction_id``.
84
+ country: ISO country code (e.g. ``"CI"``, ``"SN"``).
85
+
86
+ Returns:
87
+ Transaction status with user information.
88
+
89
+ Raises:
90
+ ApiError: If the transaction is not found (NOT_FOUND).
91
+ """
92
+ auth = self._resolve_authenticator(country)
93
+ path = f"/v1/payment/{quote(identifier, safe='')}"
94
+
95
+ try:
96
+ token = auth.get_token()
97
+ raw: dict[str, Any] = self._http_client.get(path, token)
98
+ return to_payment_status(raw)
99
+ except ApiError as exc:
100
+ if _is_token_expired(exc):
101
+ token = auth.force_refresh()
102
+ raw = self._http_client.get(path, token)
103
+ return to_payment_status(raw)
104
+ raise
105
+
106
+ def _resolve_authenticator(self, country: str) -> Authenticator:
107
+ key = country.upper()
108
+ auth = self._authenticators.get(key)
109
+ if auth is None:
110
+ available = ", ".join(self._authenticators.keys())
111
+ raise TypeError(
112
+ f'No credentials configured for country "{key}". Available: {available}'
113
+ )
114
+ return auth
115
+
116
+
117
+ def _is_token_expired(error: ApiError) -> bool:
118
+ return error.api_code == 1003 or error.api_status == "EXPIRED_TOKEN"