diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/iap/tools | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/iap/tools')
| -rw-r--r-- | addons/iap/tools/__init__.py | 4 | ||||
| -rw-r--r-- | addons/iap/tools/iap_tools.py | 170 |
2 files changed, 174 insertions, 0 deletions
diff --git a/addons/iap/tools/__init__.py b/addons/iap/tools/__init__.py new file mode 100644 index 00000000..d2ad0468 --- /dev/null +++ b/addons/iap/tools/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from . import iap_tools diff --git a/addons/iap/tools/iap_tools.py b/addons/iap/tools/iap_tools.py new file mode 100644 index 00000000..86c42d6d --- /dev/null +++ b/addons/iap/tools/iap_tools.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +import contextlib +import logging +import json +import requests +import uuid +from unittest.mock import patch + +from odoo import exceptions, _ +from odoo.tests.common import BaseCase +from odoo.tools import pycompat + +_logger = logging.getLogger(__name__) + +DEFAULT_ENDPOINT = 'https://iap.odoo.com' + + +# We need to mock iap_jsonrpc during tests as we don't want to perform real calls to RPC endpoints +def iap_jsonrpc_mocked(*args, **kwargs): + raise exceptions.AccessError("Unavailable during tests.") + + +iap_patch = patch('odoo.addons.iap.tools.iap_tools.iap_jsonrpc', iap_jsonrpc_mocked) + + +def setUp(self): + old_setup_func(self) + iap_patch.start() + self.addCleanup(iap_patch.stop) + + +old_setup_func = BaseCase.setUp +BaseCase.setUp = setUp + +#---------------------------------------------------------- +# Helpers for both clients and proxy +#---------------------------------------------------------- + +def iap_get_endpoint(env): + url = env['ir.config_parameter'].sudo().get_param('iap.endpoint', DEFAULT_ENDPOINT) + return url + +#---------------------------------------------------------- +# Helpers for clients +#---------------------------------------------------------- + +class InsufficientCreditError(Exception): + pass + + +def iap_jsonrpc(url, method='call', params=None, timeout=15): + """ + Calls the provided JSON-RPC endpoint, unwraps the result and + returns JSON-RPC errors as exceptions. + """ + payload = { + 'jsonrpc': '2.0', + 'method': method, + 'params': params, + 'id': uuid.uuid4().hex, + } + + _logger.info('iap jsonrpc %s', url) + try: + req = requests.post(url, json=payload, timeout=timeout) + req.raise_for_status() + response = req.json() + if 'error' in response: + name = response['error']['data'].get('name').rpartition('.')[-1] + message = response['error']['data'].get('message') + if name == 'InsufficientCreditError': + e_class = InsufficientCreditError + elif name == 'AccessError': + e_class = exceptions.AccessError + elif name == 'UserError': + e_class = exceptions.UserError + else: + raise requests.exceptions.ConnectionError() + e = e_class(message) + e.data = response['error']['data'] + raise e + return response.get('result') + except (ValueError, requests.exceptions.ConnectionError, requests.exceptions.MissingSchema, requests.exceptions.Timeout, requests.exceptions.HTTPError) as e: + raise exceptions.AccessError( + _('The url that this service requested returned an error. Please contact the author of the app. The url it tried to contact was %s', url) + ) + +#---------------------------------------------------------- +# Helpers for proxy +#---------------------------------------------------------- + +class IapTransaction(object): + + def __init__(self): + self.credit = None + + +def iap_authorize(env, key, account_token, credit, dbuuid=False, description=None, credit_template=None): + endpoint = iap_get_endpoint(env) + params = { + 'account_token': account_token, + 'credit': credit, + 'key': key, + 'description': description, + } + if dbuuid: + params.update({'dbuuid': dbuuid}) + try: + transaction_token = iap_jsonrpc(endpoint + '/iap/1/authorize', params=params) + except InsufficientCreditError as e: + if credit_template: + arguments = json.loads(e.args[0]) + arguments['body'] = pycompat.to_text(env['ir.qweb']._render(credit_template)) + e.args = (json.dumps(arguments),) + raise e + return transaction_token + + +def iap_cancel(env, transaction_token, key): + endpoint = iap_get_endpoint(env) + params = { + 'token': transaction_token, + 'key': key, + } + r = iap_jsonrpc(endpoint + '/iap/1/cancel', params=params) + return r + + +def iap_capture(env, transaction_token, key, credit): + endpoint = iap_get_endpoint(env) + params = { + 'token': transaction_token, + 'key': key, + 'credit_to_capture': credit, + } + r = iap_jsonrpc(endpoint + '/iap/1/capture', params=params) + return r + + +@contextlib.contextmanager +def iap_charge(env, key, account_token, credit, dbuuid=False, description=None, credit_template=None): + """ + Account charge context manager: takes a hold for ``credit`` + amount before executing the body, then captures it if there + is no error, or cancels it if the body generates an exception. + + :param str key: service identifier + :param str account_token: user identifier + :param int credit: cost of the body's operation + :param description: a description of the purpose of the charge, + the user will be able to see it in their + dashboard + :type description: str + :param credit_template: a QWeb template to render and show to the + user if their account does not have enough + credits for the requested operation + :type credit_template: str + """ + transaction_token = iap_authorize(env, key, account_token, credit, dbuuid, description, credit_template) + try: + transaction = IapTransaction() + transaction.credit = credit + yield transaction + except Exception as e: + r = iap_cancel(env,transaction_token, key) + raise e + else: + r = iap_capture(env,transaction_token, key, transaction.credit) |
