├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── cloudipsp ├── __init__.py ├── api.py ├── checkout.py ├── configuration.py ├── exceptions.py ├── helpers.py ├── order.py ├── payment.py ├── resources.py └── utils.py ├── serv.py ├── setup.py ├── test.py ├── tests ├── __init__.py ├── api_tests.py ├── checkout_tests.py ├── data │ └── test_data.json ├── order_tests.py ├── payment_tests.py ├── tests_helper.py └── utils_tests.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = cloudipsp -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask instance folder 57 | instance/ 58 | 59 | # Scrapy stuff: 60 | .scrapy 61 | 62 | # Sphinx documentation 63 | docs/_build/ 64 | 65 | # PyBuilder 66 | target/ 67 | 68 | # IPython Notebook 69 | .ipynb_checkpoints 70 | 71 | # pyenv 72 | .python-version 73 | 74 | # celery beat schedule file 75 | celerybeat-schedule 76 | 77 | # dotenv 78 | .env 79 | 80 | # virtualenv 81 | venv2.7/ 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | .idea/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | sudo: false 4 | 5 | matrix: 6 | include: 7 | - python: 2.7 8 | env: 9 | - TOXENV=py27 10 | - python: 3.4 11 | env: 12 | - TOXENV=py34 13 | - python: 3.5 14 | env: 15 | - TOXENV=py35 16 | - python: 3.6 17 | env: 18 | - TOXENV=py36 19 | - python: '3.7-dev' 20 | env: 21 | - TOXENV=py37 22 | install: 23 | - pip install tox pycodestyle nose coverage unittest2 requests 24 | 25 | script: 26 | - tox 27 | 28 | after_success: 29 | - nosetests --with-coverage --cover-package=cloudipsp -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 cloudipsp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, 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, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cloudipsp Python SDK client 2 | 3 |

4 | 5 |

6 | 7 | [![Downloads](https://pepy.tech/badge/cloudipsp)](https://pepy.tech/project/cloudipsp) 8 | [![Downloads](https://pepy.tech/badge/cloudipsp/month)](https://pepy.tech/project/cloudipsp) 9 | [![Downloads](https://pepy.tech/badge/cloudipsp/week)](https://pepy.tech/project/cloudipsp) 10 | 11 | ## Payment service provider 12 | A payment service provider (PSP) offers shops online services for accepting electronic payments by a variety of payment methods including credit card, bank-based payments such as direct debit, bank transfer, and real-time bank transfer based on online banking. Typically, they use a software as a service model and form a single payment gateway for their clients (merchants) to multiple payment methods. 13 | [read more](https://en.wikipedia.org/wiki/Payment_service_provider) 14 | 15 | Requirements 16 | ------------ 17 | - Python (2.4, 2.7, 3.3, 3.4, 3.5, 3.6, 3.7) 18 | 19 | Dependencies 20 | ------------ 21 | - requests 22 | - six 23 | 24 | Installation 25 | ------------ 26 | ```bash 27 | pip install cloudipsp 28 | ``` 29 | ### Simple start 30 | 31 | ```python 32 | from cloudipsp import Api, Checkout 33 | api = Api(merchant_id=1396424, 34 | secret_key='test') 35 | checkout = Checkout(api=api) 36 | data = { 37 | "currency": "USD", 38 | "amount": 10000 39 | } 40 | url = checkout.url(data).get('checkout_url') 41 | ``` 42 | 43 | Tests 44 | ----------------- 45 | First, install `tox` `` 46 | 47 | To run testing: 48 | 49 | ```bash 50 | tox 51 | ``` 52 | 53 | This will run all tests, against all supported Python versions. -------------------------------------------------------------------------------- /cloudipsp/__init__.py: -------------------------------------------------------------------------------- 1 | from cloudipsp.configuration import __version__, __api_url__ 2 | from cloudipsp.api import Api 3 | from cloudipsp.checkout import Checkout 4 | from cloudipsp.order import Order 5 | from cloudipsp.payment import Payment, Pcidss 6 | from cloudipsp.resources import Resource 7 | -------------------------------------------------------------------------------- /cloudipsp/api.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | from cloudipsp.configuration import (__api_url__, __protocol__, __r_type__) 3 | from cloudipsp import exceptions 4 | 5 | import os 6 | import requests 7 | import logging 8 | import cloudipsp.helpers as helper 9 | import cloudipsp.utils as utils 10 | 11 | log = logging.getLogger(__name__) 12 | 13 | 14 | class Api(object): 15 | user_agent = 'Python SDK' 16 | 17 | def __init__(self, **kwargs): 18 | """ 19 | :param kwargs: args 20 | :arg merchant_id Merchant id numeric 21 | :arg secret_key Secret key string 22 | :arg request_type request type allowed json, xml, form 23 | :arg api_domain api domain 24 | :arg api_protocol allowed protocols 1.0, 2.0 25 | """ 26 | self.merchant_id = kwargs.get('merchant_id', '') 27 | self.secret_key = kwargs.get('secret_key', '') 28 | self.request_type = kwargs.get('request_type', __r_type__) 29 | if not self.merchant_id or not self.secret_key: 30 | self.merchant_id = os.environ.get('CLOUDIPSP_MERCHANT_ID', '') 31 | self.secret_key = os.environ.get('CLOUDIPSP_SECRETKEY', '') 32 | domain = kwargs.get('api_domain', 'api.fondy.eu') 33 | self.api_url = __api_url__.format(api_domain=domain) 34 | self.api_protocol = kwargs.get('api_protocol', __protocol__) 35 | if self.api_protocol not in ('1.0', '2.0'): 36 | raise ValueError('Incorrect protocol version') 37 | if self.api_protocol == '2.0' and self.request_type != 'json': 38 | raise ValueError('In protocol \'2.0\' only json allowed') 39 | 40 | def _headers(self): 41 | """ 42 | :return: request headers 43 | """ 44 | return { 45 | 'User-Agent': self.user_agent, 46 | 'Content-Type': helper.get_request_type(self.request_type), 47 | } 48 | 49 | def _request(self, url, method, data, headers): 50 | """ 51 | :param url: request url 52 | :param method: request method, POST default 53 | :param data: request data 54 | :param headers: request headers 55 | :return: api response 56 | """ 57 | log.debug('Request Type: %s' % self.request_type) 58 | log.debug('URL: %s' % url) 59 | log.debug('Data: %s' % str(data)) 60 | log.debug('Headers: %s' % str(headers)) 61 | 62 | response = requests.request(method, url, data=data, headers=headers) 63 | return self._response(response, response.content.decode('utf-8')) 64 | 65 | def _response(self, response, content): 66 | """ 67 | :param response: api response 68 | :param content: api response body 69 | :return: if response header 200 or 201 return response data 70 | """ 71 | status = response.status_code 72 | 73 | log.debug('Status: %s' % str(status)) 74 | log.debug('Content: %s' % content) 75 | 76 | if status in (200, 201): 77 | return content 78 | 79 | raise exceptions.ServiceError( 80 | 'Response code is: {status}'.format(status=status)) 81 | 82 | def post(self, url, data=list, headers=None): 83 | """ 84 | :param url: endpoint api url 85 | :param data: request data 86 | :param headers: request headers 87 | :return: request 88 | """ 89 | log.debug('Protocol version: %s' % self.request_type) 90 | 91 | if 'merchant_id' not in data: 92 | data['merchant_id'] = self.merchant_id 93 | if 'reservation_data' in data: 94 | data['reservation_data'] = utils.to_b64( 95 | data['reservation_data']) 96 | 97 | if self.api_protocol == '2.0': 98 | b64_data = utils.to_b64({'order': data}) 99 | data_v2 = { 100 | 'data': b64_data, 101 | 'version': self.api_protocol, 102 | 'signature': helper.get_signature(self.secret_key, 103 | b64_data, 104 | self.api_protocol) 105 | } 106 | data_string = utils.to_json({'request': data_v2}) 107 | else: 108 | if 'signature' not in data: 109 | data['signature'] = helper.get_signature(self.secret_key, 110 | data, 111 | self.api_protocol) 112 | data_string = helper.get_data({'request': data}, self.request_type) 113 | 114 | return self._request( 115 | utils.join_url(self.api_url, url), 'POST', 116 | data=data_string, 117 | headers=utils.merge_dict(headers, self._headers())) 118 | -------------------------------------------------------------------------------- /cloudipsp/checkout.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | from cloudipsp.resources import Resource 3 | from datetime import datetime 4 | 5 | import cloudipsp.helpers as helper 6 | 7 | 8 | class Checkout(Resource): 9 | def url(self, data): 10 | """ 11 | Method to generate checkout url 12 | :param data: order data 13 | :return: api response 14 | """ 15 | path = '/checkout/url/' 16 | params = self._required(data) 17 | result = self.api.post(path, data=params, headers=self.__headers__) 18 | 19 | return self.response(result) 20 | 21 | def token(self, data): 22 | """ 23 | Method to generate checkout token 24 | :param data: order data 25 | :return: api response 26 | """ 27 | path = '/checkout/token/' 28 | params = self._required(data) 29 | result = self.api.post(path, data=params, headers=self.__headers__) 30 | 31 | return self.response(result) 32 | 33 | def verification(self, data): 34 | """ 35 | Method to generate checkout verification url 36 | :param data: order data 37 | :return: api response 38 | """ 39 | path = '/checkout/url/' 40 | verification_data = { 41 | 'verification': 'Y', 42 | 'verification_type': data.get('verification_type', 'code') 43 | } 44 | data.update(verification_data) 45 | params = self._required(data) 46 | result = self.api.post(path, data=params, headers=self.__headers__) 47 | 48 | return self.response(result) 49 | 50 | def subscription(self, data): 51 | """ 52 | Method to generate checkout url with calendar 53 | :param data: order data 54 | data = { 55 | "currency": "UAH", -> currency ('UAH', 'RUB', 'USD') 56 | "amount": 10000, -> amount of the order (int) 57 | "recurring_data": { 58 | "every": 1, -> frequency of the recurring order (int) 59 | "amount": 10000, -> amount of the recurring order (int) 60 | "period": 'month', -> period of the recurring order ('day', 'month', 'year') 61 | "start_time": '2020-07-24', -> start date of the recurring order ('YYYY-MM-DD') 62 | "readonly": 'y', -> possibility to change parameters of the recurring order by user ('y', 'n') 63 | "state": 'y' -> default state of the recurring order after opening url of the order ('y', 'n') 64 | } 65 | } 66 | :return: api response 67 | """ 68 | if self.api.api_protocol != '2.0': 69 | raise Exception('This method allowed only for v2.0') 70 | path = '/checkout/url/' 71 | recurring_data = data.get('recurring_data', '') 72 | subscription_data = { 73 | 'subscription': 'Y', 74 | 'recurring_data': { 75 | 'start_time': recurring_data.get('start_time', ''), 76 | 'amount': recurring_data.get('amount', ''), 77 | 'every': recurring_data.get('every', ''), 78 | 'period': recurring_data.get('period', ''), 79 | 'readonly': recurring_data.get('readonly', ''), 80 | 'state': recurring_data.get('state', '') 81 | } 82 | } 83 | 84 | helper.check_data(subscription_data['recurring_data']) 85 | self._validate_recurring_data(subscription_data['recurring_data']) 86 | subscription_data.update(data) 87 | params = self._required(subscription_data) 88 | result = self.api.post(path, data=params, headers=self.__headers__) 89 | 90 | return self.response(result) 91 | 92 | def subscription_stop(self, order_id): 93 | """ 94 | Stop calendar payments 95 | """ 96 | if self.api.api_protocol != '2.0': 97 | raise Exception('This method allowed only for v2.0') 98 | path = '/subscription/' 99 | params = {'order_id': order_id, 'action': 'stop'} 100 | result = self.api.post(path, data=params, headers=self.__headers__) 101 | 102 | return self.response(result) 103 | 104 | @staticmethod 105 | def _validate_recurring_data(data): 106 | """ 107 | Validation recurring data params 108 | :param data: recurring data 109 | :return: exception 110 | """ 111 | try: 112 | datetime.strptime(data['start_time'], '%Y-%m-%d') 113 | except ValueError: 114 | raise ValueError( 115 | "Incorrect date format. 'Y-m-d' is allowed") 116 | if data['period'] not in ('day', 'week', 'month'): 117 | raise ValueError( 118 | "Incorrect period. ('day','week','month') is allowed") 119 | 120 | def _required(self, data): 121 | """ 122 | Required data to send 123 | :param data: 124 | :return: parameters to send 125 | """ 126 | self.order_id = data.get('order_id') or helper.generate_order_id() 127 | order_desc = data.get('order_desc') or helper.get_desc(self.order_id) 128 | params = { 129 | 'order_id': self.order_id, 130 | 'order_desc': order_desc, 131 | 'amount': data.get('amount', ''), 132 | 'currency': data.get('currency', '') 133 | } 134 | helper.check_data(params) 135 | params.update(data) 136 | 137 | return params 138 | -------------------------------------------------------------------------------- /cloudipsp/configuration.py: -------------------------------------------------------------------------------- 1 | __version__ = '1.0.3' 2 | __api_url__ = 'https://{api_domain}/api' 3 | __sign_sep__ = '|' 4 | __protocol__ = '1.0' 5 | __r_type__ = 'json' 6 | -------------------------------------------------------------------------------- /cloudipsp/exceptions.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | 4 | class RequestError(Exception): 5 | """ 6 | Some required param is missing. 7 | """ 8 | 9 | def __init__(self, param): 10 | self.param = param 11 | 12 | def __str__(self): 13 | return "Required parameter '%s' is missing." % self.param 14 | 15 | 16 | class ResponseError(Exception): 17 | """ 18 | Handling api response error. 19 | """ 20 | 21 | def __init__(self, response): 22 | self.response = response 23 | 24 | def __str__(self): 25 | message = '' 26 | if 'response_status' in self.response: 27 | message += "Response status is %s." \ 28 | % self.response.get('response_status', '') 29 | if 'error_message' in self.response: 30 | message += " Error message: %s." \ 31 | % (self.response.get('error_message')) 32 | if 'error_code' in self.response: 33 | message += " Error code: %s." \ 34 | % (self.response.get('error_code')) 35 | if 'request_id' in self.response: 36 | message += " Request id: %s." \ 37 | % (self.response.get('request_id')) 38 | message += " Check parameters" 39 | return message 40 | 41 | 42 | class ServiceError(Exception): 43 | """ 44 | If response code not in (200, 201). 45 | """ 46 | -------------------------------------------------------------------------------- /cloudipsp/helpers.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | from hashlib import sha1 3 | from cloudipsp.configuration import __sign_sep__ as sep 4 | from cloudipsp.exceptions import RequestError 5 | 6 | import cloudipsp.utils as utils 7 | import uuid 8 | 9 | 10 | def get_data(data, req_type): 11 | """ 12 | :param data: data to prepare 13 | :param req_type: request type 14 | :return: prepared data 15 | """ 16 | if req_type == 'json': 17 | return utils.to_json(data) 18 | if req_type == 'xml': 19 | return utils.to_xml(data) 20 | if req_type == 'form': 21 | return utils.to_form(data.get('request')) 22 | 23 | 24 | def get_request_type(req_type): 25 | """ 26 | :param req_type: request type 27 | :return: post header 28 | """ 29 | types = { 30 | 'json': 'application/json; charset=utf-8', 31 | 'xml': 'application/xml; charset=utf-8', 32 | 'form': 'application/x-www-form-urlencoded; charset=utf-8' 33 | } 34 | return types.get(req_type, types['json']) 35 | 36 | 37 | def get_signature(secret_key, params, protocol): 38 | """ 39 | :param secret_key: merchant secret 40 | :param params: post params 41 | :param protocol: api protocol version 42 | :return: signature string 43 | """ 44 | if protocol == '2.0': 45 | str_sign = sep.join([secret_key, params]) 46 | calc_sign = sha1(str_sign.encode('utf-8')).hexdigest() 47 | return calc_sign 48 | else: 49 | data = [secret_key] 50 | data.extend([str(params[key]) for key in sorted(iter(params.keys())) 51 | if params[key] != '' and not params[key] is None]) 52 | return sha1(sep.join(data).encode('utf-8')).hexdigest() 53 | 54 | 55 | def get_desc(order_id): 56 | """ 57 | :param order_id: order id 58 | :return: description string 59 | """ 60 | return 'Pay for order #: %s' % order_id 61 | 62 | 63 | def generate_order_id(): 64 | """ 65 | :return: unic order id 66 | """ 67 | return str(uuid.uuid4()) 68 | 69 | 70 | def check_data(data): 71 | """ 72 | :param data: required data 73 | :return: checking required data not empty 74 | """ 75 | for key, value in data.items(): 76 | if value == '' or None: 77 | raise RequestError(key) 78 | if key == 'amount': 79 | try: 80 | int(value) 81 | except ValueError: 82 | raise ValueError('Amount must numeric') 83 | 84 | 85 | def is_valid(data, secret_key, protocol): 86 | if 'signature' in data: 87 | result_signature = data['signature'] 88 | del data['signature'] 89 | else: 90 | raise ValueError('Incorrect data') 91 | if 'response_signature_string' in data: 92 | del data['response_signature_string'] 93 | signature = get_signature(secret_key=secret_key, 94 | params=data, 95 | protocol=protocol) 96 | return result_signature == signature 97 | 98 | 99 | def is_approved(data, secret_key, protocol): 100 | if 'order_status' not in data: 101 | raise ValueError('Incorrect data') 102 | if not is_valid(data, secret_key, protocol): 103 | raise Exception('Payment invalid') 104 | return data.get('order_status') == 'approved' 105 | -------------------------------------------------------------------------------- /cloudipsp/order.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | from cloudipsp.resources import Resource 3 | 4 | import cloudipsp.utils as utils 5 | import cloudipsp.helpers as helper 6 | 7 | 8 | class Order(Resource): 9 | def settlement(self, data): 10 | """ 11 | Method for create split order 12 | :param data: split order data 13 | :return: api response 14 | """ 15 | if self.api.api_protocol != '2.0': 16 | raise Exception('This method allowed only for v2.0') 17 | path = '/settlement/' 18 | params = { 19 | 'order_type': data.get('order_type', 'settlement'), 20 | 'order_id': data.get('order_id') or helper.generate_order_id(), 21 | 'operation_id': data.get('operation_id', ''), 22 | 'receiver': data.get('receiver', []) 23 | } 24 | helper.check_data(params) 25 | params.update(data) 26 | result = self.api.post(path, data=params, headers=self.__headers__) 27 | 28 | return self.response(result) 29 | 30 | def capture(self, data): 31 | """ 32 | Method for capturing order 33 | :param data: capture order data 34 | :return: api response 35 | """ 36 | path = '/capture/order_id/' 37 | params = { 38 | 'order_id': data.get('order_id', ''), 39 | 'amount': data.get('amount', ''), 40 | 'currency': data.get('currency', '') 41 | } 42 | helper.check_data(params) 43 | params.update(data) 44 | result = self.api.post(path, data=params, headers=self.__headers__) 45 | return self.response(result) 46 | 47 | def reverse(self, data): 48 | """ 49 | Method to reverse order 50 | :param data: reverse order data 51 | :return: api response 52 | """ 53 | path = '/reverse/order_id/' 54 | params = { 55 | 'order_id': data.get('order_id', ''), 56 | 'amount': data.get('amount', ''), 57 | 'currency': data.get('currency', '') 58 | } 59 | helper.check_data(params) 60 | params.update(data) 61 | result = self.api.post(path, data=params, headers=self.__headers__) 62 | return self.response(result) 63 | 64 | def status(self, data): 65 | """ 66 | Method for checking order status 67 | :param data: order data 68 | :return: api response 69 | """ 70 | path = '/status/order_id/' 71 | params = { 72 | 'order_id': data.get('order_id', '') 73 | } 74 | helper.check_data(params) 75 | params.update(data) 76 | result = self.api.post(path, data=params, headers=self.__headers__) 77 | return self.response(result) 78 | 79 | def transaction_list(self, data): 80 | """ 81 | Method for getting order transaction list 82 | :param data: order data 83 | :return: api response 84 | """ 85 | path = '/transaction_list/' 86 | params = { 87 | 'order_id': data.get('order_id', '') 88 | } 89 | helper.check_data(params) 90 | params.update(data) 91 | """ 92 | only json allowed all other methods returns 500 error 93 | """ 94 | self.api.request_type = 'json' 95 | result = self.api.post(path, data=params, headers=self.__headers__) 96 | return self.response(result) 97 | 98 | def atol_logs(self, data): 99 | """ 100 | Method for getting order atol logs 101 | :param data: order data 102 | :return: api response 103 | """ 104 | path = '/get_atol_logs/' 105 | params = { 106 | 'order_id': data.get('order_id', '') 107 | } 108 | helper.check_data(params) 109 | params.update(data) 110 | result = self.api.post(path, data=params, headers=self.__headers__) 111 | return utils.from_json(result).get('response') 112 | -------------------------------------------------------------------------------- /cloudipsp/payment.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | from cloudipsp.resources import Resource 3 | from datetime import datetime 4 | 5 | import cloudipsp.helpers as helper 6 | 7 | 8 | class Pcidss(Resource): 9 | def step_one(self, data): 10 | """ 11 | Accept purchase Pcidss step one 12 | :param data: order data 13 | :return: payment result or step two data 14 | """ 15 | path = '/3dsecure_step1/' 16 | self.order_id = data.get('order_id') or helper.generate_order_id() 17 | order_desc = data.get('order_desc') or helper.get_desc(self.order_id) 18 | params = { 19 | 'order_id': self.order_id, 20 | 'order_desc': order_desc, 21 | 'currency': data.get('currency', ''), 22 | 'amount': data.get('amount', ''), 23 | 'card_number': data.get('card_number', ''), 24 | 'cvv2': data.get('cvv2', ''), 25 | 'expiry_date': data.get('expiry_date', '') 26 | } 27 | helper.check_data(params) 28 | params.update(data) 29 | result = self.api.post(path, data=params, headers=self.__headers__) 30 | return self.response(result) 31 | 32 | def step_two(self, data): 33 | """ 34 | Accept purchase Pcidss step two 35 | :param data: order data 36 | :return: payment result 37 | """ 38 | path = '/3dsecure_step2/' 39 | params = { 40 | 'order_id': data.get('order_id', ''), 41 | 'pares': data.get('pares', ''), 42 | 'md': data.get('md', '') 43 | } 44 | helper.check_data(params) 45 | params.update(data) 46 | result = self.api.post(path, data=params, headers=self.__headers__) 47 | return self.response(result) 48 | 49 | 50 | class Payment(Resource): 51 | def p2pcredit(self, data): 52 | """ 53 | Method P2P card credit 54 | :param data: order data 55 | :return: api response 56 | """ 57 | path = '/p2pcredit/' 58 | self.order_id = data.get('order_id') or helper.generate_order_id() 59 | order_desc = data.get('order_desc') or helper.get_desc(self.order_id) 60 | params = { 61 | 'order_id': self.order_id, 62 | 'order_desc': order_desc, 63 | 'amount': data.get('amount', ''), 64 | 'currency': data.get('currency', '') 65 | } 66 | helper.check_data(params) 67 | params.update(data) 68 | result = self.api.post(path, data=params, headers=self.__headers__) 69 | return self.response(result) 70 | 71 | def reports(self, data): 72 | """ 73 | Method to get payment reports from date range 74 | :param data: date range 75 | :return: api response 76 | """ 77 | path = '/reports/' 78 | params = { 79 | 'date_from': data.get('date_from', ''), 80 | 'date_to': data.get('date_to', '') 81 | } 82 | helper.check_data(params) 83 | """ 84 | from api only one response if data invalid "General Decline" 85 | """ 86 | self._validate_reports_date(params) 87 | params.update(data) 88 | result = self.api.post(path, data=params, headers=self.__headers__) 89 | return self.response(result) 90 | 91 | def recurring(self, data): 92 | """ 93 | Method for recurring payment 94 | :param data: order data 95 | :return: api response 96 | """ 97 | path = '/recurring/' 98 | self.order_id = data.get('order_id') or helper.generate_order_id() 99 | order_desc = data.get('order_desc') or helper.get_desc(self.order_id) 100 | params = { 101 | 'order_id': self.order_id, 102 | 'order_desc': order_desc, 103 | 'amount': data.get('amount', ''), 104 | 'currency': data.get('currency', ''), 105 | 'rectoken': data.get('rectoken', '') 106 | } 107 | helper.check_data(params) 108 | params.update(data) 109 | result = self.api.post(path, data=params, headers=self.__headers__) 110 | return self.response(result) 111 | 112 | @staticmethod 113 | def _validate_reports_date(date): 114 | """ 115 | Validating date range 116 | :param date: date 117 | """ 118 | try: 119 | date_from = datetime.strptime( 120 | date['date_from'], '%d.%m.%Y %H:%M:%S') 121 | date_to = datetime.strptime( 122 | date['date_to'], '%d.%m.%Y %H:%M:%S') 123 | except ValueError: 124 | raise ValueError("Incorrect date format.") 125 | if date_from > date_to: 126 | raise ValueError("`date_from` can't be greater than `date_to`") 127 | -------------------------------------------------------------------------------- /cloudipsp/resources.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | from cloudipsp import utils 3 | from cloudipsp import exceptions 4 | 5 | 6 | class Resource(object): 7 | def __init__(self, api=None, headers=None): 8 | self.__dict__['api'] = api 9 | 10 | super(Resource, self).__setattr__('__data__', {}) 11 | super(Resource, self).__setattr__('__headers__', headers or {}) 12 | super(Resource, self).__setattr__('order_id', None) 13 | 14 | def __str__(self): 15 | return self.__data__.__str__() 16 | 17 | def __repr__(self): 18 | return self.__data__.__str__() 19 | 20 | def __getattr__(self, name): 21 | try: 22 | return self.__data__[name] 23 | except KeyError: 24 | return super(Resource, self).__getattribute__(name) 25 | 26 | def __setattr__(self, name, value): 27 | try: 28 | super(Resource, self).__setattr__(name, value) 29 | except AttributeError: 30 | self.__data__[name] = self.convert(name, value) 31 | 32 | def __contains__(self, name): 33 | return name in self.__data__ 34 | 35 | def get_url(self): 36 | if 'checkout_url' in self.__data__: 37 | return self.__getattr__('checkout_url') 38 | 39 | def response(self, response): 40 | """ 41 | :param response: api response 42 | :return: result 43 | """ 44 | try: 45 | result = None 46 | if self.api.request_type == 'json': 47 | result = utils.from_json(response).get('response', '') 48 | if self.api.request_type == 'xml': 49 | result = utils.from_xml(response).get('response', '') 50 | if self.api.request_type == 'form': 51 | result = utils.from_form(response) 52 | return self._get_result(result) 53 | except KeyError: 54 | raise ValueError('Undefined format error.') 55 | 56 | def _get_result(self, result): 57 | """ 58 | in some api param response_status not exist... 59 | :param result: api result 60 | :return: exception 61 | """ 62 | if 'error_message' in result: 63 | raise exceptions.ResponseError(result) 64 | if 'data' in result and self.api.api_protocol == '2.0': 65 | result['data'] = utils.from_b64(result['data']) 66 | self.__data__ = result 67 | return result 68 | -------------------------------------------------------------------------------- /cloudipsp/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | from collections import OrderedDict 3 | 4 | import re 5 | import json 6 | import base64 7 | import six.moves.urllib as urllib 8 | import xml.etree.cElementTree as ElementTree 9 | 10 | 11 | def to_b64(data): 12 | """ 13 | Encoding data string base64 algorithm 14 | """ 15 | return base64.b64encode(json.dumps(data).encode('utf-8')).decode('utf-8') 16 | 17 | 18 | def from_b64(data): 19 | """ 20 | Encoding data string base64 algorithm 21 | """ 22 | return base64.b64decode(json.dumps(data).encode('utf-8')).decode('utf-8') 23 | 24 | 25 | def to_xml(data, start=''): 26 | """ 27 | :param data: params to convert to xml 28 | :param start: start xml string 29 | :return: xml string 30 | """ 31 | data = OrderedDict(sorted(data.items())) 32 | return start + _data2xml(data) 33 | 34 | 35 | def to_json(data): 36 | """ 37 | to json string 38 | :param data: params to convert to xml 39 | :return: json string 40 | """ 41 | return json.dumps(data) 42 | 43 | 44 | def to_form(data): 45 | """ 46 | to form string 47 | :param data: params to convert to form data 48 | :return: encoded url string 49 | """ 50 | data = OrderedDict(sorted(data.items())) 51 | return urllib.parse.urlencode(data) 52 | 53 | 54 | def merge_dict(x, y): 55 | """ 56 | :param x: firs dict 57 | :param y: second dict 58 | :return: merged dict 59 | """ 60 | z = x.copy() 61 | z.update(y) 62 | return z 63 | 64 | 65 | def join_url(url, *paths): 66 | """ 67 | :param url: api url 68 | :param paths: endpoint 69 | :return: full url 70 | """ 71 | for path in paths: 72 | url = re.sub(r'/?$', re.sub(r'^/?', '/', path), url) 73 | return url 74 | 75 | 76 | def from_json(json_string): 77 | """ 78 | :param json_string: json data string to encode 79 | :return: data dict 80 | """ 81 | return json.loads(json_string) 82 | 83 | 84 | def from_form(form_string): 85 | """ 86 | :param form_string: form data string to encode 87 | :return: data dict 88 | """ 89 | return dict(urllib.parse.parse_qsl(form_string)) 90 | 91 | 92 | def from_xml(xml): 93 | """ 94 | :param xml: xml string to encode 95 | :return: data dict 96 | """ 97 | element = ElementTree.fromstring(xml) 98 | return _xml_to_dict(element.tag, _parse(element), element.attrib) 99 | 100 | 101 | def _data2xml(d): 102 | result_list = list() 103 | 104 | if isinstance(d, list): 105 | for sub_elem in d: 106 | result_list.append(_data2xml(sub_elem)) 107 | 108 | return ''.join(d) 109 | 110 | if isinstance(d, dict): 111 | for tag_name, sub_obj in d.items(): 112 | result_list.append("<%s>" % tag_name) 113 | result_list.append(_data2xml(sub_obj)) 114 | result_list.append("" % tag_name) 115 | 116 | return ''.join(result_list) 117 | 118 | return "%s" % d 119 | 120 | 121 | def _parse(node): 122 | tree = {} 123 | for c in node.getchildren(): 124 | c_tag = c.tag 125 | c_attr = c.attrib 126 | ctext = c.text.strip() if c.text is not None else '' 127 | c_tree = _parse(c) 128 | 129 | if not c_tree: 130 | c_dict = _xml_to_dict(c_tag, ctext, c_attr) 131 | else: 132 | c_dict = _xml_to_dict(c_tag, c_tree, c_attr) 133 | if c_tag not in tree: 134 | tree.update(c_dict) 135 | continue 136 | atag = '@' + c_tag 137 | atree = tree[c_tag] 138 | if not isinstance(atree, list): 139 | if not isinstance(atree, dict): 140 | atree = {} 141 | if atag in tree: 142 | atree['#' + c_tag] = tree[atag] 143 | del tree[atag] 144 | tree[c_tag] = [atree] 145 | 146 | if c_attr: 147 | c_tree['#' + c_tag] = c_attr 148 | 149 | tree[c_tag].append(c_tree) 150 | return tree 151 | 152 | 153 | def _xml_to_dict(tag, value, attr=None): 154 | ret = {tag: value} 155 | if attr: 156 | atag = '@' + tag 157 | aattr = {} 158 | for k, v in attr.items(): 159 | aattr[k] = v 160 | ret[atag] = aattr 161 | del atag 162 | del aattr 163 | return ret 164 | -------------------------------------------------------------------------------- /serv.py: -------------------------------------------------------------------------------- 1 | from http.server import BaseHTTPRequestHandler, HTTPServer 2 | import logging 3 | import cgi 4 | import urllib.parse 5 | 6 | 7 | class S(BaseHTTPRequestHandler): 8 | def _set_response(self): 9 | self.send_response(200) 10 | self.send_header('Content-type', 'text/html') 11 | self.end_headers() 12 | 13 | def do_GET(self): 14 | return 15 | 16 | def do_POST(self): 17 | content_type, pdict = cgi.parse_header(self.headers['content-type']) 18 | if content_type == 'multipart/form-data': 19 | postvars = cgi.parse_multipart(self.rfile, pdict) 20 | elif content_type == 'application/x-www-form-urlencoded': 21 | length = int(self.headers['content-length']) 22 | postvars = urllib.parse.parse_qs(self.rfile.read(length), keep_blank_values=1) 23 | else: 24 | postvars = {} 25 | if len(postvars): 26 | i = 0 27 | for key in sorted(postvars): 28 | logging.debug('ARG[%d] %s=%s' % (i, key, postvars[key])) 29 | i += 1 30 | self.send_response(200) 31 | self.send_header('Content-type', 'text/html') 32 | self.end_headers() 33 | str = '' 34 | str += '' 35 | str += ' ' 36 | str += ' Server POST Response' 37 | str += ' ' 38 | str += ' ' 39 | str += '

POST variables (%d).

' % (len(postvars)) 40 | if len(postvars): 41 | str += ' ' 42 | str += ' ' 43 | i = 0 44 | for key in sorted(postvars): 45 | i += 1 46 | val = postvars[key] 47 | str += ' ' 48 | str += ' ' % (i) 49 | str += ' ' % key 50 | str += ' ' % val 51 | str += ' ' 52 | str += ' ' 53 | str += '
%d%s%s
' 54 | str += ' ' 55 | str += '' 56 | self.wfile.write(str.format(self.path).encode('utf-8')) 57 | 58 | 59 | def run(server_class=HTTPServer, handler_class=S, port=8080): 60 | logging.basicConfig(level=logging.INFO) 61 | server_address = ('', port) 62 | httpd = server_class(server_address, handler_class) 63 | logging.info('Starting httpd...\n') 64 | try: 65 | httpd.serve_forever() 66 | except KeyboardInterrupt: 67 | pass 68 | httpd.server_close() 69 | logging.info('Stopping httpd...\n') 70 | 71 | 72 | if __name__ == '__main__': 73 | from sys import argv 74 | 75 | if len(argv) == 2: 76 | run(port=int(argv[1])) 77 | else: 78 | run() 79 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import find_packages, setup 2 | 3 | from cloudipsp.configuration import __version__ 4 | 5 | desc = """ 6 | Cloudipsp python sdk. 7 | Docs - https://docs.fondy.eu/ 8 | README - https://github.com/cloudipsp/python-sdk/blob/master/README.md 9 | """ 10 | 11 | requires_list = [ 12 | 'requests', 13 | 'six' 14 | ] 15 | 16 | setup( 17 | name='cloudipsp', 18 | version=__version__, 19 | url='https://github.com/cloudipsp/python-sdk/', 20 | license='MIT', 21 | description='Python SDK for cloudipsp clients.', 22 | long_description=desc, 23 | author='Dmitriy Miroshnikov', 24 | packages=find_packages(where='.', exclude=('tests*',)), 25 | install_requires=requires_list, 26 | classifiers=[ 27 | 'Environment :: Web Environment', 28 | 'Natural Language :: English', 29 | 'License :: OSI Approved :: MIT License', 30 | 'Operating System :: OS Independent', 31 | 'Programming Language :: Python', 32 | 'Programming Language :: Python :: 2.7', 33 | 'Programming Language :: Python :: 3', 34 | 'Programming Language :: Python :: 3.4', 35 | 'Programming Language :: Python :: 3.6', 36 | 'Programming Language :: Python :: 3.7', 37 | 'Programming Language :: Python :: 3.8', 38 | ]) 39 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | from cloudipsp import Api, Checkout 2 | 3 | api = Api(merchant_id=1396424, 4 | secret_key='test', 5 | request_type='xml', 6 | api_protocol='1.0', 7 | api_domain='api.fondy.eu') # json - is default 8 | checkout = Checkout(api=api) 9 | data = { 10 | "preauth": 'Y', 11 | "currency": "RUB", 12 | "amount": 10000, 13 | "reservation_data": { 14 | 'test': 1, 15 | 'test2': 2 16 | } 17 | } 18 | response = checkout.url(data) 19 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudipsp/python-sdk/7f87ba1ca88c4d80145acba7f42bd828f16702e9/tests/__init__.py -------------------------------------------------------------------------------- /tests/api_tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | from cloudipsp import Api, exceptions 3 | from .tests_helper import TestCase 4 | 5 | 6 | class ApiTest(TestCase): 7 | def setUp(self): 8 | self.data = self.get_dummy_data() 9 | self.api = Api(merchant_id=self.data['merchant']['id'], 10 | secret_key=self.data['merchant']['secret']) 11 | 12 | def test_request_type(self): 13 | api = Api(merchant_id=self.data['merchant']['id'], 14 | secret_key=self.data['merchant']['secret'], 15 | request_type='xml') 16 | self.assertEqual(api.request_type, 'xml') 17 | 18 | def test_api_domain(self): 19 | api = Api(merchant_id=self.data['merchant']['id'], 20 | secret_key=self.data['merchant']['secret'], 21 | api_domain='api.test.eu') 22 | self.assertEqual(api.api_url, 'https://api.test.eu/api') 23 | 24 | def test_api_protocol(self): 25 | api = Api(merchant_id=self.data['merchant']['id'], 26 | secret_key=self.data['merchant']['secret'], 27 | api_protocol='2.0') 28 | self.assertEqual(api.api_protocol, '2.0') 29 | 30 | def test_api_except(self): 31 | with self.assertRaises(ValueError): 32 | Api(merchant_id=self.data['merchant']['id'], 33 | secret_key=self.data['merchant']['secret'], 34 | api_protocol='2.0', 35 | request_type='xml' 36 | ) 37 | 38 | def test_post(self): 39 | with self.assertRaises(exceptions.ServiceError): 40 | self.api._request(self.api.api_url, 41 | method="POST", 42 | data=None, 43 | headers=None) 44 | 45 | def test_headers(self): 46 | self.assertEqual(self.api._headers().get('User-Agent'), 47 | 'Python SDK') 48 | self.assertEqual(self.api._headers().get('Content-Type'), 49 | 'application/json; charset=utf-8') 50 | -------------------------------------------------------------------------------- /tests/checkout_tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | from cloudipsp import Checkout 3 | from .tests_helper import TestCase 4 | 5 | import uuid 6 | 7 | 8 | class CheckoutTest(TestCase): 9 | def setUp(self): 10 | self.api = self.get_api() 11 | self.checkout = Checkout(api=self.api) 12 | 13 | def test_create_url_json(self): 14 | response = self.checkout.url(self.data.get('checkout_data')) 15 | self.assertEqual(response.get('response_status'), 'success') 16 | self.assertEqual(self.api._headers().get('Content-Type'), 17 | 'application/json; charset=utf-8') 18 | self.assertIn('checkout_url', response) 19 | self.assertEqual(len(response.get('checkout_url')) > 0, True) 20 | 21 | def test_get_order_id(self): 22 | data = { 23 | 'order_id': str(uuid.uuid4()) 24 | } 25 | data.update(self.data.get('checkout_data')) 26 | self.checkout.url(data) 27 | self.assertEqual(self.checkout.order_id, data.get('order_id')) 28 | 29 | def test_create_url_json_v2(self): 30 | self.api.api_protocol = '2.0' 31 | response = self.checkout.url(self.data.get('checkout_data')) 32 | self.assertEqual(self.api._headers().get('Content-Type'), 33 | 'application/json; charset=utf-8') 34 | self.assertEqual(response.get('version'), '2.0') 35 | self.assertEqual(len(response.get('data')) > 0, True) 36 | 37 | def test_create_subscb_json_v2(self): 38 | self.api.api_protocol = '2.0' 39 | data = self.data.get('checkout_data') 40 | recurring_data = { 41 | 'recurring_data': { 42 | 'start_time': '2028-11-11', 43 | 'amount': '234324', 44 | 'every': '40', 45 | 'period': 'day' 46 | } 47 | } 48 | data.update(recurring_data) 49 | response = self.checkout.url(data) 50 | self.assertEqual(self.api._headers().get('Content-Type'), 51 | 'application/json; charset=utf-8') 52 | self.assertEqual(response.get('version'), '2.0') 53 | self.assertEqual(len(response.get('data')) > 0, True) 54 | 55 | def test_create_url_xml(self): 56 | self.api.request_type = 'xml' 57 | response = self.checkout.url(self.data.get('checkout_data')) 58 | 59 | self.assertEqual(response.get('response_status'), 'success') 60 | self.assertEqual(self.api._headers().get('Content-Type'), 61 | 'application/xml; charset=utf-8') 62 | self.assertIn('checkout_url', response) 63 | self.assertEqual(len(response.get('checkout_url')) > 0, True) 64 | 65 | def test_create_url_form(self): 66 | self.api.request_type = 'form' 67 | response = self.checkout.url(self.data.get('checkout_data')) 68 | 69 | self.assertEqual(response.get('response_status'), 'success') 70 | self.assertEqual(self.api._headers().get('Content-Type'), 71 | 'application/x-www-form-urlencoded; charset=utf-8') 72 | self.assertIn('checkout_url', response) 73 | self.assertEqual(len(response.get('checkout_url')) > 0, True) 74 | 75 | def test_create_token(self): 76 | response = self.checkout.token(self.data.get('checkout_data')) 77 | self.assertEqual(response.get('response_status'), 'success') 78 | self.assertEqual(self.api._headers().get('Content-Type'), 79 | 'application/json; charset=utf-8') 80 | self.assertIn('token', response) 81 | self.assertEqual(len(response.get('token')) > 0, True) 82 | 83 | def test_create_url_verify(self): 84 | response = self.checkout.verification(self.data.get('checkout_data')) 85 | self.assertEqual(response.get('response_status'), 'success') 86 | self.assertEqual(self.api._headers().get('Content-Type'), 87 | 'application/json; charset=utf-8') 88 | self.assertIn('checkout_url', response) 89 | self.assertEqual(len(response.get('checkout_url')) > 0, True) 90 | -------------------------------------------------------------------------------- /tests/data/test_data.json: -------------------------------------------------------------------------------- 1 | { 2 | "merchant": { 3 | "id": 1396424, 4 | "secret": "test" 5 | }, 6 | "checkout_data": { 7 | "amount": "100", 8 | "currency": "USD" 9 | }, 10 | "order_data": { 11 | "order_id": 14290 12 | }, 13 | "order_full_data": { 14 | "amount": "100", 15 | "currency": "RUB" 16 | }, 17 | "payment_p2p": { 18 | "receiver_card_number": "4444555566661111", 19 | "currency": "RUB", 20 | "amount": "100" 21 | }, 22 | "payment_pcidss_non3ds": { 23 | "currency": "RUB", 24 | "amount": "100", 25 | "card_number": "4444555511116666", 26 | "cvv2": "123", 27 | "expiry_date": "1224" 28 | }, 29 | "payment_pcidss_3ds": { 30 | "currency": "RUB", 31 | "amount": "100", 32 | "card_number": "4444555566661111", 33 | "cvv2": "123", 34 | "expiry_date": "1224" 35 | } 36 | } -------------------------------------------------------------------------------- /tests/order_tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | 3 | import json 4 | from cloudipsp import Order 5 | from .tests_helper import TestCase 6 | 7 | 8 | class OrderTest(TestCase): 9 | def setUp(self): 10 | self.api = self.get_api() 11 | self.order = Order(api=self.api) 12 | self.order_id = self.create_order().get('order_id') 13 | 14 | def test_get_order_status(self): 15 | data = { 16 | 'order_id': self.order_id 17 | } 18 | response = self.order.status(data) 19 | self.assertEqual(response.get('response_status'), 'success') 20 | self.assertIn('order_status', response) 21 | 22 | def test_get_order_trans_list(self): 23 | data = { 24 | 'order_id': self.order_id 25 | } 26 | response = self.order.transaction_list(data) 27 | self.assertIsInstance(response, list) 28 | self.assertIn('order_id', response[0]) 29 | 30 | def test_refund(self): 31 | data = { 32 | 'order_id': self.order_id 33 | } 34 | data.update(self.data['order_full_data']) 35 | response = self.order.reverse(data) 36 | self.assertEqual(response.get('response_status'), 'success') 37 | self.assertIn('reverse_status', response) 38 | 39 | def test_capture(self): 40 | data = { 41 | 'order_id': self.order_id 42 | } 43 | data.update(self.data['order_full_data']) 44 | response = self.order.capture(data) 45 | self.assertEqual(response.get('response_status'), 'success') 46 | self.assertEqual(response.get('order_id'), self.order_id) 47 | self.assertEqual(response.get('capture_status'), 'captured') 48 | 49 | def test_settlement(self): 50 | self.api.api_protocol = '2.0' 51 | data = { 52 | 'operation_id': self.order_id, 53 | 'receiver': [ 54 | { 55 | 'requisites': { 56 | 'amount': 500, 57 | 'merchant_id': 600001 58 | }, 59 | 'type': 'merchant' 60 | }, 61 | { 62 | 'requisites': { 63 | 'amount': 500, 64 | 'merchant_id': 700001 65 | }, 66 | 'type': 'merchant' 67 | } 68 | ] 69 | } 70 | data.update(self.data['order_full_data']) 71 | data_capture = { 72 | 'order_id': self.order_id 73 | } 74 | data_capture.update(self.data['order_full_data']) 75 | self.order.capture(data_capture) 76 | response = self.order.settlement(data) 77 | response_data = json.loads(response.get('data')) 78 | self.assertEqual(response_data.get('order')['order_status'], 'created') 79 | self.assertIn('payment_id', response_data.get('order')) 80 | -------------------------------------------------------------------------------- /tests/payment_tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | from cloudipsp import Api, Payment, Pcidss 3 | from .tests_helper import TestCase 4 | from datetime import datetime, timedelta 5 | 6 | 7 | class PaymentTest(TestCase): 8 | def setUp(self): 9 | self.api = self.get_api() 10 | self.payment = Payment(api=self.api) 11 | self.pcidss = Pcidss(api=self.api) 12 | 13 | def test_recurring_payment(self): 14 | token = self.create_order().get('rectoken') 15 | data = { 16 | "rectoken": token 17 | } 18 | data.update(self.data['checkout_data']) 19 | response = self.payment.recurring(data) 20 | self.assertEqual(response.get('response_status'), 'success') 21 | self.assertIn('order_status', response) 22 | self.assertEqual(response.get('order_status'), 'approved') 23 | 24 | def test_reports(self): 25 | data = { 26 | "date_from": (datetime.now() - timedelta(minutes=240)).strftime('%d.%m.%Y %H:%M:%S'), 27 | "date_to": datetime.now().strftime('%d.%m.%Y %H:%M:%S') 28 | } 29 | response = self.payment.reports(data) 30 | self.assertIsInstance(response, list) 31 | 32 | def test_p2pcredit(self): 33 | api = Api(merchant_id=1000, secret_key='testcredit') 34 | payment = Payment(api=api) 35 | response = payment.p2pcredit(self.data['payment_p2p']) 36 | self.assertEqual(response.get('response_status'), 'success') 37 | self.assertIn('order_status', response) 38 | 39 | def test_non3dpcidss_step_one(self): 40 | data = self.data['payment_pcidss_non3ds'] 41 | response = self.pcidss.step_one(data) 42 | self.assertEqual(response.get('response_status'), 'success') 43 | self.assertIn('order_status', response) 44 | self.assertEqual(response.get('order_status'), 'approved') 45 | 46 | def test_3dspcidss_step_one(self): 47 | data = self.data['payment_pcidss_3ds'] 48 | response = self.pcidss.step_one(data) 49 | self.assertEqual(response.get('response_status'), 'success') 50 | self.assertIn('acs_url', response) 51 | self.assertIn('pareq', response) 52 | -------------------------------------------------------------------------------- /tests/tests_helper.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from unittest import TestCase 5 | from cloudipsp import Api, Pcidss, helpers 6 | 7 | 8 | class TestCase(TestCase): 9 | def get_api(self): 10 | self.data = self.get_dummy_data() 11 | return Api(merchant_id=self.data['merchant']['id'], 12 | secret_key=self.data['merchant']['secret']) 13 | 14 | def get_dummy_data(self): 15 | dummy_data = os.path.join(os.path.dirname(__file__), 16 | 'data', 17 | 'test_data.json') 18 | with open(dummy_data) as f: 19 | self.data = json.load(f) 20 | return self.data 21 | 22 | def create_order(self): 23 | pcidss = Pcidss(api=self.get_api()) 24 | params = { 25 | "preauth": "Y", 26 | "required_rectoken": "Y" 27 | } 28 | params.update(self.data['payment_pcidss_non3ds']) 29 | return pcidss.step_one(params) 30 | 31 | def test_validate_order(self): 32 | payment = self.create_order() 33 | is_valid = helpers.is_valid(data=payment, 34 | secret_key=self.data['merchant']['secret'], 35 | protocol='1.0') 36 | self.assertEqual(payment.get('response_status'), 'success') 37 | self.assertEqual(is_valid, True) 38 | -------------------------------------------------------------------------------- /tests/utils_tests.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import, unicode_literals 2 | from cloudipsp import utils 3 | from .tests_helper import TestCase 4 | 5 | 6 | class UtilTest(TestCase): 7 | def setUp(self): 8 | self.data = self.get_dummy_data() 9 | 10 | def test_to_xml(self): 11 | xml = utils.to_xml(self.data['checkout_data']) 12 | self.assertEqual(xml, '100USD') 13 | 14 | def test_from_xml(self): 15 | xml = utils.to_xml({'req': self.data['checkout_data']}) 16 | json = utils.from_xml(xml) 17 | self.assertEqual(json, {'req': self.data['checkout_data']}) 18 | 19 | def test_to_form(self): 20 | form = utils.to_form(self.data['checkout_data']) 21 | self.assertEqual(form, 'amount=100¤cy=USD') 22 | 23 | def test_from_from(self): 24 | form = utils.to_form(self.data['checkout_data']) 25 | json = utils.from_form(form) 26 | self.assertEqual(json, self.data['checkout_data']) 27 | 28 | def test_join_url(self): 29 | joined_url = utils.join_url("checkout", "order") 30 | self.assertEqual(joined_url, "checkout/order") 31 | joined_url = utils.join_url("order", "/3ds") 32 | self.assertEqual(joined_url, "order/3ds") 33 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py34, py35, py36, py37 3 | 4 | [testenv] 5 | commands = 6 | pycodestyle cloudipsp 7 | nosetests --with-coverage --cover-package=cloudipsp 8 | deps = 9 | nose 10 | coverage 11 | unittest2 12 | requests 13 | pycodestyle 14 | skip_missing_interpreters = 15 | true --------------------------------------------------------------------------------