epoint-python-sdk 0.2.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.
- epoint_python_sdk-0.2.0.dist-info/METADATA +154 -0
- epoint_python_sdk-0.2.0.dist-info/RECORD +19 -0
- epoint_python_sdk-0.2.0.dist-info/WHEEL +4 -0
- epoint_python_sdk-0.2.0.dist-info/licenses/LICENSE +21 -0
- epoint_sdk/__init__.py +73 -0
- epoint_sdk/_core.py +209 -0
- epoint_sdk/_version.py +1 -0
- epoint_sdk/callbacks.py +31 -0
- epoint_sdk/client.py +55 -0
- epoint_sdk/config.py +108 -0
- epoint_sdk/enums.py +61 -0
- epoint_sdk/errors.py +73 -0
- epoint_sdk/messages.py +60 -0
- epoint_sdk/messages.yaml +43 -0
- epoint_sdk/models.py +278 -0
- epoint_sdk/money.py +40 -0
- epoint_sdk/py.typed +0 -0
- epoint_sdk/resources.py +374 -0
- epoint_sdk/signing.py +49 -0
epoint_sdk/resources.py
ADDED
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
from typing import Any, Mapping, Optional, Union
|
|
5
|
+
|
|
6
|
+
from ._core import Op
|
|
7
|
+
from .enums import Currency, Language
|
|
8
|
+
from .errors import InvalidRequestError
|
|
9
|
+
from .messages import text
|
|
10
|
+
from .models import (
|
|
11
|
+
Card,
|
|
12
|
+
CardRegistration,
|
|
13
|
+
InstallmentPlan,
|
|
14
|
+
Invoice,
|
|
15
|
+
Redirect,
|
|
16
|
+
Result,
|
|
17
|
+
Transaction,
|
|
18
|
+
Transfer,
|
|
19
|
+
Wallet,
|
|
20
|
+
Widget,
|
|
21
|
+
)
|
|
22
|
+
from .money import Amount, amount, wire
|
|
23
|
+
|
|
24
|
+
LanguageLike = Union[Language, str]
|
|
25
|
+
CurrencyLike = Union[Currency, str]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def required(value: Any, name: str, limit: int = 255) -> str:
|
|
29
|
+
result = str(value if value is not None else "").strip()
|
|
30
|
+
if not result:
|
|
31
|
+
raise InvalidRequestError(text("value.empty", field=name))
|
|
32
|
+
if len(result) > limit:
|
|
33
|
+
raise InvalidRequestError(text("value.too_long", field=name, limit=limit))
|
|
34
|
+
return result
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def optional(value: Any, name: str, limit: int = 1000) -> Optional[str]:
|
|
38
|
+
if value is None or str(value).strip() == "":
|
|
39
|
+
return None
|
|
40
|
+
result = str(value).strip()
|
|
41
|
+
if len(result) > limit:
|
|
42
|
+
raise InvalidRequestError(text("value.too_long", field=name, limit=limit))
|
|
43
|
+
return result
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def flag(value: Optional[bool]) -> Optional[int]:
|
|
47
|
+
return None if value is None else int(bool(value))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def day(value: Union[date, str]) -> str:
|
|
51
|
+
return value.isoformat() if isinstance(value, date) else required(value, "date", 32)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class _Resource:
|
|
55
|
+
def __init__(self, client: Any) -> None:
|
|
56
|
+
self._client = client
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def _settings(self) -> Any:
|
|
60
|
+
return self._client.settings
|
|
61
|
+
|
|
62
|
+
def _language(self, value: Optional[LanguageLike]) -> str:
|
|
63
|
+
return str(Language(str(value))) if value else str(self._settings.language)
|
|
64
|
+
|
|
65
|
+
def _currency(self, value: Optional[CurrencyLike]) -> str:
|
|
66
|
+
return str(Currency(str(value).upper())) if value else str(self._settings.currency)
|
|
67
|
+
|
|
68
|
+
def _checkout(self, total: Amount, order_id: str, description: Optional[str], language: Optional[LanguageLike],
|
|
69
|
+
currency: Optional[CurrencyLike], success_url: Optional[str], error_url: Optional[str],
|
|
70
|
+
other_attr: Any = None) -> dict[str, Any]:
|
|
71
|
+
return {
|
|
72
|
+
"amount": wire(amount(total)),
|
|
73
|
+
"order_id": required(order_id, "order_id"),
|
|
74
|
+
"currency": self._currency(currency),
|
|
75
|
+
"language": self._language(language),
|
|
76
|
+
"description": optional(description, "description"),
|
|
77
|
+
"success_redirect_url": success_url or self._settings.success_url,
|
|
78
|
+
"error_redirect_url": error_url or self._settings.error_url,
|
|
79
|
+
"other_attr": other_attr,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
def _run(self, op: Op) -> Any:
|
|
83
|
+
return self._client._run(op)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class Payments(_Resource):
|
|
87
|
+
def create(self, amount: Amount, order_id: str, *, description: Optional[str] = None,
|
|
88
|
+
language: Optional[LanguageLike] = None, currency: Optional[CurrencyLike] = None,
|
|
89
|
+
success_url: Optional[str] = None, error_url: Optional[str] = None, installment: Optional[bool] = None,
|
|
90
|
+
other_attr: Any = None) -> Any:
|
|
91
|
+
payload = self._checkout(amount, order_id, description, language, currency, success_url, error_url, other_attr)
|
|
92
|
+
payload["is_installment"] = flag(installment)
|
|
93
|
+
return self._run(Op("payment", payload, Redirect.from_api, order_id=str(payload["order_id"])))
|
|
94
|
+
|
|
95
|
+
def form(self, amount: Amount, order_id: str, *, description: Optional[str] = None,
|
|
96
|
+
language: Optional[LanguageLike] = None, currency: Optional[CurrencyLike] = None,
|
|
97
|
+
success_url: Optional[str] = None, error_url: Optional[str] = None, installment: Optional[bool] = None,
|
|
98
|
+
other_attr: Any = None) -> Any:
|
|
99
|
+
from .models import CheckoutForm
|
|
100
|
+
|
|
101
|
+
payload = self._checkout(amount, order_id, description, language, currency, success_url, error_url, other_attr)
|
|
102
|
+
payload["is_installment"] = flag(installment)
|
|
103
|
+
signed = self._client.sign(payload)
|
|
104
|
+
return CheckoutForm(self._settings.url("checkout"), signed["data"], signed["signature"])
|
|
105
|
+
|
|
106
|
+
def status(self, transaction: str) -> Any:
|
|
107
|
+
payload = {"transaction": required(transaction, "transaction")}
|
|
108
|
+
return self._run(Op("status", payload, Transaction.from_api, safe=True, payment_state=True))
|
|
109
|
+
|
|
110
|
+
def reverse(self, transaction: str, amount_: Optional[Amount] = None, *, currency: Optional[CurrencyLike] = None,
|
|
111
|
+
language: Optional[LanguageLike] = None) -> Any:
|
|
112
|
+
payload = {
|
|
113
|
+
"transaction": required(transaction, "transaction"),
|
|
114
|
+
"amount": wire(amount(amount_)) if amount_ is not None else None,
|
|
115
|
+
"currency": self._currency(currency),
|
|
116
|
+
"language": self._language(language),
|
|
117
|
+
}
|
|
118
|
+
return self._run(Op("reverse", payload, Result.from_api))
|
|
119
|
+
|
|
120
|
+
def hold(self, amount: Amount, order_id: str, *, description: Optional[str] = None,
|
|
121
|
+
language: Optional[LanguageLike] = None, currency: Optional[CurrencyLike] = None,
|
|
122
|
+
success_url: Optional[str] = None, error_url: Optional[str] = None, other_attr: Any = None) -> Any:
|
|
123
|
+
payload = self._checkout(amount, order_id, description, language, currency, success_url, error_url, other_attr)
|
|
124
|
+
return self._run(Op("hold", payload, Redirect.from_api, order_id=str(payload["order_id"])))
|
|
125
|
+
|
|
126
|
+
def capture(self, transaction: str, amount_: Amount) -> Any:
|
|
127
|
+
payload = {"transaction": required(transaction, "transaction"), "amount": wire(amount(amount_))}
|
|
128
|
+
return self._run(Op("capture", payload, Result.from_api))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Cards(_Resource):
|
|
132
|
+
def register(self, *, payouts: bool = False, description: Optional[str] = None,
|
|
133
|
+
language: Optional[LanguageLike] = None, success_url: Optional[str] = None,
|
|
134
|
+
error_url: Optional[str] = None) -> Any:
|
|
135
|
+
payload = {
|
|
136
|
+
"language": self._language(language),
|
|
137
|
+
"refund": int(payouts),
|
|
138
|
+
"description": optional(description, "description"),
|
|
139
|
+
"success_redirect_url": success_url or self._settings.success_url,
|
|
140
|
+
"error_redirect_url": error_url or self._settings.error_url,
|
|
141
|
+
}
|
|
142
|
+
return self._run(Op("card_register", payload, CardRegistration.from_api))
|
|
143
|
+
|
|
144
|
+
def register_and_pay(self, amount: Amount, order_id: str, *, description: Optional[str] = None,
|
|
145
|
+
language: Optional[LanguageLike] = None, currency: Optional[CurrencyLike] = None,
|
|
146
|
+
success_url: Optional[str] = None, error_url: Optional[str] = None) -> Any:
|
|
147
|
+
payload = self._checkout(amount, order_id, description, language, currency, success_url, error_url)
|
|
148
|
+
payload["refund"] = 0
|
|
149
|
+
return self._run(Op("card_register_pay", payload, CardRegistration.from_api, order_id=str(payload["order_id"])))
|
|
150
|
+
|
|
151
|
+
def charge(self, card_id: str, amount_: Amount, order_id: str, *, description: Optional[str] = None,
|
|
152
|
+
language: Optional[LanguageLike] = None, currency: Optional[CurrencyLike] = None) -> Any:
|
|
153
|
+
payload = {
|
|
154
|
+
"card_id": required(card_id, "card_id"),
|
|
155
|
+
"amount": wire(amount(amount_)),
|
|
156
|
+
"order_id": required(order_id, "order_id"),
|
|
157
|
+
"currency": self._currency(currency),
|
|
158
|
+
"language": self._language(language),
|
|
159
|
+
"description": optional(description, "description"),
|
|
160
|
+
}
|
|
161
|
+
return self._run(Op("card_charge", payload, Transaction.from_api, payment_state=True,
|
|
162
|
+
order_id=str(payload["order_id"])))
|
|
163
|
+
|
|
164
|
+
def payout(self, card_id: str, amount_: Amount, order_id: str, *, description: Optional[str] = None,
|
|
165
|
+
language: Optional[LanguageLike] = None, currency: Optional[CurrencyLike] = None) -> Any:
|
|
166
|
+
payload = {
|
|
167
|
+
"card_id": required(card_id, "card_id"),
|
|
168
|
+
"amount": wire(amount(amount_)),
|
|
169
|
+
"order_id": required(order_id, "order_id"),
|
|
170
|
+
"currency": self._currency(currency),
|
|
171
|
+
"language": self._language(language),
|
|
172
|
+
"description": optional(description, "description"),
|
|
173
|
+
}
|
|
174
|
+
return self._run(Op("payout", payload, Transaction.from_api, payment_state=True,
|
|
175
|
+
order_id=str(payload["order_id"])))
|
|
176
|
+
|
|
177
|
+
def status(self, card_id: str) -> Any:
|
|
178
|
+
payload = {"card_id": required(card_id, "card_id")}
|
|
179
|
+
return self._run(Op("card_status", payload, Card.from_api, safe=True))
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def split_parts(total: Amount, partner: str, partner_amount: Amount) -> dict[str, Any]:
|
|
183
|
+
whole, part = amount(total), amount(partner_amount, "split_amount")
|
|
184
|
+
if part > whole:
|
|
185
|
+
raise InvalidRequestError(text("value.split_amount", split=part, amount=whole))
|
|
186
|
+
return {"split_user": required(partner, "split_user"), "split_amount": wire(part)}
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class Split(_Resource):
|
|
190
|
+
|
|
191
|
+
def create(self, amount: Amount, order_id: str, partner: str, partner_amount: Amount, *,
|
|
192
|
+
description: Optional[str] = None, language: Optional[LanguageLike] = None,
|
|
193
|
+
currency: Optional[CurrencyLike] = None, success_url: Optional[str] = None,
|
|
194
|
+
error_url: Optional[str] = None, other_attr: Any = None) -> Any:
|
|
195
|
+
payload = self._checkout(amount, order_id, description, language, currency, success_url, error_url, other_attr)
|
|
196
|
+
payload.update(split_parts(amount, partner, partner_amount))
|
|
197
|
+
return self._run(Op("split_payment", payload, Redirect.from_api, order_id=str(payload["order_id"])))
|
|
198
|
+
|
|
199
|
+
def charge(self, card_id: str, amount_: Amount, order_id: str, partner: str, partner_amount: Amount, *,
|
|
200
|
+
description: Optional[str] = None, language: Optional[LanguageLike] = None,
|
|
201
|
+
currency: Optional[CurrencyLike] = None) -> Any:
|
|
202
|
+
payload = {
|
|
203
|
+
"card_id": required(card_id, "card_id"),
|
|
204
|
+
"amount": wire(amount(amount_)),
|
|
205
|
+
"order_id": required(order_id, "order_id"),
|
|
206
|
+
"currency": self._currency(currency),
|
|
207
|
+
"language": self._language(language),
|
|
208
|
+
"description": optional(description, "description"),
|
|
209
|
+
**split_parts(amount_, partner, partner_amount),
|
|
210
|
+
}
|
|
211
|
+
return self._run(Op("split_charge", payload, Transaction.from_api, payment_state=True,
|
|
212
|
+
order_id=str(payload["order_id"])))
|
|
213
|
+
|
|
214
|
+
def register_and_pay(self, amount: Amount, order_id: str, partner: str, partner_amount: Amount, *,
|
|
215
|
+
description: Optional[str] = None, language: Optional[LanguageLike] = None,
|
|
216
|
+
currency: Optional[CurrencyLike] = None, success_url: Optional[str] = None,
|
|
217
|
+
error_url: Optional[str] = None) -> Any:
|
|
218
|
+
payload = self._checkout(amount, order_id, description, language, currency, success_url, error_url)
|
|
219
|
+
payload.update(split_parts(amount, partner, partner_amount))
|
|
220
|
+
return self._run(Op("split_register_pay", payload, CardRegistration.from_api,
|
|
221
|
+
order_id=str(payload["order_id"])))
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _plans(body: Any) -> list[InstallmentPlan]:
|
|
225
|
+
rows = body if isinstance(body, list) else (body or {}).get("data") or (body or {}).get("installments") or []
|
|
226
|
+
return [InstallmentPlan.from_api(row) for row in rows if isinstance(row, Mapping)]
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class Installments(_Resource):
|
|
230
|
+
def plans(self, order_id: str, amount_: Optional[Amount] = None, *, language: Optional[LanguageLike] = None,
|
|
231
|
+
currency: Optional[CurrencyLike] = None) -> Any:
|
|
232
|
+
payload = {
|
|
233
|
+
"order_id": required(order_id, "order_id"),
|
|
234
|
+
"amount": wire(amount(amount_)) if amount_ is not None else None,
|
|
235
|
+
"currency": self._currency(currency),
|
|
236
|
+
"language": self._language(language),
|
|
237
|
+
}
|
|
238
|
+
return self._run(Op("installment_plans", payload, _plans, safe=True))
|
|
239
|
+
|
|
240
|
+
def create(self, amount: Amount, order_id: str, card_id: str, months: int, *, description: Optional[str] = None,
|
|
241
|
+
language: Optional[LanguageLike] = None, currency: Optional[CurrencyLike] = None,
|
|
242
|
+
success_url: Optional[str] = None, error_url: Optional[str] = None) -> Any:
|
|
243
|
+
if isinstance(months, bool) or not isinstance(months, int) or months < 1:
|
|
244
|
+
raise InvalidRequestError(text("value.months", value=months))
|
|
245
|
+
payload = self._checkout(amount, order_id, description, language, currency, success_url, error_url)
|
|
246
|
+
payload.update({"installment_card_id": required(card_id, "installment_card_id"), "installment_month": months})
|
|
247
|
+
return self._run(Op("installment_payment", payload, Redirect.from_api, order_id=str(payload["order_id"])))
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _wallets(body: Any) -> list[Wallet]:
|
|
251
|
+
if isinstance(body, Mapping) and isinstance(body.get("wallets"), list):
|
|
252
|
+
return [Wallet.from_api(row) for row in body["wallets"] if isinstance(row, Mapping)]
|
|
253
|
+
if isinstance(body, Mapping):
|
|
254
|
+
skip = {"status", "trace_id", "message", "code"}
|
|
255
|
+
return [Wallet(str(key), value) for key, value in body.items() if key not in skip and isinstance(value, str)]
|
|
256
|
+
return []
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class Wallets(_Resource):
|
|
260
|
+
def list(self) -> Any:
|
|
261
|
+
return self._run(Op("wallets", {}, _wallets, safe=True))
|
|
262
|
+
|
|
263
|
+
def pay(self, wallet_id: str, amount_: Amount, order_id: str, *, description: Optional[str] = None,
|
|
264
|
+
language: Optional[LanguageLike] = None, currency: Optional[CurrencyLike] = None) -> Any:
|
|
265
|
+
payload = {
|
|
266
|
+
"wallet_id": required(wallet_id, "wallet_id"),
|
|
267
|
+
"amount": wire(amount(amount_)),
|
|
268
|
+
"order_id": required(order_id, "order_id"),
|
|
269
|
+
"currency": self._currency(currency),
|
|
270
|
+
"language": self._language(language),
|
|
271
|
+
"description": optional(description, "description"),
|
|
272
|
+
}
|
|
273
|
+
return self._run(Op("wallet_payment", payload, Redirect.from_api, order_id=str(payload["order_id"])))
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
class Widgets(_Resource):
|
|
277
|
+
def create(self, amount_: Amount, order_id: str, description: str) -> Any:
|
|
278
|
+
payload = {"amount": wire(amount(amount_)), "order_id": required(order_id, "order_id"),
|
|
279
|
+
"description": required(description, "description")}
|
|
280
|
+
return self._run(Op("widget", payload, Widget.from_api, order_id=str(payload["order_id"])))
|
|
281
|
+
|
|
282
|
+
def split(self, amount: Amount, order_id: str, partner: str, partner_amount: Amount, *,
|
|
283
|
+
description: Optional[str] = None, language: Optional[LanguageLike] = None,
|
|
284
|
+
currency: Optional[CurrencyLike] = None, success_url: Optional[str] = None,
|
|
285
|
+
error_url: Optional[str] = None) -> Any:
|
|
286
|
+
payload = self._checkout(amount, order_id, description, language, currency, success_url, error_url)
|
|
287
|
+
payload.update(split_parts(amount, partner, partner_amount))
|
|
288
|
+
return self._run(Op("widget_split", payload, Redirect.from_api, order_id=str(payload["order_id"])))
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _invoice_body(total: Amount, period_from: Union[date, str], period_to: Union[date, str], name: Optional[str],
|
|
292
|
+
description: Optional[str], phone: Optional[str], email: Optional[str], tin: Optional[str],
|
|
293
|
+
contract_number: Optional[str], merchant_order_id: Optional[str], display: bool,
|
|
294
|
+
save_as_template: bool, installment: bool) -> dict[str, Any]:
|
|
295
|
+
return {
|
|
296
|
+
"sum": wire(amount(total, "sum")),
|
|
297
|
+
"period_from": day(period_from),
|
|
298
|
+
"period_to": day(period_to),
|
|
299
|
+
"name": optional(name, "name"),
|
|
300
|
+
"description": optional(description, "description"),
|
|
301
|
+
"phone": optional(phone, "phone", 32),
|
|
302
|
+
"email": optional(email, "email", 254),
|
|
303
|
+
"inn": optional(tin, "tin", 32),
|
|
304
|
+
"contract_number": optional(contract_number, "contract_number"),
|
|
305
|
+
"merchant_order_id": optional(merchant_order_id, "merchant_order_id"),
|
|
306
|
+
"display": int(display),
|
|
307
|
+
"save_as_template": int(save_as_template),
|
|
308
|
+
"status_installment": int(installment),
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
class Invoices(_Resource):
|
|
313
|
+
def create(self, amount: Amount, period_from: Union[date, str], period_to: Union[date, str], *,
|
|
314
|
+
name: Optional[str] = None, description: Optional[str] = None, phone: Optional[str] = None,
|
|
315
|
+
email: Optional[str] = None, tin: Optional[str] = None, contract_number: Optional[str] = None,
|
|
316
|
+
merchant_order_id: Optional[str] = None, display: bool = True, save_as_template: bool = False,
|
|
317
|
+
installment: bool = False) -> Any:
|
|
318
|
+
payload = _invoice_body(amount, period_from, period_to, name, description, phone, email, tin, contract_number,
|
|
319
|
+
merchant_order_id, display, save_as_template, installment)
|
|
320
|
+
return self._run(Op("invoice_create", payload, lambda body: int(body["id"]) if body.get("id") else None))
|
|
321
|
+
|
|
322
|
+
def update(self, invoice_id: int, amount: Amount, period_from: Union[date, str], period_to: Union[date, str], *,
|
|
323
|
+
name: Optional[str] = None, description: Optional[str] = None, phone: Optional[str] = None,
|
|
324
|
+
email: Optional[str] = None, tin: Optional[str] = None, contract_number: Optional[str] = None,
|
|
325
|
+
merchant_order_id: Optional[str] = None, display: bool = True, save_as_template: bool = False,
|
|
326
|
+
installment: bool = False) -> Any:
|
|
327
|
+
payload = _invoice_body(amount, period_from, period_to, name, description, phone, email, tin, contract_number,
|
|
328
|
+
merchant_order_id, display, save_as_template, installment)
|
|
329
|
+
payload["id"] = int(invoice_id)
|
|
330
|
+
return self._run(Op("invoice_update", payload, Result.from_api))
|
|
331
|
+
|
|
332
|
+
def get(self, invoice_id: int) -> Any:
|
|
333
|
+
return self._run(Op("invoice_view", {"id": int(invoice_id)},
|
|
334
|
+
lambda body: Invoice.from_api(body.get("invoice") or {}), safe=True))
|
|
335
|
+
|
|
336
|
+
def list(self, *, kind: Optional[str] = None, order: Optional[str] = None) -> Any:
|
|
337
|
+
return self._run(Op("invoice_list", {"type": kind, "order": order},
|
|
338
|
+
lambda body: [Invoice.from_api(row) for row in body.get("invoices") or []], safe=True))
|
|
339
|
+
|
|
340
|
+
def send_sms(self, invoice_id: int, phone: str) -> Any:
|
|
341
|
+
payload = {"id": int(invoice_id), "phone": required(phone, "phone", 32)}
|
|
342
|
+
return self._run(Op("invoice_sms", payload, Result.from_api))
|
|
343
|
+
|
|
344
|
+
def send_email(self, invoice_id: int, email: str) -> Any:
|
|
345
|
+
payload = {"id": int(invoice_id), "email": required(email, "email", 254)}
|
|
346
|
+
return self._run(Op("invoice_email", payload, Result.from_api))
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
class Transfers(_Resource):
|
|
350
|
+
def create(self, order_id: str, amount_: Amount, *, iban: str, name: str, tin: str, bank_code: str,
|
|
351
|
+
description: str, bank_name: Optional[str] = None, email: Optional[str] = None,
|
|
352
|
+
address: Optional[str] = None, additional_info: Optional[str] = None,
|
|
353
|
+
webhook_url: Optional[str] = None) -> Any:
|
|
354
|
+
payload = {
|
|
355
|
+
"order_id": required(order_id, "order_id"),
|
|
356
|
+
"amount": wire(amount(amount_)),
|
|
357
|
+
"description": required(description, "description"),
|
|
358
|
+
"iban": required(iban, "iban", 34).replace(" ", "").upper(),
|
|
359
|
+
"name": required(name, "name"),
|
|
360
|
+
"tin": required(tin, "tin", 32),
|
|
361
|
+
"bank_code": required(bank_code, "bank_code", 32),
|
|
362
|
+
"bank_name": optional(bank_name, "bank_name"),
|
|
363
|
+
"email": optional(email, "email", 254),
|
|
364
|
+
"address": optional(address, "address"),
|
|
365
|
+
"additional_info": optional(additional_info, "additional_info"),
|
|
366
|
+
"webhook_url": optional(webhook_url, "webhook_url", 2048),
|
|
367
|
+
}
|
|
368
|
+
return self._run(Op("b2b_transfer", payload, Transfer.from_api, json_body=True,
|
|
369
|
+
order_id=str(payload["order_id"])))
|
|
370
|
+
|
|
371
|
+
def status(self, order_id: str) -> Any:
|
|
372
|
+
key = required(order_id, "order_id")
|
|
373
|
+
return self._run(Op("b2b_status", None, Transfer.from_api, safe=True, method="GET", signed=False,
|
|
374
|
+
path_params={"order_id": key}))
|
epoint_sdk/signing.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import binascii
|
|
5
|
+
import hashlib
|
|
6
|
+
import hmac
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from .errors import SignatureError
|
|
11
|
+
from .messages import text
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def encode(payload: dict[str, Any]) -> str:
|
|
15
|
+
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=True)
|
|
16
|
+
return base64.b64encode(body.encode("utf-8")).decode("ascii")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def sign(private_key: str, data: str) -> str:
|
|
20
|
+
digest = hashlib.sha1(f"{private_key}{data}{private_key}".encode()).digest()
|
|
21
|
+
return base64.b64encode(digest).decode("ascii")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def matches(private_key: str, data: str, signature: str) -> bool:
|
|
25
|
+
if not private_key or not data or not signature or len(data) > MAX_DATA or len(signature) > 128:
|
|
26
|
+
return False
|
|
27
|
+
return hmac.compare_digest(sign(private_key, data).encode(), str(signature).strip().encode())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
MAX_DATA = 65536
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def decode(data: str) -> dict[str, Any]:
|
|
34
|
+
if len(data) > MAX_DATA:
|
|
35
|
+
raise SignatureError(text("error.too_large", limit=MAX_DATA))
|
|
36
|
+
try:
|
|
37
|
+
raw = base64.b64decode(data, validate=True)
|
|
38
|
+
parsed = json.loads(raw.decode("utf-8"))
|
|
39
|
+
except (binascii.Error, ValueError, UnicodeDecodeError):
|
|
40
|
+
raise SignatureError(text("error.data")) from None
|
|
41
|
+
if not isinstance(parsed, dict):
|
|
42
|
+
raise SignatureError(text("error.data_object"))
|
|
43
|
+
return parsed
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def verify(private_key: str, data: str, signature: str) -> dict[str, Any]:
|
|
47
|
+
if not matches(private_key, data, signature):
|
|
48
|
+
raise SignatureError(text("error.signature"))
|
|
49
|
+
return decode(data)
|