├── .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 | [](https://pepy.tech/project/cloudipsp)
8 | [](https://pepy.tech/project/cloudipsp)
9 | [](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("%s>" % 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 += ' %d | ' % (i)
49 | str += ' %s | ' % key
50 | str += ' %s | ' % val
51 | str += '
'
52 | str += ' '
53 | str += '
'
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
--------------------------------------------------------------------------------