Evolve the member discount into a sitewide stepped promo

Replaces the flat "$10 off $100+, members only" discount with $5 off
per $50 spent, capped at $200 spend ($20 max) — an evolution of the
same idea, not an addition to it. Now sitewide by default: the guest
tier carries the same step_discount benefit as free_member, so
anonymous shoppers get it too, not just members. Toggling it on/off, or
switching between sitewide and members-only, needs no code change — an
admin unchecks the benefit on one tier's row in the settings screen.

Migration (schema v3->v4) overwrites guest/free_member's benefits to the
new shape directly rather than merging, per the established pre-launch
convention (no real customer/tier data exists to preserve yet).

Live-verified beyond the unit suite: the full discount table checked
against the real DB for both a guest and a real member, and the
sitewide/members-only toggle exercised through a real authenticated
settings-screen form POST (not just read back from code).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MqhgnCRjjVLFnZ3Jpn93Vg
This commit is contained in:
twooeyandClaude Sonnet 5 committed 2026-08-28 12:20:18 -04:00
1 parent 13e7362579
commit 773883a0f3
7 files changed
+122 -62

No files matched your search

@@ -8,11 +8,14 @@ use Brain\Monkey\Functions;
use Mockery;
/**
* "$10 off orders $100+, members only" (design doc §07 extension). The
* real Membership\Tier class runs unmocked — only $wpdb (underneath
* Tier's queries), get_current_user_id(), and a WC_Cart mock are
* stubbed, so these tests exercise the actual threshold/amount lookup,
* not just a canned discount value.
* The stepped, sitewide promo: $5 off per $50 spent, capped at $200
* spend for a max $20 off (evolved from an earlier flat "$10 off $100+,
* members only" version — design doc §07 extension). "Sitewide" means
* both `guest` and `free_member` carry the same `step_discount` benefit
* by default, so a guest (user_id 0) getting the discount is the thing
* that specifically proves this isn't members-only anymore. The real
* Membership\Tier class runs unmocked — only $wpdb (underneath Tier's
* queries), get_current_user_id(), and a WC_Cart mock are stubbed.
*/
class MemberDiscountTest extends TestCase
{
@@ -23,40 +26,52 @@ class MemberDiscountTest extends TestCase
parent::tearDown();
}
public function test_guest_gets_no_discount(): void
{
Functions\when('get_current_user_id')->justReturn(0);
// Guest tier has no spend_discount benefit configured.
$this->stub_wpdb_anonymous(['free_shipping_threshold' => ['threshold' => 25.00]]);
$cart = Mockery::mock(\WC_Cart::class);
$cart->shouldReceive('get_subtotal')->andReturn('500.00');
$cart->shouldNotReceive('add_fee');
MemberDiscount::apply($cart);
}
public function test_member_below_min_spend_gets_no_discount(): void
/**
* @dataProvider subtotalCases
*/
public function test_member_gets_the_stepped_discount(float $subtotal, float $expected_discount): void
{
Functions\when('get_current_user_id')->justReturn(7);
$this->stub_wpdb_with_member_lookup(5, [
'free_shipping_threshold' => ['threshold' => 15.00],
'spend_discount' => ['min_spend' => 100.00, 'discount_amount' => 10.00],
'step_discount' => ['step_amount' => 50, 'discount_per_step' => 5, 'max_spend' => 200],
]);
$cart = Mockery::mock(\WC_Cart::class);
$cart->shouldReceive('get_subtotal')->andReturn('80.00');
$cart->shouldNotReceive('add_fee');
$cart->shouldReceive('get_subtotal')->andReturn((string) $subtotal);
if ($expected_discount > 0) {
$cart->shouldReceive('add_fee')->once()->with('Member discount', -1 * $expected_discount, false);
} else {
$cart->shouldNotReceive('add_fee');
}
MemberDiscount::apply($cart);
}
public function test_member_at_min_spend_gets_the_discount(): void
public static function subtotalCases(): array
{
Functions\when('get_current_user_id')->justReturn(7);
$this->stub_wpdb_with_member_lookup(5, [
'free_shipping_threshold' => ['threshold' => 15.00],
'spend_discount' => ['min_spend' => 100.00, 'discount_amount' => 10.00],
return [
'nothing in cart' => [0.00, 0.00],
'below the first step' => [30.00, 0.00],
'exactly the first step' => [50.00, 5.00],
'just under the second step' => [99.99, 5.00],
'exactly the second step' => [100.00, 10.00],
'exactly the third step' => [150.00, 15.00],
'just under the cap' => [199.99, 15.00],
'exactly at the cap' => [200.00, 20.00],
'above the cap stays capped' => [250.00, 20.00],
];
}
public function test_guest_gets_the_discount_too_sitewide_by_default(): void
{
Functions\when('get_current_user_id')->justReturn(0);
// guest tier carries the same step_discount benefit as
// free_member by default -- this is what makes it sitewide.
$this->stub_wpdb_anonymous([
'free_shipping_threshold' => ['threshold' => 25.00],
'step_discount' => ['step_amount' => 50, 'discount_per_step' => 5, 'max_spend' => 200],
]);
$cart = Mockery::mock(\WC_Cart::class);
@@ -66,16 +81,19 @@ class MemberDiscountTest extends TestCase
MemberDiscount::apply($cart);
}
public function test_member_above_min_spend_gets_the_discount(): void
public function test_guest_gets_nothing_when_admin_disables_it_on_the_guest_tier(): void
{
Functions\when('get_current_user_id')->justReturn(7);
$this->stub_wpdb_with_member_lookup(5, [
'spend_discount' => ['min_spend' => 100.00, 'discount_amount' => 10.00],
Functions\when('get_current_user_id')->justReturn(0);
// Admin unchecked step_discount on the guest row via the
// settings screen -- members-only mode, proven by the absence
// of the benefit key here (not a hardcoded "guest excluded").
$this->stub_wpdb_anonymous([
'free_shipping_threshold' => ['threshold' => 25.00],
]);
$cart = Mockery::mock(\WC_Cart::class);
$cart->shouldReceive('get_subtotal')->andReturn('142.50');
$cart->shouldReceive('add_fee')->once()->with('Member discount', -10.00, false);
$cart->shouldReceive('get_subtotal')->andReturn('250.00');
$cart->shouldNotReceive('add_fee');
MemberDiscount::apply($cart);
}