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 coupon campaigns 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 an alias at TargetingEvaluator::evaluate(); the v4.18.0 migration rewrites stored trees in place on first load.


The targeting tree is the single source of truth for which campaigns gate a rule. A rule references campaigns via dino_coupon_applied nodes in its targeting_tree. Campaigns 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.
CampaignA row (or group of rows for bulk pools) in the wp_dino_coupon_codes table. Campaigns are standalone entities that own coupon codes.
LinkDerived at runtime: a campaign is “linked” to a rule when the rule’s targeting tree contains a dino_coupon_applied node referencing that campaign’s code. There is no denormalized rule_id column used for this purpose.
DraftA campaign 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 campaign

Section titled “Multiple rules can reference the same campaign”

Unlike the old architecture, there is no one-to-one constraint. Multiple rules may include the same campaign 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 /coupon-campaigns CouponCodesController::create_campaign()
{ campaign, activation_type } -> CouponCodeManager::create_campaign() (inserts row)
<- returns { success, 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)

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

The admin picks an existing campaign 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 campaign 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 campaign was created in step 1 but the rule was never Published with it, the campaign still exists in the DB as an unlinked (orphan) campaign. Orphans can be linked to other rules or archived.


The Coupons table shows which rule(s) reference each campaign 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 getCampaignsFromTree(tree) extracts campaign codes from dino_coupon_applied nodes (and the legacy dino_campaign_applied alias). The function findLinkedRule(campaignCode) 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 coupon codes match the campaigns listed in the node.
  3. The engine resolves coupon codes to campaigns via a DB lookup on the wp_dino_coupon_codes table (campaign column).
  4. The rule_id column is not read during evaluation.

ColumnTypePurpose
idBIGINT AUTO_INCREMENTPrimary key
uuidCHAR(36)Unique identifier
campaignVARCHAR(100)Campaign slug (e.g. SUMMER20). Groups bulk pool rows.
campaign_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 coupon code
url_tokenVARCHAR(100)URL activation token
usage_limitINTMax redemptions (0 = unlimited)
used_countINTCurrent redemption count
expires_atDATETIMEExpiry (NULL = never)

FileKey Methods
CouponCodeManager.phpcreate_campaign(), generate_codes(), list_campaigns(), archive_campaign()
CouponCodesController.phpREST endpoints: POST /coupon-campaigns (create), GET /coupon-campaigns (list)
RulesController.phpsave_rules() — saves rules with targeting trees
TargetingEvaluator.phpevaluate_dino_coupon_applied() — runtime coupon matching
FileRole
useCouponCreation.jsShared hook for creating/generating campaigns (used by both recipe and BYO)
CampaignSelector.jsRecipe campaign dropdown (single-select with Create/Generate)
FieldRenderers.jsBYO targeting field DinoCampaignTargetingField (multi-select dropdown with Create/Generate + chips)
CouponCampaigns/index.jsCoupons table — derives linked rule from targeting trees via findLinkedRule()
targetingTreeHelpers.jsgetCampaignsFromTree(), addCampaignNode(), removeCampaignNode()