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 under the section-shaped rule model.

A rule names the coupons that unlock it in a flat trigger.coupon_ids: string[] | null field (includes/Engine/Schema/TriggerSchema.php). Coupon-side data (codes, activation, usage limits, typed-code resolution) lives in the Coupon entity (includes/Cart/CouponEntity.php, stored by CouponRepository).


The rule’s trigger.coupon_ids field is the single source of truth for which coupons gate it. A rule fires when any one of the listed coupons is applied to the cart (OR-of-values within the field; null or [] ≡ no coupon requirement). Coupon rows carry no reference to rules — multiple rules may reference the same coupon id.

The Coupon entity owns: the coupon id, activation kind (single_code / url_token / bulk), usage limit, status (active / expired, shown as archived in the admin), and the typed-code-to-coupon-id resolver. The rule owns: which coupon ids unlock it.


TermDescription
RuleA section-shaped discount configuration stored in the dino_discounts_active_rules option. A rule may set trigger.coupon_ids: ["BF20", "BLACKFRIDAY"] to require one of those coupons to be applied.
CouponA row in the wp_dino_coupons table, keyed by coupon_id (e.g. "BF20", stored uppercase). Bulk coupons also own a pool of generated codes in wp_dino_bulk_codes. Codes a customer types resolve to the parent coupon id via CouponResolver.
LinkDerived at runtime: a coupon is “linked” to a rule when the rule’s trigger.coupon_ids array contains that coupon’s id. No table stores a rule id against a coupon.
Draft linkA coupon referenced only by an unpublished editor change. Shown in the Coupons table as a draft pill with the tooltip “Linked to an unsaved discount. Publish to finalise.”

Multiple rules can reference the same coupon

Section titled “Multiple rules can reference the same coupon”

There is no one-to-one constraint. Multiple rules may list the same coupon id in their trigger.coupon_ids. Conversely a rule may require one of several coupons (OR-of-values).

trigger.coupon_ids: ["A", "B"] fires when either A or B is applied. Cross-coupon AND (“must apply A and B”) is not expressible.


1. Create coupon (inline, inside the rule editor’s Trigger section)

Section titled “1. Create coupon (inline, inside the rule editor’s Trigger section)”

In What triggers it?, the admin ticks Type a coupon code at checkout, opens the coupon picker and chooses + Create new coupon. That opens the same New Coupon modal the Coupons tab uses, where Generate Random Code fills in a code and Create Coupon saves it.

