1.3.0 — discounted and variable rewards
Two features that widen what the reward can be: it no longer has to be
free, and it no longer has to be a simple product.
Added
-
Variable products can be rewards, with the customer choosing the variation
(DECISION.mdD-017, which supersedes D-006 and answersOPEN-QUESTIONS.md
Q-003). Add a variable product to the Get list and its card carries a dropdown
of every variation that can be given, each with its own price and, where it
cannot, the reason. Add a single variation instead and the reward is pinned to
it with no choice shown. The Buy side already counted variations and is
unchanged.D-006 refused these because "one free t-shirt" does not say which size. The
answer turned out to be to ask, which is what a chooser exists to do. Its
reasoning survives for grouped and external products, and for a variation that
leaves an attribute set to "Any" — that still needs a choice making, so it is
not offered.The card is one card per product, never one per variation, so a catalogue of
fifty variable products stays fifty cards. It quotes a variation rather than
the parent, whose price is the low end of a range and need not match any of
them. Where variations share a parent's stock record they compete with each
other, and the dropdown says so against the option rather than leaving the
customer to work it out.Internally a reward is no longer one product ID but a product and variation
pair, because a variation's cart line stores its parent inproduct_idand the
two are otherwise indistinguishable. The Store API reports both, additively. -
An integration scenario for the variable reward. The block job seeds a
variable product priced differently per variation, chooses one through the
Store API, and asserts the line is charged from the chosen variation rather
than the parent's range, that the parent alone is refused, and that the cart
renders one selector listing both options. -
The reward can be discounted rather than only given away (
DECISION.md
D-016, answeringOPEN-QUESTIONS.mdQ-008). "Buy 2, get 1 at 50% off" is now
as configurable as "get 1 free", through a Reward price control on the
settings screen and a percentage field beside it. Two new settings keys carry
it,get_discount_typeandget_discount_value, both defaulting to the free
behaviour — an option row saved before this release reads back as a free gift,
so nothing changes for an existing store until someone changes it.The reward's price is worked out from a product loaded fresh on every pricing
pass rather than from the cart line's own product object. WooCommerce
recalculates totals more than once in some requests, and setting a price to
zero survives that where taking half off does not: reading back its own output
would compound the discount to a quarter, then an eighth. The cost of the
approach is that a price another plugin set on the cart item is overwritten
rather than discounted, which matters to stores running dynamic pricing.The discount comes off the effective selling price, so a reward already on
sale is discounted from its sale price. Eligible coupons still apply on top,
as they do to any reduced price — a 20% coupon over a 50% reward leaves the
customer paying 40% of list, where that coupon's own product, category, and
sale-exclusion rules allow it at all. That follows from where the pricing hook
sits rather than from a test; the unit stubs have no coupon support.Fixed-amount discounts were deliberately left out. A percentage is linear, so
it needs no clamping against negative prices and raises no per-unit versus
per-line question; "$5 off" can be added later on the same field. -
An integration scenario for the discounted reward. The block job now
switches the seeded store to 50% off and runs a second browser pass, asserting
through the Store API that WooCommerce charges the discounted figure, that it
was not applied twice, and that the cart says "Discounted item" where it used
to say "Free gift". -
An automated WordPress + WooCommerce integration job (
CODEX-REVIEW.md
M-02). CI now installs the built zip into a real WordPress with WooCommerce —
the compatibility floor (9.9.5) and whatever islatest— seeds a store, and
drives the Cart and Checkout blocks in headless Chromium. It asserts the two
things the unit suite structurally cannot: that the chooser slot never takes
the block root'sdata-block-name, and that each block leavesis-loading
and renders — a real checkout form, both cart lines, and the visible gift
label, plus the Store API's zero price, locked quantity, and label members.This is the gap that let H-01 ship: 127 unit tests passed while the Checkout
block was unusable. The job was rehearsed against a live store before
landing — 28/28 checks pass, and reverting the injection priority to 10 turns
10 of them red, checkout included. Because the matrix includeslatest, a
future WooCommerce that breaks the blocks now fails CI rather than a customer's
store.Classic cart and checkout, stock reduction, and order placement remain manual
(BRIEF.md §8.6).
Changed
-
Customer-facing wording follows the offer instead of assuming it is free.
Roughly twenty strings across the cart, chooser, notices, and block metadata
ask the engine what the reward costs rather than hardcoding "free". Every one
of them is byte-identical to the previous wording while the reward is free, so
a store that never configures a discount sees no change at all. -
Order lines record the offer that produced them. A hidden
_bogo_select_discountmeta storesfreeorpercent:50as it stood when
the order was placed, because the settings can move afterwards and an order
has to be able to explain its own pricing. The existing_bogo_select_free
flag keeps its name and value on discounted lines — it is a persisted key that
existing reports query, and now marks a line as the offer's rather than
claiming it was free.