wc-variations-pricing-filters

Mutate WooCommerce variation prices via filters — pick the right layer (woocommerce_product_get_price for simple/non-variation products, woocommerce_product_variation_get_price for variations, the woocommerce_variation_prices_* family for the parent min/max aggregation that feeds the catalog "From X to Y" range), and the critical step of filtering woocommerce_get_variation_prices_hash whenever your price logic depends on context outside the default cache key (user role, custom toggle, time of day) — otherwise one user's filtered price gets cached and served to everyone. Use for role-based pricing, time-based discounts, or "filter variation prices but the min/max range doesn't update" debugging. Triggers on woocommerce_product_get_price, woocommerce_product_variation_get_price, woocommerce_variation_prices_*, woocommerce_get_variation_prices_hash, wc_var_prices.

Best use case

wc-variations-pricing-filters is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Mutate WooCommerce variation prices via filters — pick the right layer (woocommerce_product_get_price for simple/non-variation products, woocommerce_product_variation_get_price for variations, the woocommerce_variation_prices_* family for the parent min/max aggregation that feeds the catalog "From X to Y" range), and the critical step of filtering woocommerce_get_variation_prices_hash whenever your price logic depends on context outside the default cache key (user role, custom toggle, time of day) — otherwise one user's filtered price gets cached and served to everyone. Use for role-based pricing, time-based discounts, or "filter variation prices but the min/max range doesn't update" debugging. Triggers on woocommerce_product_get_price, woocommerce_product_variation_get_price, woocommerce_variation_prices_*, woocommerce_get_variation_prices_hash, wc_var_prices.

Teams using wc-variations-pricing-filters should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/wc-variations-pricing-filters/SKILL.md --create-dirs "https://raw.githubusercontent.com/nvdigitalsolutions/mcp-ai-wpoos/main/addons/pro/includes/bundled-skills/wc-variations-pricing-filters/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/wc-variations-pricing-filters/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How wc-variations-pricing-filters Compares

Feature / Agentwc-variations-pricing-filtersStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Mutate WooCommerce variation prices via filters — pick the right layer (woocommerce_product_get_price for simple/non-variation products, woocommerce_product_variation_get_price for variations, the woocommerce_variation_prices_* family for the parent min/max aggregation that feeds the catalog "From X to Y" range), and the critical step of filtering woocommerce_get_variation_prices_hash whenever your price logic depends on context outside the default cache key (user role, custom toggle, time of day) — otherwise one user's filtered price gets cached and served to everyone. Use for role-based pricing, time-based discounts, or "filter variation prices but the min/max range doesn't update" debugging. Triggers on woocommerce_product_get_price, woocommerce_product_variation_get_price, woocommerce_variation_prices_*, woocommerce_get_variation_prices_hash, wc_var_prices.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# WooCommerce: variation pricing filter chain

For plugin code that **modifies variation prices via filters** without rewriting the stored data — role-based pricing, time-based discounts, B2B tier overrides, currency-conversion display, "X% off when feature flag Y" experiments. The CRUD side (programmatic create/update of stored prices) is sibling skill `wc-variations-data`.

This is the topic with the most "I added the filter, why doesn't the catalog update" tickets. The reason is almost always: you filtered the wrong layer, or you didn't bust the cache, or you didn't add to the hash.

## Misconception this skill corrects

> "I'll add a `woocommerce_product_get_price` filter and variations will update everywhere."

`woocommerce_product_get_price` does not fire for `WC_Product_Variation`; variations use the type-specific `woocommerce_product_variation_get_price` hook. Even that single-variation hook **does NOT cover the parent variable product's min/max price range** — the range is built by iterating variations through the `woocommerce_variation_prices_*` chain, then cached in `wc_var_prices_<parent_id>`. If you only filter the single-product getter layer, the catalog "From €X to €Y" range stays at the unfiltered values until cache expires or until you also filter the aggregation layer.

## When to use this skill

Trigger when ANY of the following is true:

- Adding role-based, time-based, currency-based, or feature-flag-based pricing for variations.
- Reviewing a `woocommerce_product_*get_price` filter — verify it covers all the layers it needs to.
- Debugging "my variation price filter works on the variation page but the catalog still shows old prices".
- Debugging "my filter works for one user but caches incorrectly for everyone else".
- The diff or file contains: `woocommerce_product_get_price`, `woocommerce_product_variation_get_price`, `woocommerce_variation_prices_*`, `woocommerce_get_variation_prices_hash`, `wc_var_prices_`.

## The chain — top to bottom

```
                         Single product getter layer:
                  ┌──────────────────────────┐
                  │  $variation->get_price() │   WC_Product_Variation
                  └──────────────────────────┘
                                │
                                ▼
                woocommerce_product_variation_get_price

                  ┌──────────────────────────────┐
                  │  $simple->get_price()        │   simple/non-variation product
                  └──────────────────────────────┘
                                │
                                ▼
                woocommerce_product_get_price


                  ┌──────────────────────────────────────┐
                  │  $variable->get_variation_prices()   │   parent's min/max aggregation
                  └──────────────────────────────────────┘
                                │
                                ▼
                  Iterates each variation, calling:
                woocommerce_variation_prices_price            (variation->get_price( 'edit' ) raw active price)
                woocommerce_variation_prices_regular_price    (variation->get_regular_price( 'edit' ))
                woocommerce_variation_prices_sale_price       (variation->get_sale_price( 'edit' ))
                                │
                                ▼
                Accumulating price arrays:
                woocommerce_variation_prices_array            ({price, regular_price, sale_price} keyed by variation ID)
                                │
                                ▼
                Aggregated, sorted, cached as wc_var_prices_<parent_id>:
                woocommerce_variation_prices                  (final array used by get_price_html, range, etc.)


                  ┌─────────────────────────────────────────┐
                  │  Cache-key control                      │
                  │  woocommerce_get_variation_prices_hash  │   (CRITICAL when filter depends on
                  └─────────────────────────────────────────┘       context outside the default hash)
```

Source-verified in [wp-content/plugins/woocommerce/includes/data-stores/class-wc-product-variable-data-store-cpt.php](class-wc-product-variable-data-store-cpt.php) — lines 383, 401, 414, 490, 540, 653.

## Choosing the right filter for the job

| Goal | Filter to use |
|---|---|
| Change a single variation's price when displayed on the variation page (AJAX swap) | `woocommerce_product_variation_get_price` |
| Change a single variation's price for any `$variation->get_price()` call | Same as above |
| Change a simple / non-variation product's price | `woocommerce_product_get_price` |
| Change all product prices including variations | Hook both `woocommerce_product_get_price` and `woocommerce_product_variation_get_price`; they are different getter prefixes. |
| Make the parent's min/max RANGE reflect your override | `woocommerce_variation_prices_price` (+ `_regular_price`, `_sale_price` if you change those too) |
| Modify the FINAL aggregated array (sorting, removing entries, etc.) | `woocommerce_variation_prices` |
| Modify the accumulating aggregation arrays during each variation iteration | `woocommerce_variation_prices_array` |

The common composition: **filter both `woocommerce_product_variation_get_price` AND `woocommerce_variation_prices_price`** with the same logic. The first covers ad-hoc reads (variation page, cart, REST), the second covers the parent's aggregated range cache.

## The cache-key trap

`get_variation_prices( $for_display )` caches results in `wc_var_prices_<parent_id>` keyed by:

- A transient version (busted by `wc_delete_product_transients`).
- A price hash built from the default inputs: `$for_display` setting, current tax rate table, customer VAT-exempt status.
- Active callbacks on `woocommerce_variation_prices_price`, `woocommerce_variation_prices_regular_price`, and `woocommerce_variation_prices_sale_price` are also folded into the hash. WC 10.5+ has `woocommerce_use_legacy_get_variations_price_hash` to switch the callback-signature algorithm.

