summaryrefslogtreecommitdiff
path: root/design-documents/007-payment.rst
blob: 432816a013408fa3be6934f26bfaaaab8da556d7 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
Design Doc 007: Specification of the Payment Flow
#################################################

Summary
=======

This design document describes how the payment flow works in the browser, and how
features like session IDs, re-purchase detection and refunds interact.

Requirements
============

* The payment flow must both support wallets that are integrated in the browser,
  as well as external wallets (mobile phone, command line)
* The initiator of the payment can be a Website or another channel,
  such as an e-mail or a messaging service.
* For paid digital works, there should be a reasonable technical barrier to
  sharing the information with unauthorized users
* A simple API should be offered to shops
* Sharing of links or re-visiting of bookmarks should result in well-defined
  behavior instead of random, unclear error messages.
* The payment flow must degrade gracefully when JavaScript is disabled.

Proposed Solution
=================



Session-bound payment flow for Web resources
--------------------------------------------

In this payment flow, the user initiates the payment by navigating to a
paywalled Web resource.  Let *resource-URL* be the URL of the paywalled resource.

Storefront
^^^^^^^^^^

When *resource-URL* is requested, the storefront runs the following steps:

1. Extract the *resource name* from the *resource-URL*.
2. Extract the *session-ID* (or null) from the request's validated cookie (for example, by using signed cookies).
3. Extract the *order-ID* (or null) from the request's ``order_id`` cookie.  This cookie may optionally be validated.

   ..
      is "invalid" equivalent to "null"?

4. If *session-ID* or *order-ID* is null, assign a fresh session ID and
   create a new order for *resource name* by doing a ``POST /private/orders``
   to the merchant backend. Set both in the cookie to be sent with the response.
5. Check the status of the payment for *order-ID* under *session-ID* by doing a ``GET /private/orders/{order-ID}?session_id={session-ID}``.
   This results in the *order-status*, *refund-amount* and the *client-order-status-URL*.
6. If the *order-status* is claimed, set *order-ID* to null and go back to step 4.
7. If the *order-status* is paid and *refund-amount* is non-zero,
   return to the client a page with an explanation that the payment has been refunded. **Terminate.**
8. If the client has not (fully) obtained the granted refunds yet, show a link to the public order page
   of the backend to allow the client to obtain the refund.  **Terminate.**
9. If the *order-status* is paid, return to the client the resource associated with *resource name*.  **Terminate.**
10. Otherwise, either the *order-status* is unpaid or the customer tried to access a paid resource after having deleted their cookies.  Redirect the client to *client-order-status-URL*. **Terminate.**
11.  If the wallet detects that the resource was paid before, it will resend the same payment again, and will get the item; if not, the wallet will create a new payment and send to the merchant.

.. note::

   Instead of making a request to the merchant backend on every request to *resource-URL*, the storefront
   may use a *session-page-cache* that stores (*session-ID*, *order-ID*, *resource-name*) tuples.
   When a refund is given, the corresponding tuple must be removed from the *session-page-cache*.

Backend Private Order Status
^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The merchant backend runs the following steps to generate the
*client-order-status-URL* when processing a request for ``GET
/private/orders/{order-ID}?session_id={session-ID}&timeout_ms={timeout}``:

1. Let *session-ID* be the session ID of the request or null if not given (note: **not** the last paid session ID)
2. If *order-ID* does not identify an existing order, return a 404 Not Found response.  **Terminate**.
3. If *order-ID* identifies an order that is *unclaimed* and has claim token *claim-token*, return the URL

   .. code-block:: none

     {backendBaseUrl}/orders/{order-ID}?token={claim-token}&session_id={session-ID}

   (if no claim-token was generated, omit that parameter from the above URI). **Terminate.**

4. Here *order-ID* identifies an order that is *claimed*.  If the order is *unpaid*, wait until timeout or payment.

