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:
1 parent
13e7362579
commit
773883a0f3
7 files changed
+122
-62
No files matched your search
@@ -58,7 +58,7 @@ class TierSettings extends \WC_Settings_Page
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php foreach ($tiers as $tier): $fs = $tier['benefits']['free_shipping_threshold'] ?? null; $sd = $tier['benefits']['spend_discount'] ?? null; ?>
|
||||
<?php foreach ($tiers as $tier): $fs = $tier['benefits']['free_shipping_threshold'] ?? null; $sd = $tier['benefits']['step_discount'] ?? null; ?>
|
||||
<tr>
|
||||
<td>
|
||||
<input type="text" name="bsc_tier[<?php echo esc_attr($tier['tier_id']); ?>][name]" value="<?php echo esc_attr($tier['name']); ?>" />
|
||||
@@ -72,9 +72,10 @@ class TierSettings extends \WC_Settings_Page
|
||||
$<input type="number" step="0.01" style="width:6em" name="bsc_tier[<?php echo esc_attr($tier['tier_id']); ?>][free_shipping_threshold][threshold]" value="<?php echo esc_attr($fs['threshold'] ?? '0.00'); ?>" />
|
||||
</td>
|
||||
<td>
|
||||
<label><input type="checkbox" name="bsc_tier[<?php echo esc_attr($tier['tier_id']); ?>][spend_discount][enabled]" value="1" <?php checked($sd !== null); ?> /> enabled</label><br>
|
||||
save $<input type="number" step="0.01" style="width:6em" name="bsc_tier[<?php echo esc_attr($tier['tier_id']); ?>][spend_discount][discount_amount]" value="<?php echo esc_attr($sd['discount_amount'] ?? '0.00'); ?>" />
|
||||
on orders $<input type="number" step="0.01" style="width:6em" name="bsc_tier[<?php echo esc_attr($tier['tier_id']); ?>][spend_discount][min_spend]" value="<?php echo esc_attr($sd['min_spend'] ?? '0.00'); ?>" />+
|
||||
<label><input type="checkbox" name="bsc_tier[<?php echo esc_attr($tier['tier_id']); ?>][step_discount][enabled]" value="1" <?php checked($sd !== null); ?> /> enabled</label><br>
|
||||
$<input type="number" step="0.01" style="width:5em" name="bsc_tier[<?php echo esc_attr($tier['tier_id']); ?>][step_discount][discount_per_step]" value="<?php echo esc_attr($sd['discount_per_step'] ?? '0.00'); ?>" />
|
||||
off per $<input type="number" step="0.01" style="width:5em" name="bsc_tier[<?php echo esc_attr($tier['tier_id']); ?>][step_discount][step_amount]" value="<?php echo esc_attr($sd['step_amount'] ?? '0.00'); ?>" />
|
||||
spent, capped at $<input type="number" step="0.01" style="width:6em" name="bsc_tier[<?php echo esc_attr($tier['tier_id']); ?>][step_discount][max_spend]" value="<?php echo esc_attr($sd['max_spend'] ?? '0.00'); ?>" />
|
||||
</td>
|
||||
<td>
|
||||
<?php if ($tier['slug'] !== Tier::GUEST_SLUG): ?>
|
||||
@@ -125,10 +126,11 @@ class TierSettings extends \WC_Settings_Page
|
||||
'threshold' => (float) ($fields['free_shipping_threshold']['threshold'] ?? 0),
|
||||
];
|
||||
}
|
||||
if (!empty($fields['spend_discount']['enabled'])) {
|
||||
$benefits['spend_discount'] = [
|
||||
'min_spend' => (float) ($fields['spend_discount']['min_spend'] ?? 0),
|
||||
'discount_amount' => (float) ($fields['spend_discount']['discount_amount'] ?? 0),
|
||||
if (!empty($fields['step_discount']['enabled'])) {
|
||||
$benefits['step_discount'] = [
|
||||
'step_amount' => (float) ($fields['step_discount']['step_amount'] ?? 0),
|
||||
'discount_per_step' => (float) ($fields['step_discount']['discount_per_step'] ?? 0),
|
||||
'max_spend' => (float) ($fields['step_discount']['max_spend'] ?? 0),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -449,10 +449,12 @@ class Commands
|
||||
* Sets (or replaces) one benefit on a tier. Benefit types with real
|
||||
* code behind them today: free_shipping_threshold (Shipping\
|
||||
* BSC_Free_Shipping, expects {"threshold": <number>}) and
|
||||
* spend_discount (Membership\MemberDiscount, expects {"min_spend":
|
||||
* <number>, "discount_amount": <number>}). Any other type is stored
|
||||
* but has no runtime effect until something reads it — see the
|
||||
* class docblocks for why.
|
||||
* step_discount (Membership\MemberDiscount, expects {"step_amount":
|
||||
* <number>, "discount_per_step": <number>, "max_spend": <number>} —
|
||||
* e.g. step_amount=50/discount_per_step=5/max_spend=200 means $5 off
|
||||
* per $50 spent, capped at $20 off). Any other type is stored but
|
||||
* has no runtime effect until something reads it — see the class
|
||||
* docblocks for why.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
@@ -467,7 +469,7 @@ class Commands
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* wp bookstore tier-set-benefit free_member spend_discount '{"min_spend":100,"discount_amount":10}'
|
||||
* wp bookstore tier-set-benefit free_member step_discount '{"step_amount":50,"discount_per_step":5,"max_spend":200}'
|
||||
*
|
||||
* @subcommand tier-set-benefit
|
||||
*/
|
||||
|
||||
@@ -5,14 +5,25 @@ namespace Bookstore\Core\Membership;
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Design doc §07 extension: "$10 off orders $100+, members only." A flat
|
||||
* per-cart-subtotal discount via WC_Cart::add_fee (a negative fee),
|
||||
* driven entirely by the current customer's `spend_discount` tier
|
||||
* benefit — same shape as Shipping\BSC_Free_Shipping's threshold, just a
|
||||
* fee instead of a shipping rate. Business case: BooksRun charges a flat
|
||||
* per-order shipping fee regardless of order size, so bundling a
|
||||
* customer's purchases into one $100+ order rather than several smaller
|
||||
* ones saves that fee outright — more than covering the $10 discount.
|
||||
* Design doc §07 extension: a stepped, sitewide promo — $5 off per $50
|
||||
* spent, capping at $200 spend for a max $20 off (evolved from an
|
||||
* earlier flat "$10 off $100+, members only" version). Applied via
|
||||
* WC_Cart::add_fee (a negative fee), driven entirely by the current
|
||||
* customer's `step_discount` tier benefit — same shape as
|
||||
* Shipping\BSC_Free_Shipping's threshold, just a fee instead of a
|
||||
* shipping rate. Business case: BooksRun charges a flat per-order
|
||||
* shipping fee regardless of order size, so bundling a customer's
|
||||
* purchases into one larger order rather than several smaller ones
|
||||
* saves that fee outright — more than covering the discount.
|
||||
*
|
||||
* "Sitewide" isn't a separate concept from the member-tier system — the
|
||||
* `guest` tier (the universal fallback for every non-member customer;
|
||||
* see Membership\Tier::tier_for_user()) carries the same `step_discount`
|
||||
* benefit as `free_member` by default, so every customer gets it unless
|
||||
* an admin unchecks it on one tier or the other via the settings screen
|
||||
* (WooCommerce > Settings > Membership Tiers) — e.g. unchecking it only
|
||||
* on `guest` turns this into a members-only promo without touching any
|
||||
* code.
|
||||
*
|
||||
* Runs on woocommerce_cart_calculate_fees, which fires well before
|
||||
* Order\CheckoutRecheck's hooks and bakes the fee into cart/order totals
|
||||
@@ -30,16 +41,22 @@ class MemberDiscount
|
||||
public static function apply(\WC_Cart $cart): void
|
||||
{
|
||||
$user_id = get_current_user_id();
|
||||
$config = Tier::benefit_config_for_user($user_id, 'spend_discount');
|
||||
if (empty($config['min_spend']) || empty($config['discount_amount'])) {
|
||||
$config = Tier::benefit_config_for_user($user_id, 'step_discount');
|
||||
if (empty($config['step_amount']) || empty($config['discount_per_step']) || empty($config['max_spend'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$step_amount = (float) $config['step_amount'];
|
||||
$subtotal = (float) $cart->get_subtotal();
|
||||
if ($subtotal < (float) $config['min_spend']) {
|
||||
return;
|
||||
}
|
||||
|
||||
$cart->add_fee('Member discount', -1 * (float) $config['discount_amount'], false);
|
||||
$steps = min(
|
||||
floor($subtotal / $step_amount),
|
||||
floor((float) $config['max_spend'] / $step_amount)
|
||||
);
|
||||
$discount = $steps * (float) $config['discount_per_step'];
|
||||
|
||||
if ($discount > 0) {
|
||||
$cart->add_fee('Member discount', -1 * $discount, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -205,12 +205,13 @@ class Tier
|
||||
if (!self::find_by_slug(self::GUEST_SLUG)) {
|
||||
self::create(self::GUEST_SLUG, 'Guest', 0, [
|
||||
'free_shipping_threshold' => ['threshold' => 25.00],
|
||||
'step_discount' => ['step_amount' => 50, 'discount_per_step' => 5, 'max_spend' => 200],
|
||||
]);
|
||||
}
|
||||
if (!self::find_by_slug(self::FREE_MEMBER_SLUG)) {
|
||||
self::create(self::FREE_MEMBER_SLUG, 'Free Member', 10, [
|
||||
'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],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,5 +174,25 @@ class Schema
|
||||
}
|
||||
|
||||
self::install();
|
||||
|
||||
// v3 -> v4: the flat "$10 off $100+" spend_discount benefit
|
||||
// became a stepped, sitewide promo ($5 per $50 spent, capped at
|
||||
// $200) — see Membership\MemberDiscount. A genuine product-
|
||||
// default change, not a hypothetical admin customization worth
|
||||
// preserving (pre-launch — see project memory on schema
|
||||
// migrations), so this overwrites guest/free_member's benefits
|
||||
// directly rather than merging. Post-launch, a change like this
|
||||
// must target only the specific keys being replaced instead.
|
||||
if ($current !== false && version_compare((string) $current, '4', '<')) {
|
||||
$step_discount = ['step_amount' => 50, 'discount_per_step' => 5, 'max_spend' => 200];
|
||||
Tier::update_benefits(Tier::GUEST_SLUG, [
|
||||
'free_shipping_threshold' => ['threshold' => 25.00],
|
||||
'step_discount' => $step_discount,
|
||||
]);
|
||||
Tier::update_benefits(Tier::FREE_MEMBER_SLUG, [
|
||||
'free_shipping_threshold' => ['threshold' => 15.00],
|
||||
'step_discount' => $step_discount,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user