**If your filter depends on anything outside that hash** — current user's role, B2B group membership, a feature flag, the time of day, an A/B test bucket — your filtered result gets cached under the default-hash key. The first user to hit the page warms the cache with their variant; everyone else gets the same cached value. You'll see "the wrong user's price". This is the second-most-common bug after "filter doesn't fire on the parent range".

The fix is `woocommerce_get_variation_prices_hash`:

```php
add_filter( 'woocommerce_get_variation_prices_hash', static function (
    array $hash,
    WC_Product $product,
    bool $for_display
): array {
    // Add user role to the hash so each role gets its own cached entry.
    $user = wp_get_current_user();
    $hash['_role'] = $user->roles ? array_values( $user->roles ) : array( 'guest' );
    return $hash;
}, 10, 3 );
```

Anything you read in your price filters that varies per request → must be reflected in the hash.

## Concrete example — role-based 10% discount on variations

```php
// 1. Per-variation reads (variation page AJAX, cart, REST)
add_filter( 'woocommerce_product_variation_get_price', 'myplugin_apply_role_discount', 10, 2 );

// 2. Per-variation values inside the parent's aggregation
add_filter( 'woocommerce_variation_prices_price',         'myplugin_apply_role_discount_to_aggregation', 10, 3 );
add_filter( 'woocommerce_variation_prices_regular_price', 'myplugin_apply_role_discount_to_aggregation', 10, 3 );
add_filter( 'woocommerce_variation_prices_sale_price',    'myplugin_apply_role_discount_to_aggregation', 10, 3 );

// 3. CRITICAL — add user role to the cache key
add_filter( 'woocommerce_get_variation_prices_hash', 'myplugin_add_role_to_price_hash', 10, 3 );

function myplugin_should_discount(): bool {
    return current_user_can( 'wholesale_customer' );
}

function myplugin_apply_role_discount( $price, $product ) {
    if ( '' === $price || ! myplugin_should_discount() ) {
        return $price;
    }
    return (float) $price * 0.9; // 10% off
}

function myplugin_apply_role_discount_to_aggregation( $price, $variation, $product ) {
    if ( '' === $price || ! myplugin_should_discount() ) {
        return $price;
    }
    return (float) $price * 0.9;
}

function myplugin_add_role_to_price_hash( array $hash, $product, $for_display ): array {
    $user = wp_get_current_user();
    $hash['_myplugin_wholesale'] = current_user_can( 'wholesale_customer' ) ? 'yes' : 'no';
    return $hash;
}
```

The three layers all carry the same pricing logic, and the hash filter ensures the discounted result is cached under a separate key for wholesale users so guest visitors don't see (or get cached) the wholesale price.

## Cache busting — when the price logic itself changes

The hash mechanism handles per-request variation. But if the **rule itself** changes (you flip a "discount campaign active" option from off to on), you need to invalidate everything that was cached under the old rule:

```php
add_action( 'update_option_myplugin_discount_active', static function ( $old, $new ): void {
    if ( $old !== $new ) {
        // Bust ALL product transients site-wide. Granular invalidation isn't
        // straightforward — the version-key approach below is the WC-native way.
        WC_Cache_Helper::get_transient_version( 'product', true ); // true = regenerate
    }
}, 10, 2 );
```

`WC_Cache_Helper::get_transient_version( 'product', true )` regenerates the product transient version used by the variation-price cache validation, so the next `get_variation_prices()` read recomputes instead of trusting old `wc_var_prices_<id>` entries.

For per-product invalidation:

```php
wc_delete_product_transients( $parent_id ); // clears wc_var_prices_<id> and related product transients
```

## Critical rules

