MPS-20 + MPS-21: fix physical Add To Cart bypassing auction/offer
Defect: product.j2 always rendered "Add To Cart" at list price for
physical products, even when pricing_mode=1 (auction only) or
pricing_mode=3 (offer only). A buyer browsing a physical auction
could click Add To Cart and pay list price, completely bypassing
the bidding flow.
Fix: wrap the physical Add To Cart / Sold Out branches in
{% if product.is_buy_now_allowed %} so they only render in modes
that permit direct purchase (0 fixed, 2 auction+buy_now, 4 offer+
buy_now). Modes 1 (auction-only) and 3 (offer-only) hide both the
Add To Cart button and the Sold Out indicator entirely; buyers must
use the View Auction or Make Offer entry points.
Tests:
- test_digital_fixed_price_shows_add_to_cart
- test_digital_auction_only_hides_add_to_cart
- test_digital_auction_with_buy_now_shows_add_to_cart
- test_digital_offer_only_hides_add_to_cart
- test_physical_auction_only_hides_add_to_cart (the regression fix)
- test_physical_offer_only_hides_add_to_cart (same)
Total: 949 tests pass (was 943 + 6).
This commit is contained in:
parent
d79082c6ff
commit
364af1fcca
2 changed files with 116 additions and 7 deletions
|
|
@ -202,18 +202,24 @@
|
|||
{% endif %}
|
||||
|
||||
{% if product.is_physical %}
|
||||
{% set inventory = product.inventories | selectattr('shop_location_id', 'equalto', request.shop_location.id) | first %}
|
||||
{% if inventory and inventory.quantity > 0 %}
|
||||
{# MPS-20 + MPS-21: physical Add To Cart only renders when
|
||||
pricing_mode permits direct purchase (modes 0, 2, 4).
|
||||
Auction-only (1) and offer-only (3) modes hide it so buyers
|
||||
use the bidding or offer flow instead. #}
|
||||
{% if product.is_buy_now_allowed %}
|
||||
{% set inventory = product.inventories | selectattr('shop_location_id', 'equalto', request.shop_location.id) | first %}
|
||||
{% if inventory and inventory.quantity > 0 %}
|
||||
<form method="POST" action="{{ request.route_url('cart_add_product') }}">
|
||||
{% include "snippets/csrf.j2" %}
|
||||
<input type="hidden" name="product_id" value="{{ product.id }}">
|
||||
<button type="submit" class="product-download-button mps-button">Add To Cart</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<button class="product-download-button mps-button sold-out-button" disabled>Sold Out</button>
|
||||
{% if request.shop.shop_locations.all()|length > 1 %}
|
||||
<p>This item is sold out at this location.
|
||||
<br> You can <a href="{{ request.route_url('shop_locations', shop_id=request.shop.id, _query={'next': request.url}) }}">switch shop locations</a> to check availability.</p>
|
||||
{% else %}
|
||||
<button class="product-download-button mps-button sold-out-button" disabled>Sold Out</button>
|
||||
{% if request.shop.shop_locations.all()|length > 1 %}
|
||||
<p>This item is sold out at this location.
|
||||
<br> You can <a href="{{ request.route_url('shop_locations', shop_id=request.shop.id, _query={'next': request.url}) }}">switch shop locations</a> to check availability.</p>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% else %}
|
||||
|
|
|
|||
|
|
@ -6464,3 +6464,106 @@ class TestOfferCheckout(_AuthenticatedBase):
|
|||
# Redirect back to offer page, not /cart.
|
||||
self.assertIn("/o/", res.location)
|
||||
self.assertNotIn("/cart", res.location)
|
||||
|
||||
|
||||
class TestBuyNowGating(_AuthenticatedBase):
|
||||
"""MPS-20 + MPS-21: Add To Cart only renders when pricing_mode allows
|
||||
direct purchase (modes 0, 2, 4). For both physical and digital products."""
|
||||
|
||||
def _make_product(self, pricing_mode=0, is_physical=False,
|
||||
is_ready=True):
|
||||
from ..models.product import Product
|
||||
shop = self._create_shop_helper(user_creds=self.user1_creds)
|
||||
product = Product(title="Gated", description="...")
|
||||
product.shop = shop
|
||||
product.price_in_cents = 5000
|
||||
product.is_physical = is_physical
|
||||
product.is_sellable = True
|
||||
product.pricing_mode = pricing_mode
|
||||
if is_ready:
|
||||
import json
|
||||
if is_physical:
|
||||
# Physical needs thumbnail1 in extensions for is_ready.
|
||||
product.json_file_metadata = json.dumps(
|
||||
{"originals": {}, "extensions": {"thumbnail1": "jpg"}},
|
||||
)
|
||||
else:
|
||||
product.json_file_metadata = json.dumps(
|
||||
{"originals": {}, "extensions": {"product": "pdf"}},
|
||||
)
|
||||
self.dbsession.add(product)
|
||||
self.dbsession.flush()
|
||||
|
||||
# If auction mode, create the MpsAuction the form_section handler
|
||||
# would have created, so the View Auction link renders.
|
||||
if pricing_mode in (1, 2):
|
||||
from ..models.auction import MpsAuction, AUCTION_STATE_ACTIVE, now_timestamp
|
||||
auction = MpsAuction(
|
||||
product=product, shop=shop,
|
||||
start_price_in_cents=product.price_in_cents,
|
||||
)
|
||||
auction.state = AUCTION_STATE_ACTIVE
|
||||
auction.start_timestamp = now_timestamp() - 60_000
|
||||
auction.end_timestamp = now_timestamp() + 3_600_000
|
||||
self.dbsession.add(auction)
|
||||
self.dbsession.flush()
|
||||
|
||||
product_id = product.uuid_str
|
||||
product_slug = product.slug
|
||||
if is_physical:
|
||||
from ..models.inventory import Inventory
|
||||
loc = shop.shop_locations.first()
|
||||
if loc is not None:
|
||||
inv = Inventory(
|
||||
product=product, shop_location=loc, quantity=10,
|
||||
)
|
||||
self.dbsession.add(inv)
|
||||
self.dbsession.flush()
|
||||
transaction.commit()
|
||||
return product_id, product_slug
|
||||
|
||||
# The fix: pricing_mode=1 (auction only) and pricing_mode=3 (offer only)
|
||||
# must hide the Add To Cart button on both physical and digital products.
|
||||
|
||||
def test_digital_fixed_price_shows_add_to_cart(self):
|
||||
pid, slug = self._make_product(pricing_mode=0, is_physical=False)
|
||||
res = self.testapp.get(f"/p/{pid}/{slug}", status=200)
|
||||
self.assertIn(b"Add To Cart", res.body)
|
||||
|
||||
def test_digital_auction_only_hides_add_to_cart(self):
|
||||
pid, slug = self._make_product(pricing_mode=1, is_physical=False)
|
||||
res = self.testapp.get(f"/p/{pid}/{slug}", status=200)
|
||||
self.assertNotIn(b"Add To Cart", res.body)
|
||||
self.assertIn(b"View live auction", res.body)
|
||||
|
||||
def test_digital_auction_with_buy_now_shows_add_to_cart(self):
|
||||
pid, slug = self._make_product(pricing_mode=2, is_physical=False)
|
||||
res = self.testapp.get(f"/p/{pid}/{slug}", status=200)
|
||||
self.assertIn(b"Add To Cart", res.body)
|
||||
self.assertIn(b"View live auction", res.body)
|
||||
|
||||
def test_digital_offer_only_hides_add_to_cart(self):
|
||||
pid, slug = self._make_product(pricing_mode=3, is_physical=False)
|
||||
res = self.testapp.get(f"/p/{pid}/{slug}", status=200)
|
||||
self.assertNotIn(b"Add To Cart", res.body)
|
||||
|
||||
# Physical product gating: the regression fix. Physical products in
|
||||
# auction-only or offer-only mode must NOT render Add To Cart at list
|
||||
# price (which would bypass the bidding/negotiation flow).
|
||||
# Sold Out IS expected for physical products without inventory matching
|
||||
# the request shop_location — that's normal physical-product behavior;
|
||||
# we only assert the absence of Add To Cart, not the presence.
|
||||
|
||||
def test_physical_auction_only_hides_add_to_cart(self):
|
||||
# Pre-fix this rendered Add To Cart at list price.
|
||||
pid, slug = self._make_product(pricing_mode=1, is_physical=True)
|
||||
res = self.testapp.get(f"/p/{pid}/{slug}", status=200)
|
||||
self.assertNotIn(b"Add To Cart", res.body)
|
||||
self.assertNotIn(b"sold-out-button", res.body) # gated entirely
|
||||
self.assertIn(b"View live auction", res.body)
|
||||
|
||||
def test_physical_offer_only_hides_add_to_cart(self):
|
||||
pid, slug = self._make_product(pricing_mode=3, is_physical=True)
|
||||
res = self.testapp.get(f"/p/{pid}/{slug}", status=200)
|
||||
self.assertNotIn(b"Add To Cart", res.body)
|
||||
self.assertNotIn(b"sold-out-button", res.body)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue