[ADD] iap: In app purchase
IAP allow app publishers to charge for ongoing services. In that context, Odoo acts mostly as a payment platform between the service user (client) and the service provider (Odoo App developer).
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import models
|
||||
@@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
|
||||
{
|
||||
'name': 'In-App Purchases',
|
||||
'category': 'Tools',
|
||||
'summary': 'Basic models and helpers to support In-App purchases.',
|
||||
'description': """
|
||||
This module provides standard tools (account model, context manager and helpers) to support In-App purchases inside Odoo.
|
||||
""",
|
||||
'depends': ['web'],
|
||||
'data': [
|
||||
'security/ir.model.access.csv',
|
||||
'security/ir_rule.xml',
|
||||
'views/assets.xml',
|
||||
'views/iap_views.xml',
|
||||
],
|
||||
'qweb': [
|
||||
'static/src/xml/iap_templates.xml',
|
||||
],
|
||||
'auto_install': True,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
||||
|
||||
from . import iap
|
||||
@@ -0,0 +1,157 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import contextlib
|
||||
import logging
|
||||
import json
|
||||
import uuid
|
||||
|
||||
import werkzeug.urls
|
||||
import requests
|
||||
from requests.packages import urllib3
|
||||
|
||||
from odoo import api, fields, models, exceptions
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_ENDPOINT = 'https://iap.odoo.com'
|
||||
|
||||
|
||||
#----------------------------------------------------------
|
||||
# Helpers for both clients and proxy
|
||||
#----------------------------------------------------------
|
||||
def get_endpoint(env):
|
||||
url = env['ir.config_parameter'].sudo().get_param('iap.endpoint', DEFAULT_ENDPOINT)
|
||||
return url
|
||||
|
||||
|
||||
#----------------------------------------------------------
|
||||
# Helpers for clients
|
||||
#----------------------------------------------------------
|
||||
class InsufficientCreditError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class AuthenticationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def jsonrpc(url, method='call', params=None):
|
||||
"""
|
||||
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)
|
||||
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
|
||||
else:
|
||||
e_class = exceptions.UserError
|
||||
e = e_class(message)
|
||||
e.data = response['error']['data']
|
||||
raise e
|
||||
return response.get('result')
|
||||
except (ValueError, requests.exceptions.ConnectionError, requests.exceptions.MissingSchema, urllib3.exceptions.MaxRetryError) as e:
|
||||
raise exceptions.AccessError('The url that this service requested returned an error. Please contact the author the app. The url it tried to contact was ' + url)
|
||||
|
||||
#----------------------------------------------------------
|
||||
# Helpers for proxy
|
||||
#----------------------------------------------------------
|
||||
@contextlib.contextmanager
|
||||
def charge(env, key, account_token, credit, 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 str description:
|
||||
"""
|
||||
end_point = get_endpoint(env)
|
||||
params = {
|
||||
'account_token': account_token,
|
||||
'credit': credit,
|
||||
'key': key,
|
||||
'description': description,
|
||||
}
|
||||
try:
|
||||
transaction_token = jsonrpc(endpoint + '/iap/1/authorize', params=params)
|
||||
except InsufficientCreditError as e:
|
||||
if credit_template:
|
||||
arguments = json.loads(e.args[0])
|
||||
arguments['body'] = env['ir.qweb'].render(credit_template)
|
||||
e.args = (json.dumps(arguments),)
|
||||
|
||||
try:
|
||||
yield
|
||||
except Exception as e:
|
||||
params = {
|
||||
'token': transaction_token,
|
||||
'key': key,
|
||||
}
|
||||
r = jsonrpc(end_point + '/iap/1/cancel', params=params)
|
||||
raise e
|
||||
else:
|
||||
params = {
|
||||
'token': transaction_token,
|
||||
'key': key,
|
||||
}
|
||||
r = jsonrpc(end_point + '/iap/1/capture', params=params) # noqa
|
||||
|
||||
|
||||
#----------------------------------------------------------
|
||||
# Models for client
|
||||
#----------------------------------------------------------
|
||||
class IapAccount(models.Model):
|
||||
_name = 'iap.account'
|
||||
_rec_name = 'service_name'
|
||||
|
||||
service_name = fields.Char()
|
||||
account_token = fields.Char(default=lambda s: uuid.uuid4().hex)
|
||||
company_id = fields.Many2one('res.company', default=lambda self: self.env.user.company_id)
|
||||
|
||||
@api.model
|
||||
def get(self, service_name):
|
||||
account = self.search([('service_name', '=', service_name), ('company_id', 'in', [self.env.user.company_id.id, False])])
|
||||
if not account:
|
||||
account = self.create({'service_name': service_name})
|
||||
# Since the account did not exist yet, we will encounter a NoCreditError,
|
||||
# which is going to rollback the database and undo the account creation,
|
||||
# preventing the process to continue any further.
|
||||
self.env.cr.commit()
|
||||
return account
|
||||
|
||||
@api.model
|
||||
def get_credits_url(self, base_url, service_name, credit):
|
||||
dbuuid = self.env['ir.config_parameter'].sudo().get_param('database.uuid')
|
||||
account_token = self.get(service_name).account_token
|
||||
d = {
|
||||
'dbuuid': dbuuid,
|
||||
'service_name': service_name,
|
||||
'account_token': account_token,
|
||||
'credit': credit,
|
||||
}
|
||||
return '%s?%s' % (base_url, werkzeug.urls.url_encode(d))
|
||||
|
||||
@api.model
|
||||
def get_account_url(self):
|
||||
route = '/iap/services'
|
||||
endpoint = get_endpoint(self.env)
|
||||
d = {'dbuuid': self.env['ir.config_parameter'].sudo().get_param('database.uuid')}
|
||||
|
||||
return '%s?%s' % (endpoint + route, werkzeug.urls.url_encode(d))
|
||||
@@ -0,0 +1,3 @@
|
||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_client_iap_account_manager,iap.account.manager,model_iap_account,base.group_system,1,1,1,0
|
||||
access_client_iap_account_user,iap.account.user,model_iap_account,base.group_user,1,0,0,0
|
||||
|
@@ -0,0 +1,13 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<record id="user_iap_account" model="ir.rule">
|
||||
<field name="name">User IAP Account</field>
|
||||
<field name="model_id" ref="model_iap_account"/>
|
||||
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||
<!-- partners can CUD services linked to themselves -->
|
||||
<field name="domain_force">[
|
||||
('company_id', 'in', [user.company_id.id, False]),
|
||||
]</field>
|
||||
</record>
|
||||
</data>
|
||||
</odoo>
|
||||
@@ -0,0 +1,49 @@
|
||||
odoo.define('iap.CrashManager', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
var CrashManager = require('web.CrashManager');
|
||||
var Dialog = require('web.Dialog');
|
||||
|
||||
var _t = core._t;
|
||||
var QWeb = core.qweb;
|
||||
|
||||
CrashManager.include({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
rpc_error: function (error) {
|
||||
if (error.data.name === "odoo.addons.iap.models.iap.InsufficientCreditError") {
|
||||
var error_data = JSON.parse(error.data.message);
|
||||
ajax.jsonRpc('/web/dataset/call_kw', 'call', {
|
||||
model: 'iap.account',
|
||||
method: 'get_credits_url',
|
||||
args: [],
|
||||
kwargs: {
|
||||
base_url: error_data.base_url,
|
||||
service_name: error_data.service_name,
|
||||
credit: error_data.credit,
|
||||
}
|
||||
}).then(function (url) {
|
||||
new Dialog(this, {
|
||||
size: 'large',
|
||||
title: error_data.title || _t("Insufficient Balance"),
|
||||
$content: $(QWeb.render('iap.redirect_to_odoo_credit', {
|
||||
data: error_data,
|
||||
})).css('padding', 0),
|
||||
buttons: [
|
||||
{text: 'Buy credits at Odoo', classes : "btn-primary", click: function() {
|
||||
window.open(url, '_blank');
|
||||
}, close:true},
|
||||
{text: _t("Cancel"), close: true}
|
||||
],
|
||||
}).open();
|
||||
});
|
||||
} else {
|
||||
this._super.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
odoo.define('iap.Dashboard', function (require) {
|
||||
"use strict";
|
||||
|
||||
var ajax = require('web.ajax');
|
||||
var core = require('web.core');
|
||||
var Dashboard = require('web_settings_dashboard');
|
||||
|
||||
var _t = core._t;
|
||||
var QWeb = core.qweb;
|
||||
|
||||
Dashboard.Dashboard.include({
|
||||
/**
|
||||
* @override
|
||||
*/
|
||||
load_apps: function (data) {
|
||||
var _super = this._super.bind(this);
|
||||
return ajax.jsonRpc('/web/dataset/call_kw', 'call', {
|
||||
model: 'iap.account',
|
||||
method: 'get_account_url',
|
||||
args: [],
|
||||
kwargs: {},
|
||||
}).then(function (url) {
|
||||
data.apps.url = url;
|
||||
return _super(data);
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
odoo.define('iap.redirect_odoo_credit_widget', function(require) {
|
||||
"use strict";
|
||||
|
||||
var core = require('web.core');
|
||||
var framework = require('web.framework');
|
||||
var Widget = require('web.Widget');
|
||||
var QWeb = core.qweb;
|
||||
|
||||
|
||||
var IapOdooCreditRedirect = Widget.extend({
|
||||
template: 'iap.redirect_to_odoo_credit',
|
||||
events : {
|
||||
"click .redirect_confirm" : "odoo_redirect",
|
||||
},
|
||||
init: function (parent, action) {
|
||||
this._super(parent, action);
|
||||
this.url = action.params.url;
|
||||
},
|
||||
|
||||
odoo_redirect: function () {
|
||||
window.open(this.url, '_blank');
|
||||
this.do_action({type: 'ir.actions.act_window_close'});
|
||||
// framework.redirect(this.url);
|
||||
},
|
||||
|
||||
});
|
||||
core.action_registry.add('iap_odoo_credit_redirect', IapOdooCreditRedirect);
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<template id="template" xml:space="preserve">
|
||||
|
||||
<!-- LAYOUT TEMPLATES -->
|
||||
<div t-name="iap.redirect_to_odoo_credit">
|
||||
<t t-if="data.body">
|
||||
<div t-raw="data.body"/>
|
||||
</t>
|
||||
<t t-if="!data.body">
|
||||
<t t-if="data.message">
|
||||
<span t-esc="data.message"/>
|
||||
</t>
|
||||
<t t-if="!data.message">
|
||||
<span>Insufficient credit to perform this service.</span>
|
||||
</t>
|
||||
</t>
|
||||
</div>
|
||||
|
||||
<t t-extend="DashboardApps">
|
||||
<t t-jquery=".o_web_settings_dashboard_pills" t-operation="append">
|
||||
<a class="pull-right" t-att-href="widget.data.url" target="_blank">
|
||||
<i class="fa fa-money fa-2x text-muted"/> In-App Purchases</a>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
</template>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<template id="assets_backend" name="iap assets" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<script type="text/javascript" src="/iap/static/src/js/iap_credit.js"></script>
|
||||
<script type="text/javascript" src="/iap/static/src/js/crash_manager.js"></script>
|
||||
<script type="text/javascript" src="/iap/static/src/js/dashboard.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
</odoo>
|
||||
@@ -0,0 +1,53 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
|
||||
<!-- iap Client Account Views -->
|
||||
<record id="iap_account_view_form" model="ir.ui.view">
|
||||
<field name="name">iap.account.form</field>
|
||||
<field name="model">iap.account</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="IAP Account">
|
||||
<sheet>
|
||||
<group name="account" string="Account Information">
|
||||
<field name="service_name"/>
|
||||
<field name="company_id"/>
|
||||
<field name="account_token"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
<record id="iap_account_view_tree" model="ir.ui.view">
|
||||
<field name="name">iap.account.tree</field>
|
||||
<field name="model">iap.account</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="IAP Accounts">
|
||||
<field name="service_name"/>
|
||||
<field name="company_id"/>
|
||||
<field name="account_token" readonly="1"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
<!-- Actions -->
|
||||
<record id="iap_account_action" model="ir.actions.act_window">
|
||||
<field name="name">IAP Account</field>
|
||||
<field name="res_model">iap.account</field>
|
||||
<field name='view_type'>form</field>
|
||||
<field name='view_mode'>tree,form</field>
|
||||
</record>
|
||||
|
||||
<!-- Menus -->
|
||||
<menuitem
|
||||
id="iap_root_menu"
|
||||
name="IAP"
|
||||
parent="base.menu_custom"
|
||||
sequence="4"/>
|
||||
|
||||
<menuitem
|
||||
id="iap_account_menu"
|
||||
name="IAP Accounts"
|
||||
parent="iap_root_menu"
|
||||
action="iap_account_action"
|
||||
sequence="10"/>
|
||||
|
||||
</odoo>
|
||||
@@ -345,6 +345,7 @@ core.action_registry.add('web_settings_dashboard.main', Dashboard);
|
||||
|
||||
return {
|
||||
Dashboard: Dashboard,
|
||||
DashboardApps: DashboardApps,
|
||||
DashboardInvitations: DashboardInvitations,
|
||||
DashboardPlanner: DashboardPlanner,
|
||||
DashboardShare: DashboardShare,
|
||||
|
||||
@@ -103,7 +103,7 @@ main.has_code_col{
|
||||
> *{
|
||||
max-width: 100%;
|
||||
}
|
||||
section {
|
||||
> section {
|
||||
position: relative;
|
||||
display:block;
|
||||
float: left;
|
||||
@@ -116,7 +116,7 @@ main.has_code_col{
|
||||
&:before {
|
||||
.code-col();
|
||||
}
|
||||
section {
|
||||
> section {
|
||||
> * {
|
||||
width: 54.633333%;
|
||||
max-width: 600px;
|
||||
|
||||
@@ -9708,7 +9708,7 @@ main.has_code_col article.doc-body {
|
||||
main.has_code_col article.doc-body > * {
|
||||
max-width: 100%;
|
||||
}
|
||||
main.has_code_col article.doc-body section {
|
||||
main.has_code_col article.doc-body > section {
|
||||
position: relative;
|
||||
display: block;
|
||||
float: left;
|
||||
@@ -9730,36 +9730,36 @@ main.has_code_col article.doc-body section {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
main.has_code_col article.doc-body section > * {
|
||||
main.has_code_col article.doc-body > section > * {
|
||||
width: 54.633333%;
|
||||
max-width: 600px;
|
||||
float: left;
|
||||
clear: left;
|
||||
}
|
||||
main.has_code_col article.doc-body section > h1,
|
||||
main.has_code_col article.doc-body section > h2,
|
||||
main.has_code_col article.doc-body section > h3,
|
||||
main.has_code_col article.doc-body section > h4,
|
||||
main.has_code_col article.doc-body section > h5,
|
||||
main.has_code_col article.doc-body section > h6 {
|
||||
main.has_code_col article.doc-body > section > h1,
|
||||
main.has_code_col article.doc-body > section > h2,
|
||||
main.has_code_col article.doc-body > section > h3,
|
||||
main.has_code_col article.doc-body > section > h4,
|
||||
main.has_code_col article.doc-body > section > h5,
|
||||
main.has_code_col article.doc-body > section > h6 {
|
||||
width: 100%;
|
||||
float: none;
|
||||
clear: none;
|
||||
}
|
||||
main.has_code_col article.doc-body section .doc-aside {
|
||||
main.has_code_col article.doc-body > section .doc-aside {
|
||||
width: 41%;
|
||||
float: none;
|
||||
clear: none;
|
||||
margin-right: 15px;
|
||||
margin-left: 57%;
|
||||
}
|
||||
main.has_code_col article.doc-body section .doc-aside .content-switcher {
|
||||
main.has_code_col article.doc-body > section .doc-aside .content-switcher {
|
||||
margin-top: 0;
|
||||
}
|
||||
main.has_code_col article.doc-body section .doc-aside .content-switcher > ul {
|
||||
main.has_code_col article.doc-body > section .doc-aside .content-switcher > ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
main.has_code_col article.doc-body section .doc-aside .content-switcher > ul > li {
|
||||
main.has_code_col article.doc-body > section .doc-aside .content-switcher > ul > li {
|
||||
color: #dcddde;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,6 +142,11 @@ class BootstrapTranslator(nodes.NodeVisitor, object):
|
||||
if not self.section_level:
|
||||
self.body.append(u'</section>')
|
||||
|
||||
def visit_topic(self, node):
|
||||
self.body.append(self.starttag(node, 'nav'))
|
||||
def depart_topic(self, node):
|
||||
self.body.append(u'</nav>')
|
||||
|
||||
def is_compact_paragraph(self, node):
|
||||
parent = node.parent
|
||||
if isinstance(parent, (nodes.document, nodes.compound,
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ Index
|
||||
:maxdepth: 2
|
||||
|
||||
tutorials
|
||||
api_integration
|
||||
webservices
|
||||
setup
|
||||
reference
|
||||
|
||||
|
||||
@@ -21,5 +21,4 @@ Reference
|
||||
reference/reports
|
||||
reference/mixins
|
||||
reference/guidelines
|
||||
reference/upgrade_api
|
||||
reference/mobile
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
:banner: banners/web_service_api.jpg
|
||||
:types: api
|
||||
|
||||
============
|
||||
Web Services
|
||||
============
|
||||
|
||||
.. toctree::
|
||||
:titlesonly:
|
||||
|
||||
webservices/odoo
|
||||
webservices/iap
|
||||
webservices/upgrade
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,235 @@
|
||||
:types: api
|
||||
|
||||
|
||||
:code-column:
|
||||
|
||||
.. _webservices/iap:
|
||||
|
||||
================
|
||||
In-App Purchases
|
||||
================
|
||||
|
||||
IAP allow providers of ongoing services through Odoo apps to be compensated
|
||||
for ongoing service use rather than — and possibly instead of — a sole initial
|
||||
purchase.
|
||||
|
||||
In that context, Odoo acts mostly as a *broker* between the service user
|
||||
(client) and the service provider (Odoo App developer):
|
||||
|
||||
* users purchase service tokens from Odoo
|
||||
* service providers draw tokens from the user's Odoo account
|
||||
|
||||
.. attention::
|
||||
|
||||
This document is intended for *service providers* and presents the latter,
|
||||
which can be done either via direct JSON-RPC2_ or if you are using Odoo
|
||||
using the convenience helpers it provides.
|
||||
|
||||
.. image:: flow.png
|
||||
:align: center
|
||||
|
||||
.. contents::
|
||||
:local:
|
||||
|
||||
JSON-RPC2_ Transaction API
|
||||
==========================
|
||||
|
||||
* The IAP transaction API does not require using Odoo when implementing your
|
||||
server gateway, calls are standard JSON-RPC2_.
|
||||
* Calls use different *endpoints* but the same *method* on all endpoints
|
||||
(``call``).
|
||||
* Exceptions are returned as JSON-RPC2_ errors, the formal exception name is
|
||||
available on ``data.name`` for programmatic manipulation.
|
||||
|
||||
.. class:: ServiceKey
|
||||
|
||||
Identifier generated for the provider's service. Each key (and service)
|
||||
matches a token of a fixed value, as generated by the service provide.
|
||||
|
||||
Multiple types of tokens correspond to multiple services e.g. SMS and MMS
|
||||
could either be the same service (with an MMS being "worth" multiple SMS)
|
||||
or could be separate services at separate price points.
|
||||
|
||||
.. danger:: your service key *is a secret*, leaking your service key
|
||||
allows other application developers to draw credits bought for
|
||||
your service(s)
|
||||
|
||||
.. class:: UserToken
|
||||
|
||||
Identifier for a user account.
|
||||
|
||||
.. class:: TransactionToken
|
||||
|
||||
Transaction identifier, returned by the authorization process and consumed
|
||||
by either capturing or cancelling the transaction
|
||||
|
||||
.. exception:: odoo.addons.iap.models.iap.NoCreditError
|
||||
|
||||
Raised during transaction authorization if the credits requested are not
|
||||
currently available on the account (either not enough credits or too many
|
||||
pending transactions/existing holds).
|
||||
|
||||
.. exception:: odoo.addons.iap.models.iap.BadAuthError
|
||||
|
||||
Raised by any operation to which a service token is required, if the
|
||||
service token is invalid.
|
||||
|
||||
Authorize
|
||||
---------
|
||||
|
||||
.. function:: /iap/1/authorize
|
||||
|
||||
Verifies that the user's account has at least as ``credit`` available
|
||||
*and creates a hold (pending transaction) on that amount*.
|
||||
|
||||
Any amount currently on hold by a pending transaction is considered
|
||||
unavailable to further authorize calls.
|
||||
|
||||
Returns a :class:`TransactionToken` identifying the pending transaction
|
||||
which can be used to capture (confirm) or cancel said transaction.
|
||||
|
||||
:param ServiceKey key:
|
||||
:param UserToken account_token:
|
||||
:param int credit:
|
||||
:param str description: optional, helps users identify the reason for
|
||||
charges on their accounts.
|
||||
:returns: :class:`TransactionToken` if the authorization succeeded.
|
||||
:raises: :class:`~odoo.addons.iap.models.iap.BadAuthError` if the service token is invalid
|
||||
:raises: :class:`~odoo.addons.iap.models.iap.NoCreditError` if the account does
|
||||
:raises: ``TypeError`` if the ``credit`` value is not an integer
|
||||
|
||||
.. rst-class:: doc-aside
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
r = requests.post(ODOO + '/iap/1/authorize', json={
|
||||
'jsonrpc': '2.0',
|
||||
'id': None,
|
||||
'method': 'call',
|
||||
'params': {
|
||||
'account_token': user_account,
|
||||
'key': SERVICE_KEY,
|
||||
'credit': 25,
|
||||
'description': "Why this is being charged",
|
||||
}
|
||||
}).json()
|
||||
if 'error' in r:
|
||||
# handle authorize error
|
||||
tx = r['result']
|
||||
|
||||
# provide your service here
|
||||
|
||||
Capture
|
||||
-------
|
||||
|
||||
.. function:: /iap/1/capture
|
||||
|
||||
Confirms the specified transaction, transferring the reserved credits from
|
||||
the user's account to the service provider's.
|
||||
|
||||
Capture calls are idempotent: performing capture calls on an already
|
||||
captured transaction has no further effect.
|
||||
|
||||
:param TransactionToken token:
|
||||
:param ServiceKey key:
|
||||
:raises: :class:`~odoo.addons.iap.models.iap.BadAuthError`
|
||||
|
||||
.. rst-class:: doc-aside
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
r2 = requests.post(ODOO + '/iap/1/capture', json={
|
||||
'jsonrpc': '2.0',
|
||||
'id': None,
|
||||
'method': 'call',
|
||||
'params': {
|
||||
'token': tx,
|
||||
'key': SERVICE_KEY,
|
||||
}
|
||||
}).json()
|
||||
if 'error' in r:
|
||||
# handle capture error
|
||||
# otherwise transaction is captured
|
||||
|
||||
Cancel
|
||||
------
|
||||
|
||||
.. function:: /iap/1/cancel
|
||||
|
||||
Cancels the specified transaction, releasing the hold on the user's
|
||||
credits.
|
||||
|
||||
Cancel calls are idempotent: performing capture calls on an already
|
||||
cancelled transaction has no further effect.
|
||||
|
||||
:param TransactionToken token:
|
||||
:param ServiceKey key:
|
||||
:raises: :class:`~odoo.addons.iap.models.iap.BadAuthError`
|
||||
|
||||
.. rst-class:: doc-aside
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
r2 = requests.post(ODOO + '/iap/1/cancel', json={
|
||||
'jsonrpc': '2.0',
|
||||
'id': None,
|
||||
'method': 'call',
|
||||
'params': {
|
||||
'token': tx,
|
||||
'key': SERVICE_KEY,
|
||||
}
|
||||
}).json()
|
||||
if 'error' in r:
|
||||
# handle cancel error
|
||||
# otherwise transaction is cancelled
|
||||
|
||||
Odoo Helpers
|
||||
============
|
||||
|
||||
For convenience, if you are implementing your service using Odoo the ``iap``
|
||||
module provides a few helpers to make IAP flow even simpler:
|
||||
|
||||
Charging
|
||||
--------
|
||||
|
||||
.. class:: odoo.addons.iap.models.iap.charge(env, key, account_token, credit[, description])
|
||||
|
||||
A *context manager* for authorizing and automatically capturing or
|
||||
cancelling transactions for use in the backend/proxy.
|
||||
|
||||
Works much like e.g. a cursor context manager:
|
||||
|
||||
* immediately authorizes a transaction with the specified parameters
|
||||
* executes the ``with`` body
|
||||
* if the body executes in full without error, captures the transaction
|
||||
* otherwise cancels it
|
||||
|
||||
:param odoo.api.Environment env: used to retrieve the ``iap.endpoint``
|
||||
configuration key
|
||||
:param ServiceKey key:
|
||||
:param UserToken token:
|
||||
:param int credit:
|
||||
:param str description:
|
||||
|
||||
.. rst-class:: doc-aside
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@route('/deathstar/superlaser', type='json')
|
||||
def superlaser(self, user_account,
|
||||
coordinates, target,
|
||||
factor=1.0):
|
||||
"""
|
||||
:param factor: superlaser power factor,
|
||||
0.0 is none, 1.0 is full power
|
||||
"""
|
||||
credits = int(MAXIMUM_POWER * factor)
|
||||
with charge(request.env, SERVICE_KEY, user_account, credits):
|
||||
# TODO: allow other targets
|
||||
self.env['systems.planets'].search([
|
||||
('grid', '=', 'M-10'),
|
||||
('name', '=', 'Alderaan'),
|
||||
]).unlink()
|
||||
|
||||
|
||||
.. _JSON-RPC2: http://www.jsonrpc.org/specification
|
||||
@@ -4,9 +4,9 @@
|
||||
|
||||
:code-column:
|
||||
|
||||
===============
|
||||
Web Service API
|
||||
===============
|
||||
===========================
|
||||
Odoo Remote Procedure Calls
|
||||
===========================
|
||||
|
||||
Odoo is usually extended internally via modules, but many of its features and
|
||||
all of its data are also available from the outside for external analysis or
|
||||
@@ -6,9 +6,9 @@
|
||||
|
||||
.. _reference/upgrade-api:
|
||||
|
||||
===========
|
||||
Upgrade API
|
||||
===========
|
||||
================
|
||||
Database Upgrade
|
||||
================
|
||||
|
||||
Introduction
|
||||
~~~~~~~~~~~~
|
||||
Reference in New Issue
Block a user