- **Apply pricing to BOTH the ad-hoc layer (`*_get_price`) AND the aggregation layer (`woocommerce_variation_prices_*`)** if you want catalog ranges to reflect the change. One layer alone leaves visible inconsistencies.
- **Add to `woocommerce_get_variation_prices_hash`** any input your filter reads that's not already in the default hash. Otherwise everyone gets the first user's cached result.
- **Use `woocommerce_product_variation_get_price` for variation changes**, `woocommerce_product_get_price` for simple/non-variation products. If the rule truly applies to both simple products and variations, register both hooks; the same product read will not hit both prefixes.
- **Filter `regular_price` and `sale_price` if you also filter `price`** in the aggregation chain. The active `_price` is the current effective price (sale price when an active sale exists, otherwise regular). In the aggregation data store, a sale price only counts as a sale when filtered `sale_price === price` and differs from `regular_price`; filtering only `price` can make sale state and range HTML inconsistent.
- **Bust caches when the RULE changes**, not when individual products change. Use `WC_Cache_Helper::get_transient_version( 'product', true )` for sweeping invalidation.
- **Test with WP_DEBUG and a fresh site cache.** "Works on my machine" means "works against a warm cache that already has my user's hash". Try as a guest, then as the role you're targeting; both should show the right price.
- **Don't try to filter `woocommerce_variation_prices_array` AND the individual `woocommerce_variation_prices_price` / `woocommerce_variation_prices_regular_price` / `woocommerce_variation_prices_sale_price` filters at the same time** with the same logic — double-application bugs. Pick one entry point per concern.

## Common mistakes

```php
// WRONG — single layer, parent range doesn't update
add_filter( 'woocommerce_product_variation_get_price', 'discount', 10, 2 );
// Variation page shows €18 (€20 - 10%)
// Catalog still shows "From €20 to €30" — wrong.

// RIGHT — both layers
add_filter( 'woocommerce_product_variation_get_price', 'discount', 10, 2 );
add_filter( 'woocommerce_variation_prices_price',      'discount_aggregation', 10, 3 );
add_filter( 'woocommerce_variation_prices_regular_price', 'discount_aggregation', 10, 3 );
add_filter( 'woocommerce_variation_prices_sale_price',    'discount_aggregation', 10, 3 );

// WRONG — context-dependent filter without hash modification
add_filter( 'woocommerce_variation_prices_price', function ( $price, $variation, $product ) {
    if ( current_user_can( 'wholesale' ) ) {
        return (float) $price * 0.9;
    }
    return $price;
}, 10, 3 );
// First wholesale user warms the cache → ALL subsequent users (including guests)
// see the wholesale price until cache invalidates.

// RIGHT — also filter the hash
add_filter( 'woocommerce_get_variation_prices_hash', function ( $hash, $product, $for_display ) {
    $hash['_role'] = current_user_can( 'wholesale' ) ? 'wholesale' : 'retail';
    return $hash;
}, 10, 3 );

// WRONG — filtering only _price, not regular/sale; "Sale!" badge breaks
add_filter( 'woocommerce_variation_prices_price', 'apply_discount', 10, 3 );
// In aggregation, sale_price only counts as a sale when filtered sale_price === price
// and differs from regular_price. Filtering only price can make sale state inconsistent.

// WRONG — filtering 'woocommerce_product_get_price' for variation-only logic
add_filter( 'woocommerce_product_get_price', function ( $price, $product ) {
    if ( $product->is_type( 'variation' ) ) {
        return /* discount */;
    }
    return $price;
} );
// This does not run for WC_Product_Variation. Variations use
// woocommerce_product_variation_get_price.

// WRONG — busting cache on every product save (overkill)
add_action( 'save_post_product', static function (): void {
    WC_Cache_Helper::get_transient_version( 'product', true );
} );
// Use wc_delete_product_transients( $product_id ) for per-product invalidation.
// Sweeping invalidation only when the RULE itself changes.

// WRONG — assuming get_variation_prices reads stored prices unfiltered
$prices = $variable->get_variation_prices( false );
// This array reflects ALL active filters. For "what was actually stored", read
// each variation's get_regular_price( 'edit' ) one by one (the 'edit' context
// bypasses the display filters).
```