Frontend Backend
-------- -------
GET /coupons/generate-random CouponCodesController::generate_random_coupon_code()
(optional)
POST /coupons CouponCodesController::create_campaign()
{ coupon_id, activation: {kind}, -> CouponRepository::save() (inserts the coupon)
... } -> bulk only: CouponManager::generate_codes_with_report()
<- returns { success, coupon, coupon_entity, ... }
React adds the coupon id to the (editor state + debounced draft autosave --
rule's trigger.coupon_ids list not live until Publish)

Result: The coupon exists in the DB. The rule references its id in the editor.

The admin picks an existing coupon from the same picker.

Frontend Backend
-------- -------
No create call. Nothing changes in the coupon tables.
React adds the coupon id to (editor state + draft autosave)
trigger.coupon_ids

Result: The rule references the coupon in the editor. Nothing is live until Publish.

Unpublished edits are autosaved (debounced, ~2 s) with PUT /dino-discounts/v1/rules/draft into the dino_discounts_draft option. Drafts are never read by the engine.

Frontend Backend
-------- -------
POST /dino-discounts/v1/rules RulesController::save_rules()
{ rules: [...] } -> enforces the 200-rule cap
-> sanitises each rule (RuleSanitizer) and
schema-checks it; incomplete rules are
skipped and reported, not published
-> writes dino_discounts_active_rules
-> archives coupons that lost their last
referencing rule
-> do_action('dino_discounts_rules_saved', $summaries)
<- returns { rules, skipped, archived_coupons, ... }

Result: The saved rules carry trigger.coupon_ids. The engine reads it at cart time. No separate linking step is needed.

A coupon that was referenced by the previously published rules and is referenced by none of the new ones is archived (status expired) by CouponManager::archive_coupons_no_longer_referenced(). It moves to the Coupons tab’s Archived filter, where it can be restored with its codes and redemption history.

Resets the editor to the last published snapshot and deletes the server draft (DELETE /rules/draft). A coupon created in step 1 still exists: if no published rule references it, the Coupons table shows it as “No discount linked”. It can be linked to another rule or archived.


The Coupons table works out which rule(s) reference each coupon client-side, by reading trigger.coupon_ids from two sources (findLinkedRules() in src/components/Coupons/couponRuleLinking.js):

  1. Editor rules (window.__ddLocalRules) — the current unpublished editor state. A link found only here is flagged as a draft.
  2. Published rules — from the rules API response.

Each source is a flat membership check on the uppercased trigger.coupon_ids values. The server draft option is not used for this.


When a shopper applies a coupon code:

  1. WooCommerce’s woocommerce_get_shop_coupon_data filter reaches CouponCodeInterceptor::intercept_coupon_code() — at priority 9 by default, or 100 when the coupon-precedence setting is wc_first (default dino_first).
  2. The interceptor resolves the typed code (via TypedCodeResolver → CouponResolver::resolve()) against the Coupon entity: a single_code coupon id, a url_token activation token, or a generated code in a bulk pool. It returns the parent coupon id(s).
  3. RulesEngine seeds the cart context’s applied_coupon_ids with those ids, and TriggerEvaluator::evaluate_coupon_ids() tests set membership: array_intersect($applied, $rule['trigger']['coupon_ids']) must be non-empty.

ColumnTypePurpose
coupon_idVARCHAR(100)Primary key — the stable id rules reference in trigger.coupon_ids. For single_code coupons it is also the code customers type.
nameVARCHAR(255)Optional display name
statusENUMactive or expired (archived). The enum also lists a retired paused value that nothing writes.
activation_kindENUM('single_code','url_token','bulk')How the coupon is redeemed
activation_tokenVARCHAR(100)URL token for url_token coupons (unique)
activation_bulk_idVARCHAR(36)Pool id for bulk coupons (unique)
url_apply_noticeVARCHAR(255)Optional notice shown when applied by URL
usage_limitINTMax redemptions (NULL = unlimited)
used_countINTCurrent redemption count
created_atDATETIMECreation time

One row per generated code in a bulk pool: id, uuid, coupon_id (parent coupon), generation_log_id, code (unique), usage_limit, used_count, used_by, used_at, expires_at, created_at. There is no database foreign key — CouponRepository::delete() removes a coupon’s codes before the coupon.

Both tables are created in Migrations::create_custom_tables() (includes/Migrations.php).


FileKey methods
includes/API/CouponCodesController.phpREST endpoints under /coupons (GET list, POST create, per-coupon archive / restore / delete / generate / export, generate-random, wc-clash)
includes/Cart/CouponRepository.phpfind_by_id(), save(), delete(), get_bulk_codes()
includes/Cart/CouponManager.phpgenerate_codes_with_report(), list_coupons(), archive_coupon(), restore_coupon(), archive_coupons_no_longer_referenced(), find_rules_for_coupon()
includes/Cart/CouponResolver.phpresolve() — typed code / URL query → coupon id(s)
includes/API/RulesController.phpsave_rules() — sanitises and schema-checks rules, persists them, archives coupons that lost their last referrer
includes/Engine/Schema/TriggerSchema.phpSchema fragment that declares trigger.coupon_ids: string[] | null
includes/Engine/Evaluators/TriggerEvaluator.phpevaluate_coupon_ids() — tests trigger.coupon_ids against the applied coupon ids
FileRole
src/components/RuleBuilder/Trigger/TriggerSection.js”Type a coupon code at checkout” picker writing to trigger.coupon_ids, with + Create new coupon
src/components/Coupons/NewCouponModal.jsCreate-coupon modal used by both the Trigger section and the Coupons tab
src/utils/ruleCouponHelpers.jsgetCouponsFromRule(), addCouponToRule(), removeCouponFromRule()
src/components/Coupons/couponRuleLinking.jsfindLinkedRules() — derives linked rules for the Coupons table from editor + published rules

WooCommerce is a trademark of Automattic, Inc. WordPress is a trademark of the WordPress Foundation. Dino Discounts is not affiliated with or endorsed by Automattic or the WordPress Foundation.

Contact · Report a bug