summaryrefslogtreecommitdiff
path: root/design-documents/007-payment.rst
blob: 4a0998b0a812505cd4bfd26cb26562d06ca59772 (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
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 content, there should be a reasonable technical barrier to
  sharing the content 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, ugly error messages.

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 the *order-ID* (or null) and *resource name* from the *resource-URL*.
2. Extract the *session-ID* (or null) from the request's signed cookie
3. If *session-ID* and *order-ID* is non-null and the storefront's
   *session-payment-cache* contains the tuple (*order-ID*, *resource-name*, *session-ID*),
   return to the client the content for *resource name*.  **Terminate.**
4. If the *session-ID* is null, assign a fresh session ID and set it in a cookie to be sent with the response
5. If *order-ID* is null, create a new order for *resource-name* by doing a ``POST /private/orders`` to
   the merchant backend.  Store the new order ID as *order-ID*.
6. 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*.
7. If the *order-status* is paid and *refund-amount* is non-zero, 
   return to the client the refund info page for *resource name*.  **Terminate.**
8. If the *order-status* is paid, store the tuple (*order-ID*, *resource-name*, *session-ID*) in *session-payment-cache*
   and return to the client the content for *resource name*.  **Terminate.**
9. Otherwise, the *order-status* is unpaid.  Redirect the client to *client-order-status-URL*. **Terminate.**

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}``:

1. Let *session-ID* be the session ID of the request (note: **not** the last paid session ID)
2. If *order-ID* does not identify an existing order, return an error.  **Terminate**.
3. If *order-ID* identifies an order that is *unclaimed* and has claim token *claim-token*, return the URL

   ::

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

   **Terminate.**
4. If *order-ID* identifies an order that is either *claimed* or *paid* and has contract terms hash *contract-hash*, return the URL

   ::

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

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}&claim_token={claim-token}&h_contract={contract-hash}``:

1. If *order-ID* does not identify an existing order, render an error page.  **Terminate.**
2. If *order-ID* identifies a paid order *ord*, run these steps:

   1. If the *contract-hash* request parameter does not match the contract terms hash of *ord*,
      return a 403 Forbidden response. **Terminate.**
   2. If *ord* has refunds that are not picked up, prompt the URI

      ::

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

      Redirect to the *fulfillment-URL* of *ord* once the refund is picked up and
      payment has been proven under *session-ID*.

      **Terminate.**

   3. Prompt the URI

      ::

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

      Redirect to the *fulfillment-URL* of *ord* once
      payment has been proven under *session-ID*.

      **Terminate.**


3. If *order-ID* identifies an unpaid order *ord*, run these steps:
  
   1. If the the *claim-token* request parameter does not matches the claim token of
      *ord*,  return a 403 Forbidden response. **Terminate**.
   2. Prompt the URI

      ::

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

      Redirect to the *fulfillment-URL* of *ord* once
      payment has been proven under *session-ID*.

      If there is a non-null *already-paid-order-ID* for *session-ID*, redirect
      to the *fulfillment-URL* of *already-paid-order-ID*

      **Terminate.**


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

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.


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

Bookmarks of Lost Purchases
^^^^^^^^^^^^^^^^^^^^^^^^^^^

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 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

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.