## When you don't actually need a filter

Two scenarios where filters are the wrong tool:

1. **Permanent, store-wide price change.** Update the stored prices via CRUD (`wc-variations-data`). Filters are for context-dependent overrides.
2. **Time-bounded sale that's the same for everyone.** Use WC's built-in sale_price + sale_price_dates_from / _to. WC's existing logic handles the cache and display. Don't reinvent.

Filters are right for: per-user / per-role / per-currency / per-feature-flag overrides where the underlying stored price stays the canonical default.

## Cross-references

- Run **`wc-variations-data`** for the CRUD side — programmatic create/update of stored variation prices, when filters are not the right tool.
- Run **`wp-plugin-options-storage`** for the rule-storage option (where the "discount campaign active" toggle lives) — autoload tradeoffs apply.
- Run **`wp-security-audit`** if your filter reads admin-controllable input — capability checks should gate write paths, but filters that READ context (like role) are usually fine without explicit checks.

## What this skill does NOT cover

- The CRUD side of variation prices — see `wc-variations-data`.
- Currency / multi-currency plugins. They typically hook the same chain plus their own conversion layer; the principles here apply but the cache hash gets one more entry.
- Frontend variation switching JS (the `found_variation` event) — frontend territory, not server-side filters.
- WC Subscriptions / membership pricing layers — they apply their own filters on top; investigate per-plugin.
- Coupon-driven discounts — those run later in the cart pipeline, not on `get_price` reads.
- Product-level discounts on simple (non-variable) products — `woocommerce_product_get_price` covers them; the variation-specific chain doesn't apply.

## References

- Per-variation read: [class-wc-product-variation.php](class-wc-product-variation.php) overrides `get_hook_prefix()` to `woocommerce_product_variation_get_`; [abstract-wc-data.php](abstract-wc-data.php) applies that prefix in view context.
- Aggregation chain in the variable data store: [includes/data-stores/class-wc-product-variable-data-store-cpt.php](class-wc-product-variable-data-store-cpt.php) — lines 383 (`woocommerce_variation_prices_price`), 401 (`_regular_price`), 414 (`_sale_price`), 490 (`woocommerce_variation_prices_array`), 540 (`woocommerce_variation_prices`).
- Cache-key control: [class-wc-product-variable-data-store-cpt.php:653](class-wc-product-variable-data-store-cpt.php) — `woocommerce_get_variation_prices_hash`.
- `WC_Cache_Helper::get_transient_version`: [includes/class-wc-cache-helper.php](class-wc-cache-helper.php) — sweeping invalidation primitive.
- `wc_delete_product_transients`: [includes/wc-product-functions.php](wc-product-functions.php) — per-product invalidation.

Related Skills

wp-rocket-cache-rejection-and-filters

5
from nvdigitalsolutions/mcp-ai-wpoos

Customize WP Rocket behavior from a third-party plugin / theme via filter hooks — exclude URIs / cookies / user agents / REST API namespaces from caching, configure CDN URL rewrites, extend lazy load handling, override capability requirements, hook into Action Scheduler integration. Critical guidance — rocket_cache_reject_uri takes URI patterns (regex-like), NOT full URLs; rocket_cache_reject_* filters all expect arrays. The rocket_buffer filter is the FULL HTML output filter — extremely powerful but dangerous; one fatal error in the callback breaks every cached page until WP Rocket is disabled. Use when extending WP Rocket's default rules, NOT for cache invalidation (see wp-rocket-cache-invalidation). Triggers on rocket_cache_reject_, rocket_cdn_, do_rocket_lazyload, rocket_buffer, rocket_capacity, "exclude from WP Rocket cache".

