Skip to content

Architecture

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

Start here: if this is your first pass at the codebase, read agency-dev-overview.md first — it’s a 30-minute orientation layered on top of this deep-dive, with the pitfalls and extension-point pointers agencies need.

Last verified against source: 2026-04-21 (branch claude/cool-williams-92e9be).

Audience: intermediate-to-senior PHP/JS developers who haven’t read this plugin yet — agency partners extending it, and integrators making non-trivial changes. If you only need to list hooks, see hooks.md. For merchant-facing guidance, see merchant-guide.md. For the day-one agency orientation (quick-start, extension patterns, pitfalls), see agency-dev-overview.md.


graph TB
subgraph Browser["Browser — wp-admin"]
React["Admin React SPA<br/>src/ build to assets/admin.js"]
end
subgraph WP["WordPress runtime"]
Bootstrap["dino-discounts.php<br/>bootstrap"]
Plugin["Plugin<br/>DI container and wiring"]
Migrations["Migrations<br/>version-gated upgrades"]
Seeder["StarterData<br/>StarterRuleSeeder — onboarding"]
subgraph API["REST API — namespace dino-discounts/v1"]
RestAPI["RestAPI"]
Rules["RulesController"]
Settings["SettingsController"]
Snapshots["SnapshotsController"]
Preview["PreviewController"]
Perf["PerformanceController"]
Analytics["AnalyticsController"]
Taxonomy["TaxonomyController"]
CouponCodes["CouponCodesController"]
Onboarding["OnboardingController"]
CartScen["CartScenariosController"]
ProdSrch["ProductSearchController"]
end
subgraph Engine["Rule engine — stateless per request"]
RE["RulesEngine"]
RM["RuleMatcher"]
TE["TargetingEvaluator"]
UT["UsageTracker — DB-backed"]
RS["RuleSanitizer"]
Strat["Strategy/*<br/>Tiered, Bulk, XForY, MixMatch"]
AT["AnalyticsTracker<br/>background-writes orders"]
end
subgraph Cart["Storefront integration"]
CC["CartCoupons<br/>virtual coupons + AppliesDiscount trait"]
CCI["CouponCodeInterceptor<br/>priority 9, before virtual factory"]
CCM["CouponCodeManager<br/>persistent code pool"]
SAE["StoreApiExtension<br/>WC Blocks, read-only surface"]
SF["Storefront/*<br/>display, spend-more nudge, filter"]
end
subgraph Diag["Diagnostics and ops"]
PM["PerformanceMonitor"]
CD["ConflictDetector"]
Log["Observability Logger"]
Snap["History RuleSnapshots"]
end
end
subgraph OptDB["wp_options (shared table)"]
OptRules[("dino_discounts_active_rules")]
OptSettings[("dino_discounts_global_settings")]
OptVersion[("dino_discounts_version")]
OptScenarios[("dino_discounts_cart_scenarios")]
OptFirstPub[("dino_discounts_first_publish_ts")]
end
subgraph CustomDB["Custom tables (prefix wp_dino_)"]
TblUsageCounts[("wp_dino_usage_counts")]
TblUsageStats[("wp_dino_usage_stats")]
TblSnapshots[("wp_dino_rule_snapshots")]
TblAnalytics[("wp_dino_analytics_events")]
TblCoupons[("wp_dino_coupon_codes")]
TblGenLog[("wp_dino_coupon_generation_log")]
end
React -- "fetch /wp-json/dino-discounts/v1/*" --> RestAPI
Bootstrap --> Plugin
Plugin --> API
Plugin --> Engine
Plugin --> Cart
Plugin --> Diag
Plugin --> Migrations
Plugin --> Seeder
Rules --> RS
Rules --> Snap
Rules -- "update_option" --> OptRules
Settings -- "update_option" --> OptSettings
Onboarding --> Seeder
Seeder -- "update_option" --> OptRules
RE -- "get_option" --> OptRules
RE --> RM
RE --> TE
RE --> UT
RE --> Strat
UT -- "SELECT/UPDATE" --> TblUsageCounts
UT -- "INSERT/UPDATE" --> TblUsageStats
Snap -- "INSERT/UPDATE" --> TblSnapshots
Snap -- "restore writes" --> OptRules
AT -- "background INSERT" --> TblAnalytics
CCI --> CCM
CCM -- "SELECT/UPDATE" --> TblCoupons
CCM -- "INSERT" --> TblGenLog
CouponCodes --> CCM
CCI --> CC
CC --> RE
SAE --> CC
SF --> RE
Migrations -- "get/update_option" --> OptVersion
Migrations -- "CREATE TABLE IF NOT EXISTS" --> CustomDB

