├── .flake8 ├── .gitignore ├── README.rst ├── liqpay ├── __init__.py ├── liqpay.py ├── liqpay3.py └── test_liqpay3.py └── setup.py /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | exclude = 3 | .git, 4 | .idea, 5 | __pycache__, 6 | doc, 7 | logs, 8 | __init__.py, 9 | max-line-length = 150 10 | max-complexity = 10 11 | filename = *.py 12 | format = default 13 | inline-quotes = " 14 | ignore = 15 | # E221 multiple spaces before operator 16 | E221, 17 | # whitespace before ':' 18 | E203, 19 | # whitespace before ')' 20 | E202, 21 | # multiple spaces after ',' 22 | E241, 23 | # unexpected spaces around keyword / parameter equals 24 | E251 -------------------------------------------------------------------------------- /.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 stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | #project files 92 | cached_uglify 93 | .idea 94 | 95 | /liqpay/liqpay_m.py 96 | /liqpay/index.html 97 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. |liqpaylogo| image:: https://www.liqpay.ua/1508940109424071/static/img/images/logo.svg 2 | 3 | ===== 4 | |liqpaylogo| SDK-Python 5 | ===== 6 | 7 | :Version: 1.1.0 8 | :Web: https://www.liqpay.ua/ 9 | :Download: https://pypi.python.org/pypi/liqpay-python 10 | :Source: https://github.com/liqpay/sdk-python 11 | :Documentation: https://liqpay.ua/doc 12 | :Keywords: liqpay, privat24, privatbank, python, internet acquiring, P2P payments, two-step payments 13 | 14 | 15 | What python version is supported? 16 | ============ 17 | - Python 2.7 (deprecated) 18 | - Python 3.11 19 | 20 | Get Started 21 | ============ 22 | 1. Sign up in https://www.liqpay.ua/en/authorization. 23 | 2. Create a company. 24 | 3. In company settings, on API tab, get **Public key** and **Private key**. 25 | 4. Done. 26 | 27 | Installation 28 | ============ 29 | From pip 30 | :: 31 | $ pip install liqpay-python 32 | 33 | From github 34 | :: 35 | $ pip install git+https://github.com/liqpay/sdk-python#egg=liqpay-python 36 | 37 | Working with LiqPay Callback locally 38 | ============ 39 | If you need debugging API Callback on local environment use https://localtunnel.github.io/www/ 40 | 41 | How it use? 42 | ============ 43 | 44 | Example 1: Basic 45 | ------- 46 | 47 | **Backend** 48 | 49 | :: 50 | 51 | liqpay = LiqPay(public_key, private_key) 52 | html = liqpay.cnb_form({ 53 | 'action': 'pay', 54 | 'amount': '1', 55 | 'currency': 'USD', 56 | 'description': 'description text', 57 | 'order_id': 'order_id_1', 58 | 'version': '3' 59 | }) 60 | 61 | **Frontend** 62 | 63 | Variable ``html`` will contain next html form 64 | 65 | :: 66 | 67 |
68 | 71 | 72 | 73 | 74 |
75 | 76 | Example 2: Integrate Payment widget to Django 77 | ------- 78 | `Payment widget documentation`_ 79 | 80 | .. _`Payment widget documentation`: 81 | https://www.liqpay.ua/documentation/en/api/aquiring/widget/ 82 | 83 | **Backend** 84 | 85 | views.py 86 | 87 | :: 88 | 89 | from liqpay import LiqPay 90 | 91 | from django.views.generic import TemplateView 92 | from django.shortcuts import render 93 | from django.http import HttpResponse 94 | 95 | class PayView(TemplateView): 96 | template_name = 'billing/pay.html' 97 | 98 | def get(self, request, *args, **kwargs): 99 | liqpay = LiqPay(settings.LIQPAY_PUBLIC_KEY, settings.LIQPAY_PRIVATE_KEY) 100 | params = { 101 | 'action': 'pay', 102 | 'amount': '100', 103 | 'currency': 'USD', 104 | 'description': 'Payment for clothes', 105 | 'order_id': 'order_id_1', 106 | 'version': '3', 107 | 'sandbox': 0, # sandbox mode, set to 1 to enable it 108 | 'server_url': 'https://test.com/billing/pay-callback/', # url to callback view 109 | } 110 | signature = liqpay.cnb_signature(params) 111 | data = liqpay.cnb_data(params) 112 | return render(request, self.template_name, {'signature': signature, 'data': data}) 113 | 114 | @method_decorator(csrf_exempt, name='dispatch') 115 | class PayCallbackView(View): 116 | def post(self, request, *args, **kwargs): 117 | liqpay = LiqPay(settings.LIQPAY_PUBLIC_KEY, settings.LIQPAY_PRIVATE_KEY) 118 | data = request.POST.get('data') 119 | signature = request.POST.get('signature') 120 | sign = liqpay.str_to_sign(settings.LIQPAY_PRIVATE_KEY + data + settings.LIQPAY_PRIVATE_KEY) 121 | if sign == signature: 122 | print('callback is valid') 123 | response = liqpay.decode_data_from_str(data) 124 | print('callback data', response) 125 | return HttpResponse() 126 | 127 | urls.py 128 | 129 | :: 130 | 131 | from django.conf.urls import url 132 | 133 | from billing.views import PayView, PayCallbackView 134 | 135 | 136 | urlpatterns = [ 137 | url(r'^pay/$', PayView.as_view(), name='pay_view'), 138 | url(r'^pay-callback/$', PayCallbackView.as_view(), name='pay_callback'), 139 | ] 140 | 141 | **Frontend** 142 | 143 | :: 144 | 145 |
146 | 163 | 164 | -------------------------------------------------------------------------------- /liqpay/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | if sys.version_info >= (3, 0): 4 | from .liqpay3 import * 5 | else: 6 | from .liqpay import * 7 | -------------------------------------------------------------------------------- /liqpay/liqpay.py: -------------------------------------------------------------------------------- 1 | """ 2 | LiqPay Python SDK 3 | ~~~~~~~~~~~~~~~~~ 4 | supports python 2.7.x version 5 | requires requests module 6 | """ 7 | 8 | __title__ = 'LiqPay Python SDK' 9 | __version__ = '1.0' 10 | 11 | import base64 12 | from copy import deepcopy 13 | import hashlib 14 | import json 15 | from urlparse import urljoin 16 | 17 | import requests 18 | 19 | 20 | def to_unicode(s): 21 | """ 22 | :param s: 23 | :return: unicode value (decoded utf-8) 24 | """ 25 | if isinstance(s, unicode): 26 | return s 27 | 28 | if isinstance(s, basestring): 29 | return s.decode('utf-8', 'strict') 30 | 31 | if hasattr(s, '__unicode__'): 32 | return s.__unicode__() 33 | 34 | return unicode(bytes(s), 'utf-8', 'strict') 35 | 36 | 37 | class ParamValidationError(Exception): 38 | pass 39 | 40 | 41 | class LiqPay(object): 42 | FORM_TEMPLATE = u'''\ 43 |
44 | \t{param_inputs} 45 | 46 |
''' 47 | INPUT_TEMPLATE = u'' 48 | 49 | SUPPORTED_PARAMS = [ 50 | 'public_key', 'amount', 'currency', 'description', 'order_id', 51 | 'result_url', 'server_url', 'type', 'signature', 'language', 'sandbox' 52 | ] 53 | 54 | def __init__(self, public_key, private_key, host='https://www.liqpay.ua/api/'): 55 | self._public_key = public_key 56 | self._private_key = private_key 57 | self._host = host 58 | 59 | def _make_signature(self, *args): 60 | smart_str = lambda x: to_unicode(x).encode('utf-8') 61 | joined_fields = ''.join(smart_str(x) for x in args) 62 | return base64.b64encode(hashlib.sha1(joined_fields).digest()) 63 | 64 | def _prepare_params(self, params): 65 | params = {} if params is None else deepcopy(params) 66 | params.update(public_key=self._public_key) 67 | return params 68 | 69 | def api(self, url, params=None): 70 | params = self._prepare_params(params) 71 | 72 | json_encoded_params = json.dumps(params) 73 | private_key = self._private_key 74 | signature = self._make_signature(private_key, json_encoded_params, private_key) 75 | 76 | request_url = urljoin(self._host, url) 77 | request_data = {'data': json_encoded_params, 'signature': signature} 78 | response = requests.post(request_url, data=request_data, verify=False) 79 | return json.loads(response.content) 80 | 81 | def cnb_form(self, params): 82 | params = self._prepare_params(params) 83 | params_validator = ( 84 | ('amount', lambda x: x is not None and float(x) > 0), 85 | ('description', lambda x: x is not None) 86 | ) 87 | for key, validator in params_validator: 88 | if validator(params.get(key)): 89 | continue 90 | 91 | raise ParamValidationError('Invalid param: "%s"' % key) 92 | 93 | # spike to set correct values for language, currency and sandbox params 94 | language = params.get('language', 'ru') 95 | currency = params['currency'] 96 | params.update( 97 | language=language, 98 | currency=currency if currency != 'RUR' else 'RUB', 99 | sandbox=int(bool(params.get('sandbox'))) 100 | ) 101 | params_templ = {'data': self.data_to_sign(params)} 102 | params_templ['signature'] = self._make_signature(self._private_key, params_templ['data'], self._private_key) 103 | form_action_url = urljoin(self._host, '3/checkout/') 104 | format_input = lambda k, v: self.INPUT_TEMPLATE.format(name=k, value=to_unicode(v)) 105 | inputs = [format_input(k, v) for k, v in params_templ.iteritems()] 106 | return self.FORM_TEMPLATE.format( 107 | action=form_action_url, 108 | language=language, 109 | param_inputs=u'\n\t'.join(inputs) 110 | ) 111 | 112 | def cnb_signature(self, params): 113 | params = self._prepare_params(params) 114 | 115 | data_to_sign = self.data_to_sign(params) 116 | return self._make_signature(self._private_key, data_to_sign, self._private_key) 117 | 118 | def cnb_data(self, params): 119 | params = self._prepare_params(params) 120 | return self.data_to_sign(params) 121 | 122 | def str_to_sign(self, str): 123 | return base64.b64encode(hashlib.sha1(str).digest()) 124 | 125 | def data_to_sign(self, params): 126 | return base64.b64encode(json.dumps(params)) 127 | 128 | def decode_data_from_str(self, data): 129 | """Decoding data that were encoded by base64.b64encode(str) 130 | 131 | Note: 132 | Often case of using is decoding data from LiqPay Callback. 133 | Dict contains all information about payment. 134 | More info about callback params see in documentation 135 | https://www.liqpay.ua/documentation/api/callback. 136 | 137 | Args: 138 | data: json string with api params and encoded by base64.b64encode(str). 139 | 140 | Returns: 141 | Dict 142 | 143 | Example: 144 | liqpay = LiqPay(settings.LIQPAY_PUBLIC_KEY, settings.LIQPAY_PRIVATE_KEY) 145 | data = request.POST.get('data') 146 | response = liqpay.decode_data_from_str(data) 147 | print(response) 148 | {'commission_credit': 0.0, 'order_id': 'order_id_1', 'liqpay_order_id': 'T8SRXWM71509085055293216', ...} 149 | 150 | """ 151 | return json.loads(base64.b64decode(data).decode('utf-8')) 152 | -------------------------------------------------------------------------------- /liqpay/liqpay3.py: -------------------------------------------------------------------------------- 1 | """ 2 | LiqPay Python SDK 3 | ~~~~~~~~~~~~~~~~~ 4 | supports python 3 version 5 | requires requests module 6 | """ 7 | 8 | __title__ = "LiqPay Python SDK" 9 | __version__ = "1.1" 10 | 11 | import base64 12 | from copy import deepcopy 13 | import hashlib 14 | import json 15 | from urllib.parse import urljoin 16 | 17 | import requests 18 | 19 | 20 | class ParamValidationError(Exception): 21 | pass 22 | 23 | 24 | class LiqPay(object): 25 | _supportedCurrencies = ['EUR', 'USD', 'UAH'] 26 | _supportedLangs = ['uk', 'en'] 27 | _supportedActions = ['pay', 'hold', 'subscribe', 'paydonate'] 28 | 29 | _button_translations = { 30 | 'uk': 'Сплатити', 31 | 'en': 'Pay' 32 | } 33 | 34 | _FORM_TEMPLATE = """ 35 |
36 | 37 | 38 | 39 | 40 |
41 | """ 42 | 43 | SUPPORTED_PARAMS = [ 44 | "public_key", "amount", "currency", "description", "order_id", 45 | "result_url", "server_url", "type", "signature", "language", 46 | "version", "action" 47 | ] 48 | 49 | def __init__(self, public_key, private_key, host="https://www.liqpay.ua/api/"): 50 | self._public_key = public_key 51 | self._private_key = private_key 52 | self._host = host 53 | 54 | def _make_signature(self, *args): 55 | joined_fields = "".join(x for x in args) 56 | joined_fields = joined_fields.encode("utf-8") 57 | return base64.b64encode(hashlib.sha1(joined_fields).digest()).decode("ascii") 58 | 59 | 60 | def _prepare_params(self, params): 61 | params = {} if params is None else deepcopy(params) 62 | params.update(public_key=self._public_key) 63 | return params 64 | 65 | def api(self, url, params=None): 66 | params = self._prepare_params(params) 67 | params_validator = ( 68 | ("version", lambda x: x is not None), 69 | ("action", lambda x: x is not None), 70 | ) 71 | for key, validator in params_validator: 72 | if validator(params.get(key)): 73 | continue 74 | raise ParamValidationError("Invalid param: '{}'".format(key)) 75 | 76 | encoded_data, signature = self.get_data_end_signature('api', params) 77 | 78 | request_url = urljoin(self._host, url) 79 | request_data = {"data": encoded_data, "signature": signature} 80 | response = requests.post(request_url, data=request_data, verify=True) 81 | return json.loads(response.content.decode("utf-8")) 82 | 83 | def cnb_form(self, params): 84 | params = self._prepare_params(params) 85 | 86 | params_validator = ( 87 | ("version", lambda x: x is not None), 88 | ("amount", lambda x: x is not None and float(x) > 0), 89 | ("currency", lambda x: x is not None and x in self._supportedCurrencies), 90 | ("action", lambda x: x is not None), 91 | ("description", lambda x: x is not None and isinstance(x, str)) 92 | ) 93 | for key, validator in params_validator: 94 | if validator(params.get(key)): 95 | continue 96 | 97 | raise ParamValidationError("Invalid param: '{}'".format(key)) 98 | 99 | if 'language' in params: 100 | language = params['language'].lower() 101 | if language not in self._supportedLangs: 102 | params['language'] = 'uk' 103 | language = 'uk' 104 | else: 105 | language = 'uk' 106 | 107 | encoded_data, signature = self.get_data_end_signature('cnb_form', params) 108 | 109 | form_action_url = urljoin(self._host, "3/checkout/") 110 | return self._FORM_TEMPLATE.format( 111 | action=form_action_url, 112 | data=encoded_data, 113 | signature=signature, 114 | label=self._button_translations[language] 115 | ) 116 | 117 | def get_data_end_signature(self, type, params): 118 | json_encoded_params = json.dumps(params, sort_keys=True) 119 | if type == "cnb_form": 120 | bytes_data = json_encoded_params.encode('utf-8') 121 | base64_encoded_params = base64.b64encode(bytes_data).decode('utf-8') 122 | signature = self._make_signature(self._private_key, base64_encoded_params, self._private_key) 123 | return base64_encoded_params, signature 124 | else: 125 | signature = self._make_signature(self._private_key, json_encoded_params, self._private_key) 126 | return json_encoded_params, signature 127 | 128 | def cnb_signature(self, params): 129 | params = self._prepare_params(params) 130 | 131 | data_to_sign = self.data_to_sign(params) 132 | return self._make_signature(self._private_key, data_to_sign, self._private_key) 133 | 134 | def cnb_data(self, params): 135 | params = self._prepare_params(params) 136 | return self.data_to_sign(params) 137 | 138 | def str_to_sign(self, str): 139 | return base64.b64encode(hashlib.sha1(str.encode("utf-8")).digest()).decode("ascii") 140 | 141 | def data_to_sign(self, params): 142 | json_encoded_params = json.dumps(params, sort_keys=True) 143 | bytes_data = json_encoded_params.encode('utf-8') 144 | return base64.b64encode(bytes_data).decode('utf-8') 145 | 146 | def decode_data_from_str(self, data, signature=None): 147 | """Decoding data that were encoded by base64.b64encode(str) 148 | 149 | Args: 150 | data: json string with api params and encoded by base64.b64encode(str). 151 | signature: signature received from LiqPay (optional). 152 | 153 | Returns: 154 | Dict 155 | 156 | Raises: 157 | ParamValidationError: If the signature is provided and is invalid. 158 | 159 | """ 160 | if signature: 161 | expected_signature = self._make_signature(self._private_key, base64.b64decode(data).decode('utf-8'), self._private_key) 162 | if expected_signature != signature: 163 | raise ParamValidationError("Invalid signature") 164 | 165 | return json.loads(base64.b64decode(data).decode('utf-8')) 166 | 167 | -------------------------------------------------------------------------------- /liqpay/test_liqpay3.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import base64 3 | import hashlib 4 | import json 5 | from liqpay3 import LiqPay, ParamValidationError 6 | 7 | class TestLiqPay(unittest.TestCase): 8 | 9 | def setUp(self): 10 | self.public_key = "your_public_key" 11 | self.private_key = "your_private_key" 12 | self.liqpay = LiqPay(self.public_key, self.private_key) 13 | 14 | 15 | def test_valid_params(self): 16 | params = { 17 | 'version': 3, 18 | 'amount': '10', 19 | 'currency': 'USD', 20 | 'action': 'pay', 21 | 'order_id': '123456', 22 | 'description': 'Test Order', 23 | 'language': 'en' 24 | } 25 | try: 26 | form = self.liqpay.cnb_form(params) 27 | self.assertIn(' 161 | 162 | 163 | 164 | 165 | 166 | """.format(encoded_data=expected_data_placeholder, signature=expected_signature).strip() 167 | 168 | generated_form = self.liqpay.cnb_form(params).strip() 169 | 170 | # Порівняти всі частини форми окрім значення data 171 | self.assertEqual(generated_form.replace(self.liqpay.data_to_sign(params), expected_data_placeholder), 172 | expected_form) 173 | 174 | # Порівняти значення data окремо 175 | self.assertIn(self.liqpay.data_to_sign(params), generated_form) 176 | 177 | def test_decode_data_without_signature(self): 178 | data = { 179 | "order_id": "123456", 180 | "status": "success", 181 | "amount": "10.00" 182 | } 183 | encoded_data = base64.b64encode(json.dumps(data, sort_keys=True).encode("utf-8")).decode("utf-8") 184 | decoded_data = self.liqpay.decode_data_from_str(encoded_data) 185 | self.assertEqual(decoded_data, data) 186 | 187 | 188 | def test_decode_data_with_valid_signature(self): 189 | self.maxDiff = None 190 | expected_data = { 191 | 'amount': '10.00', 192 | 'order_id': '123456', 193 | 'status': 'success' 194 | } 195 | encoded_data = self.encode_params_to_data(expected_data) 196 | # signature = "q9O87s+HTm4Ij+9z2Wtv6F7rzE8=" 197 | signature = "mImHiMlo8z80jSh7+tWOz0enjIk=" 198 | decoded_data = self.liqpay.decode_data_from_str(encoded_data, signature) 199 | 200 | self.assertEqual(decoded_data, expected_data) 201 | 202 | def test_decode_data_with_invalid_signature(self): 203 | encoded_data = "eyJhbW91bnQiOiAiMTAuMDAiLCAib3JkZXJfaWQiOiAiMTIzNDU2IiwgInN0YXR1cyI6ICJzdWNjZXNzIn0=" 204 | invalid_signature = "invalid_signature" 205 | with self.assertRaises(ParamValidationError): 206 | self.liqpay.decode_data_from_str(encoded_data, invalid_signature) 207 | 208 | def test_signature_generation(self): 209 | params = { 210 | 'version': 3, 211 | 'amount': '10', 212 | 'currency': 'USD', 213 | 'action': 'pay', 214 | 'order_id': '123456', 215 | 'description': 'Test Order', 216 | 'language': 'en', 217 | 'public_key': self.public_key 218 | } 219 | expected_signature = "x4uWEaw2f35T0IoiVfECyKsbIeY=" 220 | data, generated_signature = self.liqpay.get_data_end_signature("cnb_form", params) 221 | self.assertEqual(generated_signature, expected_signature) 222 | 223 | def encode_params_to_data(self, params): 224 | json_str = json.dumps(params, sort_keys=True) 225 | bytes_data = json_str.encode('utf-8') 226 | return base64.b64encode(bytes_data).decode("ascii") 227 | 228 | if __name__ == '__main__': 229 | unittest.main() 230 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | 4 | setup( 5 | name='liqpay-python', 6 | version='1.1', 7 | description='LiqPay Python SDK', 8 | packages=find_packages(), 9 | include_package_data=True, 10 | install_requires=['requests'] 11 | ) 12 | --------------------------------------------------------------------------------