fix: offer/auction checkout — land in the new cart, not a stale empty one

offer_checkout and auction_checkout were creating a cart and hand-setting
cart.active = True, which left the user with two active carts for the
shop; /cart then resolved to the older, empty one — so "Pay $X" appeared
to do nothing. Both now use shop.create_new_cart_for_user() (deactivates
the user's other carts for that shop, activates the new one) and redirect
to /cart/{cart_id} directly, so it works regardless of which shop the
request is scoped to.

Tests: TestOfferCheckout / TestAuctionCheckout now assert there is exactly
one active cart, it carries the cart_offer/cart_auction association, it is
non-empty, and the redirect targets that cart by id. 990 passed.
This commit is contained in:
russell@unturf.com 2026-05-12 20:37:07 -04:00
parent adf6175d04
commit 5b44132058
No known key found for this signature in database
3 changed files with 37 additions and 32 deletions

View file

@ -6623,10 +6623,9 @@ class TestAuctionCheckout(_AuthenticatedBase):
self.testapp.get("/log-out")
self.log_in_user(self.user2_creds)
# POST /a/{id}/checkout redirects to /cart.
self.testapp.post(f"/a/{auction_id}/checkout", status=302)
# POST /a/{id}/checkout redirects into the new cart by id.
res = self.testapp.post(f"/a/{auction_id}/checkout", status=302)
# The user's active cart now has the cart_auction association.
from ..models.cart import Cart
from ..models.cart_auction import MpsCartAuction
from ..models.user import get_or_create_user_by_email
@ -6636,13 +6635,13 @@ class TestAuctionCheckout(_AuthenticatedBase):
.filter(Cart.user_id == winner.id, Cart.active.is_(True))
.all()
)
# At least one active cart with an MpsCartAuction row.
ca_count = self.dbsession.query(MpsCartAuction).count()
self.assertGreaterEqual(ca_count, 1)
# That cart's total uses the winning bid amount.
match = [c for c in active_carts if c.cart_auctions]
self.assertEqual(len(match), 1)
self.assertEqual(match[0].total_price_in_cents, 3700)
# Exactly one active cart, and it's the auction cart with the
# product in it — not a stale empty cart.
self.assertEqual(len(active_carts), 1)
self.assertTrue(active_carts[0].cart_auctions)
self.assertFalse(active_carts[0].is_empty)
self.assertEqual(active_carts[0].total_price_in_cents, 3700)
self.assertIn(f"/cart/{active_carts[0].uuid_str}", res.location)
def test_non_winner_checkout_blocked(self):
auction_id = self._ended_auction_with_winner()
@ -6734,7 +6733,7 @@ class TestOfferCheckout(_AuthenticatedBase):
self.testapp.get("/log-out")
self.log_in_user(self.user2_creds)
self.testapp.post(f"/o/{offer_id}/checkout", status=302)
res = self.testapp.post(f"/o/{offer_id}/checkout", status=302)
from ..models.cart import Cart
from ..models.cart_offer import MpsCartOffer
@ -6748,6 +6747,15 @@ class TestOfferCheckout(_AuthenticatedBase):
match = [c for c in carts_with_offer if c.cart_offers]
self.assertEqual(len(match), 1)
self.assertEqual(match[0].total_price_in_cents, 7500)
# Regression: the offer cart must be the buyer's ONE active cart
# and must contain the product — otherwise /cart lands on a stale
# empty cart ("pressing Pay didn't add the item").
active_carts = [c for c in carts_with_offer if c.active]
self.assertEqual(len(active_carts), 1)
self.assertTrue(active_carts[0].cart_offers)
self.assertFalse(active_carts[0].is_empty)
# ...and the redirect lands on that cart by id.
self.assertIn(f"/cart/{match[0].uuid_str}", res.location)
def test_non_buyer_checkout_blocked(self):
offer_id = self._accepted_offer()

View file

@ -298,7 +298,6 @@ def auction_checkout(request):
Cart total is overridden to auction.winning_bid amount via the
cart_auction association (Cart.auction_offer_override_in_cents).
"""
from ..models.cart import Cart
from ..models.cart_auction import MpsCartAuction
auction = get_auction_by_id(
@ -319,16 +318,13 @@ def auction_checkout(request):
request.session.flash(("Only the auction winner can check out.", "error"))
return HTTPFound(f"/a/{auction.uuid_str}")
# Build a fresh cart for this auction. The product is added with
# quantity=auction.quantity so update_inventory() (cart success hook)
# naturally deducts the right number of units from inventory.
# Cart total comes from auction.winning_bid (override), not the
# quantity × product.price computation.
cart = Cart(user=request.user)
cart.shop = auction.shop
cart.active = True
request.dbsession.add(cart)
request.dbsession.flush()
# Build a fresh active cart for this auction (create_new_cart_for_user
# deactivates the user's other carts for this shop and activates this
# one — a hand-set cart.active = True left two active carts and /cart
# resolved to the wrong, empty one). The product is added with
# quantity=auction.quantity so update_inventory() deducts the right
# number of units; cart total comes from the cart_auction override.
cart = auction.shop.create_new_cart_for_user(request.user)
cart.add_product(auction.product)
if auction.quantity > 1:
cart.set_product_quantity(auction.product, auction.quantity)
@ -336,4 +332,4 @@ def auction_checkout(request):
MpsCartAuction(cart=cart, auction=auction)
)
request.dbsession.flush()
return HTTPFound("/cart")
return HTTPFound(f"/cart/{cart.uuid_str}")

View file

@ -341,11 +341,10 @@ def offer_withdraw(request):
@view_config(route_name="offer_checkout", request_method="POST")
@user_required(flash_msg="Please log in to pay.")
def offer_checkout(request):
"""Buyer checkout for an accepted offer: build a cart linked to the
offer, redirect to /cart. Cart total is overridden to the agreed
"""Buyer checkout for an accepted offer: build a fresh cart linked to
the offer and redirect into it. Cart total is overridden to the agreed
amount via the cart_offer association.
"""
from ..models.cart import Cart
from ..models.cart_offer import MpsCartOffer
from ..models.offer import OFFER_STATE_ACCEPTED
@ -359,15 +358,17 @@ def offer_checkout(request):
request.session.flash(("Only the buyer can check out.", "error"))
return HTTPFound(f"/o/{offer.uuid_str}")
cart = Cart(user=request.user)
cart.shop = offer.shop
cart.active = True
request.dbsession.add(cart)
request.dbsession.flush()
# create_new_cart_for_user flips every other cart for this user+shop
# to inactive and activates the new one — so /cart resolves to it.
# (Setting cart.active = True by hand left two active carts and /cart
# would land on the old empty one.)
cart = offer.shop.create_new_cart_for_user(request.user)
cart.add_product(offer.product)
request.dbsession.add(MpsCartOffer(cart=cart, offer=offer))
request.dbsession.flush()
return HTTPFound("/cart")
# Land directly in the cart by id — works regardless of which shop the
# request happens to be scoped to (SaaS domain vs the offer's shop).
return HTTPFound(f"/cart/{cart.uuid_str}")
@view_config(route_name="shop_offers", renderer="shop_offers.j2")