Skip to content

Hooks for Discount Calculation

Filters for modifying discount calculation and price rounding.

Each rule can have a calculation_base_mode setting that determines which price the discount is calculated from:

ValueConstantDescription
current_priceCalculationBaseModes::CURRENT_PRICEDiscount from current price (after previous discounts) - default
original_priceCalculationBaseModes::ORIGINAL_PRICEDiscount always from regular price (regular_price)

Behavior Example — multiple Auto Price rules

Section titled “Behavior Example — multiple Auto Price rules”
Product: regular_price = $100
Rule 1 (Product): -20% → $80
Rule 2 (Bulk): -5%
With calculation_base_mode = 'current_price' on Rule 2:
→ 5% of $80 = $4 → final $76 (stacks on Rule 1)
With calculation_base_mode = 'original_price' on Rule 2:
→ 5% of $100 = $5 → final $95 (overrides Rule 1, recalculated from regular_price)

When multiple Auto Price rules match the same product, they are applied in priority order (lower menu_order first). Each rule’s calculation_base_mode decides whether it stacks on the running price or restarts from regular_price. The stop_processing flag halts iteration over lower-priority rules.

Product: regular_price = $100
Rule 1 (Product): -20% → price = $80
Rule 2 (BOGO): -50%
With calculation_base_mode = 'current_price':
→ 50% of 80 = $40 (discounts stack)
With calculation_base_mode = 'original_price':
→ 50% of 100 = $50 (discount from original price)
use WpifyWooDiscountRule\Services\Rules;
use WpifyWooDiscountRule\Enums\CalculationBaseModes;
// Within a rule
$calc_base_mode = $rule[ Rules::CALCULATION_BASE_MODE ] ?? CalculationBaseModes::CURRENT_PRICE;
if ( CalculationBaseModes::ORIGINAL_PRICE === $calc_base_mode ) {
// Use regular_price
$base = $product->get_regular_price();
} else {
// Use current price (may include other discounts)
$base = $product->get_price();
}

Modifying Data Before Discount Calculation

Section titled “Modifying Data Before Discount Calculation”

This filter allows you to modify data before the actual discount calculation.

apply_filters( 'wpify_woo_discounts_calculate_data', $data, $product, $rule, $range );
ParameterTypeDescription
$dataarrayData for discount calculation (original_price, calculation_base, discount_type, discount_value)
$productWC_ProductProduct the discount is applied to
$rulearrayRule settings (including calculation_base_mode)
$rangearrayCurrent discount range/tier

Adjusting Discount for a Specific Category

Section titled “Adjusting Discount for a Specific Category”
/**
* Increase discount for products in the "sale" category
*
* @param array $data Calculation data
* @param WC_Product $product Product
* @param array $rule Rule
* @param array $range Discount range
*
* @return array
*/
function boost_discount_for_sale_category( $data, $product, $rule, $range ): array {
if ( has_term( 'akce', 'product_cat', $product->get_id() ) ) {
// Increase percentage discount by 5%
if ( $data['discount_type'] === 'percentage' && isset( $data['discount_value'] ) ) {
$data['discount_value'] += 5;
}
}
return $data;
}
add_filter( 'wpify_woo_discounts_calculate_data', 'boost_discount_for_sale_category', 10, 4 );
/**
* Ensure a minimum discount of 10%
*
* @param array $data Calculation data
* @param WC_Product $product Product
* @param array $rule Rule
* @param array $range Discount range
*
* @return array
*/
function ensure_minimum_discount( $data, $product, $rule, $range ): array {
if ( $data['discount_type'] === 'percentage' ) {
$data['discount_value'] = max( 10, $data['discount_value'] ?? 0 );
}
return $data;
}
add_filter( 'wpify_woo_discounts_calculate_data', 'ensure_minimum_discount', 10, 4 );

This filter allows you to modify the result after the discount has been calculated.

apply_filters( 'wpify_woo_discounts_calculate_product_discount', $result, $product, $rule, $range );
ParameterTypeDescription
$resultarrayCalculation result (discounted_price, discount_amount, discount_percentage)
$productWC_ProductProduct
$rulearrayRule settings
$rangearrayDiscount range
/**
* Log all discount calculations
*
* @param array $result Calculation result
* @param WC_Product $product Product
* @param array $rule Rule
* @param array $range Discount range
*
* @return array
*/
function log_discount_calculation( $result, $product, $rule, $range ): array {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
error_log( sprintf(
'Discount calculated: Product #%d, Original: %s, Discounted: %s',
$product->get_id(),
$result['original_price'] ?? 'N/A',
$result['discounted_price'] ?? 'N/A'
) );
}
return $result;
}
add_filter( 'wpify_woo_discounts_calculate_product_discount', 'log_discount_calculation', 10, 4 );

This filter allows you to modify the rounding logic of the final discounted price.