wc-variations-data

5
from nvdigitalsolutions/mcp-ai-wpoos

Read, query, and write WooCommerce product variations correctly — the WC_Product_Variable (parent) vs WC_Product_Variation (child) class split, the parent's get_variation_prices( $for_display ) cached aggregation and how to bust the wc_var_prices_<id> transient, programmatic variation creation via WC_Product_Variation + set_parent_id + set_attributes + save followed by WC_Product_Variable::sync, the inherited-vs-own variation stock model (parent flag, variation flag, derived stock_status), get_available_variations for frontend variation data, and the $for_display tax-handling parameter that breaks display logic when forgotten. Use when scaffolding plugin code that creates / queries / modifies variations or when debugging stale variation data after programmatic writes. Triggers on WC_Product_Variation, WC_Product_Variable, get_variation_prices, get_available_variations, product_variation post type, wc_var_prices, sync_variation, or programmatic variation creation context.

webapp-testing

5
from nvdigitalsolutions/mcp-ai-wpoos

Toolkit for interacting with and testing local web applications using Playwright. Supports verifying frontend functionality, debugging UI behavior, capturing browser screenshots, and viewing browser logs.

web-artifacts-builder

5
from nvdigitalsolutions/mcp-ai-wpoos

Suite of tools for creating elaborate, multi-component claude.ai HTML artifacts using modern frontend web technologies (React, Tailwind CSS, shadcn/ui). Use for complex artifacts requiring state management, routing, or shadcn/ui components - not for simple single-file HTML/JSX artifacts.

valyu

5
from nvdigitalsolutions/mcp-ai-wpoos

Search the live web and 36+ specialised data sources including SEC filings, PubMed, ChEMBL, clinical trials, FRED economic indicators, and patent databases. Use when current, authoritative, or paywalled data is required.

ui-ux-pro-max

5
from nvdigitalsolutions/mcp-ai-wpoos

AI-powered design intelligence with 67 UI styles, 161 color palettes, 57 font pairings, 99 UX guidelines, and 25 chart types across 15+ tech stacks. Generates complete design systems for any product type with industry-specific reasoning rules.

theme-factory

5
from nvdigitalsolutions/mcp-ai-wpoos

Toolkit for styling artifacts with a theme. These artifacts can be slides, docs, reportings, HTML landing pages, etc. There are 10 pre-set themes with colors/fonts that you can apply to any artifact that has been creating, or can generate a new theme on-the-fly.

slack-gif-creator

5
from nvdigitalsolutions/mcp-ai-wpoos

Knowledge and utilities for creating animated GIFs optimized for Slack. Provides constraints, validation tools, and animation concepts. Use when users request animated GIFs for Slack like "make me a GIF of X doing Y for Slack."

skill-creator

5
from nvdigitalsolutions/mcp-ai-wpoos

Create new skills, modify and improve existing skills, and measure skill performance. Use when users want to create a skill from scratch, update or optimize an existing skill, run evals to test a skill, benchmark skill performance with variance analysis, or optimize a skill's description for better triggering accuracy.

shannon

5
from nvdigitalsolutions/mcp-ai-wpoos

Autonomous AI security pen testing. Executes real exploits against web applications to find SQL injection, XSS, SSRF, authentication flaws, and IDOR vulnerabilities. Reports only confirmed, reproducible findings — no false positives.

remotion

5
from nvdigitalsolutions/mcp-ai-wpoos

Create programmatic videos using React and Remotion. Translate natural language descriptions into working Remotion components for product demos, release announcements, explainer videos, and animated content.

planetscale

5
from nvdigitalsolutions/mcp-ai-wpoos

Design schemas and write queries for PlanetScale serverless MySQL using branch-based workflows. Ensures index coverage, avoids foreign key anti-patterns, and treats every schema change as a reviewable, reversible deploy request.