From 6b22476e20fb7eade959194bfabd94d353e2d5ff Mon Sep 17 00:00:00 2001
From: Antony Lesuisse <al@openerp.com>
Date: Wed, 4 Oct 2017 20:05:05 +0200
Subject: [PATCH] [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).
---
 addons/iap/__init__.py                        |   4 +
 addons/iap/__manifest__.py                    |  23 ++
 addons/iap/models/__init__.py                 |   4 +
 addons/iap/models/iap.py                      | 157 ++++++++++++
 addons/iap/security/ir.model.access.csv       |   3 +
 addons/iap/security/ir_rule.xml               |  13 +
 addons/iap/static/src/js/crash_manager.js     |  49 ++++
 addons/iap/static/src/js/dashboard.js         |  29 +++
 addons/iap/static/src/js/iap_credit.js        |  28 +++
 addons/iap/static/src/xml/iap_templates.xml   |  26 ++
 addons/iap/views/assets.xml                   |  10 +
 addons/iap/views/iap_views.xml                |  53 ++++
 .../static/src/js/dashboard.js                |   1 +
 doc/_extensions/odoo_ext/static/layout.less   |   4 +-
 doc/_extensions/odoo_ext/static/style.css     |  24 +-
 doc/_extensions/odoo_ext/translator.py        |   5 +
 doc/index.rst                                 |   2 +-
 doc/reference.rst                             |   1 -
 doc/webservices.rst                           |  13 +
 doc/webservices/flow.png                      | Bin 0 -> 14726 bytes
 doc/webservices/iap.rst                       | 235 ++++++++++++++++++
 .../odoo.rst}                                 |   6 +-
 .../upgrade.rst}                              |   6 +-
 23 files changed, 674 insertions(+), 22 deletions(-)
 create mode 100644 addons/iap/__init__.py
 create mode 100644 addons/iap/__manifest__.py
 create mode 100644 addons/iap/models/__init__.py
 create mode 100644 addons/iap/models/iap.py
 create mode 100644 addons/iap/security/ir.model.access.csv
 create mode 100644 addons/iap/security/ir_rule.xml
 create mode 100644 addons/iap/static/src/js/crash_manager.js
 create mode 100644 addons/iap/static/src/js/dashboard.js
 create mode 100644 addons/iap/static/src/js/iap_credit.js
 create mode 100644 addons/iap/static/src/xml/iap_templates.xml
 create mode 100644 addons/iap/views/assets.xml
 create mode 100644 addons/iap/views/iap_views.xml
 create mode 100644 doc/webservices.rst
 create mode 100644 doc/webservices/flow.png
 create mode 100644 doc/webservices/iap.rst
 rename doc/{api_integration.rst => webservices/odoo.rst} (99%)
 rename doc/{reference/upgrade_api.rst => webservices/upgrade.rst} (99%)

diff --git a/addons/iap/__init__.py b/addons/iap/__init__.py
new file mode 100644
index 000000000000..dc5e6b693d19
--- /dev/null
+++ b/addons/iap/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import models
diff --git a/addons/iap/__manifest__.py b/addons/iap/__manifest__.py
new file mode 100644
index 000000000000..35124df76ebc
--- /dev/null
+++ b/addons/iap/__manifest__.py
@@ -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,
+}
diff --git a/addons/iap/models/__init__.py b/addons/iap/models/__init__.py
new file mode 100644
index 000000000000..4af61c83a0d7
--- /dev/null
+++ b/addons/iap/models/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+from . import iap
diff --git a/addons/iap/models/iap.py b/addons/iap/models/iap.py
new file mode 100644
index 000000000000..87394fd053b1
--- /dev/null
+++ b/addons/iap/models/iap.py
@@ -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))
diff --git a/addons/iap/security/ir.model.access.csv b/addons/iap/security/ir.model.access.csv
new file mode 100644
index 000000000000..0eeb34c40cd5
--- /dev/null
+++ b/addons/iap/security/ir.model.access.csv
@@ -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
\ No newline at end of file
diff --git a/addons/iap/security/ir_rule.xml b/addons/iap/security/ir_rule.xml
new file mode 100644
index 000000000000..2935ce2b7461
--- /dev/null
+++ b/addons/iap/security/ir_rule.xml
@@ -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>
\ No newline at end of file
diff --git a/addons/iap/static/src/js/crash_manager.js b/addons/iap/static/src/js/crash_manager.js
new file mode 100644
index 000000000000..3c8d330013ea
--- /dev/null
+++ b/addons/iap/static/src/js/crash_manager.js
@@ -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);
+        }
+    },
+});
+
+});
diff --git a/addons/iap/static/src/js/dashboard.js b/addons/iap/static/src/js/dashboard.js
new file mode 100644
index 000000000000..b3490da2eda2
--- /dev/null
+++ b/addons/iap/static/src/js/dashboard.js
@@ -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);
+        });
+    },
+});
+
+});
diff --git a/addons/iap/static/src/js/iap_credit.js b/addons/iap/static/src/js/iap_credit.js
new file mode 100644
index 000000000000..37cddd72aa8c
--- /dev/null
+++ b/addons/iap/static/src/js/iap_credit.js
@@ -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);
+});
diff --git a/addons/iap/static/src/xml/iap_templates.xml b/addons/iap/static/src/xml/iap_templates.xml
new file mode 100644
index 000000000000..9b52a4ec445b
--- /dev/null
+++ b/addons/iap/static/src/xml/iap_templates.xml
@@ -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>
diff --git a/addons/iap/views/assets.xml b/addons/iap/views/assets.xml
new file mode 100644
index 000000000000..0e7294ca5867
--- /dev/null
+++ b/addons/iap/views/assets.xml
@@ -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>
diff --git a/addons/iap/views/iap_views.xml b/addons/iap/views/iap_views.xml
new file mode 100644
index 000000000000..ebd64fedee8f
--- /dev/null
+++ b/addons/iap/views/iap_views.xml
@@ -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>
diff --git a/addons/web_settings_dashboard/static/src/js/dashboard.js b/addons/web_settings_dashboard/static/src/js/dashboard.js
index 060f7dca6127..7848b4934fd1 100644
--- a/addons/web_settings_dashboard/static/src/js/dashboard.js
+++ b/addons/web_settings_dashboard/static/src/js/dashboard.js
@@ -345,6 +345,7 @@ core.action_registry.add('web_settings_dashboard.main', Dashboard);
 
 return {
     Dashboard: Dashboard,
+    DashboardApps: DashboardApps,
     DashboardInvitations: DashboardInvitations,
     DashboardPlanner: DashboardPlanner,
     DashboardShare: DashboardShare,
diff --git a/doc/_extensions/odoo_ext/static/layout.less b/doc/_extensions/odoo_ext/static/layout.less
index c350defa2a1b..4c8a5c643869 100644
--- a/doc/_extensions/odoo_ext/static/layout.less
+++ b/doc/_extensions/odoo_ext/static/layout.less
@@ -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;
diff --git a/doc/_extensions/odoo_ext/static/style.css b/doc/_extensions/odoo_ext/static/style.css
index ead78f9101dd..1442d4bdd675 100644
--- a/doc/_extensions/odoo_ext/static/style.css
+++ b/doc/_extensions/odoo_ext/static/style.css
@@ -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;
   }
 }