5. If the order remains unpaid or was paid for a different *session-ID*, obtain the contract terms hash *contract-hash* and return the URL

   .. code-block:: none

     {backendBaseUrl}/orders/{order-ID}?h_contract={contract-hash}&session_id={session-ID}

   together with the status *unpaid*. (If *session-ID* is null, it does not
   matter for which session the contract was paid.) **Terminate.**

6. Here *order-ID* must now identify an order that is *paid* or *refunded*. Obtain the contract terms hash *contract-hash* and return the URL

   .. code-block:: none

     {backendBaseUrl}/orders/{order-ID}?h_contract={contract-hash}&session_id={session-ID}

   together with the status *paid* or *refunded* (and if applicable, with
   details about the applied refunds). **Terminate.**



Backend Client Order Status Page
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The merchant backend runs the following steps to generate the HTML page for
``GET /orders/{order-ID}?session_id={session-ID}&token={claim-token}&h_contract={contract-hash}``:

1. If *order-ID* does not identify an existing order, render a 404 Not Found response.  **Terminate.**
2. If *order-ID* identifies a paid order (where the *session-ID* matches the one from the payment), run these steps:

   1. If the *contract-hash* request parameter does not match the contract terms hash of the order,
      return a 403 Forbidden response. **Terminate.**

   2. If the order has granted refunds that have not been obtained by the wallet yet, prompt the URI

      .. code-block:: none

        taler{proto_suffix}://refund/{/merchant_prefix*}/{order-id}/{session-id}

      The generated Web site should long-poll until all refunds have been obtained,
      then redirect to the *fulfillment-URL* of the order once the refunds have been
      obtained.  **Terminate.**
      ----- FIXME: IIRC our long-polling API does only allow waiting for the granted refund amount, not for the *obtained* refund amount. => API change?

   3. Here the order has been paid and possibly refunded.
      Redirect to the *fulfillment-URL* of the order.
      **Terminate.**


3. If *order-ID* identifies an *unclaimed* order, run these steps:

   1. If the order is *unclaimed* and the *claim-token* request parameter does not
      match the claim token of the order, return a 403 Forbidden response. **Terminate**.

   2. Prompt the URI

      .. code-block:: none

        taler{proto_suffix}://pay/{/merchant_prefix*}/{order-id}/{session-ID}?c={claim-token}

      The generated Web site should long-poll to check for the payment happening.
      It should then redirect to the *fulfillment-URL* of the order once
      payment has been proven under *session-ID*, or possibly redirect to the
      *already-paid-order-ID*. Which of these happens depends on the (long-polled) JSON replies.
      **Terminate.**

4. If *order-ID* identifies an *claimed* and *unpaid* order, run these steps:

   1. If the *claim-token* request parameter is given and the *contract-hash* requesst parameter is
      not given, redirect to the fulfillment URL of the order. (**Note**: We do not check
      the claim token, as the merchant might have already deleted it when the order is paid,
      and the fulfillment URL is not considered to be secret/private.)

   2. If the *contract-hash* request parameter does not
      match the contract hash of the order, return a 403 Forbidden response. **Terminate**.

   3. If there is a non-null *already-paid-order-ID* for *session-ID* stored under the current order,
      redirect to the *fulfillment-URL* of *already-paid-order-ID*. **Terminate**.

   4. Prompt the URI

      .. code-block:: none

        taler{proto_suffix}://pay/{/merchant_prefix*}/{order-id}/{session-ID}

      The generated Web site should long-poll to check for the payment happening.
      It should then redirect to the *fulfillment-URL* of the order once
      payment has been proven under *session-ID*, or possibly redirect to the
      *already-paid-order-ID*. Which of these happens depends on the (long-polled) JSON replies.
      **Terminate.**

Examples
========

The examples use the prefix ``S:`` for the storefront, ``B:`` for the customer's browser
and ``W:`` for the wallet.

The following example uses a detached wallet:

