This design document describes how the payment flow works in the browser, and how features like session IDs, re-purchase detection and refunds interact.
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.
When resource-URL is requested, the storefront runs the following steps:
order_id
cookie. This cookie may optionally be validated.POST /private/orders
to the merchant backend. Set both in the cookie to be sent with the response.GET /private/orders/{order-ID}?session_id={session-ID}
.
This results in the order-status, refund-amount and the client-order-status-URL.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.
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}
:
Let session-ID be the session ID of the request or null if not given (note: not the last paid session ID)
If order-ID does not identify an existing order, return a 404 Not Found response. Terminate.
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}
(if no claim-token was generated, omit that parameter from the above URI). Terminate.
Here order-ID identifies an order that is claimed. If the order is unpaid, wait until timeout or payment.
If the order remains unpaid or was paid for a different session-ID, obtain the contract terms hash contract-hash and return the URL
{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.
Here order-ID must now identify an order that is paid or refunded. Obtain the contract terms hash contract-hash and return the URL
{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.
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}
:
If order-ID does not identify an existing order, render a 404 Not Found response. Terminate.
If order-ID identifies a paid order (where the session-ID matches the one from the payment), run these steps:
If the contract-hash request parameter does not match the contract terms hash of the order, return a 403 Forbidden response. Terminate.
If the order has granted refunds that have not been obtained by the wallet yet, prompt the URI
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?
Here the order has been paid and possibly refunded. Redirect to the fulfillment-URL of the order. Terminate.
If order-ID identifies an unclaimed order, run these steps:
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.
Prompt the URI
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.
If order-ID identifies an claimed and unpaid order, run these steps:
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.)
If the contract-hash request parameter does not match the contract hash of the order, return a 403 Forbidden response. Terminate.
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.
Prompt the URI
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.
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:
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 ID ``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_id=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_id=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_id=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_id=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
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.
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.