Every controller above extends AbstractController (includes/API/AbstractController.php) for its shared check_permission() callback; it is omitted from the graph to keep the router intent legible. RestAPI.php is the registrar; VirtualCart.php, VirtualProduct.php, and CartScenarioDefaults.php are data-shaping helpers rather than route owners.

LayerPurposeEntry points
BootstrapPlugin loader, constants, autoloaddino-discounts.php, includes/Plugin.php
Admin React SPAAll rule authoring UI, settings, analytics, historysrc/index.js, src/App.js, mounts to #dino-discounts-admin-app (rendered by includes/Admin.php)
REST APISingle source of truth for admin writes and previewsincludes/API/RestAPI.php registers routes on rest_api_init under dino-discounts/v1
Rule enginePer-request evaluation of rules → discount resultsincludes/Engine/RulesEngine.php
StorefrontCart-level application of discounts; Blocks + classicincludes/Cart/CartCoupons.php is the engine entry point. includes/Cart/StoreApiExtension.php is read-only — it surfaces CartCoupons::get_pending_discounts() into the WC Store API response for Blocks checkout, but does NOT invoke RulesEngine itself (includes/Cart/StoreApiExtension.php:152). includes/Storefront/* covers templates and the spend-more nudge.
Persistent coupon codesReal (non-virtual) codes a shopper can type at checkoutincludes/Cart/CouponCodeManager.php (code pool CRUD) + includes/Cart/CouponCodeInterceptor.php (priority-9 hook before virtual factory). Backed by wp_dino_coupon_codes and wp_dino_coupon_generation_log. See §3.5.
Analytics (write path)Order-level discount events written in the backgroundincludes/Engine/AnalyticsTracker.php — hooks woocommerce_checkout_order_processed, enqueues a single background action, writes to wp_dino_analytics_events.
Analytics (read path)Exposure to admin via RESTincludes/API/AnalyticsController.php — aggregates from wp_dino_analytics_events for the admin dashboard.
DiagnosticsPerformance monitor, conflict/caching warnings, structured logsincludes/Diagnostics/PerformanceMonitor.php, includes/ConflictDetector.php, includes/Observability/Logger.php
HistoryRule snapshots (undo / audit)includes/History/RuleSnapshots.php — persists to wp_dino_rule_snapshots (NOT to wp_options). Snapshot restore writes the chosen snapshot back into dino_discounts_active_rules.
Starter dataOnboarding seeds sample rules on first publishincludes/StarterData/StarterRuleSeeder.php — writes dino_discounts_active_rules on the onboarding flow (4th write path to that option).
MigrationsIdempotent, version-gated data upgrades + custom-table creationincludes/Migrations.php — runs on admin_init. Also owns create_custom_tables() which issues CREATE TABLE IF NOT EXISTS for all six wp_dino_* tables.

2. Data flow: admin save → storefront display

Section titled “2. Data flow: admin save → storefront display”
sequenceDiagram
autonumber
participant U as Merchant
participant R as React SPA
participant API as RulesController
participant Options as wp_options
participant Shopper
participant WC as WC_Cart
participant CC as CartCoupons
participant RE as RulesEngine
U->>R: Edit rule, click Save
R->>API: POST /dino-discounts/v1/rules
API->>API: RuleSanitizer sanitize_rules
API->>Options: update_option dino_discounts_active_rules
API->>API: do_action dino_discounts_rules_saved
API-->>R: 200 and sanitized payload
Shopper->>WC: Add or change cart item
WC-->>CC: woocommerce_before_calculate_totals
CC->>CC: fingerprint cart, check memo
alt cache hit
CC->>WC: reuse prior virtual coupons
else cache miss
CC->>RE: evaluate_cart with cart, total, currency, country
RE->>Options: get_option dino_discounts_active_rules (object-cached)
RE->>RE: apply Readers (kill-switch, stacking mode)
loop each active rule
RE->>RE: RuleMatcher matches_rule_criteria
RE->>RE: TargetingEvaluator evaluate_node (tree walk)
alt matches
RE->>RE: Strategy calculate, produces DiscountResult
end
end
RE-->>CC: array of DiscountResult
CC->>CC: mint virtual coupon code dino_dd_RULEID
CC->>WC: apply_coupon dino_dd_RULEID
end
WC-->>Shopper: Render totals with discount line

Key properties:

  • Write paths to dino_discounts_active_rules, in order of frequency.
    1. Admin UIRulesController::save_rules() (includes/API/RulesController.php:225), which delegates to RuleSanitizer and then calls update_option('dino_discounts_active_rules', …) at line 270. This is the only path the React app uses.
    2. Snapshot restoreRuleSnapshots::restore_snapshot() at includes/History/RuleSnapshots.php:231 writes the chosen snapshot back into the rules option (for the “undo” flow). The snapshots themselves live in wp_dino_rule_snapshots, not wp_options.
    3. Onboarding / starter dataStarterRuleSeeder::seed() at includes/StarterData/StarterRuleSeeder.php:99 writes the sample-rule set when a merchant accepts the onboarding offer. Runs at most once per site.
    4. Migrations — several steps in includes/Migrations.php (e.g. lines 628, 865, 1014, 1105) rewrite the option in place on version bumps, normalising legacy targeting aliases. These run once per version step under admin_init.
  • Read-path for rules is get_option only. The RulesEngine constructor (includes/Engine/RulesEngine.php:154) pulls the rule blob via wp_cache_get('rules', …) first and falls back to get_option('dino_discounts_active_rules', …) on a miss (line 161), then caches into the object cache. The resulting array is held on $this->active_rules for the life of the instance. ⚠️ Caveat: the engine does query custom tables during evaluation — UsageTracker reads wp_dino_usage_counts to enforce total-usage caps (see §3.3), and AnalyticsTracker writes wp_dino_analytics_events on order completion. The “no custom tables” rule applies only to the rule blob itself.
  • Two-layer discount memoisation in AppliesDiscount::apply_cart_discount() (includes/Cart/Traits/AppliesDiscount.php:136–163). Both layers key on the same cart fingerprint from build_cart_fingerprint() (includes/Cart/Traits/ManagesCartState.php:190). The fingerprint is an MD5 of cart_hash | subtotal | subtotal_incl_tax | currency | country | applied_real_coupons | u<user_id>. The applied_real_coupons list starts from CartCoupons::partition_applied_coupons() and adds back any Dino code whose activation_type is NOT 'virtual' (i.e. persistent / proxy codes DO contribute to the fingerprint; only dino_dd_* virtual codes are excluded, because they’re re-derived each pass and would cause self-invalidation). User ID 0 for guests, WP user ID for logged-in users.
    1. Request-scoped: $last_fingerprint / $last_discounts skip the engine on repeated woocommerce_before_calculate_totals passes within the same PHP request.
    2. Cross-request, 60-second transient: dino_eval_<fingerprint> survives to subsequent requests, so a shopper refreshing the cart page doesn’t re-run the engine.
  • Virtual coupons are the application mechanism. No row ever appears in wp_posts (shop_coupon) for a Dino discount.

graph LR
CC["CartCoupons<br/>uses AppliesDiscount trait"] --> RE[RulesEngine]
RE --> RM[RuleMatcher]
RE --> TE[TargetingEvaluator]
RE --> UT[UsageTracker]
RE --> Strategies
RM --> TE
RM --> UT
Strategies --> AS[AbstractStrategy]
AS --> Unified[UnifiedStrategy<br/>dispatches on rule.type]

Locations:

  • includes/Engine/RulesEngine.php — orchestrator. Holds active rules, dispatches to RuleMatcherStrategy.
  • includes/Engine/RuleMatcher.php — does the per-rule checks (active flag, date window, zone, usage caps, targeting root node).
  • includes/Engine/TargetingEvaluator.php — recursive tree walker over {op, children} / {field, op, values} nodes. Depth and node caps enforced as a DoS guard.
  • includes/Engine/UsageTracker.php — store-wide total usage counters. Custom-table-backed: reads wp_dino_usage_counts (O(1) per rule) and writes dimensional aggregates to wp_dino_usage_stats on order completion. The constructor warms a per-request cache ($usage_counts_cache) so each rule is fetched at most once per evaluate_cart() pass. Per-user limits were removed in v4.1.0 alongside the move from wp_options to custom tables (see class docblock, line 24).
  • includes/Engine/RuleSanitizer.php — canonicalises and strips anything the authoring UI didn’t produce; the ONLY gate between REST input and stored JSON.
  • includes/Engine/Strategy/*UnifiedStrategy dispatches internally on rule['type'] into one of four allocator branches (cart_level / group_prorated / set_walk_free_lines / set_walk_prorated). Takes the RulesEngineInterface in its ctor (AbstractStrategy::__construct) so it can reuse item-matching logic.

3.2 The four “Readers” — engine-level short-circuit gates

Section titled “3.2 The four “Readers” — engine-level short-circuit gates”

Terminology: “Readers” in this codebase refers specifically to the four engine-level short-circuits below, which decide whether any rule gets a chance to run at all. Per-rule checks (usage caps, date ranges, zones, etc.) are a separate layer inside RuleMatcher::matches_rule_criteria() and are listed in §3.2.1.

These four Readers are the load-bearing extension seams for merchants and integrators:

#GateWhereWhat it does
1Global kill-switchRulesEngine::evaluate_cart() around includes/Engine/RulesEngine.php:330, setting key wc_coupon_kill_switchWhen wc_coupon_kill_switch === 'disable_dino_when_wc_coupon_applied' AND CartCoupons::partition_applied_coupons() reports any non-Dino coupon in the cart, the engine returns [] and records a kill_switch_fired trace.
2Stacking modeSame method, just below the kill-switch. Reads stacking_mode from global settings; StackingMode::DISABLE_ALL returns [] immediately.Gives the merchant a “freeze all discounts” escape hatch without deactivating rules one-by-one.
3Targeting node wc_coupon_applied (alias coupon_applied)TargetingEvaluator::evaluate_node() case block at includes/Engine/TargetingEvaluator.php:780781. The v4.18.0 migration rewrites the legacy coupon_applied alias to the canonical wc_coupon_applied.Per-rule — the author opts an individual rule out when a real WC coupon is present. Narrower than the global kill-switch.
4Targeting node dino_coupon_applied (alias dino_campaign_applied)Same file, includes/Engine/TargetingEvaluator.php:816817, dispatching to evaluate_dino_coupon_applied() at line 1099. Aliases also migrated in v4.18.0.Per-rule conditional on whether another Dino rule already matched earlier in the same request — enables “apply this nudge only when nothing else fired.”

Note on naming. Earlier internal docs referred to a RuleMatcher::check_wc_coupon_stacking method; that symbol no longer exists. The responsibility moved up a level into the RulesEngine kill-switch (gate #1 above) during the v4.19.0 normalisation. When you see the old name in comments or test fixtures, treat it as a pointer to the kill-switch.

3.2.1 Per-rule gates (not Readers — but also gating)

Section titled “3.2.1 Per-rule gates (not Readers — but also gating)”

Once the engine is past the four Readers, each rule is tested by RuleMatcher::matches_rule_criteria() (includes/Engine/RuleMatcher.php:168). Any of these failing causes the individual rule to be skipped, and the reason is recorded on $last_fail_reason (line 72) for diagnostics:

GateCheckFail reason code
Total usage limitUsageTracker::check_usage_limits($rule) at line 172usage_limit
Scheduled date windowStart/end timestamps, store timezonescheduling
Time-of-day windowDaily repeating HH:MM window in store tzscheduling
Currency allow-list$rule['currencies'] vs active currencycurrency
Zone (country / region)ZoneMatcher::matches() at line 349zone
User role allow-listMatched against wp_get_current_user()user_role
Targeting treeTargetingEvaluator::evaluate_node()targeting

These gates run per rule per request; the Readers run once per request. If you’re debugging “why didn’t my rule apply?”, $last_fail_reason is the fast path — the Readers would have short-circuited the whole engine, so a rule that reaches the matcher and is rejected is always one of the per-rule gates.

3.3 Strategies and the AppliesDiscount trait

Section titled “3.3 Strategies and the AppliesDiscount trait”

Discount calculation is performed by a single UnifiedStrategy (includes/Engine/Strategy/UnifiedStrategy.php) extending the AbstractStrategy base (includes/Engine/Strategy/AbstractStrategy.php). UnifiedStrategy::evaluate() dispatches on rule['type'] into one of four allocator branches:

  • tieredcart_level allocator — thresholds on cart total or quantity.
  • bulkgroup_prorated allocator — per-product quantity breaks.
  • x_for_yset_walk_free_lines allocator — “buy X get Y” shapes.
  • mix_matchset_walk_prorated allocator — quantity-based across a set of products.

Discount application to the cart is a trait, not a strategy: includes/Cart/Traits/AppliesDiscount.php provides apply_cart_discount( $cart ) and is used only by CartCoupons (see includes/Cart/CartCoupons.php:69). Pulling this into a trait keeps CartCoupons focused on virtual-coupon bookkeeping while allowing the evaluation loop itself to be unit-tested in isolation. Other files that mention AppliesDiscount only reference it for parity (e.g. Storefront/StorefrontFilter.php mirrors its currency/country resolution order).

Dino never persists a shop_coupon post. Instead:

  1. After evaluation, CartCoupons mints a code dino_dd_<rule_id> (prefix constant CartCoupons::COUPON_PREFIX at includes/Cart/CartCoupons.php:79).
  2. VirtualCouponFactory::virtual_coupon_data() hooks woocommerce_get_shop_coupon_data and materialises the coupon from the in-memory $pending_discounts map keyed by code.
  3. CartCoupons::is_dino_coupon() (includes/Cart/CartCoupons.php:252) and CartCoupons::partition_applied_coupons() (line 303) split the cart’s applied coupons into { dino: [...], wc: [...] }. The kill-switch (Reader #1) and stacking logic both consume this partition.

This is what “virtual coupon partitioning” refers to: the engine treats dino_dd_* codes as engine output, and any other code as external input that may or may not trigger the kill-switch depending on settings.

3.5 Persistent coupon codes (real, not virtual)

Section titled “3.5 Persistent coupon codes (real, not virtual)”

Separate from the dino_dd_* auto-applied virtual coupons, the plugin also supports real codes a shopper types into the cart — single codes per rule, bulk single-use code pools, and URL-token codes. This is a distinct code path that sits in front of the virtual-coupon factory:

ComponentRoleFile
CouponCodeManagerStatic utility: CRUD for the code pool, bulk generation, CSV export, redemption bookkeeping. Owns the wp_dino_coupon_codes + wp_dino_coupon_generation_log tables.includes/Cart/CouponCodeManager.php
CouponCodeInterceptorHooks woocommerce_get_shop_coupon_data at priority 9 — one priority before the virtual-coupon factory (priority 10). When a shopper enters a code, it resolves the code against the DB pool, confirms the owning rule is active, and either injects a virtual coupon into the existing CartCoupons pipeline OR falls through silently on a collision with a native WC shop_coupon.includes/Cart/CouponCodeInterceptor.php
CouponCodesControllerREST surface for the admin UI to manage the pool.includes/API/CouponCodesController.php

Key properties:

  • Collision guard. If a native shop_coupon post exists with the same code, the interceptor falls through silently and sets a transient (dino_coupon_collision_*) so the merchant sees a warning on their next admin visit. Dino never overrides an existing WC coupon.
  • Same application pipeline. Once resolved, a persistent code ultimately flows through the same CartCoupons virtual-coupon machinery as an auto-applied rule. The discount calculation is identical.
  • No shop_coupon posts are ever created. Even for persistent codes — they live in wp_dino_coupon_codes, not wp_posts.

KeyPurposeWritten byRead by
dino_discounts_active_rulesThe rule blob (single JSON-serialised array)RulesController::save_rules, RuleSnapshots::restore_snapshot, StarterRuleSeeder::seed, Migrations::step_*RulesEngine constructor (via object-cache first, get_option on miss)
dino_discounts_global_settingsPlugin settings (kill-switch, stacking mode, zones, etc.)SettingsController, Migrations::step_*RulesEngine::evaluate_cart, CartCoupons, numerous readers
dino_discounts_versionCurrently-installed schema versionMigrations::maybe_upgrade step-by-stepMigrations::maybe_upgrade gate
dino_discounts_cart_scenariosPreview tool’s saved cart scenarios (one blob with defaults + custom)CartScenariosController, CartScenarioDefaultsPreviewController, admin Preview tab
dino_discounts_first_publish_tsUnix timestamp of the merchant’s first publish (onboarding signal)RulesController::save_rules (once)OnboardingController, WelcomePanel
dino_discounts_debugDeveloper flag; toggles debug loggingManually setRulesEngine around includes/Engine/RulesEngine.php:942

4.2 Custom tables (prefix {wp_prefix}dino_)

Section titled “4.2 Custom tables (prefix {wp_prefix}dino_)”

Created idempotently by Migrations::create_custom_tables() via dbDelta with CREATE TABLE IF NOT EXISTS. All six tables exist even on a fresh install.

TablePurposeWritten byRead by
dino_usage_countsOne row per rule — store-wide total count (hot path)UsageTracker::increment_usage on order completionUsageTracker::check_usage_limits, pre-warmed each evaluate_cart
dino_usage_statsDimensional aggregates (rule × currency × country × …)UsageTracker::record_usage_statsAdmin analytics; not read by the engine
dino_rule_snapshotsAppend-only undo/audit log of rule-state changesRuleSnapshots::create_snapshot on every rule saveSnapshotsController for the admin History tab; restore_snapshot for undo
dino_analytics_eventsOne row per order where a Dino discount appliedAnalyticsTracker::process_order_analytics via background action dino_discounts_track_analyticsAnalyticsController for dashboard aggregates
dino_coupon_codesPersistent coupon-code pool (single codes, bulk codes, URL tokens)CouponCodeManager CRUD, CouponCodeInterceptor on redemptionCouponCodeInterceptor::virtual_coupon_data at priority 9 (see §3.5)
dino_coupon_generation_logAudit row per bulk-generation batchCouponCodeManager::generate_bulk_codesAdmin code-management UI
LayerScopeInvalidation trigger
WordPress object cache (wp_cache_*, group dino_discounts_rules)Per-request; shared across processes if a persistent cache is installedupdate_option_dino_discounts_active_rules hook in Plugin::define_admin_hooks() flushes it
RulesEngine::$active_rulesPer instance (one per request)N/A — instance dies at end of request
UsageTracker::$usage_counts_cachePer evaluate_cart passN/A — reset per pass
CartCoupons::$last_fingerprint / $last_discountsPer requestCart contents change → fingerprint changes
Transient dino_eval_<fingerprint>Cross-request, 60 secondsNatural TTL, or dino_discounts_cache_flush action

Invalidation story for the rule blob. On save, RulesController fires dino_discounts_rules_saved. Plugin::define_admin_hooks() (around includes/Plugin.php:54–98) registers a cache-flush closure on update_option_dino_discounts_active_rules plus several WC hooks (save_post_product, edited_product_cat, woocommerce_shipping_zone_*, …) that invalidate the object-cache entry. The per-request static on the engine is inherently bounded to one request, so storefront evaluations following the save see the new rules on the next request.


Theme and plugin developers extend Dino Discounts via the hooks surface. The full list is auto-generated — always read it from hooks.md (regenerate with make docs-hooks).

Where to mount your listeners. Prefer a small companion plugin (/wp-content/plugins/my-shop-dino-hooks/) that loads on plugins_loaded with a priority later than Dino Discounts. A child theme’s functions.php works for trivial filters but is fragile: theme switches silently drop your customisations. Never add listeners inside the Dino Discounts plugin folder — they’ll be wiped on update.

The hooks that matter most when embedding into another theme/plugin:

  • dino_discounts_before_apply_coupons / dino_discounts_after_apply_coupons (actions) — wrap the per-request application phase.
  • dino_discounts_discount_applied (action) — fires after each virtual coupon is applied; use for attribution or analytics forwarding.
  • dino_discounts_rules_saved / dino_discounts_settings_saved (actions) — fire after admin writes land; use to invalidate external caches or warm CDN purges.
  • dino_discounts_coupon_label (filter) — rewrite the label rendered for a virtual coupon.
  • dino_discounts_evaluated_discounts (filter) — last-mile mutation of the DiscountResult[] before application (use sparingly; you will see every request).
  • dino_discounts_strategies (filter) — register additional strategies.
  • dino_discounts_targeting_result (filter) — observe targeting decisions for debugging / overrides.
  • dino_discounts_incompatible_plugins / dino_discounts_caching_plugins (filters) — extend the ConflictDetector lists so your own plugin shows up (or doesn’t).

The hook doc is verified against source via make docs-hooks-check, which also runs as part of make qa. If you ship a PR that adds a hook without regenerating the doc, CI fails.


includes/Migrations.php implements an idempotent, version-gated dispatcher:

  1. maybe_upgrade() runs on admin_init.
  2. It reads dino_discounts_version (default 1.0.0) and compares against the DINO_DISCOUNTS_VERSION constant defined in dino-discounts.php.
  3. Each step is a { key, after, run } record (see includes/Migrations.php:64–115). Steps run in array order; the after version is written to dino_discounts_version only once the step’s run callback has completed without an error, so an interrupted upgrade resumes cleanly on the next admin load.
  4. Steps that target a version higher than the currently-installed plugin (shipped but not-yet-released) are skipped — this prevents stored_version > DINO_DISCOUNTS_VERSION from ever happening.
  5. Migrations primarily rewrite the rule JSON in-place (normalising legacy targeting aliases, canonicalising settings keys). The engine and readers then only need to handle canonical shapes.

Version-skew for extenders: because all storefront reads go through the RulesEngine constructor’s cache+get_option path, downgrading the plugin while leaving migrated data in place is safe provided your downgrade target still understands the canonical schema. In practice, we support one major version back — older readers fall back to legacy aliases via the targeting tree switches above.


I want to …Start here
Add a new hook for my themehooks.md (auto-generated list)
Call the REST API from an external servicerest-api-reference.md
Override a shopper-facing templatetemplate-overrides.md
Understand bootstrap orderingarchitecture-bootstrap-notes.md
Troubleshoot cart/coupon interactionstroubleshooting.md
Day-one workflow for agency teamsagency-dev-overview.md (parallel WIP)

PatternWherePurpose
Strategyincludes/Engine/Strategy/*Discount calculation per rule type.
Trait compositionincludes/Cart/Traits/AppliesDiscount.phpKeeps CartCoupons lean while making the evaluation loop reusable.
Dependency injectionCartCoupons(RulesEngineInterface $engine), strategies receive the engineUnit tests substitute a fake engine; avoids hard coupling.
MemoisationCartCoupons::apply_cart_discount() fingerprint mapOne engine pass per distinct cart state per request.
Object-cache readRulesEngine::__construct() (wp_cache_getget_option)One DB read per request, often zero when the object cache is warm.
Observerdino_discounts_* hooksExtensibility without code modification.
Virtual proxyCartCoupons + VirtualCouponFactoryParticipate in WC’s coupon system with no DB entries.
Guard clauseTargetingEvaluator depth/node limitsProtects against pathological rule trees.
Version-gated migrationMigrations::maybe_upgrade()Idempotent, resume-safe upgrades.