.. code:: none

   B: [user nagivates to the book "Moby Dick" in the demo storefront]
   B: -> GET https://shop.demo.taler.net/books/moby-dick
      (content-type: application/html)

     S: [Assigns session sess01 to browser]
     S: -> POST https://merchant-backend.demo.taler.net/orders
     S: -> GET https://merchant-backend.demo.taler.net/orders/ord01?session_id-sess01

   B: <- HTTP 307, redirect to https://merchant-backend.demo.taler.net/orders/ord01?token=ct01&session=sess01

   B: -> GET https://merchant-backend.demo.taler.net/orders/ord01?token=ct01
      (content-type: application/html)
   B: <- HTTP status 402 Payment Required, QR code / link to
      taler://pay/shop.demo.taler.net/ord01/sess01?c=ct01

   B: [via JavaScript on page]
   B: -> GET https://merchant-backend.demo.taler.net/orders/ord01?token=ct01&session=sess01
      (content-type: application/json)
   B: <- HTTP status 402 Payment Required

   W: [user scans QR taler://pay code]
   W: POST https://shop.demo.taler.net/orders/ord01/claim

   B: [via JavaScript on page]
   B: -> GET https://merchant-backend.demo.taler.net/orders/ord01?token=ct01&session=sess01
      (content-type: application/json)
   B: <- HTTP status 402 Payment Required

   W: POST https://shop.demo.taler.net/orders/ord01/pay

   B: [via JavaScript on page]
   B: -> GET https://merchant-backend.demo.taler.net/orders/ord01?token=ct01&session=sess01
      (content-type: application/json)
   B: <- HTTP status 202 Accepted
   B: [redirects to fulfillment URL of ord01 baked into the JavaScript code]

   B: -> GET https://shop.demo.taler.net/books/moby-dick
         (content-type: application/html)
     S: -> GET https://merchant-backend.demo.taler.net/orders/ord01?session_id-sess01
     S: <- HTTP 200, order status "paid"
   B: <- HTTP 200, content of "moby-dick" is rendered


Discussion / Q&A
================

Notes
-----

* The *timeout_ms* argument is expected to be ignored when generating HTML.
  Long-polling simply makes no sense if a browser accesses the site directly.


Covered Scenarios
-----------------

* **Re-purchase detection**. Let's say a detached wallet has already successfully paid for a resource URL.
  A browser navigates to the resource URL.  The storefront will generate a new order and assign a session ID.
  Upon scanning the QR code, the wallet will detect that it already has puchased the resource (checked via the fulfillment URL).
  It will then prove the payment of the **old** order ID under the **new** session ID.


* **Bookmarks of Lost Purchases / Social Sharing of Fulfillment URLs**

  FIXME: explain how we covered this by moving order ID into session cookie!
  Let's say I bought some article a few months ago and I lost my wallet. I still have the augmented fulfillment URL
  for the article bookmarked.  When I re-visit the URL, I will be prompted via QR code, but I can *never* prove
  that I already paid, because I lost my wallet!

  In this case, it might make sense to include some "make new purchase" link on the client order status page.
  It's not clear if this is a common/important scenario though.

  But we might want to make clear on the client order status page that it's showing a QR code for something
  that was already paid.

  The same concern applies when sending the fulfillment URL of a paid paywalled Web resource to somebody else.



Problematic Scenarios
---------------------

The Back Button
^^^^^^^^^^^^^^^

The following steps lead to unintuitive navigation:
1. Purchase a paywalled URL for the first time via a detached wallet
2. Marvel at the fulfillment page
3. Press the back button (or go back to bookmarked page 1), possibly press reload if page was still cached).

This will display an error message, as the authentication via the claim token on the
``/orders/{order-ID}`` page is not valid anymore.

We could consider still allowing authentication with the claim token in this case.

Proposal: generate 410 Gone in case token is provided for claimed order. For now
in JSON, eventually possibly with a nice HTML page if respective content type is
provided.