Skip to content

Add percentage and fixed amount discount code types - #3727

Draft
dparker1005 wants to merge 4 commits into
strangerstudios:v3.9from
dparker1005:formula-discount-codes
Draft

Add percentage and fixed amount discount code types#3727
dparker1005 wants to merge 4 commits into
strangerstudios:v3.9from
dparker1005:formula-discount-codes

Conversation

@dparker1005

Copy link
Copy Markdown
Member

What this does

Extends discount codes with formula-based discounts alongside the existing per-level price overrides. Each code now has a discount_type:

  • set_price — existing behavior, unchanged; remains the default. Existing codes need no migration.
  • percentage — e.g. 20% off the level's regular pricing.
  • fixed — e.g. $10 off the level's regular pricing.

Formula codes store one rule on pmpro_discount_codes (discount_value, apply_to_initial, apply_to_recurring) shared across all eligible levels, and inherit everything else — billing cycle, billing limit, trial, expiration — from the level. Sites that need custom structure plus a discount keep using set_price.

How pricing is resolved

New pmpro_get_discounted_level_for_code( $level, $code ) in includes/discount-codes.php is the single source of truth: it confirms the level is eligible via pmpro_discount_codes_levels, applies the rule to the level's current pricing, rounds with pmpro_round_price(), and clamps at $0. All four runtime paths that previously read pmpro_discount_codes_levels as final pricing now use it:

  • pmpro_getLevelAtCheckout()
  • the AJAX applydiscountcode service (resolved per level before the multi-level combined sum)
  • MemberOrder::getMembershipLevel() (the order fallback used by IPN handlers)
  • the 2Checkout INS handler

The pmpro_discount_code_level filter still runs at its existing call sites after calculation, so documented filter-based extensions keep receiving final prices. A new pmpro_get_discounted_level_for_code filter runs inside the resolver.

Backwards compatibility for direct table readers

pmpro_discount_codes_levels still holds one row per eligible level for every code type. For formula codes, its pricing columns hold a snapshot of the calculated prices — refreshed on code save and on level save (pmpro_save_membership_level) — so add-ons reading the table directly see plausible, usually-exact prices instead of zeros. Checkout never trusts the snapshot; it always recalculates. This also means the existing gateway-compatibility checks evaluate correct cycle/trial values for formula codes with no changes.

Admin / API

  • Edit screen: Discount Type, Discount Amount, and Applies To (initial/recurring) fields; for formula codes the per-level pricing tables collapse to an explanatory note. New Select All / Deselect All buttons for the levels list.
  • Codes list table shows the rule (e.g. "20% off initial payment, recurring payments") under the code.
  • PMPro_Discount_Code and POST /pmpro/v1/discount_code accept the new fields.
  • Schema via dbDelta, db version bumped to 3.9.

Also included

  • Fixed pmpro_get_discount_code() to match code strings before IDs so fully numeric codes (e.g. a code literally named 2025) resolve correctly at checkout.
  • Replaced duplicate raw code-lookup queries in pmpro_complete_checkout(), pmpro_getLevelAtCheckout(), and the AJAX service with the helper; the resolver now receives the already-fetched code row instead of refetching (once per level in multi-level checkout).

Testing done (local site, Stripe-less flows)

  • Percentage and fixed math on one-time, recurring, and trial-bearing levels; fixed discount clamping ($10 off a $1 level → $0); ineligible levels return null; legacy set_price codes resolve identically (including the level_id property MemberOrder depends on).
  • Live checkout page renders discounted price text; AJAX apply-code works single-level and sums correctly for multi-level (2+3 → $88 for 20% off $10 + $100).
  • Admin form round-trips all three types, JS toggles verified, snapshots written correctly and refreshed after a level price change.
  • Numeric code 2025 applies its set_price override at checkout after the lookup-order fix.

Notes for reviewers

  • The merge-subscription-delays-set-expiration-dates branch touches the same per-level UI region of adminpages/discountcodes.php; whichever lands second has a small mechanical conflict.
  • Pre-existing (untouched here): pmpro_check_discount_code_level_for_gateway_compatibility() queries pmpro_discount_codes_levels by an id column that doesn't exist, so its check-all branch silently passes — worth a separate issue.
  • @since TBD placeholders to be filled at release.

🤖 Generated with Claude Code

Discount codes now have a discount_type: the legacy set_price behavior
(per-level price overrides, unchanged and still the default), percentage
(e.g. 20% off), or fixed (e.g. $10 off). Formula codes store one rule on
pmpro_discount_codes (discount_value, apply_to_initial, apply_to_recurring)
shared across all eligible levels, and inherit the level's billing cycle,
billing limit, trial, and expiration.

Pricing is resolved through a new pmpro_get_discounted_level_for_code()
in includes/discount-codes.php, now used by pmpro_getLevelAtCheckout(),
the AJAX applydiscountcode service (per level, before multi-level
summing), MemberOrder::getMembershipLevel(), and the 2Checkout INS
handler. Percentage/fixed results are rounded with pmpro_round_price()
and clamped at 0. The pmpro_discount_code_level filter still runs at the
existing call sites after calculation, and a new
pmpro_get_discounted_level_for_code filter runs inside the resolver.

For formula codes, pmpro_discount_codes_levels keeps marking eligible
levels and its pricing columns hold a snapshot of the calculated prices
for backwards compatibility with code reading the table directly.
Snapshots refresh on code save and on level save.

The discount code edit screen gets Discount Type / Discount Amount /
Applies To fields (per-level pricing collapses to a note for formula
codes) plus Select All / Deselect All buttons for the levels list, the
list table shows the discount rule, and PMPro_Discount_Code and the
REST discount_code endpoint accept the new fields. Existing codes
default to set_price with no migration needed (dbDelta, db version 3.9).

Also fixes pmpro_get_discount_code() lookups to match code strings
before IDs so fully numeric codes resolve correctly, and reuses the
helper in place of duplicate raw code-lookup queries in checkout
completion and the AJAX service.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dparker1005
dparker1005 changed the base branch from dev to v3.9 August 11, 2026 19:20
dparker1005 and others added 3 commits August 11, 2026 15:36
…ount-codes

# Conflicts:
#	adminpages/discountcodes.php
#	includes/upgradecheck.php
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he panel

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@dparker1005 dparker1005 added this to the 3.9 milestone Aug 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant