├── .gitignore
├── .travis.yml
├── CHANGES.rst
├── LICENSE
├── README.rst
├── cloudpayments
├── __init__.py
├── client.py
├── enums.py
├── errors.py
├── models.py
├── tests.py
└── utils.py
└── setup.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.py[cod]
2 |
3 | # C extensions
4 | *.so
5 |
6 | # Packages
7 | *.egg
8 | *.egg-info
9 | dist
10 | build
11 | eggs
12 | parts
13 | bin
14 | var
15 | sdist
16 | develop-eggs
17 | .installed.cfg
18 | lib
19 | lib64
20 | __pycache__
21 |
22 | # Installer logs
23 | pip-log.txt
24 |
25 | # Unit test / coverage reports
26 | .coverage
27 | .tox
28 | nosetests.xml
29 |
30 | # Translations
31 | *.mo
32 |
33 | # Mr Developer
34 | .mr.developer.cfg
35 | .project
36 | .pydevproject
37 |
38 | # IDE
39 | .idea
40 | *.iml
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 | python:
3 | - '2.7'
4 | - '3.4'
5 | - '3.5'
6 | - 3.5-dev
7 | - '3.6'
8 | - 3.6-dev
9 | - 3.7-dev
10 | - nightly
11 | install: python setup.py install
12 | script: nosetests
13 | deploy:
14 | provider: pypi
15 | user: i-shevchenko
16 | password:
17 | secure: JRLhgbvUqzMCCN/bJpvNS0doBHeTUt8MGVB1iISZrQFEqJTl5f1m4I3rvKop/Hp8lyQ27mpkCEnixaw2hqtyomraA5CjzPcZzsc4jvj1wQTLyyEhYQemEdhNT8IFMMpZveAWxHZXzSQ9pfVv2O7Fw72t2i87Gql2kU137hUU25HfRQ9kCbez0chfTHthv1ChdO0okAaY5G+zeRL3r7HtFApHDtfh0U8kfGrqm8hRf0pYLdc0R9xZ9ysFu5Qy8cqJp7z9wqXbeWb96pwpb7rFw6SiRr0dDSKEIo6HkYGbyiHubFDeGEIk5aeSNC+dK99BuyPm7jz/nf7tvj/nSi/MhozVGrObGsM0VHeJHe01C/BXGL+mLYKlkFBQtwCcgqX1ZKWtDh7ANXwMinnLpbeOnn6ZLwbgVnEK1x6uBjjISP6rs5BTow6mKzelCLDv61Y0Lb8FqafjSokt35XhJmI/kM7T28yKfwpSaAdlL9DgIlSNFpAaR9gvlponm1Itx8fJ7j1s22+Te6UBnJ/8p1p7NUnUdHl1DeVjasKCfOCI2SwRma0q1EWqRUB3HlLoCC6+WZQW7wydY0AgafpGKIOV8YDqXX/oxzUJ8jKzzULZeS8vA8OGxSb6iPvHKxlM44p9utfeEKs9yoXALxiETr3/bOUFJBbyOfi5c85A5W6MuOU=
18 | on:
19 | tags: true
20 |
--------------------------------------------------------------------------------
/CHANGES.rst:
--------------------------------------------------------------------------------
1 | 1.6.4 (2021-09-13)
2 | ==================
3 |
4 | * Исправлен баг с форматированием даты без часового пояса при вызове ``CloudPayments.create_subscription``.
5 |
6 | 1.6.3 (2021-03-15)
7 | ==================
8 |
9 | * Добавлен метод для получения списка подписок ``CloudPayments.list_subscriptions``.
10 |
11 | 1.6.2 (2020-10-22)
12 | ==================
13 |
14 | * Добавлена расшифровка кодов ошибок 5007, 5014, 5075 и 5207
15 |
16 | 1.6.1 (2020-06-04)
17 | ==================
18 |
19 | * В метод ``CloudPayments.refund`` добавлен аргумент ``request_id`` для опционального идентификатора идемпотентного запроса.
20 | * Метод ``CloudPayments.refund`` теперь возвращает идентификатор транзакции возврата.
21 |
22 |
23 | 1.6.0 (2019-06-27)
24 | ==================
25 |
26 | * В метод ``CloudPayments.charge_card`` добавлено поле ``service_fee``.
27 | * Прекращена поддержка Python 2.6 и Python 3.3
28 |
29 |
30 | 1.5.1 (2019-01-26)
31 | ==================
32 |
33 | * Исправлен запуск на Python 2
34 |
35 |
36 | 1.5.0 (2019-01-26)
37 | ==================
38 |
39 | * Добавлен метод для получения данных чека ``CloudPayments.get_receipt``.
40 |
41 |
42 | 1.4.2 (2018-06-07)
43 | ==================
44 |
45 | * Добавлен метод для получения информации о траннзакии ``CloudPayments.get_transaction``.
46 | * В ``PaymentError`` добавлено поле ``transaction_id``.
47 |
48 |
49 | 1.4.1 (2018-04-15)
50 | ==================
51 |
52 | * Добавлен метод для выплат по токену ``CloudPayments.topup``.
53 |
54 |
55 | 1.4.0 (2017-09-15)
56 | ==================
57 |
58 | * Метод ``CloudPayments.create_receipt`` теперь возвращает уникальный идентификатор чека, поставленного в очередь.
59 |
60 |
61 | 1.3.2 (2017-08-30)
62 | ==================
63 |
64 | * В метод ``CloudPayments.confirm_payment`` добавлен необязательный аргумент ``data`` для передачи в запросе произвольных данных.
65 |
66 |
67 | 1.3.1 (2017-06-10)
68 | ==================
69 |
70 | * Исправлена ошибка при установке на Python 3.
71 |
72 |
73 | 1.3 (2017-06-10)
74 | ================
75 |
76 | * Добавлен метод ``CloudPayments.create_receipt`` для формирования кассового чека.
77 | * В метод ``CloudPayments.test`` добавлен необязательный аргумент ``request_id`` для передачи идентификатора идемпотентного запроса.
78 | * Метод ``CloudPayments.test`` теперь возвращает сообщение, полученное от сервера.
79 | * Тип исключения, бросаемого методом ``CloudPayments.finish_3d_secure_authentication``, изменен с ``CloudPaymentsError`` на ``PaymentError``.
80 | * В исключение ``PaymentError`` добавлены поля ``verbose_reason`` и ``verbose_message``, содержащие описания ошибок из документации.
81 |
82 |
83 | 1.2 (2016-06-21)
84 | ================
85 |
86 | * Исправлено неверное форматирование дат, из-за которого сервис неправильно интерпретировал передаваемые даты.
87 | * В исключение ``PaymentError`` добавлено поле cardholder_message с сообщением, которое выводится для пользователя.
88 | * Метод ``redirect_url`` у ``Secure3d`` заменен на ``redirect_params``, возвращающий словарь с параметрами для POST-запроса.
89 |
90 |
91 | 1.1 (2016-06-03)
92 | ================
93 |
94 | * Исправлены ошибки, возникающие, когда при оплате по криптограмме требуется 3-D Secure аутентификация.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2016 Antida Software
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | ===================================
2 | CloudPayments Python Client Library
3 | ===================================
4 |
5 | .. image:: https://img.shields.io/pypi/v/cloudpayments.svg
6 | :target: https://pypi.python.org/pypi/cloudpayments/
7 | :alt: Python Package Index
8 |
9 | .. image:: https://img.shields.io/travis/antidasoftware/cloudpayments-python-client.svg
10 | :target: https://travis-ci.org/antidasoftware/cloudpayments-python-client
11 | :alt: Travis CI
12 |
13 | Клиент для платежного сервиса `CloudPayments `_. Позволяет обращаться к `API CloudPayments `_ из кода на Python.
14 |
15 | Установка
16 | =========
17 |
18 | ::
19 |
20 | pip install cloudpayments
21 |
22 |
23 | Требования
24 | ==========
25 |
26 | Python 2.7 или 3.4+
27 |
28 |
29 | Использование
30 | =============
31 |
32 | .. code:: python
33 |
34 | from cloudpayments import CloudPayments
35 |
36 | client = CloudPayments('public_id', 'api_secret')
37 | client.test()
38 |
39 | При создании клиента задаются аутентификационные параметры: Public ID и Api Secret. Оба этих значения можно получить в личном кабинете.
40 |
41 | Обращение к API осуществляется через методы клиента.
42 |
43 |
44 | | **Тестовый метод** (`описание `__)
45 |
46 | .. code:: python
47 |
48 | test(self, request_id=None)
49 |
50 | ``request_id`` — идентификатор для `идемпотентного запроса `__.
51 |
52 | В случае успеха возвращает строку с сообщением от сервиса.
53 |
54 |
55 | | **Оплата по криптограмме** (`описание `__)
56 |
57 | .. code:: python
58 |
59 | charge_card(self, cryptogram, amount, currency, name, ip_address,
60 | invoice_id=None, description=None, account_id=None,
61 | email=None, data=None, require_confirmation=False,
62 | service_fee=None)
63 |
64 | ``currency`` — одна из констант, определенных в классе ``Currency``.
65 |
66 | ``data`` — произвольные данные, при отправке будут сериализованы в JSON.
67 |
68 | ``service_fee`` — сервисный сбор.
69 |
70 | ``require_confirmation`` — если установлено в ``True``, платеж будет выполняться по двухстадийной схеме.
71 |
72 | В случае успеха возвращает объект типа ``Transaction`` (если не требуется 3-D Secure аутентификация) либо ``Secure3d`` (если требуется).
73 |
74 |
75 | | **Завершение оплаты после прохождения 3-D Secure** (`описание `__)
76 |
77 | .. code:: python
78 |
79 | finish_3d_secure_authentication(self, transaction_id, pa_res)
80 |
81 | В случае успеха возвращает объект типа ``Transaction``.
82 |
83 |
84 | | **Оплата по токену** (`описание `__)
85 |
86 | .. code:: python
87 |
88 | charge_token(self, token, account_id, amount, currency,
89 | ip_address=None, invoice_id=None, description=None,
90 | email=None, data=None, require_confirmation=False)
91 |
92 | ``currency`` — одна из констант, определенных в классе ``Currency``
93 |
94 | ``data`` — произвольные данные, при отправке будут сериализованы в JSON.
95 |
96 | ``require_confirmation`` — если установлено в ``True``, платеж будет выполняться по двухстадийной схеме.
97 |
98 | В случае успеха возвращает объект типа ``Transaction``.
99 |
100 |
101 | | **Подтверждение оплаты** (`описание `__)
102 |
103 | .. code:: python
104 |
105 | confirm_payment(self, transaction_id, amount, data=None)
106 |
107 | ``data`` — произвольные данные, при отправке будут сериализованы в JSON.
108 |
109 | В случае успеха метод ничего не возвращает, при ошибке бросает исключение.
110 |
111 |
112 | | **Отмена оплаты** (`описание `__)
113 |
114 | .. code:: python
115 |
116 | void_payment(self, transaction_id)
117 |
118 | В случае успеха метод ничего не возвращает, при ошибке бросает исключение.
119 |
120 |
121 | | **Возврат денег** (`описание `__)
122 |
123 | .. code:: python
124 |
125 | refund(self, transaction_id, amount, request_id=None)
126 |
127 | ``request_id`` — идентификатор для `идемпотентного запроса `__.
128 |
129 | В случае успеха возвращает идентификатор транзакции возврата.
130 |
131 |
132 | | **Выплата по токену** (`описание `__)
133 |
134 | .. code:: python
135 |
136 | topup(self, token, amount, account_id, currency, invoice_id=None)
137 |
138 | ``currency`` — одна из констант, определенных в классе ``Currency``
139 |
140 | В случае успеха возвращает объект типа ``Transaction``.
141 |
142 |
143 | | **Получение транзакции** (`описание `__)
144 |
145 | .. code:: python
146 |
147 | get_transaction(self, transaction_id)
148 |
149 | ``transaction_id`` — ID транзакции
150 |
151 | В случае успеха возвращает объект типа ``Transaction``.
152 |
153 |
154 | | **Проверка статуса платежа** (`описание `__)
155 |
156 | .. code:: python
157 |
158 | find_payment(self, invoice_id)
159 |
160 | В случае успеха возвращает объект типа ``Transaction``.
161 |
162 |
163 | | **Выгрузка списка транзакций** (`описание `__)
164 |
165 | .. code:: python
166 |
167 | list_payments(self, date, timezone=None)
168 |
169 | ``date`` — объект типа ``datetime.date``.
170 |
171 | ``timezone`` — одна из констант, определенных в классе ``Timezone``.
172 |
173 | В случае успеха возвращает список объектов типа ``Transaction``.
174 |
175 |
176 | | **Создание подписки** (`описание `__)
177 |
178 | .. code:: python
179 |
180 | create_subscription(self, token, account_id, amount, currency,
181 | description, email, start_date, interval, period,
182 | require_confirmation=False, max_periods=None)
183 |
184 | ``currency`` — одна из констант, определенных в классе ``Currency``.
185 |
186 | ``start_date`` — объект типа ``datetime.datetime``.
187 |
188 | ``interval`` — одна из констант, определенных в классе ``Interval``.
189 |
190 | В случае успеха возвращает объект типа ``Subscription``.
191 |
192 |
193 | | **Выгрузка списка подписок** (`описание `__)
194 |
195 | .. code:: python
196 |
197 | list_subscriptions(self, account_id)
198 |
199 | ``account_id`` — идентификатор пользователя.
200 |
201 | В случае успеха возвращает список объектов типа ``Subscription``.
202 |
203 |
204 | | **Запрос статуса подписки** (`описание `__)
205 |
206 | .. code:: python
207 |
208 | get_subscription(self, subscription_id)
209 |
210 | В случае успеха возвращает объект типа ``Subscription``.
211 |
212 |
213 | | **Изменение подписки** (`описание `__)
214 |
215 | .. code:: python
216 |
217 | update_subscription(self, subscription_id, amount=None, currency=None,
218 | description=None, start_date=None, interval=None,
219 | period=None, require_confirmation=None,
220 | max_periods=None)
221 |
222 | ``currency`` — одна из констант, определенных в классе ``Currency``.
223 |
224 | ``start_date`` — объект типа ``datetime.datetime``.
225 |
226 | ``interval`` — одна из констант, определенных в классе ``Interval``.
227 |
228 | В случае успеха возвращает объект типа ``Subscription``.
229 |
230 |
231 | | **Отмена подписки** (`описание `__)
232 |
233 | .. code:: python
234 |
235 | cancel_subscription(self, subscription_id)
236 |
237 | В случае успеха метод ничего не возвращает, при ошибке бросает исключение.
238 |
239 |
240 | | **Отправка счета по почте** (`описание `__)
241 |
242 | .. code:: python
243 |
244 | create_order(self, amount, currency, description, email=None,
245 | send_email=None, require_confirmation=None,
246 | invoice_id=None, account_id=None, phone=None,
247 | send_sms=None, send_whatsapp=None, culture_info=None)
248 |
249 | ``currency`` — одна из констант, определенных в классе ``Currency``.
250 |
251 | ``culture_info`` — одна из констант, определенных в классе ``CultureInfo``.
252 |
253 | В случае успеха возвращает объект типа ``Order``.
254 |
255 |
256 | | **Формирование кассового чека** (`описание `__)
257 |
258 | .. code:: python
259 |
260 | create_receipt(self, inn, receipt_type, customer_receipt,
261 | invoice_id=None, account_id=None, request_id=None)
262 |
263 | ``receipt_type`` — одна из констант, определенных в классе ``ReceiptType``.
264 |
265 | ``customer_receipt`` — объект типа ``Receipt`` или словарь с данными чека.
266 |
267 | ``request_id`` — идентификатор для `идемпотентного запроса `__.
268 |
269 | В случае успеха возвращает строку с уникальным идентификатором чека.
270 |
271 |
272 | | **Получение данных чека** (`описание `__)
273 |
274 | .. code:: python
275 |
276 | get_receipt(self, receipt_id)
277 |
278 |
279 | ``receipt_id`` — идентификатор чека
280 |
281 | В случае успеха возвращает объект типа ``Receipt``
282 |
283 | | **Изменение настроек уведомлений** (`описание `__)
284 |
285 | .. code:: python
286 |
287 | def update_webhook(
288 | self,
289 | webhook_type: WebhookType,
290 | address,
291 | is_enabled: bool = True,
292 | method="GET",
293 | encoding="UTF8",
294 | format_notifications="CloudPayments"
295 | )
296 |
297 | ``webhook_type`` — тип уведомления: Pay/Fail и т.д.
298 |
299 | ``address`` — адрес для отправки уведомлений (для HTTPS-схемы необходим валидный SSL-сертификат)
300 |
301 | ``is_enabled`` — Если значение true — то уведомление включено.
302 |
303 | ``method`` — HTTP-метод для отправки уведомлений. Возможные значения: GET, POST. Значение по умолчанию — GET
304 |
305 | ``encoding`` — кодировка уведомлений. Возможные значения: UTF8, Windows1251. Значение по умолчанию — UTF8
306 |
307 | ``format_notifications`` — Формат уведомлений. Возможные значения: CloudPayments, QIWI, RT. Значение по умолчанию — CloudPayments
308 |
309 |
310 | Авторы
311 | ======
312 |
313 | Разработано в `Antida software `_.
314 | Мы создаем SaaS-продукты и сервисы, интегрированные с платежными системами.
315 | Пишите нам, если вам нужна консультация по работе с биллинговыми системами: `info@antidasoftware.com `_.
316 |
317 |
318 | Лицензия
319 | ========
320 |
321 | MIT
322 |
--------------------------------------------------------------------------------
/cloudpayments/__init__.py:
--------------------------------------------------------------------------------
1 | from .client import CloudPayments
2 | from .errors import CloudPaymentsError, PaymentError
3 | from .models import Subscription, Transaction, Secure3d, Order, \
4 | Receipt, ReceiptItem
5 | from .enums import Currency, Interval, Timezone, CultureInfo, \
6 | SubscriptionStatus, TransactionStatus, ReasonCode, ReceiptType, \
7 | TaxationSystem
--------------------------------------------------------------------------------
/cloudpayments/client.py:
--------------------------------------------------------------------------------
1 | import decimal
2 |
3 | import requests
4 | from requests.auth import HTTPBasicAuth
5 |
6 | from .errors import CloudPaymentsError, PaymentError
7 | from .models import Order, Receipt, Secure3d, Subscription, Transaction
8 | from .utils import format_date, format_datetime
9 |
10 |
11 | class CloudPayments(object):
12 | URL = 'https://api.cloudpayments.ru/'
13 |
14 | def __init__(self, public_id, api_secret):
15 | self.public_id = public_id
16 | self.api_secret = api_secret
17 |
18 | def _send_request(self, endpoint, params=None, request_id=None):
19 | auth = HTTPBasicAuth(self.public_id, self.api_secret)
20 |
21 | headers = None
22 | if request_id is not None:
23 | headers = {'X-Request-ID': request_id}
24 |
25 | response = requests.post(self.URL + endpoint, json=params, auth=auth,
26 | headers=headers)
27 | return response.json(parse_float=decimal.Decimal)
28 |
29 | def test(self, request_id=None):
30 | response = self._send_request('test', request_id=request_id)
31 |
32 | if not response['Success']:
33 | raise CloudPaymentsError(response)
34 | return response['Message']
35 |
36 | def get_transaction(self, transaction_id):
37 | """Get transaction info by its id."""
38 | params = {'TransactionId': transaction_id}
39 | response = self._send_request('payments/get', params)
40 | if 'Model' in response.keys():
41 | return Transaction.from_dict(response['Model'])
42 | else:
43 | raise CloudPaymentsError(response)
44 |
45 | def charge_card(self, cryptogram, amount, currency, name, ip_address,
46 | invoice_id=None, description=None, account_id=None,
47 | email=None, data=None, require_confirmation=False,
48 | service_fee=None):
49 | params = {
50 | 'Amount': amount,
51 | 'Currency': currency,
52 | 'IpAddress': ip_address,
53 | 'Name': name,
54 | 'CardCryptogramPacket': cryptogram
55 | }
56 | if invoice_id is not None:
57 | params['InvoiceId'] = invoice_id
58 | if description is not None:
59 | params['Description'] = description
60 | if account_id is not None:
61 | params['AccountId'] = account_id
62 | if email is not None:
63 | params['Email'] = email
64 | if service_fee is not None:
65 | params['PayerServiceFee'] = service_fee
66 | if data is not None:
67 | params['JsonData'] = data
68 |
69 | endpoint = ('payments/cards/auth' if require_confirmation else
70 | 'payments/cards/charge')
71 | response = self._send_request(endpoint, params)
72 |
73 | if response['Success']:
74 | return Transaction.from_dict(response['Model'])
75 | if response['Message']:
76 | raise CloudPaymentsError(response)
77 | if 'ReasonCode' in response['Model']:
78 | raise PaymentError(response)
79 | return Secure3d.from_dict(response['Model'])
80 |
81 | def finish_3d_secure_authentication(self, transaction_id, pa_res):
82 | params = {
83 | 'TransactionId': transaction_id,
84 | 'PaRes': pa_res
85 | }
86 | response = self._send_request('payments/cards/post3ds', params)
87 |
88 | if response['Success']:
89 | return Transaction.from_dict(response['Model'])
90 | raise PaymentError(response)
91 |
92 | def charge_token(self, token, account_id, amount, currency,
93 | ip_address=None, invoice_id=None, description=None,
94 | email=None, data=None, tr_initiator_code: int = 1, require_confirmation=False):
95 | params = {
96 | 'Amount': amount,
97 | 'Currency': currency,
98 | 'AccountId': account_id,
99 | 'Token': token,
100 | 'TrInitiatorCode': tr_initiator_code
101 | }
102 | if invoice_id is not None:
103 | params['InvoiceId'] = invoice_id
104 | if description is not None:
105 | params['Description'] = description
106 | if ip_address is not None:
107 | params['IpAddress'] = ip_address
108 | if email is not None:
109 | params['Email'] = email
110 | if data is not None:
111 | params['JsonData'] = data
112 |
113 | endpoint = ('payments/tokens/auth' if require_confirmation else
114 | 'payments/tokens/charge')
115 | response = self._send_request(endpoint, params)
116 | if response['Success']:
117 | return Transaction.from_dict(response['Model'])
118 | if 'Model' in response and 'ReasonCode' in response['Model']:
119 | raise PaymentError(response)
120 | raise CloudPaymentsError(response)
121 |
122 | def confirm_payment(self, transaction_id, amount, data=None):
123 | params = {
124 | 'Amount': amount,
125 | 'TransactionId': transaction_id
126 | }
127 |
128 | if data is not None:
129 | params['JsonData'] = data
130 |
131 | response = self._send_request('payments/confirm', params)
132 |
133 | if not response['Success']:
134 | raise CloudPaymentsError(response)
135 |
136 | def void_payment(self, transaction_id):
137 | params = {'TransactionId': transaction_id}
138 | response = self._send_request('payments/void', params)
139 |
140 | if not response['Success']:
141 | raise CloudPaymentsError(response)
142 |
143 | def refund(self, transaction_id, amount, request_id=None):
144 | params = {
145 | 'Amount': amount,
146 | 'TransactionId': transaction_id
147 | }
148 | response = self._send_request('payments/refund', params, request_id)
149 |
150 | if not response['Success']:
151 | raise CloudPaymentsError(response)
152 |
153 | return response['Model']['TransactionId']
154 |
155 | def topup(self, token, amount, account_id, currency, invoice_id=None):
156 | params = {
157 | 'Token': token,
158 | 'Amount': amount,
159 | 'AccountId': account_id,
160 | 'Currency': currency
161 | }
162 | if invoice_id is not None:
163 | params['InvoiceId'] = invoice_id
164 | response = self._send_request('payments/cards/topup', params)
165 |
166 | if response['Success']:
167 | return Transaction.from_dict(response['Model'])
168 |
169 | raise CloudPaymentsError(response)
170 |
171 | def find_payment(self, invoice_id):
172 | params = {'InvoiceId': invoice_id}
173 | response = self._send_request('payments/find', params)
174 |
175 | if response['Success']:
176 | return Transaction.from_dict(response['Model'])
177 | raise CloudPaymentsError(response)
178 |
179 | def list_payments(self, date, timezone=None):
180 | params = {'Date': format_date(date)}
181 | if timezone is not None:
182 | params['Timezone'] = timezone
183 |
184 | response = self._send_request('payments/list', params)
185 |
186 | if response['Success']:
187 | return map(Transaction.from_dict, response['Model'])
188 | raise CloudPaymentsError(response)
189 |
190 | def create_subscription(self, token, account_id, amount, currency,
191 | description, email, start_date, interval, period,
192 | require_confirmation=False, max_periods=None,
193 | customer_receipt=None):
194 | params = {
195 | 'Token': token,
196 | 'AccountId': account_id,
197 | 'Description': description,
198 | 'Email': email,
199 | 'Amount': amount,
200 | 'Currency': currency,
201 | 'RequireConfiramtion': require_confirmation,
202 | 'StartDate': format_datetime(start_date),
203 | 'Interval': interval,
204 | 'Period': period,
205 | }
206 | if max_periods is not None:
207 | params['MaxPeriods'] = max_periods
208 | if customer_receipt is not None:
209 | params['CustomerReceipt'] = customer_receipt
210 |
211 | response = self._send_request('subscriptions/create', params)
212 |
213 | if response['Success']:
214 | return Subscription.from_dict(response['Model'])
215 | raise CloudPaymentsError(response)
216 |
217 | def list_subscriptions(self, account_id):
218 | params = {'accountId': account_id}
219 | response = self._send_request('subscriptions/find', params)
220 |
221 | if response['Success']:
222 | return map(Subscription.from_dict, response['Model'])
223 | raise CloudPaymentsError(response)
224 |
225 |
226 | def get_subscription(self, subscription_id):
227 | params = {'Id': subscription_id}
228 | response = self._send_request('subscriptions/get', params)
229 |
230 | if response['Success']:
231 | return Subscription.from_dict(response['Model'])
232 | raise CloudPaymentsError(response)
233 |
234 | def update_subscription(self, subscription_id, amount=None, currency=None,
235 | description=None, start_date=None, interval=None,
236 | period=None, require_confirmation=None,
237 | max_periods=None):
238 | params = {
239 | 'Id': subscription_id
240 | }
241 | if description is not None:
242 | params['Description'] = description
243 | if amount is not None:
244 | params['Amount'] = amount
245 | if currency is not None:
246 | params['Currency'] = currency
247 | if require_confirmation is not None:
248 | params['RequireConfirmation'] = require_confirmation
249 | if start_date is not None:
250 | params['StartDate'] = format_datetime(start_date)
251 | if interval is not None:
252 | params['Interval'] = interval
253 | if period is not None:
254 | params['Period'] = period
255 | if max_periods is not None:
256 | params['MaxPeriods'] = max_periods
257 |
258 | response = self._send_request('subscriptions/update', params)
259 |
260 | if response['Success']:
261 | return Subscription.from_dict(response['Model'])
262 | raise CloudPaymentsError(response)
263 |
264 | def cancel_subscription(self, subscription_id):
265 | params = {'Id': subscription_id}
266 |
267 | response = self._send_request('subscriptions/cancel', params)
268 |
269 | if not response['Success']:
270 | raise CloudPaymentsError(response)
271 |
272 | def create_order(self, amount, currency, description, email=None,
273 | send_email=None, require_confirmation=None,
274 | invoice_id=None, account_id=None, phone=None,
275 | send_sms=None, send_whatsapp=None, culture_info=None,
276 | data=None):
277 | params = {
278 | 'Amount': amount,
279 | 'Currency': currency,
280 | 'Description': description,
281 | }
282 | if email is not None:
283 | params['Email'] = email
284 | if require_confirmation is not None:
285 | params['RequireConfirmation'] = require_confirmation
286 | if send_email is not None:
287 | params['SendEmail'] = send_email
288 | if invoice_id is not None:
289 | params['InvoiceId'] = invoice_id
290 | if account_id is not None:
291 | params['AccountId'] = account_id
292 | if phone is not None:
293 | params['Phone'] = phone
294 | if send_sms is not None:
295 | params['SendSms'] = send_sms
296 | if send_whatsapp is not None:
297 | params['SendWhatsApp'] = send_whatsapp
298 | if culture_info is not None:
299 | params['CultureInfo'] = culture_info
300 | if data is not None:
301 | params['JsonData'] = data
302 |
303 | response = self._send_request('orders/create', params)
304 |
305 | if response['Success']:
306 | return Order.from_dict(response['Model'])
307 | raise CloudPaymentsError(response)
308 |
309 | def create_receipt(self, inn, receipt_type, customer_receipt,
310 | invoice_id=None, account_id=None, request_id=None):
311 | if isinstance(customer_receipt, Receipt):
312 | customer_receipt = customer_receipt.to_dict()
313 |
314 | params = {
315 | 'Inn': inn,
316 | 'Type': receipt_type,
317 | 'CustomerReceipt': customer_receipt,
318 | }
319 | if invoice_id is not None:
320 | params['InvoiceId'] = invoice_id
321 | if account_id is not None:
322 | params['AccountId'] = account_id
323 |
324 | response = self._send_request('kkt/receipt', params, request_id)
325 |
326 | if not response['Success']:
327 | raise CloudPaymentsError(response)
328 | return response['Model']['Id']
329 |
330 | def get_receipt(self, receipt_id):
331 | params = {'Id': receipt_id}
332 | response = self._send_request('kkt/receipt/get', params)
333 |
334 | if response['Success']:
335 | return Receipt.from_dict(response['Model'])
336 | raise CloudPaymentsError(response)
337 |
338 | def update_webhook(
339 | self,
340 | webhook_type: WebhookType,
341 | address,
342 | is_enabled: bool = True,
343 | method="GET",
344 | encoding="UTF8",
345 | format_notifications="CloudPayments"
346 | ):
347 | params = {
348 | 'IsEnabled': is_enabled,
349 | 'Address': address,
350 | 'HttpMethod': method,
351 | 'Encoding': encoding,
352 | 'Format': format_notifications
353 | }
354 | response = self._send_request(f'site/notifications/{webhook_type}/update', params)
355 | if response['Success']:
356 | return response
357 | raise CloudPaymentsError(response)
358 |
--------------------------------------------------------------------------------
/cloudpayments/enums.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 | from __future__ import unicode_literals
3 |
4 |
5 | class ReasonCode(object):
6 | APPROVED = 0
7 | REFER_TO_CARD_ISSUER = 5001
8 | DO_NOT_HONOR = 5005
9 | ERROR = 5006
10 | INVALID_TRANSACTION = 5012
11 | AMOUNT_ERROR = 5013
12 | FORMAT_ERROR = 5030
13 | BANK_NOT_SUPPORTED_BY_SWITCH = 5031
14 | SUSPECTED_FRAUD = 5034
15 | LOST_CARD = 5041
16 | STOLEN_CARD = 5043
17 | INSUFFICIENT_FUNDS = 5051
18 | EXPIRED_CARD = 5054
19 | TRANSACTION_NOT_PERMITTED = 5057
20 | EXCEED_WITHDRAWAL_FREQUENCY = 5065
21 | INCORRECT_CVV = 5082
22 | TIMEOUT = 5091
23 | CANNOT_REACH_NETWORK = 5092
24 | SYSTEM_ERROR = 5096
25 | UNABLE_TO_PROCESS = 5204
26 | AUTHENTICATION_FAILED = 5206
27 | AUTHENTICATION_UNAVAILABLE = 5207
28 | ANTI_FRAUD = 5300
29 |
30 |
31 | ERROR_MESSAGES = {
32 | 5001: ('Отказ эмитента проводить онлайн операцию',
33 | 'Свяжитесь с вашим банком или воспользуйтесь другой картой'),
34 | 5005: ('Отказ эмитента без объяснения причин',
35 | 'Свяжитесь с вашим банком или воспользуйтесь другой картой'),
36 | 5006: ('Отказ сети проводить операцию или неправильный CVV код',
37 | 'Проверьте правильность введенных данных карты или воспользуйтесь ' +
38 | 'другой картой'),
39 | 5007: ('Карта потеряна',
40 | 'Свяжитесь с вашим банком или воспользуйтесь другой картой'),
41 | 5012: ('Карта не предназначена для онлайн платежей',
42 | 'Воспользуйтесь другой картой или свяжитесь с банком, выпустившим ' +
43 | 'карту'),
44 | 5013: ('Слишком маленькая или слишком большая сумма операции',
45 | 'Проверьте корректность суммы'),
46 | 5014: ('Некорректный номер карты',
47 | 'Проверьте правильность введенных данных карты или ' +
48 | 'воспользуйтесь другой картой'),
49 | 5030: ('Ошибка на стороне эквайера — неверно сформирована транзакция',
50 | 'Повторите попытку позже'),
51 | 5031: ('Неизвестный эмитент карты', 'Воспользуйтесь другой картой'),
52 | 5034: ('Отказ эмитента — подозрение на мошенничество',
53 | 'Свяжитесь с вашим банком или воспользуйтесь другой картой'),
54 | 5041: ('Карта потеряна',
55 | 'Свяжитесь с вашим банком или воспользуйтесь другой картой'),
56 | 5043: ('Карта украдена',
57 | 'Свяжитесь с вашим банком или воспользуйтесь другой картой'),
58 | 5051: ('Недостаточно средств', 'Недостаточно средств на карте'),
59 | 5054: ('Карта просрочена или неверно указан срок действия',
60 | 'Проверьте правильность введенных данных карты или воспользуйтесь ' +
61 | 'другой картой'),
62 | 5057: ('Ограничение на карте',
63 | 'Свяжитесь с вашим банком или воспользуйтесь другой картой'),
64 | 5065: ('Превышен лимит операций по карте',
65 | 'Свяжитесь с вашим банком или воспользуйтесь другой картой'),
66 | 5075: ('Превышено количество неправильных вводов PIN',
67 | 'Свяжитесь с вашим банком или воспользуйтесь другой картой'),
68 | 5082: ('Неверный CVV код', 'Неверно указан код CVV'),
69 | 5091: ('Эмитент недоступен',
70 | 'Повторите попытку позже или воспользуйтесь другой картой'),
71 | 5092: ('Эмитент недоступен',
72 | 'Повторите попытку позже или воспользуйтесь другой картой'),
73 | 5096: ('Ошибка банка-эквайера или сети', 'Повторите попытку позже'),
74 | 5204: ('Операция не может быть обработана по прочим причинам',
75 | 'Свяжитесь с вашим банком или воспользуйтесь другой картой'),
76 | 5206: ('3-D Secure авторизация не пройдена',
77 | 'Свяжитесь с вашим банком или воспользуйтесь другой картой'),
78 | 5207: ('3-D Secure авторизация недоступна',
79 | 'Свяжитесь с вашим банком или воспользуйтесь другой картой'),
80 | 5209: ('3-D Secure авторизация недоступна',
81 | 'Свяжитесь с вашим банком или воспользуйтесь другой картой'),
82 | 5300: ('Лимиты эквайера на проведение операций',
83 | 'Воспользуйтесь другой картой')
84 | }
85 |
86 |
87 | class TransactionStatus(object):
88 | AWAITING_AUTHENTICATION = 'AwaitingAuthentication'
89 | AUTHORIZED = 'Authorized'
90 | COMPLETED = 'Completed'
91 | CANCELLED = 'Cancelled'
92 | DECLINED = 'Declined'
93 |
94 |
95 | class SubscriptionStatus(object):
96 | ACTIVE = 'Active'
97 | PAST_DUE = 'PastDue'
98 | CANCELLED = 'Cancelled'
99 | REJECTED = 'Rejected'
100 | EXPIRED = 'Expired'
101 |
102 |
103 | class Currency(object):
104 | RUB = 'RUB'
105 | USD = 'USD'
106 | EUR = 'EUR'
107 | GBP = 'GBP'
108 |
109 |
110 | class Interval(object):
111 | WEEK = 'Week'
112 | MONTH = 'Month'
113 |
114 |
115 | class Timezone(object):
116 | HST = 'HST'
117 | AKST = 'AKST'
118 | PST = 'PST'
119 | MST = 'MST'
120 | CST = 'CST'
121 | EST = 'EST'
122 | AST = 'AST'
123 | BRT = 'BRT'
124 | UTC = 'UTC'
125 | GMT = 'GMT'
126 | CET = 'CET'
127 | EET = 'EET'
128 | MSK = 'MSK'
129 | AZT = 'AZT'
130 | AMT = 'AMT'
131 | SAMT = 'SAMT'
132 | GET = 'GET'
133 | TJT = 'TJT'
134 | YEKT = 'YEKT'
135 | ALMT = 'ALMT'
136 | NOVT = 'NOVT'
137 | KRAT = 'KRAT'
138 | HKT = 'HKT'
139 | IRKT = 'IRKT'
140 | SGT = 'SGT'
141 | ULAT = 'ULAT'
142 | YAKT = 'YAKT'
143 | VLAT = 'VLAT'
144 | SAKT = 'SAKT'
145 | ANAT = 'ANAT'
146 |
147 |
148 | class CultureInfo(object):
149 | RU_RU = 'ru-RU'
150 | EN_US = 'en-US'
151 |
152 |
153 | class ReceiptType(object):
154 | INCOME = 'Income'
155 | INCOME_RETURN = 'IncomeReturn'
156 | EXPENSE = 'Expense'
157 | EXPENSE_RETURN = 'ExpenseReturn'
158 |
159 |
160 | class TaxationSystem(object):
161 | OSNO = 0
162 | USN_INCOME = 1
163 | USN_INCOME_MINUS_EXPENSES = 2
164 | ENVD = 3
165 | ESHN = 4
166 | PSN = 5
167 |
168 | class WebhookType(object):
169 | CHECK = 'Check'
170 | PAY = 'Pay'
171 | FAIL = 'Fail'
172 | CONFIRM = 'Confirm'
173 | REFUND = 'Refund'
174 | RECURRENT = 'Recurrent'
175 | CANCEL = 'Cancel'
176 |
177 |
178 | class VAT(object):
179 | """Vat options according to CloudPayaments docs.
180 |
181 | https://cloudpayments.ru/wiki/integration/instrumenti/apikassa#nds
182 | """
183 |
184 | WITHOUT = None # null или не указано — НДС не облагается
185 | VAT20 = 20 # НДС 20%
186 | VAT18 = 18 # НДС 18%
187 | VAT10 = 10 # НДС 10%
188 | VAT0 = 0 # НДС 0%
189 | VAT110 = 110 # расчетный НДС 10/110
190 | VAT118 = 118 # расчетный НДС 18/118
191 | VAT120 = 120 # расчетный НДС 20/120
192 |
--------------------------------------------------------------------------------
/cloudpayments/errors.py:
--------------------------------------------------------------------------------
1 | from .enums import ERROR_MESSAGES
2 |
3 |
4 | class CloudPaymentsError(Exception):
5 | def __init__(self, response, message=None):
6 | self.response = response
7 | super(CloudPaymentsError, self).__init__(message or
8 | response.get('Message'))
9 |
10 |
11 | class PaymentError(CloudPaymentsError):
12 | def __init__(self, response):
13 | self.reason = response['Model']['Reason']
14 | self.reason_code = response['Model']['ReasonCode']
15 | self.cardholder_message = response['Model']['CardHolderMessage']
16 | self.transaction_id = response['Model']['TransactionId']
17 | super(PaymentError, self).__init__(response, self.reason)
18 |
19 | @property
20 | def verbose_reason(self):
21 | if self.reason_code not in ERROR_MESSAGES:
22 | return None
23 | return ERROR_MESSAGES[self.reason_code][0]
24 |
25 | @property
26 | def verbose_message(self):
27 | if self.reason_code not in ERROR_MESSAGES:
28 | return None
29 | return ERROR_MESSAGES[self.reason_code][1]
30 |
--------------------------------------------------------------------------------
/cloudpayments/models.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 | from .utils import parse_datetime
3 |
4 |
5 | class Model(object):
6 | @classmethod
7 | def from_dict(cls, model_dict):
8 | raise NotImplementedError
9 |
10 | def __repr__(self):
11 | state = ['%s=%s' % (k, repr(v)) for (k, v) in vars(self).items()]
12 | return '%s(%s)' % (self.__class__.__name__, ', '.join(state))
13 |
14 |
15 | class Transaction(Model):
16 | def __init__(self, id, amount, currency, currency_code, invoice_id,
17 | account_id, email, description, data, created_date,
18 | auth_date, confirm_date, auth_code, test_mode, ip_address,
19 | ip_country, ip_city, ip_region, ip_district, ip_latitude,
20 | ip_longitude, card_first_six, card_last_four, card_exp_date,
21 | card_type, card_type_code, issuer, issuer_bank_country,
22 | status, status_code, reason, reason_code, cardholder_message,
23 | name, token):
24 | super(Transaction, self).__init__()
25 | self.id = id
26 | self.amount = amount
27 | self.currency = currency
28 | self.currency_code = currency_code
29 | self.invoice_id = invoice_id
30 | self.account_id = account_id
31 | self.email = email
32 | self.description = description
33 | self.data = data
34 | self.created_date = created_date
35 | self.auth_date = auth_date
36 | self.confirm_date = confirm_date
37 | self.auth_code = auth_code
38 | self.test_mode = test_mode
39 | self.ip_address = ip_address
40 | self.ip_country = ip_country
41 | self.ip_city = ip_city
42 | self.ip_region = ip_region
43 | self.ip_district = ip_district
44 | self.ip_latitude = ip_latitude
45 | self.ip_longitude = ip_longitude
46 | self.card_first_six = card_first_six
47 | self.card_last_four = card_last_four
48 | self.card_exp_date = card_exp_date
49 | self.card_type = card_type
50 | self.card_type_code = card_type_code
51 | self.issuer = issuer
52 | self.issuer_bank_country = issuer_bank_country
53 | self.status = status
54 | self.status_code = status_code
55 | self.reason = reason
56 | self.reason_code = reason_code
57 | self.cardholder_message = cardholder_message
58 | self.name = name
59 | self.token = token
60 |
61 | @property
62 | def secure3d_required(self):
63 | return False
64 |
65 | @classmethod
66 | def from_dict(cls, transaction_dict):
67 | return cls(transaction_dict['TransactionId'],
68 | transaction_dict['Amount'],
69 | transaction_dict['Currency'],
70 | transaction_dict['CurrencyCode'],
71 | transaction_dict['InvoiceId'],
72 | transaction_dict['AccountId'],
73 | transaction_dict['Email'],
74 | transaction_dict['Description'],
75 | transaction_dict['JsonData'],
76 | parse_datetime(transaction_dict['CreatedDateIso']),
77 | parse_datetime(transaction_dict['AuthDateIso']),
78 | parse_datetime(transaction_dict['ConfirmDateIso']),
79 | transaction_dict['AuthCode'],
80 | transaction_dict['TestMode'],
81 | transaction_dict['IpAddress'],
82 | transaction_dict['IpCountry'],
83 | transaction_dict['IpCity'],
84 | transaction_dict['IpRegion'],
85 | transaction_dict['IpDistrict'],
86 | transaction_dict['IpLatitude'],
87 | transaction_dict['IpLongitude'],
88 | transaction_dict['CardFirstSix'],
89 | transaction_dict['CardLastFour'],
90 | transaction_dict['CardExpDate'],
91 | transaction_dict['CardType'],
92 | transaction_dict['CardTypeCode'],
93 | transaction_dict['Issuer'],
94 | transaction_dict['IssuerBankCountry'],
95 | transaction_dict['Status'],
96 | transaction_dict['StatusCode'],
97 | transaction_dict['Reason'],
98 | transaction_dict['ReasonCode'],
99 | transaction_dict['CardHolderMessage'],
100 | transaction_dict['Name'],
101 | transaction_dict['Token'])
102 |
103 |
104 | class Secure3d(Model):
105 | def __init__(self, transaction_id, pa_req, acs_url):
106 | super(Secure3d, self).__init__()
107 | self.transaction_id = transaction_id
108 | self.pa_req = pa_req
109 | self.acs_url = acs_url
110 |
111 | @property
112 | def secure3d_required(self):
113 | return True
114 |
115 | def redirect_params(self, term_url):
116 | return {'MD': self.transaction_id,
117 | 'PaReq': self.pa_req,
118 | 'TermUrl': term_url}
119 |
120 | @classmethod
121 | def from_dict(cls, secure3d_dict):
122 | return cls(secure3d_dict['TransactionId'],
123 | secure3d_dict['PaReq'],
124 | secure3d_dict['AcsUrl'])
125 |
126 |
127 | class Subscription(Model):
128 | def __init__(self, id, account_id, description, email, amount,
129 | currency_code, currency, require_confirmation, start_date,
130 | interval_code, interval, period, max_periods, status_code,
131 | status, successful_transactions_number,
132 | failed_transactions_number, last_transaction_date,
133 | next_transaction_date):
134 | super(Subscription, self).__init__()
135 | self.id = id
136 | self.account_id = account_id
137 | self.description = description
138 | self.email = email
139 | self.amount = amount
140 | self.currency_code = currency_code
141 | self.currency = currency
142 | self.require_confirmation = require_confirmation
143 | self.start_date = start_date
144 | self.interval_code = interval_code
145 | self.interval = interval
146 | self.period = period
147 | self.max_periods = max_periods
148 | self.status_code = status_code
149 | self.status = status
150 | self.successful_transactions_number = successful_transactions_number
151 | self.failed_transactions_number = failed_transactions_number
152 | self.last_transaction_date = last_transaction_date
153 | self.next_transaction_date = next_transaction_date
154 |
155 | @classmethod
156 | def from_dict(cls, subscription_dict):
157 | return cls(subscription_dict['Id'],
158 | subscription_dict['AccountId'],
159 | subscription_dict['Description'],
160 | subscription_dict['Email'],
161 | subscription_dict['Amount'],
162 | subscription_dict['CurrencyCode'],
163 | subscription_dict['Currency'],
164 | subscription_dict['RequireConfirmation'],
165 | parse_datetime(subscription_dict['StartDateIso']),
166 | subscription_dict['IntervalCode'],
167 | subscription_dict['Interval'],
168 | subscription_dict['Period'],
169 | subscription_dict['MaxPeriods'],
170 | subscription_dict['StatusCode'],
171 | subscription_dict['Status'],
172 | subscription_dict['SuccessfulTransactionsNumber'],
173 | subscription_dict['FailedTransactionsNumber'],
174 | parse_datetime(subscription_dict['LastTransactionDateIso']),
175 | parse_datetime(subscription_dict['NextTransactionDateIso']))
176 |
177 |
178 | class Order(Model):
179 | def __init__(self, id, number, amount, currency, currency_code, email,
180 | description, require_confirmation, url):
181 | super(Order, self).__init__()
182 | self.id = id
183 | self.number = number
184 | self.amount = amount
185 | self.currency = currency
186 | self.currency_code = currency_code
187 | self.email = email
188 | self.description = description
189 | self.require_confirmation = require_confirmation
190 | self.url = url
191 |
192 | @classmethod
193 | def from_dict(cls, order_dict):
194 | return cls(order_dict['Id'],
195 | order_dict['Number'],
196 | order_dict['Amount'],
197 | order_dict['Currency'],
198 | order_dict['CurrencyCode'],
199 | order_dict['Email'],
200 | order_dict['Description'],
201 | order_dict['RequireConfirmation'],
202 | order_dict['Url'])
203 |
204 |
205 | class Receipt(Model):
206 | """Receipt model representation."""
207 |
208 | def __init__(self, items, taxation_system, email='', phone='', amounts=None):
209 | self.items = items
210 | self.taxation_system = taxation_system
211 | self.email = email
212 | self.phone = phone
213 | self.amounts = amounts
214 |
215 | @classmethod
216 | def from_dict(cls, receipt_dict):
217 | """Retrieve data from dict and place it as object properties."""
218 | items = [ReceiptItem.from_dict(item) for item in receipt_dict['Items']]
219 | amounts_dict = receipt_dict.get('Amounts', None)
220 | amounts = Amount.from_dict(amounts_dict) if amounts_dict else None
221 | return cls(items=items,
222 | taxation_system=receipt_dict.get('TaxationSystem'),
223 | email=receipt_dict.get('Email'),
224 | phone=receipt_dict.get('Phone'),
225 | amounts=amounts)
226 |
227 | def to_dict(self):
228 | """Convert instance properties to dict."""
229 | items = [item.to_dict() for item in self.items]
230 | result = {
231 | 'items': items,
232 | 'taxationSystem': self.taxation_system,
233 | 'email': self.email,
234 | 'phone': self.phone,
235 | }
236 | if isinstance(self.amounts, Amount):
237 | result.update({'Amounts': self.amounts.to_dict()})
238 | return result
239 |
240 |
241 | class Amount(Model):
242 | """Amount model representation."""
243 |
244 | def __init__(self, electronic=None, advance_payment=None, credit=None, provision=None):
245 | """Initial method."""
246 | self.electronic = electronic # Сумма оплаты электронными деньгами
247 | self.advance_payment = advance_payment # Сумма из предоплаты (зачетом аванса) (2 знака после запятой)
248 | self.credit = credit # Сумма постоплатой(в кредит) (2 знака после запятой)
249 | self.provision = provision # Сумма оплаты встречным предоставлением (сертификаты, др. мат.ценности) (2 знака после запятой)
250 |
251 | def to_dict(self):
252 | """Convert instance properties to dict."""
253 | return {
254 | 'electronic': self.electronic,
255 | 'advancePayment': self.advance_payment,
256 | 'credit': self.credit,
257 | 'provision': self.provision
258 | }
259 |
260 | @classmethod
261 | def from_dict(cls, amount_dict):
262 | """Retrieve data from dict and place it as object properties."""
263 | return cls(
264 | electronic=amount_dict.get('Electronic'),
265 | advance_payment=amount_dict.get('AdvancePayment'),
266 | credit=amount_dict.get('Credit'),
267 | provision=amount_dict.get('Provision')
268 | )
269 |
270 |
271 | class ReceiptItem(Model):
272 | """Receipt Item model representation."""
273 |
274 | def __init__(self, label, price, quantity, amount, vat, ean13=None,
275 | method=0, item_object=0, measurement_unit=None):
276 | """Initial method."""
277 | self.label = label
278 | self.price = price
279 | self.quantity = quantity
280 | self.amount = amount
281 | self.vat = vat # Propety of enums.VAT
282 | self.ean13 = ean13
283 | self.method = method # признак способа расчета
284 | self.item_object = item_object # признак предмета товара, работы, услуги, платежа, выплаты, иного предмета расчета
285 | self.measurement_unit = measurement_unit # единица измерения
286 |
287 | @classmethod
288 | def from_dict(cls, item_dict):
289 | """Retrieve data from dict and place it as object properties."""
290 | return cls(label=item_dict.get('Label'),
291 | price=item_dict.get('Price'),
292 | quantity=item_dict.get('Quantity'),
293 | amount=item_dict.get('Amount'),
294 | vat=item_dict.get('Vat'),
295 | ean13=item_dict.get('EAN13'),
296 | measurement_unit=item_dict.get('MeasurementUnit'),
297 | item_object=item_dict.get('Object'),
298 | method=item_dict.get('Method'))
299 |
300 | def to_dict(self):
301 | """Convert instance properties to dict."""
302 | return {
303 | 'label': self.label,
304 | 'price': self.price,
305 | 'quantity': self.quantity,
306 | 'amount': self.amount,
307 | 'vat': self.vat,
308 | 'ean13': self.ean13,
309 | 'method': self.method,
310 | 'object': self.item_object,
311 | 'measurementUnit': self.measurement_unit
312 | }
313 |
--------------------------------------------------------------------------------
/cloudpayments/tests.py:
--------------------------------------------------------------------------------
1 | # coding=utf-8
2 | from datetime import datetime, date
3 | import json
4 | from unittest import TestCase
5 |
6 | import pytz
7 |
8 | from .models import Transaction, Secure3d, Subscription, Order
9 | from .utils import parse_datetime, format_datetime, format_date
10 | from .enums import Currency, TransactionStatus, ReasonCode, Interval, \
11 | SubscriptionStatus
12 |
13 |
14 | class ParseDateTimeTest(TestCase):
15 | def test_parses_datetime(self):
16 | self.assertEqual(parse_datetime('2014-08-09T11:49:42'),
17 | datetime(2014, 8, 9, 11, 49, 42, tzinfo=pytz.utc))
18 |
19 |
20 | class FormatDateTimeTest(TestCase):
21 | def test_formats_datetime(self):
22 | dt = datetime(2016, 8, 9, 11, 49, 42, tzinfo=pytz.utc)
23 | self.assertEqual(format_datetime(dt), '2016-08-09T11:49:42Z')
24 |
25 | def test_formats_local_datetime(self):
26 | timezone = pytz.timezone('Europe/Moscow')
27 | dt = timezone.localize(datetime(2016, 8, 9, 14, 49, 42))
28 | self.assertEqual(format_datetime(dt), '2016-08-09T11:49:42Z')
29 |
30 |
31 | class FormatDateTest(TestCase):
32 | def test_formats_date(self):
33 | d = date(2016, 8, 9)
34 | self.assertEqual(format_date(d), '2016-08-09')
35 |
36 |
37 | class TransactionTest(TestCase):
38 | def test_reads_transaction_from_dict(self):
39 | model = json.loads(u'''{
40 | "TransactionId": 504,
41 | "Amount": 10.00000,
42 | "Currency": "RUB",
43 | "CurrencyCode": 0,
44 | "InvoiceId": "1234567",
45 | "AccountId": "user_x",
46 | "Email": null,
47 | "Description": "Оплата товаров в example.com",
48 | "JsonData": {"key": "value"},
49 | "CreatedDate": "\/Date(1401718880000)\/",
50 | "CreatedDateIso":"2014-08-09T11:49:41",
51 | "AuthDate": "\/Date(1401733880523)\/",
52 | "AuthDateIso":"2014-08-09T11:49:42",
53 | "ConfirmDate": "\/Date(1401733880523)\/",
54 | "ConfirmDateIso":"2014-08-09T11:49:42",
55 | "AuthCode": "123456",
56 | "TestMode": true,
57 | "IpAddress": "195.91.194.13",
58 | "IpCountry": "RU",
59 | "IpCity": "Уфа",
60 | "IpRegion": "Республика Башкортостан",
61 | "IpDistrict": "Приволжский федеральный округ",
62 | "IpLatitude": 54.7355,
63 | "IpLongitude": 55.991982,
64 | "CardFirstSix": "411111",
65 | "CardLastFour": "1111",
66 | "CardExpDate": "05/19",
67 | "CardType": "Visa",
68 | "CardTypeCode": 0,
69 | "Issuer": "Sberbank of Russia",
70 | "IssuerBankCountry": "RU",
71 | "Status": "Completed",
72 | "StatusCode": 3,
73 | "Reason": "Approved",
74 | "ReasonCode": 0,
75 | "CardHolderMessage":"Оплата успешно проведена",
76 | "Name": "CARDHOLDER NAME",
77 | "Token": "a4e67841-abb0-42de-a364-d1d8f9f4b3c0"
78 | }''')
79 | transaction = Transaction.from_dict(model)
80 |
81 | self.assertEqual(transaction.id, 504)
82 | self.assertEqual(transaction.amount, 10)
83 | self.assertEqual(transaction.currency, Currency.RUB)
84 | self.assertEqual(transaction.currency_code, 0)
85 | self.assertEqual(transaction.invoice_id, '1234567')
86 | self.assertEqual(transaction.account_id, 'user_x')
87 | self.assertEqual(transaction.email, None)
88 | self.assertEqual(transaction.description,
89 | u'Оплата товаров в example.com')
90 | self.assertEqual(transaction.data, {'key': 'value'})
91 | self.assertEqual(transaction.created_date,
92 | datetime(2014, 8, 9, 11, 49, 41, tzinfo=pytz.utc))
93 | self.assertEqual(transaction.auth_date,
94 | datetime(2014, 8, 9, 11, 49, 42, tzinfo=pytz.utc))
95 | self.assertEqual(transaction.confirm_date,
96 | datetime(2014, 8, 9, 11, 49, 42, tzinfo=pytz.utc))
97 | self.assertEqual(transaction.auth_code, '123456')
98 | self.assertTrue(transaction.test_mode)
99 | self.assertEqual(transaction.ip_address, '195.91.194.13')
100 | self.assertEqual(transaction.ip_country, 'RU')
101 | self.assertEqual(transaction.ip_city, u'Уфа')
102 | self.assertEqual(transaction.ip_region, u'Республика Башкортостан')
103 | self.assertEqual(transaction.ip_district,
104 | u'Приволжский федеральный округ')
105 | self.assertEqual(transaction.ip_latitude, 54.7355)
106 | self.assertEqual(transaction.ip_longitude, 55.991982)
107 | self.assertEqual(transaction.card_first_six, '411111')
108 | self.assertEqual(transaction.card_last_four, '1111')
109 | self.assertEqual(transaction.card_exp_date, '05/19')
110 | self.assertEqual(transaction.card_type, 'Visa')
111 | self.assertEqual(transaction.card_type_code, 0)
112 | self.assertEqual(transaction.issuer, 'Sberbank of Russia')
113 | self.assertEqual(transaction.issuer_bank_country, 'RU')
114 | self.assertEqual(transaction.status, TransactionStatus.COMPLETED)
115 | self.assertEqual(transaction.status_code, 3)
116 | self.assertEqual(transaction.reason, 'Approved')
117 | self.assertEqual(transaction.reason_code, ReasonCode.APPROVED)
118 | self.assertEqual(transaction.cardholder_message,
119 | u'Оплата успешно проведена')
120 | self.assertEqual(transaction.name, 'CARDHOLDER NAME')
121 | self.assertEqual(transaction.token,
122 | 'a4e67841-abb0-42de-a364-d1d8f9f4b3c0')
123 |
124 |
125 | class Secure3dTest(TestCase):
126 | def test_reads_secure3d_from_dict(self):
127 | model = json.loads(u'''{
128 | "TransactionId": 504,
129 | "PaReq": "eJxVUdtugkAQ/RXDe9mLgo0Z1nhpU9PQasWmPhLYAKksuEChfn13uVR9mGTO7MzZM2dg3qSn0Q+X\\nRZIJxyAmNkZcBFmYiMgxDt7zw6MxZ+DFkvP1ngeV5AxcXhR+xEdJ6BhpEZnEYLBdfPAzg56JKSKT\\nAhqgGpFB7IuSgR+cl5s3NqFTG2NAPYSUy82aETqeWPYUUAdB+ClnwSmrwtz/TbkoC0BtDYKsEqX8\\nZfZkDGgAUMkTi8synyFU17V5N2nKCpBuAHRVs610VijCJgmZu17UXTxhFWP34l7evYPlegsHkO6A\\n0C85o5hMsI3piNIZHc+IBaitg59qJYzgdrUOQK7/WNy+3FZAeSqV5cMqAwLe5JlQwpny8T8HdFW8\\netFuBqUyahV+Hjf27vWCaSx22fe+KY6kXKZfJLK1x22TZkyUS8QiHaUGgDQN6s+H+tOq7O7kf8hd\\nt30=",
130 | "AcsUrl": "https://test.paymentgate.ru/acs/auth/start.do"
131 | }''')
132 | secure3d = Secure3d.from_dict(model)
133 |
134 | self.assertEqual(secure3d.transaction_id, 504)
135 | self.assertEqual(secure3d.pa_req, 'eJxVUdtugkAQ/RXDe9mLgo0Z1nhpU9PQasWmPhLYAKksuEChfn13uVR9mGTO7MzZM2dg3qSn0Q+X\nRZIJxyAmNkZcBFmYiMgxDt7zw6MxZ+DFkvP1ngeV5AxcXhR+xEdJ6BhpEZnEYLBdfPAzg56JKSKT\nAhqgGpFB7IuSgR+cl5s3NqFTG2NAPYSUy82aETqeWPYUUAdB+ClnwSmrwtz/TbkoC0BtDYKsEqX8\nZfZkDGgAUMkTi8synyFU17V5N2nKCpBuAHRVs610VijCJgmZu17UXTxhFWP34l7evYPlegsHkO6A\n0C85o5hMsI3piNIZHc+IBaitg59qJYzgdrUOQK7/WNy+3FZAeSqV5cMqAwLe5JlQwpny8T8HdFW8\netFuBqUyahV+Hjf27vWCaSx22fe+KY6kXKZfJLK1x22TZkyUS8QiHaUGgDQN6s+H+tOq7O7kf8hd\nt30=')
136 | self.assertEqual(secure3d.acs_url,
137 | 'https://test.paymentgate.ru/acs/auth/start.do')
138 |
139 | def test_builds_redirect_params(self):
140 | secure3d = Secure3d(111, 'asdas',
141 | 'https://test.paymentgate.ru/acs/auth/start.do')
142 | self.assertEqual(
143 | secure3d.redirect_params('http://example.com'),
144 | {'MD': 111, 'PaReq': 'asdas', 'TermUrl': 'http://example.com'}
145 | )
146 |
147 |
148 | class SubscriptionTest(TestCase):
149 | def test_reads_subscription_from_dict(self):
150 | model = json.loads(u'''{
151 | "Id":"sc_8cf8a9338fb8ebf7202b08d09c938",
152 | "AccountId":"user@example.com",
153 | "Description":"Ежемесячная подписка на сервис example.com",
154 | "Email":"user@example.com",
155 | "Amount":1.02,
156 | "CurrencyCode":0,
157 | "Currency":"RUB",
158 | "RequireConfirmation":false,
159 | "StartDate":"\/Date(1407343589537)\/",
160 | "StartDateIso":"2014-08-09T11:49:41",
161 | "IntervalCode":1,
162 | "Interval":"Month",
163 | "Period":1,
164 | "MaxPeriods":null,
165 | "StatusCode":0,
166 | "Status":"Active",
167 | "SuccessfulTransactionsNumber":0,
168 | "FailedTransactionsNumber":0,
169 | "LastTransactionDate":null,
170 | "LastTransactionDateIso":null,
171 | "NextTransactionDate":"\/Date(1407343589537)\/",
172 | "NextTransactionDateIso":"2014-08-09T11:49:41"
173 | }''')
174 | subscription = Subscription.from_dict(model)
175 | self.assertEqual(subscription.id, 'sc_8cf8a9338fb8ebf7202b08d09c938')
176 | self.assertEqual(subscription.account_id, 'user@example.com')
177 | self.assertEqual(subscription.description,
178 | u'Ежемесячная подписка на сервис example.com')
179 | self.assertEqual(subscription.email, 'user@example.com')
180 | self.assertEqual(subscription.amount, 1.02)
181 | self.assertEqual(subscription.currency_code, 0)
182 | self.assertEqual(subscription.currency, Currency.RUB)
183 | self.assertFalse(subscription.require_confirmation)
184 | self.assertEqual(subscription.start_date,
185 | datetime(2014, 8, 9, 11, 49, 41, tzinfo=pytz.utc))
186 | self.assertEqual(subscription.interval_code, 1)
187 | self.assertEqual(subscription.interval, Interval.MONTH)
188 | self.assertEqual(subscription.period, 1)
189 | self.assertEqual(subscription.max_periods, None)
190 | self.assertEqual(subscription.status_code, 0)
191 | self.assertEqual(subscription.status, SubscriptionStatus.ACTIVE)
192 | self.assertEqual(subscription.successful_transactions_number, 0)
193 | self.assertEqual(subscription.failed_transactions_number, 0)
194 | self.assertEqual(subscription.last_transaction_date, None)
195 | self.assertEqual(subscription.next_transaction_date,
196 | datetime(2014, 8, 9, 11, 49, 41, tzinfo=pytz.utc))
197 |
198 |
199 | class OrderTest(TestCase):
200 | def test_reads_order_from_dict(self):
201 | model = json.loads('''{
202 | "Id":"f2K8LV6reGE9WBFn",
203 | "Number":61,
204 | "Amount":10.0,
205 | "Currency":"RUB",
206 | "CurrencyCode":0,
207 | "Email":"client@test.local",
208 | "Description":"Оплата на сайте example.com",
209 | "RequireConfirmation":true,
210 | "Url":"https://orders.cloudpayments.ru/d/f2K8LV6reGE9WBFn"
211 | }''')
212 | order = Order.from_dict(model)
213 |
214 | self.assertEqual(order.id, 'f2K8LV6reGE9WBFn')
215 | self.assertEqual(order.number, 61)
216 | self.assertEqual(order.amount, 10.0)
217 | self.assertEqual(order.currency, Currency.RUB)
218 | self.assertEqual(order.currency_code, 0)
219 | self.assertEqual(order.email, 'client@test.local')
220 | self.assertEqual(order.description, u'Оплата на сайте example.com')
221 | self.assertTrue(order.require_confirmation)
222 | self.assertEqual(order.url,
223 | u'https://orders.cloudpayments.ru/d/f2K8LV6reGE9WBFn')
224 |
--------------------------------------------------------------------------------
/cloudpayments/utils.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime
2 | import pytz
3 |
4 |
5 | IN_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S"
6 | OUT_DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
7 | DATE_FORMAT = "%Y-%m-%d"
8 |
9 |
10 | def parse_datetime(s):
11 | if not s:
12 | return None
13 | return datetime.strptime(s, IN_DATETIME_FORMAT).replace(tzinfo=pytz.utc)
14 |
15 |
16 | def format_datetime(dt):
17 | return dt.astimezone(pytz.utc).strftime(OUT_DATETIME_FORMAT)
18 |
19 |
20 | def format_date(d):
21 | return d.strftime(DATE_FORMAT)
22 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | import re
2 | import sys
3 | from setuptools import setup
4 | from io import open
5 |
6 | VERSION = '1.6.4'
7 |
8 | long_description = open('README.rst', 'rt', encoding='utf8').read()
9 |
10 | # PyPI can't process links with anchors
11 | long_description = re.sub(r'<(.*)#.*>`_', '<\g<1>>`_', long_description)
12 |
13 | setup(
14 | name = 'cloudpayments',
15 | packages = ['cloudpayments'],
16 |
17 | description = 'CloudPayments Python Client Library',
18 | long_description = long_description,
19 |
20 | version = VERSION,
21 |
22 | author = 'Antida software',
23 | author_email = 'info@antidasoftware.com',
24 | license = 'MIT license',
25 |
26 | url = 'https://github.com/antidasoftware/cloudpayments-python-client',
27 | download_url = 'https://github.com/antidasoftware/cloudpayments-python-client/tarball/%s' % VERSION,
28 |
29 | requires = [
30 | 'requests (>=2.9.1)',
31 | 'pytz (>=2015.7)'
32 | ],
33 |
34 | install_requires = [
35 | 'requests >=2.9.1',
36 | 'pytz >=2015.7'
37 | ],
38 |
39 | classifiers=[
40 | 'Development Status :: 4 - Beta',
41 | 'Intended Audience :: Developers',
42 | 'License :: OSI Approved :: MIT License',
43 | 'Programming Language :: Python',
44 | 'Programming Language :: Python :: 2',
45 | 'Programming Language :: Python :: 2.7',
46 | 'Programming Language :: Python :: 3',
47 | 'Programming Language :: Python :: 3.4',
48 | 'Programming Language :: Python :: 3.5',
49 | 'Programming Language :: Python :: 3.6',
50 | 'Programming Language :: Python :: 3.7',
51 | 'Programming Language :: Python :: 3.8',
52 | 'Programming Language :: Python :: 3.9',
53 | 'Topic :: Office/Business',
54 | 'Topic :: Office/Business :: Financial',
55 | 'Topic :: Software Development :: Libraries :: Python Modules'
56 | ],
57 |
58 | zip_safe=False
59 | )
--------------------------------------------------------------------------------