Skip to content

Coupon ↔ Rule Linking

{/* AUTO-GENERATED from ../docs/coupon-rule-linking.md by scripts/sync-dev-docs.mjs — do not edit by hand. */}


How coupons and discount rules are connected.

Note on field naming: the canonical targeting node is dino_coupon_applied. Legacy dino_campaign_applied trees remain readable via a case-fallthrough alias in TargetingEvaluator::evaluate_node(); the v4.18.0 migration rewrites stored trees in place on first load.


The targeting tree is the single source of truth for which coupons gate a rule. A rule references coupons via dino_coupon_applied nodes in its targeting_tree. Coupons are independent entities with no rule awareness.


TermDescription
RuleA discount configuration stored in the dino_discounts_active_rules WP option (JSON array). Each rule has a UUID (id) and a targeting_tree that may contain dino_coupon_applied condition nodes.
CouponA row (or group of rows for bulk pools) in the wp_dino_codes table. Coupons are standalone entities that own redeemable codes.
LinkDerived at runtime: a coupon is “linked” to a rule when the rule’s targeting tree contains a dino_coupon_applied node referencing that coupon’s code. There is no denormalized rule_id column used for this purpose.
DraftA coupon referenced by a rule in the browser’s local draft state but not yet Published. Shown with a draft badge in the Coupons table.

Multiple rules can reference the same coupon

Section titled “Multiple rules can reference the same coupon”

Unlike the old architecture, there is no one-to-one constraint. Multiple rules may include the same coupon code in their targeting trees.


1. Create Coupon (inline, inside the Rule Wizard or BYO Targeting)

Section titled “1. Create Coupon (inline, inside the Rule Wizard or BYO Targeting)”

The admin types a code and clicks Create Coupon (or Generate Random).

Frontend Backend
-------- -------
POST /coupons CouponCodesController::create_campaign()
{ campaign, activation_type } -> CouponManager::create_coupon() (inserts row)
<- returns { success, coupon, campaign }
React adds a dino_coupon_applied (local targeting tree state only --
node to the rule's targeting_tree not saved to server until Publish)

The legacy POST /coupon-campaigns alias still resolves (with a Deprecation header) for one release; new integrations should target POST /coupons.

Result: The coupon exists in the DB. The rule’s targeting tree references it in local state.

The admin picks an existing coupon from the dropdown.

Frontend Backend
-------- -------
No API call. Nothing happens.
React adds/updates the (local targeting tree state only)
dino_coupon_applied node

Result: The rule references the coupon locally. No server state changes until Publish.

Updates local React state only. Nothing is posted to the server.

Frontend Backend
-------- -------
POST /dino-discounts/v1/rules RulesController::save_rules()
{ rules: [...] } -> sanitize & save to dino_discounts_active_rules
-> do_action('dino_discounts_rules_saved')
<- returns { success }

Result: The saved rules contain targeting trees with dino_coupon_applied nodes. The engine evaluates these at cart time. No separate linking/syncing step is needed.

Resets local state to last saved snapshot. If a coupon was created in step 1 but the rule was never Published with it, the coupon still exists in the DB as an unlinked (orphan) row. Orphans can be linked to other rules or archived.


The Coupons table shows which rule(s) reference each coupon by walking targeting trees client-side:

  1. Draft rules first (window.__ddLocalRules) — most up-to-date editor state.
  2. Saved rules fallback — from the rules API response (includes targeting_tree).

The utility getCouponsFromTree(tree) extracts coupon codes from dino_coupon_applied nodes (and the legacy dino_campaign_applied alias). The function findLinkedRule(couponCode) searches both sources.


When a shopper applies a coupon code at checkout:

  1. The engine evaluates each rule’s targeting tree.
  2. TargetingEvaluator::evaluate_dino_coupon_applied() checks if the applied codes match the coupons listed in the node.
  3. The engine resolves codes to coupons via a DB lookup on the wp_dino_codes table (coupon column).
  4. The rule_id column is not read during evaluation.

wp_dino_codes table (renamed from wp_dino_coupon_codes in v4.27.0)

Section titled “wp_dino_codes table (renamed from wp_dino_coupon_codes in v4.27.0)”
ColumnTypePurpose
idBIGINT AUTO_INCREMENTPrimary key
uuidCHAR(36)Unique identifier
couponVARCHAR(100)Coupon slug (e.g. SUMMER20). Groups bulk pool rows.
coupon_statusENUM('active','archived')Soft-delete flag
activation_typeVARCHAR(20)single_code, bulk_pool, or url_token
rule_idVARCHAR(36)Legacy column — always empty string. Not read or written by current code. Kept for schema compatibility.
codeVARCHAR(100)The typeable redemption code
url_tokenVARCHAR(100)URL activation token
usage_limitINTMax redemptions (0 = unlimited)
used_countINTCurrent redemption count
expires_atDATETIMEExpiry (NULL = never)

FileKey Methods
CouponManager.phpcreate_coupon(), generate_codes(), list_coupons(), archive_coupon()
CouponCodesController.phpREST endpoints: POST /coupons (create), GET /coupons (list). Legacy /coupon-campaigns/* alias kept for one release with deprecation headers.
RulesController.phpsave_rules() — saves rules with targeting trees
TargetingEvaluator.phpevaluate_dino_coupon_applied() — runtime coupon matching
FileRole
useCouponCreation.jsShared hook for creating/generating coupons (used by both recipe and BYO)
FieldRenderers.jsBYO targeting field DinoCouponTargetingField (multi-select dropdown with Create/Generate + chips)
Coupons/index.jsCoupons table — derives linked rule from targeting trees via findLinkedRule()
targetingTreeHelpers.jsgetCouponsFromTree(), addCouponNode(), removeCouponNode()