apply_filters( 'wpify_woo_discounts_round_price', $rounded_price, $original_price, $rounding_mode, $rule );
ParameterTypeDescription
$rounded_pricefloatPrice after rounding (according to the configured mode)
$original_pricefloatPrice before rounding (after discount)
$rounding_modestringMode: whole, ending_99, ending_90, ending_9
$rulearrayComplete rule data
/**
* Always round down for VIP customers
*
* @param float $rounded_price Rounded price
* @param float $original_price Price before rounding
* @param string $rounding_mode Rounding mode
* @param array $rule Rule
*
* @return float
*/
function vip_rounding( $rounded_price, $original_price, $rounding_mode, $rule ): float {
if ( current_user_can( 'vip_member' ) ) {
return floor( $original_price );
}
return $rounded_price;
}
add_filter( 'wpify_woo_discounts_round_price', 'vip_rounding', 10, 4 );
/**
* Round price to multiples of 10
*
* @param float $rounded_price Rounded price
* @param float $original_price Price before rounding
* @param string $rounding_mode Rounding mode
* @param array $rule Rule
*
* @return float
*/
function round_to_tens( $rounded_price, $original_price, $rounding_mode, $rule ): float {
return round( $original_price / 10 ) * 10;
}
add_filter( 'wpify_woo_discounts_round_price', 'round_to_tens', 10, 4 );

Controlling Discount Application (Subscriptions)

Section titled “Controlling Discount Application (Subscriptions)”

Filter to control whether a discount should be applied. Primarily used for WooCommerce Subscriptions.

apply_filters( 'wpify_woo_discounts_should_apply_discount', $should_apply, $rule );
ParameterTypeDescription
$should_applyboolDefault value (true)
$rulearrayRule data including renewal_behavior
/**
* Never apply bulk discounts on renewal orders
*
* @param bool $should_apply Default value
* @param array $rule Rule
*
* @return bool
*/
function no_bulk_on_renewals( $should_apply, $rule ): bool {
if ( function_exists( 'wcs_cart_contains_renewal' ) && wcs_cart_contains_renewal() ) {
if ( ( $rule['rule_type'] ?? '' ) === 'bulk' ) {
return false;
}
}
return $should_apply;
}
add_filter( 'wpify_woo_discounts_should_apply_discount', 'no_bulk_on_renewals', 10, 2 );

Available renewal behavior values:

  • first_order_only - Discount only on the first order
  • renewals_only - Discount only on renewal orders
  • both - Discount on both (default)

This filter allows you to modify the list of target products after they have been prepared.

apply_filters( 'wpify_woo_discounts_targets_prepared', $targets, $rule );
ParameterTypeDescription
$targetsarrayArray of target products
$rulearrayRule settings
/**
* Exclude low-margin products from discount targets
*
* @param array $targets Target products
* @param array $rule Rule
*
* @return array
*/
function exclude_low_margin_products( $targets, $rule ): array {
return array_filter( $targets, function( $target ) {
$product_id = $target['product_id'] ?? 0;
$margin = get_post_meta( $product_id, '_profit_margin', true );
return $margin > 10; // Exclude products with margin below 10%
} );
}
add_filter( 'wpify_woo_discounts_targets_prepared', 'exclude_low_margin_products', 10, 2 );

This filter allows you to add custom metadata to subrules (discount tiers).

apply_filters( 'wpify_woo_discounts_subrule_metadata', $metadata, $subrule, $rule );
ParameterTypeDescription
$metadataarraySubrule metadata
$subrulearraySubrule data
$rulearrayRule settings
/**
* Add custom metadata for tracking
*
* @param array $metadata Metadata
* @param array $subrule Subrule
* @param array $rule Rule
*
* @return array
*/
function add_tracking_metadata( $metadata, $subrule, $rule ): array {
$metadata['campaign_id'] = $rule['campaign_id'] ?? '';
$metadata['applied_at'] = current_time( 'mysql' );
return $metadata;
}
add_filter( 'wpify_woo_discounts_subrule_metadata', 'add_tracking_metadata', 10, 3 );

This filter allows you to modify the maximum quantity for a discount.

apply_filters( 'wpify_woo_discounts_quantity_limit', $limit, $product, $rule );
ParameterTypeDescription
$limitintQuantity limit
$productWC_ProductProduct
$rulearrayRule settings
/**
* Limit quantity for wholesale customers
*
* @param int $limit Limit
* @param WC_Product $product Product
* @param array $rule Rule
*
* @return int
*/
function limit_wholesale_quantity( $limit, $product, $rule ): int {
if ( current_user_can( 'wholesale_customer' ) ) {
return min( $limit, 100 ); // Max 100 pcs for wholesale
}
return $limit;
}
add_filter( 'wpify_woo_discounts_quantity_limit', 'limit_wholesale_quantity', 10, 3 );

This filter allows you to modify the fee/discount application type in the cart.

apply_filters( 'wpify_woo_discounts_fee_application_type', $type, $rule );
ParameterTypeDescription
$typestringApplication type (fixed, percent)
$rulearrayRule settings
/**
* Change application type for specific rules
*
* @param string $type Application type
* @param array $rule Rule
*
* @return string
*/
function change_fee_application_type( $type, $rule ): string {
// Use percentage for rules with the high_value tag
if ( in_array( 'high_value', $rule['tags'] ?? [], true ) ) {
return 'percent';
}
return $type;
}
add_filter( 'wpify_woo_discounts_fee_application_type', 'change_fee_application_type', 10, 2 );

