fix: payment 502s + bump existing shops 168h→72h offer expiration
Two issues fox flagged from prod observation:
1. PayPal capture succeeded but MPS returned 502 Bad Gateway —
buyer was charged, no invoice landed. Root cause traced via
tmux-hosts journalctl on mps-uwsgi1:
transaction.interfaces.NoTransaction
File "pyramid_tm/__init__.py", line 146, in tm_tween
if manager.isDoomed():
File "transaction/_manager.py", line 88, in get
raise NoTransaction()
All four payment-complete-checkout exception handlers in
views/cart.py called `request.tm.abort()` and returned
HTTPFound. abort() yanked the transaction out from under
pyramid_tm.tm_tween, whose post-view manager.isDoomed() check
then raised NoTransaction → uwsgi 500 → Caddy 502.
Fix: `request.tm.doom()` instead. Flags the txn for abort but
leaves it for pyramid_tm to clean up — the documented pattern.
Also added logging.getLogger(__name__).exception() at each
except so the original failure is captured in journalctl
instead of being swallowed into a flash message we can't see.
The flash + 302 redirect path still works for the user.
Four sites patched:
- user_cart_complete_checkout (Stripe) x2 (CardError + Exception)
- paypal_complete_checkout
- adyen_complete_checkout
The original inner exception in fox's PayPal case is still
unknown — the bug masked it. Next failed payment will surface
the real stack in journalctl.
2. Existing shops still showing 7-day (168h) seller-response
window even after DEFAULT_OFFER_EXPIRATION_HOURS dropped to 72.
The shop column default is for new rows only; rows already in
the DB kept 168. Migration 7d6af811b6a1 bumps any shop still
at the literal 168 down to 72; shops that explicitly customized
(any other value) are left alone.
This commit is contained in:
parent
7b9d7dc25e
commit
31e06ffb0a
2 changed files with 72 additions and 4 deletions
|
|
@ -0,0 +1,37 @@
|
|||
"""bump existing shops offer_expiration_hours 168 to 72
|
||||
|
||||
Revision ID: 7d6af811b6a1
|
||||
Revises: 5d01b163b805
|
||||
Create Date: 2026-05-14 20:42:51.956494
|
||||
|
||||
Default offer expiration (pre-acceptance window) dropped from 168h
|
||||
(7 days) → 48h → 72h across recent commits. Existing shops kept
|
||||
whatever value they had at table-create time. This migration bumps
|
||||
any shop still at the literal old default 168 to the new default
|
||||
72. Shops that explicitly customized via the offer-settings form
|
||||
(any value other than 168) are left alone.
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision = "7d6af811b6a1"
|
||||
down_revision = "5d01b163b805"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
op.execute(
|
||||
sa.text(
|
||||
"UPDATE mps_shop "
|
||||
"SET offer_expiration_hours = 72 "
|
||||
"WHERE offer_expiration_hours = 168"
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def downgrade():
|
||||
# No safe reversal — we don't know which shops were at 168 by
|
||||
# default vs explicit. Leave them at 72 on rollback.
|
||||
pass
|
||||
|
|
@ -21,6 +21,8 @@ from ..lib.mail import (
|
|||
)
|
||||
from ..lib.notifications import notify_purchase_and_sale
|
||||
|
||||
import logging
|
||||
|
||||
import stripe
|
||||
import traceback
|
||||
from datetime import datetime
|
||||
|
|
@ -895,13 +897,24 @@ def cart_complete_checkout(request):
|
|||
return HTTPFound(redirect_url)
|
||||
|
||||
except stripe.error.CardError as e:
|
||||
request.tm.abort()
|
||||
# doom() — not abort() — so pyramid_tm.tm_tween still owns
|
||||
# the txn lifecycle. abort() removes the txn outright, then
|
||||
# tm_tween's post-view manager.isDoomed() check raises
|
||||
# NoTransaction → uwsgi 500 → Caddy 502. doom() leaves it
|
||||
# for tm_tween to clean up.
|
||||
request.tm.doom()
|
||||
logging.getLogger(__name__).exception(
|
||||
"stripe checkout failed (user-visible)"
|
||||
)
|
||||
msg = ("Payment failed. Please check your card details.", "error")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/billing")
|
||||
|
||||
except Exception as e:
|
||||
request.tm.abort()
|
||||
request.tm.doom()
|
||||
logging.getLogger(__name__).exception(
|
||||
"stripe checkout unexpected failure"
|
||||
)
|
||||
msg = (f"Payment failed: {str(e)}", "error")
|
||||
request.session.flash(msg)
|
||||
return HTTPFound("/billing")
|
||||
|
|
@ -1117,7 +1130,16 @@ def paypal_complete_checkout(request):
|
|||
return HTTPFound("/cart")
|
||||
|
||||
except Exception as e:
|
||||
request.tm.abort()
|
||||
# doom() not abort() — see Stripe paths above for the
|
||||
# pyramid_tm rationale (502 root cause on 2026-05-14).
|
||||
# The original tm.abort() yanked the txn out from under
|
||||
# pyramid_tm.tm_tween, whose post-view manager.isDoomed()
|
||||
# check then raised NoTransaction → uwsgi 500 → Caddy 502.
|
||||
# User got charged via PayPal/Adyen but no invoice landed.
|
||||
request.tm.doom()
|
||||
logging.getLogger(__name__).exception(
|
||||
"payment complete-checkout unexpected failure"
|
||||
)
|
||||
request.session.flash((f"Payment processing failed: {str(e)}", "error"))
|
||||
return HTTPFound("/cart")
|
||||
|
||||
|
|
@ -1359,7 +1381,16 @@ def adyen_complete_checkout(request):
|
|||
return HTTPFound("/cart")
|
||||
|
||||
except Exception as e:
|
||||
request.tm.abort()
|
||||
# doom() not abort() — see Stripe paths above for the
|
||||
# pyramid_tm rationale (502 root cause on 2026-05-14).
|
||||
# The original tm.abort() yanked the txn out from under
|
||||
# pyramid_tm.tm_tween, whose post-view manager.isDoomed()
|
||||
# check then raised NoTransaction → uwsgi 500 → Caddy 502.
|
||||
# User got charged via PayPal/Adyen but no invoice landed.
|
||||
request.tm.doom()
|
||||
logging.getLogger(__name__).exception(
|
||||
"payment complete-checkout unexpected failure"
|
||||
)
|
||||
request.session.flash((f"Payment processing failed: {str(e)}", "error"))
|
||||
return HTTPFound("/cart")
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue