Skip to content

1.3.0 — discounted and variable rewards

Choose a tag to compare

@johnjanney johnjanney released this 01 Aug 01:01
· 77 commits to main since this release

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.md D-017, which supersedes D-006 and answers OPEN-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 in product_id and 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, answering OPEN-QUESTIONS.md Q-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_type and get_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 is latest — 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's data-block-name, and that each block leaves is-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 includes latest, 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_discount meta stores free or percent:50 as 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.