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.
- cinetpay/__init__.py +175 -0
- cinetpay/api/__init__.py +17 -0
- cinetpay/api/async_balance.py +70 -0
- cinetpay/api/async_payment.py +118 -0
- cinetpay/api/async_transfer.py +118 -0
- cinetpay/api/balance.py +70 -0
- cinetpay/api/payment.py +118 -0
- cinetpay/api/transfer.py +118 -0
- cinetpay/async_client.py +290 -0
- cinetpay/auth/__init__.py +11 -0
- cinetpay/auth/async_authenticator.py +119 -0
- cinetpay/auth/authenticator.py +140 -0
- cinetpay/auth/token_store.py +72 -0
- cinetpay/client.py +256 -0
- cinetpay/constants/__init__.py +15 -0
- cinetpay/constants/channels.py +12 -0
- cinetpay/constants/currencies.py +14 -0
- cinetpay/constants/payment_methods.py +43 -0
- cinetpay/constants/statuses.py +44 -0
- cinetpay/errors/__init__.py +14 -0
- cinetpay/errors/api_error.py +45 -0
- cinetpay/errors/auth_error.py +12 -0
- cinetpay/errors/base.py +18 -0
- cinetpay/errors/network_errors.py +29 -0
- cinetpay/http/__init__.py +9 -0
- cinetpay/http/async_http_client.py +173 -0
- cinetpay/http/http_client.py +173 -0
- cinetpay/logger.py +70 -0
- cinetpay/py.typed +0 -0
- cinetpay/types/__init__.py +59 -0
- cinetpay/types/balance.py +35 -0
- cinetpay/types/config.py +139 -0
- cinetpay/types/payment.py +210 -0
- cinetpay/types/transfer.py +163 -0
- cinetpay/types/webhook.py +59 -0
- cinetpay/validation.py +163 -0
- cinetpay/webhooks/__init__.py +8 -0
- cinetpay/webhooks/notification.py +79 -0
- cinetpay_python-0.1.0.dist-info/METADATA +376 -0
- cinetpay_python-0.1.0.dist-info/RECORD +41 -0
- 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
|
+
]
|
cinetpay/api/__init__.py
ADDED
|
@@ -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"
|
cinetpay/api/balance.py
ADDED
|
@@ -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"
|
cinetpay/api/payment.py
ADDED
|
@@ -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"
|