fix: PayPal + Adyen complete paths flip offer.state ACCEPTED → PAID

A negotiated cart paid through Stripe's user_cart_complete_checkout
already fired _finalize_auction_offer_state(cart, request) — which
flips linked offers to PAID and linked auctions to SETTLED. But the
PayPal complete-checkout (cart.py:paypal_complete_checkout) and the
Adyen complete-checkout (cart.py:adyen_complete_checkout) paths
never called it.

Symptom: buyer pays a negotiated offer through PayPal or Adyen,
invoice is written, sale email fires — but offer.state stays at
ACCEPTED. The offer detail page keeps rendering the Pay $X / Cancel
buttons because `{% if is_accepted and not is_paid %}` is still True.
Even worse, the buyer could click Cancel after the payment had
processed (the cancel endpoint guarded on `state != ACCEPTED`, which
also still permitted it).

Fix: add the _finalize_auction_offer_state(cart, request) call to
both paypal_complete_checkout and adyen_complete_checkout, right
when successful_invoices is populated — before the cart line items
get drained. Each takes (cart, request) which both sites have in
scope. With the offer now in PAID:
- is_paid=True flips the offer.j2 gate, hiding Pay/Cancel.
- /o/{id}/cancel raises OfferRejected ("only accepted offers can
  be cancelled by buyer").
- _user_party / shop_offers / buyer dashboards all read the right
  terminal state.

Note: this only patches the cart.py callsites. Webhook fallbacks
(views/webhooks.py) and the crypto-watcher finalize path
(lib/crypto_watcher/__init__.py) still don't call mark_paid because
they only have `invoice`, not `cart`. Follow-up will route those
through a `_finalize_invoice_negotiation(invoice, session)` helper
once that's needed; the cart.py paths cover the common case.
This commit is contained in:
russell@unturf.com 2026-05-14 17:36:44 -04:00
parent 9fc280fd7b
commit 3c8dbca7c6
No known key found for this signature in database

View file

@ -1069,6 +1069,13 @@ def paypal_complete_checkout(request):
continue
if successful_invoices:
# MPS-20 + MPS-21: flip linked auction/offer state BEFORE
# the cart_offers / cart_auctions get touched downstream.
# Without this, a PayPal- or Adyen-paid negotiated cart
# leaves the offer stuck in ACCEPTED and the UI keeps
# showing Pay/Cancel even though the invoice is settled.
_finalize_auction_offer_state(cart, request)
for invoice in successful_invoices:
for line_item in invoice.line_items:
cart.remove_product(line_item.product, line_item.quantity)
@ -1304,6 +1311,13 @@ def adyen_complete_checkout(request):
continue
if successful_invoices:
# MPS-20 + MPS-21: flip linked auction/offer state BEFORE
# the cart_offers / cart_auctions get touched downstream.
# Without this, a PayPal- or Adyen-paid negotiated cart
# leaves the offer stuck in ACCEPTED and the UI keeps
# showing Pay/Cancel even though the invoice is settled.
_finalize_auction_offer_state(cart, request)
for invoice in successful_invoices:
for line_item in invoice.line_items:
cart.remove_product(line_item.product, line_item.quantity)