:banner: banners/iap.jpg :types: api .. _webservices/iap: .. using sphinx-patchqueue: * the "queue" directive selects a *series* file which lists the patches in the patch queue, in order of application (from top to bottom). The corresponding patch files should be in the same directory. * the "patch" directive steps to the next patch in the queue, applies it and reifies its content (depending on the extension's configuration, by default it shows the changed files post-diff application, slicing to only display sections affecte by the file) .. while it's technically possible to apply and update patches by hand, it's finnicky work and easy to break. .. the easiest way is to install quilt (http://savannah.nongnu.org/projects/quilt), go to the directory where you want to reify the addon, then create a "patches" symlink to the patches directory (the iap/ folder next to this file) or set QUILT_PATCHES to that folder. .. at that point you have a "primed" queue with no patch applied, and you can move within the queue with "quilt push" and "quilt pop". * "quilt new" creates a new empty patch at the top of the stack * "quilt add" tells quilt to start tracking the file, quilt add *works per patch*, it must be called *every time you want to alter a file within a patch*: quilt is not a full VCS (since it's intended to sit on top of an existing source) and does not do permanent tracking of files * "quilt edit" is a shorthand to "quilt add" then open the file in your editor, I suggest you use that rather than open the edited module normally, it avoids forgetting to "quilt add" before doing your modifications (at which point your modifications are untracked, invisible and depending on your editor may be a PITA to revert & redo) * "quilt refresh" updates the current patch to include pending changes .. see "man quilt" for the rest of the subcommands. FWIW I could not get "quilt setup" to do anything useful. ================ In-App Purchases ================ In-App Purchase (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 a client and an Odoo App Developer: * users purchase service tokens from Odoo * service providers draw tokens from the user's Odoo account when service is requested .. 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. Overview ======== .. figure:: images/players.png :align: center The Players * The Service Provider is (probably) you the reader, you will be providing value to the client in the form of a service paid per-use * The Client installed your Odoo App, and from there will request services * Odoo brokers crediting, the Client adds credit to their account, and you can draw credits from there to provide services * The External Service is an optional player: *you* can either provide a service directly, or you can delegate the actual service acting as a bridge/translator between an Odoo system and the actual service .. figure:: images/credits.jpg :align: center The Credits Every service provided through the In-App platform can be used by the clients with tokens or *credits*. The monetary value of a credit depends on the service and is decided by the provider. This could be: * for an sms service: 1 credit = 1 sms, * for an add service: 1 credit = 1 add, * for a postage service: 1 credit = 1 post stamp. A credit can also simply be associated with a fixed amount of money to palliate the variations of price (e.g. the prices of sms and stamps may vary following the countries). The value of the credits is fixed with the help of prepaid credit packs that the clients can buy on https://iap.odoo.com (see :ref:`Packages `). .. note:: in the following explanations we will ignore the External Service, they're just a detail of the service you provide .. figure:: images/normal.png :align: center A "normal" service flow If everything goes well, the normal flow is: 1. the Client requests a service of some sort 2. the Service Provider asks Odoo if there are enough credits for the service in the Client's account, and creates a transaction over that amount 3. the Service Provider provides the service (either on their own or calling to External Services) 4. the Service Provider goes back to Odoo to capture (if the service could be provided) or cancel (if the service could not be provided) the transaction created at step 2 5. finally the Service Provider notifies the Client that the service has been rendered, possibly (depending on the service) displaying or storing its results in the client's system .. figure:: images/no-credit.png :align: center Insufficient Credits If the Client's account lacks credits for the service, however 1. the Client requests a service as previously 2. the Service Provider asks Odoo if there are enough credits on the Client's account and gets a negative reply 3. this is signaled back to the Client 4. who is redirected to their Odoo account to credit it and re-try Building your service ===================== For this example, the service we will provide is ~~mining dogecoins~~ burning 10 seconds of CPU for a credit. For your own services, you could for example: * provide an online service yourself (e.g. convert quotations to faxes for business in Japan) * provide an *offline* service yourself (e.g. provide accountancy service) * act as intermediary to an other service provider (e.g. bridge to an MMS gateway) Register the service on Odoo ---------------------------- .. queue:: iap_service/series .. todo:: complete this part with screenshots The first step is to register your service on the IAP endpoint (production and/or test) before you can actually query user accounts. To create a service, go to your *Portal Account* on the IAP endpoint (https://iap.odoo.com for production, https://iap-sandbox.odoo.com for testing, the endpoints are *independent* and *not synchronized*). .. note:: On production, there is a manual validation step before the service can be used to manage real transactions. This step is automatically passed when on sandbox to ease the tests. Log in then go to :menuselection:`My Account --> Your In-App Services`, click Create and provide the name of your service. The now created service has *two* important fields: * :samp:`name` - :class:`ServiceName`: this will identify your service in the client's :ref:`app ` communicates directly with IAP. * :samp:`key` - :class:`ServiceKey`: the developer key that identifies you in IAP (see :ref:`your service `) and allows to draw credits from the client's account. .. warning:: The :class:`ServiceName` is unique and should usually match the name of your Odoo App. .. danger:: Your :class:`ServiceKey` *is a secret*, leaking your service key allows other application developers to draw credits bought for your service(s). .. image:: images/service_select.png :align: center .. image:: images/service_create.png :align: center .. image:: images/service_packs.png :align: center You can then create *credit packs* which clients can purchase in order to use your service. .. _iap-packages: Packages -------- The credit packages are essentially a product with 4 characteristics. * Name: the name of the package, * Description: details on the package that will appear on the shop page as well as the invoice, * Credits: the amount of credits the client is entitled to when buying the package, * Price: the price in *EUROS* for the time being (USD support is planned). .. image:: images/package.png :align: center .. note:: Depending on the strategy, the price per credit can vary from one package to another. .. _iap-odoo-app: Odoo App -------- .. queue:: iap/series .. todo:: does this actually require apps? The second step is to develop an `Odoo App`_ which clients can install in their Odoo instance and through which they can *request* services you will provide. Our app will just add a button to the Partners form which lets a user request burning some CPU time on the server. First, we'll create an *odoo module* depending on ``iap``. IAP is a standard V11 module and the dependency ensures a local account is properly set up and we will have access to some necessary views and useful helpers .. patch:: Second, the "local" side of the integration, here we will only be adding an action button to the partners view, but you can of course provide significant local value via your application and additional parts via a remote service. .. patch:: .. image:: images/button.png :align: center We can now implement the action method/callback. This will *call our own server*. There are no requirements when it comes to the server or the communication protocol between the app and our server, but ``iap`` provides a :func:`~odoo.addons.iap.jsonrpc` helper to call a JSON-RPC2_ endpoint on an other Odoo instance and transparently re-raise relevant Odoo exceptions (:class:`~odoo.addons.iap.models.iap.InsufficientCreditError`, :class:`odoo.exceptions.AccessError` and :class:`odoo.exceptions.UserError`). In that call, we will need to provide: * any relevant client parameter (none here) * the :class:`token ` of the current client, this is provided by the ``iap.account`` model's ``account_token`` field. You can retrieve the account for your service by calling :samp:`env['iap.account'].get({service_name})` where :class:`service_name ` is the name of the service registered on IAP endpoint. .. patch:: .. note:: ``iap`` automatically handles :class:`~odoo.addons.iap.models.iap.InsufficientCreditError` coming from the action and prompts the user to add credits to their account. :func:`~odoo.addons.iap.jsonrpc` takes care of re-raising :class:`~odoo.addons.iap.models.iap.InsufficientCreditError` for you. .. danger:: If you are not using :func:`~odoo.addons.iap.jsonrpc` you *must* be careful to re-raise :class:`~odoo.addons.iap.models.iap.InsufficientCreditError` in your handler otherwise the user will not be prompted to credit their account, and the next call will fail the same way. .. _iap-service: Service ------- .. queue:: iap_service/series Though that is not *required*, since ``iap`` provides both a client helper for JSON-RPC2_ calls (:func:`~odoo.addons.iap.jsonrpc`) and a service helper for transactions (:class:`~odoo.addons.iap.models.iap.charge`) we will also be implementing the service side as an Odoo module: .. patch:: Since the query from the client comes as JSON-RPC2_ we will need the corresponding controller which can call :class:`~odoo.addons.iap.models.iap.charge` and perform the service within: .. patch:: .. todo:: for the actual IAP will the "portal" page be on odoo.com or iap.odoo.com? .. todo:: "My Account" > "Your InApp Services"? The :class:`~odoo.addons.iap.models.iap.charge` helper will: .. note:: Since the 15th of January 2018, a new functionality that allows one to capture a different amount than autorized has been added. See :ref:`Charging ` 1. authorize (create) a transaction with the specified number of credits, if the account does not have enough credits it will raise the relevant error 2. execute the body of the ``with`` statement 3. (NEW) if the body of the ``with`` executes succesfully, update the price of the transaction if needed 4. capture (confirm) the transaction 5. otherwise if an error is raised from the body of the ``with`` cancel the transaction (and release the hold on the credits) .. danger:: By default, :class:`~odoo.addons.iap.models.iap.charge` contacts the *production* IAP endpoint, https://iap.odoo.com. While developing and testing your service you may want to point it towards the *development* IAP endpoint https://iap-sandbox.odoo.com. To do so, set the ``iap.endpoint`` config parameter in your service Odoo: in debug/developer mode, :menuselection:`Setting --> Technical --> Parameters --> System Parameters`, just define an entry for the key ``iap.endpoint`` if none already exists). The :class:`~odoo.addons.iap.models.iap.charge` helper has two additional optional parameters we can use to make things clearer to the end-user: ``description`` is a message which will be associated with the transaction and will be displayed in the user's dashboard, it is useful to remind the user why the charge exists ``credit_template`` is the name of a :ref:`reference/qweb` template which will be rendered and shown to the user if their account has less credit available than the service provider is requesting, its purpose is to tell your users why they should be interested in your IAP offers .. patch:: .. TODO:: how do you test your service? JSON-RPC2_ Transaction API ========================== .. image:: images/flow.png :align: center * 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. 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.exceptions.AccessError` if the service token is invalid :raises: :class:`~odoo.addons.iap.models.iap.InsufficientCreditError` if the account does :raises: ``TypeError`` if the ``credit`` value is not an integer .. 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.exceptions.AccessError` .. 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.exceptions.AccessError` .. 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 Types ----- Exceptions aside, these are *abstract types* used for clarity, you should not care how they are implemented .. class:: ServiceName String identifying your service on https://iap.odoo.com (production) as well as the account related to your service in the client's database. .. 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.InsufficientCreditError 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.exceptions.AccessError Raised by: * any operation to which a service token is required, if the service token is invalid. * any failure in an inter-server call. (typically, in :func:`~odoo.addons.iap.jsonrpc`) .. exception:: odoo.exceptions.UserError Raised by any unexpeted behaviour at the discretion of the App developer (*you*). 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: .. _iap-charging: Charging -------- .. note:: A new functionality was introduced to capture a different amount of credits than reserved. As this patch was added on the 15th of January 2018, you will need to upgrade your ``iap`` module in order to use it. The specifics of the new functionality are highlighted in the code. .. class:: odoo.addons.iap.models.iap.charge(env, key, account_token, credit[, description, credit_template]) 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: :param Qweb template credit_template: .. code-block:: python :emphasize-lines: 10,13,14,15 @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) as transaction: # TODO: allow other targets transaction.credit = credits * 0.85 # Sales ongoing one the energy price, # only 85% of the price will be charged/captured. self.env['systems.planets'].search([ ('grid', '=', 'M-10'), ('name', '=', 'Alderaan'), ]).unlink() .. _JSON-RPC2: http://www.jsonrpc.org/specification .. _Odoo App: https://www.odoo.com/apps