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.
@@ -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)