WooCommerce Dynamic Pricing: Tiered Discounts and Role-Based Pricing

WooCommerce’s price hooks make it straightforward to implement dynamic pricing without a premium plugin: tiered quantity discounts (buy 5 get 10% off, buy 10 get 20% off), role-based prices (wholesale vs retail), and time-limited flash sales. All three patterns hook into woocommerce_product_get_price and woocommerce_cart_item_price.

Problem: A WooCommerce store needs tiered pricing — orders over $100 get 10% off, wholesale customers get 20% off — but WooCommerce's native discount system only supports flat coupons and does not apply discounts automatically based on cart total or user role.

Solution: Use the woocommerce_cart_calculate_fees action to add a negative fee (effectively a discount) based on WC()->cart->get_subtotal() and the current user's role from current_user_can(). Combine with woocommerce_product_get_price for role-based product-level pricing that overrides the base price before it reaches the cart.


The code below implements tiered quantity pricing stored in product meta, role-based price overrides, and a flash-sale price that activates between two timestamps — all without touching the database schema.


get_cart() as $item ) {
        $product  = $item['data'];
        $qty      = (int) $item['quantity'];
        $tiers    = $product->get_meta( '_qty_discount_tiers' );  // array of [min_qty, pct]
        if ( empty( $tiers ) || ! is_array( $tiers ) ) {
            continue;
        }

        $discount_pct = 0.0;
        foreach ( $tiers as $tier ) {
            if ( $qty >= (int) $tier['min_qty'] ) {
                $discount_pct = (float) $tier['pct'];
            }
        }
        if ( $discount_pct > 0 ) {
            $discount = - round( (float) $product->get_price() * $qty * $discount_pct / 100, 2 );
            $cart->add_fee(
                sprintf( 'Qty discount (%s%%)', $discount_pct ),
                $discount,
                false   // not taxable
            );
        }
    }
} );

// ── 2. Role-based price override ──────────────────────────────────────────
add_filter( 'woocommerce_product_get_price', function ( string $price, WC_Product $product ): string {
    if ( ! is_user_logged_in() ) {
        return $price;
    }
    $user        = wp_get_current_user();
    $role_prices = $product->get_meta( '_role_prices' );  // [ 'wholesale' => '19.99', ... ]
    if ( ! is_array( $role_prices ) ) {
        return $price;
    }
    foreach ( $user->roles as $role ) {
        if ( isset( $role_prices[ $role ] ) && $role_prices[ $role ] !== '' ) {
            return (string) $role_prices[ $role ];
        }
    }
    return $price;
}, 10, 2 );

// ── 3. Flash-sale price between two timestamps ────────────────────────────
add_filter( 'woocommerce_product_get_price', function ( string $price, WC_Product $product ): string {
    $flash_price = $product->get_meta( '_flash_price' );
    $flash_start = (int) $product->get_meta( '_flash_start' );  // Unix timestamp
    $flash_end   = (int) $product->get_meta( '_flash_end' );

    if ( $flash_price && $flash_start && $flash_end ) {
        $now = time();
        if ( $now >= $flash_start && $now <= $flash_end ) {
            return (string) $flash_price;
        }
    }
    return $price;
}, 20, 2 );


NOTE: Filtering woocommerce_product_get_price affects every call to $product->get_price(), including those used for min/max price range display in variable products — add an early return when $product->is_type('variable') to avoid corrupting the price range display on shop archives.