diff --git a/make_post_sell/tests/test_functional.py b/make_post_sell/tests/test_functional.py index 3b6c132..f41da35 100644 --- a/make_post_sell/tests/test_functional.py +++ b/make_post_sell/tests/test_functional.py @@ -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() diff --git a/make_post_sell/views/auction.py b/make_post_sell/views/auction.py index ae8c746..b553f18 100644 --- a/make_post_sell/views/auction.py +++ b/make_post_sell/views/auction.py @@ -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}") diff --git a/make_post_sell/views/offer.py b/make_post_sell/views/offer.py index 65b7a7e..01502f2 100644 --- a/make_post_sell/views/offer.py +++ b/make_post_sell/views/offer.py @@ -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")