Close the holes an independent review found in the payment handoff #6

Merged
HoroTW merged 2 commits from hakalya/picnic-payment-handoff into main 2026-10-12 16:29:58 +00:00 AGit
Collaborator
  • No payment is initiated when the attempt cannot be written to the state
    file; the record is read anew for every operation, so a second server
    process sees an unsettled attempt and no longer overwrites it. A record
    with a malformed entry counts as unreadable and is left alone.
  • A purchase that reaches its payment voids every other outstanding token.
  • A fallback to the next payment method is bound to the purchase that failed
    (mode, delivery, slot, total, at most a day old), and a server error at
    the payment initiation does not permit one.
  • An unanswered cancel request is "uncertain", not "refused": the approval
    page is withdrawn, and the attempt can be given up after the expiry once
    every cancel request was sent. Without an expiry from Picnic an attempt
    expires a day after it began.
  • The approval address is returned as it was checked (normalised), an action
    of an unexpected shape is not confirmed, and a FINISHED read after the
    completion is not hidden by the completion's own answer.
  • Reads through the client library are bounded by the request timeout, so a
    hung read cannot hold the purchase lock.

Co-Authored-By: Paperclip noreply@paperclip.ing
Co-Authored-By: Claude Opus 5.5 noreply@anthropic.com

- No payment is initiated when the attempt cannot be written to the state file; the record is read anew for every operation, so a second server process sees an unsettled attempt and no longer overwrites it. A record with a malformed entry counts as unreadable and is left alone. - A purchase that reaches its payment voids every other outstanding token. - A fallback to the next payment method is bound to the purchase that failed (mode, delivery, slot, total, at most a day old), and a server error at the payment initiation does not permit one. - An unanswered cancel request is "uncertain", not "refused": the approval page is withdrawn, and the attempt can be given up after the expiry once every cancel request was sent. Without an expiry from Picnic an attempt expires a day after it began. - The approval address is returned as it was checked (normalised), an action of an unexpected shape is not confirmed, and a FINISHED read after the completion is not hidden by the completion's own answer. - Reads through the client library are bounded by the request timeout, so a hung read cannot hold the purchase lock. Co-Authored-By: Paperclip <noreply@paperclip.ing> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A purchase whose payment initiation answered with an action was cancelled at
once and its approval address dropped, so a PayPal payment could never be
finished. The checkout now stays pending under an attempt id: the approval
page is returned when Picnic supplies one (https, on an allowed host), and a
step without a browser page is reported as such, with no link made up.

picnic_checkout_payment_status reads what Picnic shows for an attempt.
picnic_checkout_continue_payment finishes the same checkout: on Picnic's own
FINISHED status, or by sending the provider's return deeplink to
complete-payment once, as the app does; a claim of approval sends nothing.
picnic_checkout_cancel_payment gives an attempt up and counts it only when
Picnic shows the checkout as ended. Attempts are recorded on disk before the
payment request leaves, without the approval address, and an unsettled one
blocks every further purchase, also after a restart.

The payment method is chosen by a fixed policy (DIRECT_DEBIT, PAYPAL, CARD)
instead of Picnic's preferred option. Picnic's request has no payment field,
so a purchase makes the approved option the preferred one first, the way the
app does. The preview lists every method with its status, the token binds the
selected one, and the next method is only proposed after an attempt that
definitely failed (fallback_from_attempt_id).

Behaviour follows the Picnic Android app 1.249.0; see docs/CHECKOUT.md for
provenance and for what is still unverified against a live account.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- No payment is initiated when the attempt cannot be written to the state
  file; the record is read anew for every operation, so a second server
  process sees an unsettled attempt and no longer overwrites it. A record
  with a malformed entry counts as unreadable and is left alone.
- A purchase that reaches its payment voids every other outstanding token.
- A fallback to the next payment method is bound to the purchase that failed
  (mode, delivery, slot, total, at most a day old), and a server error at
  the payment initiation does not permit one.
- An unanswered cancel request is "uncertain", not "refused": the approval
  page is withdrawn, and the attempt can be given up after the expiry once
  every cancel request was sent. Without an expiry from Picnic an attempt
  expires a day after it began.
- The approval address is returned as it was checked (normalised), an action
  of an unexpected shape is not confirmed, and a FINISHED read after the
  completion is not hidden by the completion's own answer.
- Reads through the client library are bounded by the request timeout, so a
  hung read cannot hold the purchase lock.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HoroTW merged commit f7ad56b011 into main 2026-10-12 16:29:58 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
HoroTW/mcp-picnic!6
No description provided.