diff --git a/make_post_sell/static/css/common.css b/make_post_sell/static/css/common.css index 028e988..8946510 100644 --- a/make_post_sell/static/css/common.css +++ b/make_post_sell/static/css/common.css @@ -3189,6 +3189,13 @@ img.crypto-button-icon { font-weight: var(--weight-bold, 700); } +.product-add-disabled-note { + margin: var(--space-2, 8px) 0 0 0; + font-size: var(--text-sm, 0.875rem); + color: var(--text-secondary, #666); + max-width: 30em; +} + .cart-negotiation-deadline { grid-column: 1 / -1; margin: var(--space-3, 12px) 0 0 0; diff --git a/make_post_sell/templates/product.j2 b/make_post_sell/templates/product.j2 index ea502bf..a23ad5f 100644 --- a/make_post_sell/templates/product.j2 +++ b/make_post_sell/templates/product.j2 @@ -201,6 +201,15 @@ {% endif %} {% endif %} + {# Disable Add To Cart when the active cart is locked to an + accepted offer / auction. The cart total is overridden by the + negotiated amount regardless of quantity, so adding another + unit (same or different product) would let the buyer walk away + with more merch than they paid for. Disable + caption rather + than hiding the button so the affordance stays visible. #} + {% set cart_is_negotiated = request.active_cart and request.active_cart.is_negotiated %} + {% set cart_negotiation_kind = request.active_cart.negotiation_kind if cart_is_negotiated else "" %} + {% if product.is_physical %} {# MPS-20 + MPS-21: physical Add To Cart only renders when pricing_mode permits direct purchase (modes 0, 2, 4). @@ -212,8 +221,11 @@
+ {% if cart_is_negotiated %} +Your active cart is locked to an accepted {{ cart_negotiation_kind }}. Save or check out that cart before adding other items.
+ {% endif %} {% else %} {% if request.shop.shop_locations.all()|length > 1 %} @@ -227,8 +239,11 @@ + {% if cart_is_negotiated %} +Your active cart is locked to an accepted {{ cart_negotiation_kind }}. Save or check out that cart before adding other items.
+ {% endif %} {% endif %} {% endif %} diff --git a/make_post_sell/tests/test_functional.py b/make_post_sell/tests/test_functional.py index 605fb08..4dc28e1 100644 --- a/make_post_sell/tests/test_functional.py +++ b/make_post_sell/tests/test_functional.py @@ -7090,6 +7090,57 @@ class TestOfferCheckout(_AuthenticatedBase): self.dbsession.refresh(cart) self.assertEqual(cart.get_product_quantity(product), 1) + def test_product_page_disables_add_to_cart_on_negotiated_cart(self): + """When the active cart is locked to an accepted offer, the + product page renders Add To Cart with `disabled` and a one-line + caption explaining why. Per fox: disable, don't hide — the + affordance stays visible so the user knows the option exists + and what action will re-enable it. + + The offer-only product itself doesn't have an Add To Cart button + (offer-only mode hides it). So we check a *second* buy-now + product on the same shop — the realistic flow: buyer accepted + an offer on product A, browses product B and tries to add it + normally. + """ + from ..models.product import Product + import json + + offer_id = self._accepted_offer() + # _accepted_offer left user1 (seller) logged in. Add a second + # buy-now product to the same shop before the buyer logs in. + from ..models.offer import get_offer_by_id + offer = get_offer_by_id(self.dbsession, offer_id) + shop = offer.shop + other = Product(title="Buy now item", description="...") + other.shop = shop + other.price_in_cents = 500 + other.is_physical = False + other.is_sellable = True + other.pricing_mode = 0 + other.json_file_metadata = json.dumps( + {"originals": {}, "extensions": {"product": "pdf"}}, + ) + self.dbsession.add(other) + self.dbsession.flush() + other_id = other.uuid_str + other_slug = other.slug + transaction.commit() + + self.testapp.get("/log-out") + self.log_in_user(self.user2_creds) + self.testapp.post(f"/o/{offer_id}/checkout", status=302) + + body = self.testapp.get( + f"/p/{other_id}/{other_slug}", status=200 + ).body.decode() + self.assertIn("Add To Cart", body) + self.assertRegex( + body, + r'', + ) + self.assertIn("locked to an accepted offer", body) + def test_negotiated_cart_refuses_quantity_bump(self): """Same defense for /cart/{id}/quantity — the buyer can't bump quantity directly on the cart page either.