add_hooks(); } /** * Adds multiple hooks to apply the membership discount * * @return void */ public function add_hooks() { // Display the product price and membership price by default add_filter( 'woocommerce_get_price_html', array( $this, 'display_membership_price' ), 10, 2 ); // Display the membership price for subscriptions add_filter( 'woocommerce_subscriptions_product_price_string', array( $this, 'display_membership_price_for_subscriptions' ), 10, 3 ); // Replace the product prices with the membership prices add_filter( 'woocommerce_product_get_price', array( $this, 'replace_product_price_with_discount' ), 20, 2 ); add_filter( 'woocommerce_product_variation_get_price', array( $this, 'replace_product_price_with_discount' ), 20, 2 ); // Display the original product price below the product title on the cart page // (Without this, the sale price will show as the original price) add_filter( 'woocommerce_cart_product_price', array( $this, 'apply_member_discount_on_cart_price' ), 10, 2 ); // Apply membership price to displayed cart subtotals // (Works for displayed subtotal on cart. Does not affect calculations) add_filter( 'woocommerce_cart_product_subtotal', array( $this, 'apply_member_discount_on_cart_product_subtotal' ), 10, 4 ); // ❌ These filters did not work as expected: // --- add_filter( 'woocommerce_variable_subscription_price_html', array( $this, 'display_membership_price_for_subscriptions' ), 10, 2 ); // --- add_filter( 'woocommerce_get_discounted_price', array( $this, 'apply_member_discount_on_cart_item' ), 10, 4 ); // --- add_action( 'woocommerce_cart_calculate_fees', array( $this, 'add_membership_discount_on_checkout' ) ); // --- add_filter( 'woocommerce_subscriptions_cart_get_price', array( $this, 'apply_member_discount_to_subscription_products' ), 10, 2 ); } /** * Removes hooks that apply a membership discount * * @return void */ public function remove_hooks() { remove_filter( 'woocommerce_get_price_html', array( $this, 'display_membership_price' ), 10, 2 ); remove_filter( 'woocommerce_subscriptions_product_price_string', array( $this, 'display_membership_price_for_subscriptions' ), 10, 3 ); remove_filter( 'woocommerce_product_get_price', array( $this, 'replace_product_price_with_discount' ), 20, 2); remove_filter( 'woocommerce_product_variation_get_price', array( $this, 'replace_product_price_with_discount' ), 20, 2); remove_filter( 'woocommerce_cart_product_price', array( $this, 'apply_member_discount_on_cart_price' ), 10, 2 ); remove_filter( 'woocommerce_cart_product_subtotal', array( $this, 'apply_member_discount_on_cart_product_subtotal' ), 10, 4 ); } // Singleton instance protected static $instance = null; public static function get_instance() { if ( !isset( self::$instance ) ) self::$instance = new static(); return self::$instance; } // Utilities /** * Returns the amount of discount to apply as a float. 0.3 = 30% * * @return float */ public function get_discount_amount() { return 0.3; } /** * Returns true if the membership discount should apply to the current order. * You must either already be a member, or have the membership product in your cart. * * @return bool */ public function is_discount_applied() { $has_membership = false; // already a member, or product in cart // Remove hooks to prevent infinite loops when getting membership product $this->remove_hooks(); // Check if the user is a member or has the membership product in their cart do { // 1. Check if the user is a member $current_membership = MyPluginName_get_user_membership(); if ( $current_membership ) { $has_membership = true; break; } // 2. Check if the membership product is in the cart if ( WC()->cart ) foreach ( WC()->cart->get_cart() as $item ) { $product = wc_get_product( $item['product_id'] ); if ( $product->is_type( 'membership' ) ) { $has_membership = true; break 2; } } }while(false); // Re-add hooks $this->add_hooks(); return $has_membership; } /** * Checks if a product supports membership discount * @param WC_Product|int $product * @return bool */ public function is_product_eligible_for_discount( $product ) { $product = wc_get_product( $product ); // 1. Do not apply membership discount to the membership itself $membership_product_id = MyPluginName_get_membership_product_id(); if ( $product->get_id() === $membership_product_id || $product->get_parent_id() === $membership_product_id ) { return false; } return true; } /** * Gets the text to add after membership prices * * @return string */ public function get_membership_discount_suffix() { $discount = $this->get_discount_amount(); return ' (' . $discount * 100 . '% Member Discount)'; } /** * Get the regular price for a product * * @param WC_Product $product * * @return string */ public function get_regular_price( $product ) { $this->remove_hooks(); // Prefer regular price by default $regular_price = $product->get_regular_price(); // If blank, use the default price (For variations) if ( ! $regular_price ) $regular_price = $product->get_price(); $this->add_hooks(); return $regular_price; } /** * Get the membership price for a product * @param WC_Product|int $product * @return float */ public function get_product_membership_price( $product ) { $this->remove_hooks(); $product = wc_get_product( $product ); $current_price = $product->get_price(); $discount = $this->get_discount_amount(); $this->add_hooks(); if ( ! $current_price ) { return $current_price; }else{ return $current_price - ( $current_price * $discount ); } } /** * Convert a price to a subscription term, for a subscription product. For example "$1 / month" * * @param string|float $price * @param WC_Product $product * * @return string */ public function apply_subscription_formatting( $price, $product ) { $this->remove_hooks(); if ( ! str_contains($price, '$') ) { $price = wc_price($price); } $formatted_price = WC_Subscriptions_Product::get_price_string( $product, array( 'price' => $price ) ); $this->add_hooks(); return $formatted_price; } /** * Check if a product is a subscription * * @param WC_Product|int $product * * @return bool */ public function is_product_subscription( $product ) { return WC_Subscriptions_Product::is_subscription( $product ); } /** * Apply the membership discount formatting to products on the product page * * @param string $regular_price Should already be formatted with wc_price() * @param string $membership_price Should already be formatted with wc_price() * * @return string */ public function apply_product_discount_html( $regular_price, $membership_price ) { $discount_html = $this->get_membership_discount_suffix(); return '' . 'Price: '. $regular_price . ' ' . 'Member\'s Price: '. $membership_price . ' '. $discount_html .' ' . ''; } /** * Apply the membership discount formatting to products on the cart * * @param string $regular_price Should already be formatted with wc_price() * @param string $membership_price Should already be formatted with wc_price() * * @return string */ public function apply_cart_discount_html( $regular_price, $membership_price ) { $discount_html = $this->get_membership_discount_suffix(); return ''. ''. $regular_price . ' '. '' . $membership_price . ''. '' . $discount_html . ''. ''; } // Hooks /** * Display the membership price */ public function display_membership_price( $price, $product ) { // Do not apply to admin if ( is_admin() ) return $price; // Do not apply to subscriptions /** @see self::display_membership_price_for_subscriptions() */ if ( $this->is_product_subscription( $product ) ) { return $price; } // Product must support discount if ( ! $this->is_product_eligible_for_discount( $product ) ) { return $price; } // Ignore for grouped products, shows a price range instead if ( $product->is_type( 'grouped' ) ) { return $price; } // Get the regular price $regular_price = $this->get_regular_price( $product ); // Do not format if price is empty if ( ! $regular_price ) { return $price; } // Get the membership price $membership_price = $this->get_product_membership_price( $product ); return $this->apply_product_discount_html( wc_price( $regular_price ), wc_price( $membership_price ) ); } /** * Display the membership price for subscriptions * * @param string $price * @param WC_Product $product * @param array $include * * @return string */ public function display_membership_price_for_subscriptions( $price, $product, $include ) { // Do not apply to admin if ( is_admin() ) return $price; // Product must support discount if ( ! $this->is_product_eligible_for_discount( $product ) ) { return $price; } $this->remove_hooks(); // Get the regular price $regular_price = $this->get_regular_price($product); // Do not show if price is empty if ( ! $regular_price ) { $this->add_hooks(); return $regular_price; } // Get the membership price $membership_price = $this->get_product_membership_price( $product ); $regular_price_subscription = $this->apply_subscription_formatting( $regular_price, $product ); $membership_price_subscription = $this->apply_subscription_formatting( $membership_price, $product ); // Display regular and membership prices if ( is_cart() ) { $html = $this->apply_cart_discount_html( $regular_price_subscription, $membership_price_subscription ); }else{ $html = $this->apply_product_discount_html( $regular_price_subscription, $membership_price_subscription ); } $this->add_hooks(); return $html; } /** * Replace the product price with the membership price * * @param float $price * @param WC_Product $product * * @return float */ public function replace_product_price_with_discount( $price, $product ) { // Product must support discount if ( ! $this->is_product_eligible_for_discount( $product ) ) { return $price; } if ( $this->is_discount_applied() ) { // Return the membership price return $this->get_product_membership_price( $product ); }else{ // Return the regular price return $price; } } /** * Display the original product price below the product title on the cart page * * @param string $price * @param WC_Product $product * * @return string */ public function apply_member_discount_on_cart_price( $price, $product ) { if ( ! $this->is_discount_applied() ) { return $price; } // Product must support discount if ( ! $this->is_product_eligible_for_discount( $product ) ) { return $price; } // Disable hooks to prevent infinite loops $this->remove_hooks(); // Get the product $product = wc_get_product( $product ); // Get the regular price $regular_price = $product->get_price(); // Re-add hooks $this->add_hooks(); return wc_price( $regular_price ); } /** * Apply the member discount on the cart subtotal * * @param string $subtotal * @param WC_Product $product * @param int $quantity * @param WC_Cart $cart * * @return string */ public function apply_member_discount_on_cart_product_subtotal( $subtotal, $product, $quantity, $cart ) { if ( ! $this->is_discount_applied() ) { return $subtotal; } // Product must support discount if ( ! $this->is_product_eligible_for_discount( $product ) ) { return $subtotal; } // Remove hooks to prevent infinite loops $this->remove_hooks(); // Get the product $product = wc_get_product( $product ); // Get the regular price $regular_price = $product->get_price() * $quantity; $regular_price_html = wc_price( $regular_price ); // Get the membership price $membership_price = $this->get_product_membership_price( $product ) * $quantity; $membership_price_html = wc_price( $membership_price ); return $this->apply_cart_discount_html( $regular_price_html, $membership_price_html ); } } // Initialize the object MyPluginName_Membership_Discount::get_instance();