├── .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 | ) --------------------------------------------------------------------------------