Compute the per-item discounted prices for a synthetic product selection — useful for custom selectors, blocks or templates that need to display the price of a not-yet-added-to-cart selection with all stacked price-modifying rules applied (Product + Bulk + Bundle).

Unlike a filter, this is a service method on StackedPriceResolver. Resolve it through the plugin’s DI container:

use WpifyWooDiscountRule\Modules\DiscountRules\Utils\StackedPriceResolver;
$resolver = wpify_woo_discount_rules_container()->get( StackedPriceResolver::class );
$result = $resolver->compute_selection_price( $lines, $options );

A list of selection lines. Each line is an associative array:

KeyTypeRequiredDefaultNotes
productWC_Productone of these twoUsed directly when present (no DB roundtrip).
product_idintone of these twoIf product missing, the service calls wc_get_product( product_id ).
qtyintno1qty <= 0 → line silently skipped.

Normalisation rules (applied in order):

  1. If both product and product_id are provided, product wins.
  2. If neither is provided → line silently skipped.
  3. If wc_get_product() returns false for product_id → line silently skipped.
  4. Duplicate products across multiple lines (same effective product_id) → merged, qty summed.
KeyTypeDefaultEffect
include_cartboolfalseWhen true, the current WC()->cart content is merged into the qualifying pool. Cart entries contribute to Bulk tier counts and Bundle slot matching, but never appear in the per-item output.

Always an array (never null). For empty input or input where every line is silently skipped:

[
'items' => [],
'totals' => [
'regular_total' => 0.0,
'discounted_total' => 0.0,
'savings' => 0.0,
],
]

For a non-empty selection:

[
'items' => [
123 => [
'product' => WC_Product,
'product_id' => 123,
'qty' => 2,
'unit_regular_price' => 1000.00,
'unit_discounted_price' => 720.00,
'line_regular_total' => 2000.00,
'line_discounted_total' => 1440.00,
'savings' => 560.00,
],
// ... one entry per unique sent product
],
'totals' => [
'regular_total' => 2000.00,
'discounted_total' => 1440.00,
'savings' => 560.00,
],
]

items is keyed by product_id so templates can index directly ($result['items'][ 123 ]). PHP preserves insertion order, so iteration yields products in the order they first appeared in $lines. All numeric values are rounded to wc_get_price_decimals() precision.

The service returns raw numbers only — tax presentation (wc_get_price_to_display()) and HTML formatting (wc_price(), wc_format_sale_price()) are the template’s responsibility.

use WpifyWooDiscountRule\Modules\DiscountRules\Utils\StackedPriceResolver;
/**
* Render the bundle selector price inside a custom template.
*
* @param WC_Product[] $selected_products Products the customer picked.
*/
function render_bundle_selector_price( array $selected_products ): void {
$resolver = wpify_woo_discount_rules_container()->get( StackedPriceResolver::class );
$lines = array_map(
fn( WC_Product $p ) => [ 'product' => $p, 'qty' => 1 ],
$selected_products
);
$result = $resolver->compute_selection_price( $lines );
if ( empty( $result['items'] ) ) {
return;
}
echo wc_format_sale_price(
wc_price( $result['totals']['regular_total'] ),
wc_price( $result['totals']['discounted_total'] )
);
}

Pass include_cart to compute prices as if the selection were added to the current cart:

$result = $resolver->compute_selection_price(
$lines,
[ 'include_cart' => true ]
);
  • Rule triggers see combined quantity (sent + cart). A Bulk tier or Bundle slot requirement can qualify based on the merged pool even when the sent selection alone would not satisfy it.
  • Rule discounts are applied only to sent products — cart-only entries never appear in items[].

Migration from wpify_woo_discounts_bundle_set_price

Section titled “Migration from wpify_woo_discounts_bundle_set_price”

The filter wpify_woo_discounts_bundle_set_price and the public method Bundle::calculate_set_price() have been removed and are replaced by this service method.

// OLD — no longer works
$price = apply_filters(
'wpify_woo_discounts_bundle_set_price',
null,
$selected_products,
[ 'application_mode' => 'auto_price' ]
);
if ( null === $price ) { return; }
echo $price['html'];
// NEW
use WpifyWooDiscountRule\Modules\DiscountRules\Utils\StackedPriceResolver;
$resolver = wpify_woo_discount_rules_container()->get( StackedPriceResolver::class );
$lines = array_map( fn( $p ) => [ 'product' => $p, 'qty' => 1 ], $selected_products );
$result = $resolver->compute_selection_price( $lines );
if ( empty( $result['items'] ) ) { return; }
echo wc_format_sale_price(
wc_price( $result['totals']['regular_total'] ),
wc_price( $result['totals']['discounted_total'] )
);

The new method applies all active price-modifying rules (Product + Bulk + Bundle), not just Bundle. The output is per-item (keyed by product_id) plus aggregate totals, instead of the previous Bundle-specific payload.


You can add custom functions either in your child theme’s functions.php or using the Code Snippets plugin.