diff --git a/doc/_extensions/odoo_ext/translator.py b/doc/_extensions/odoo_ext/translator.py
index 443458f33c77..c2349bca315b 100644
--- a/doc/_extensions/odoo_ext/translator.py
+++ b/doc/_extensions/odoo_ext/translator.py
@@ -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,
diff --git a/doc/index.rst b/doc/index.rst
index 21231b7ce3fa..8035da789f9e 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -14,7 +14,7 @@ Index
     :maxdepth: 2
 
     tutorials
-    api_integration
+    webservices
     setup
     reference
 
diff --git a/doc/reference.rst b/doc/reference.rst
index d83ef3bb4768..1b41f4cdbd24 100644
--- a/doc/reference.rst
+++ b/doc/reference.rst
@@ -21,5 +21,4 @@ Reference
     reference/reports
     reference/mixins
     reference/guidelines
-    reference/upgrade_api
     reference/mobile
diff --git a/doc/webservices.rst b/doc/webservices.rst
new file mode 100644
index 000000000000..3dad86448ba7
--- /dev/null
+++ b/doc/webservices.rst
@@ -0,0 +1,13 @@
+:banner: banners/web_service_api.jpg
+:types: api
+
+============
+Web Services
+============
+
+.. toctree::
+    :titlesonly:
+
+    webservices/odoo
+    webservices/iap
+    webservices/upgrade
diff --git a/doc/webservices/flow.png b/doc/webservices/flow.png
new file mode 100644
index 0000000000000000000000000000000000000000..df4bf7cfbbbc24ecda679650eb5c13a467c93659
GIT binary patch
literal 14726
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
J{?P9Ee*t>7i<AHW

literal 0
HcmV?d00001

diff --git a/doc/webservices/iap.rst b/doc/webservices/iap.rst
new file mode 100644
index 000000000000..18af76900fb8
--- /dev/null
+++ b/doc/webservices/iap.rst
@@ -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
diff --git a/doc/api_integration.rst b/doc/webservices/odoo.rst
similarity index 99%
rename from doc/api_integration.rst
rename to doc/webservices/odoo.rst
index 612e91ed8542..f68862626a75 100644
--- a/doc/api_integration.rst
+++ b/doc/webservices/odoo.rst
@@ -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
diff --git a/doc/reference/upgrade_api.rst b/doc/webservices/upgrade.rst
similarity index 99%
rename from doc/reference/upgrade_api.rst
rename to doc/webservices/upgrade.rst
index 04edf919e83b..fa168935b238 100644
--- a/doc/reference/upgrade_api.rst
+++ b/doc/webservices/upgrade.rst
@@ -6,9 +6,9 @@
 
 .. _reference/upgrade-api:
 
-===========
-Upgrade API
-===========
+================
+Database Upgrade
+================
 
 Introduction
 ~~~~~~~~~~~~
-- 
GitLab