> */ protected $groupedCoupons; /** * @var array */ protected $singleCoupons; /** * @var array */ protected $wcSingleCoupons; /** * @var array */ protected $disabledWcCoupons; /** * @var bool */ protected $disableAllWcCoupons; /** * @var array */ protected $readyCouponData; /** * @var CartContext */ protected $cartContext; public function __construct($deprecated = null) { $this->context = adp_context(); $this->purge(); } public function withContext(Context $context) { $this->context = $context; } public function disableAllWcCoupons(): bool { return boolval($this->disabledWcCoupons); } public function init() { $this->replaceNotices(); } public function prepareConfig() { } /** * @param Cart $cart */ public function refreshCoupons($cart) { $context = $cart->getContext(); $this->purge(); foreach ($cart->getCoupons() as $coupon) { $coupon = clone $coupon; if (empty($coupon->getCode())) { continue; } if ($coupon instanceof CouponCart && $coupon->getValue()) { if ($coupon->isType($coupon::TYPE_FIXED_VALUE)) { if ($context->isCombineMultipleDiscounts()) { $coupon->setCode(strtolower($context->getOption('default_discount_name'))); } $this->addGroupCoupon($coupon); } elseif ($coupon->isType($coupon::TYPE_PERCENTAGE)) { $this->addSingleCoupon($coupon); } } elseif ($coupon instanceof CouponCartItem && $coupon->getValue()) { if ($context->isCombineMultipleDiscounts()) { $coupon->setCode(strtolower($context->getOption('default_discount_name'))); $this->addGroupCoupon($coupon); } else { $this->addGroupCoupon($coupon); } } elseif ($coupon instanceof WcCouponCart) { $this->wcSingleCoupons[$coupon->getCode()] = $coupon; } } // remove postfix for single %% discount if (count($this->singleCoupons) == 1) { $coupon = reset($this->singleCoupons); $coupon->setCode(str_replace(' #1', '', $coupon->getCode())); $this->singleCoupons = array($coupon->getCode() => $coupon); } foreach ($cart->getCouponsAdjustments() as $coupon) { if ($coupon instanceof DisableWcCouponsCart) { $this->disabledWcCoupons[] = $coupon->getCode(); } elseif ( $coupon instanceof DisableAllWcCouponsCart ) { $this->disableAllWcCoupons = true; } } $this->cartContext = $cart->getContext(); } public function processCouponAdjustments(Cart $cart, WC_Cart $wcCart) { $this->cartContext = $cart->getContext(); $this->disabledWcCoupons = []; $this->disableAllWcCoupons = false; foreach ($cart->getCouponsAdjustments() as $coupon) { if ($coupon instanceof DisableWcCouponsCart) { $this->disabledWcCoupons[] = $coupon->getCode(); } elseif ($coupon instanceof DisableAllWcCouponsCart) { $this->disableAllWcCoupons = true; } } } /** * @param WC_Cart $wcCart */ public function applyCoupons(&$wcCart) { $couponCodesToApply = array_merge( array_keys($this->groupedCoupons), array_keys($this->singleCoupons) ); $appliedCoupons = $wcCart->applied_coupons; $discounts = new \WC_Discounts( $wcCart ); foreach (array_keys($this->wcSingleCoupons) as $couponCode) { $coupon = new WC_Coupon($couponCode); if ($discounts->is_coupon_valid($coupon) !== true) { continue; } /** * @see WC_Cart::apply_coupon */ if ($coupon->get_individual_use() && count($appliedCoupons) > 0) { continue; } /** * Check to see if an individual use coupon is set. * @see WC_Cart::apply_coupon */ $allowWithIndividual = true; foreach ($appliedCoupons as $code) { $coupon = new WC_Coupon($code); if ($coupon->get_individual_use() && false === apply_filters( 'woocommerce_apply_with_individual_use_coupon', false, $coupon, $coupon, $appliedCoupons )) { $allowWithIndividual = false; break; } } if ( ! $allowWithIndividual) { continue; } if ( ! in_array($couponCode, $appliedCoupons, true)) { $appliedCoupons[] = $couponCode; } } $appliedCoupons = array_filter($appliedCoupons, function ($couponCode) { return ! in_array($couponCode, $this->disabledWcCoupons, true); }); if ( $this->disableAllWcCoupons === true ) { $appliedCoupons = []; } foreach ($couponCodesToApply as $couponCode) { if ( ! in_array($couponCode, $appliedCoupons, true)) { $appliedCoupons[] = $couponCode; } } $wcCart->applied_coupons = array_unique($appliedCoupons); $this->prepareCouponsData($wcCart); } /** * @param WC_Cart $wcCart */ public function sanitize(WC_Cart $wcCart) { $appliedCoupons = $wcCart->applied_coupons; $adpCoupons = (new WcCustomerSessionFacade(WC()->session))->getAdpCoupons(); foreach ($appliedCoupons as $index => $couponCode) { if (in_array($couponCode, $adpCoupons, true)) { unset($appliedCoupons[$index]); } } $wcCart->applied_coupons = array_values($appliedCoupons); $this->purge(); } public function installActions() { $this->setFilterToInstallCouponsData(); $this->setFiltersToSupportPercentLimitCoupon(); $this->setFiltersToSupportExactItemApplicationOfReplacementCoupon(); $this->setFilterToSuppressDisabledWcCoupons(); } public function setFilterToInstallCouponsData() { add_filter('woocommerce_get_shop_coupon_data', array($this, 'getCouponData'), 10, 3); } public function unsetFilterToInstallCouponsData() { remove_filter('woocommerce_get_shop_coupon_data', array($this, 'getCouponData'), 10); } public function setFiltersToSupportPercentLimitCoupon() { add_filter('woocommerce_coupon_discount_types', array($this, 'addPercentLimitCouponDiscountType'), 10, 1); add_filter('woocommerce_product_coupon_types', array($this, 'addPercentLimitCouponProductType'), 10, 1); add_filter('woocommerce_coupon_get_discount_amount', array($this, 'getPercentLimitCouponDiscountAmount'), 10, 5); add_filter('woocommerce_coupon_custom_discounts_array', array($this, 'processPercentLimitCoupon'), 10, 2); } public function unsetFiltersToSupportPercentLimitCoupon() { remove_filter('woocommerce_coupon_discount_types', array($this, 'addPercentLimitCouponDiscountType'), 10); remove_filter('woocommerce_product_coupon_types', array($this, 'addPercentLimitCouponProductType'), 10); remove_filter('woocommerce_coupon_get_discount_amount', array($this, 'getPercentLimitCouponDiscountAmount'), 10); } public function setFiltersToSupportExactItemApplicationOfReplacementCoupon() { if ( ! $this->context->isAllowExactApplicationOfReplacementCoupon()) { return; } add_filter('woocommerce_coupon_get_discount_amount', array($this, 'getExactItemApplicationCouponDiscountAmount'), 10, 5); add_filter('woocommerce_cart_coupon_types', array($this, 'addExactItemApplicationCouponCartType'), 10, 1); add_filter('woocommerce_coupon_discount_types', array($this, 'addExactItemApplicationCouponDiscountType'), 10, 1); } public function unsetFiltersToSupportExactItemApplicationOfReplacementCoupon() { remove_filter('woocommerce_coupon_get_discount_amount', array($this, 'getExactItemApplicationCouponDiscountAmount'), 10); remove_filter('woocommerce_cart_coupon_types', array($this, 'addExactItemApplicationCouponCartType'), 10); remove_filter('woocommerce_coupon_discount_types', array($this, 'addExactItemApplicationCouponDiscountType'), 10); } public function setFilterToSuppressDisabledWcCoupons() { add_filter('woocommerce_coupon_is_valid', array($this, 'hookDisabledCouponsIsValidForCart'), 10, 3); } public function unsetFilterToSuppressDisabledWcCoupons() { remove_filter('woocommerce_coupon_is_valid', array($this, 'hookDisabledCouponsIsValidForCart'), 10); } /** * This filter allows custom coupon objects to be created on the fly. * * @param false $couponData * @param mixed $couponCode Coupon code * @param WC_Coupon $wcCoupon * * @return array|mixed */ public function getCouponData($couponData, $couponCode, $wcCoupon) { if (isset($this->readyCouponData[$couponCode])) { $readyCouponFacade = $this->readyCouponData[$couponCode]; $parts = $readyCouponFacade->getParts(); $wcCouponFacade = new WcCouponFacade($this->context, $wcCoupon); $wcCouponFacade->setParts($parts); $wcCouponFacade->updateCoupon(); /** * Return $couponData as is, so it does not become virtual. * But push parts anyway. */ if (count($parts) === 1 && $parts[0] instanceof WcCouponCart) { return $couponData; } $couponData = array( 'discount_type' => $readyCouponFacade->coupon->get_discount_type('edit'), 'amount' => $readyCouponFacade->coupon->get_amount('edit'), ); //support max discount for percentage coupon if ($coupon = reset($parts)) { /** @var CouponCart $coupon */ if ($couponData['discount_type'] === WcCouponFacade::TYPE_PERCENT && $coupon->isMaxDiscountDefined()) { $couponData['discount_type'] = WcCouponFacade::TYPE_CUSTOM_PERCENT_WITH_LIMIT; } } } return $couponData; } /** * @param array $discountTypes * * @return array */ public function addPercentLimitCouponDiscountType($discountTypes) { $discountTypes[WcCouponFacade::TYPE_CUSTOM_PERCENT_WITH_LIMIT] = __('WDP Coupon', 'advanced-dynamic-pricing-for-woocommerce'); return $discountTypes; } /** * @param array $discountTypes * * @return array */ public function addPercentLimitCouponProductType($discountTypes) { $discountTypes[] = WcCouponFacade::TYPE_CUSTOM_PERCENT_WITH_LIMIT; return $discountTypes; } /** * @param array $discountTypes * * @return array */ public function addExactItemApplicationCouponDiscountType($discountTypes) { $discountTypes[WcCouponFacade::TYPE_ADP_FIXED_CART_ITEM] = __('WDP Coupon (exact item application)', 'advanced-dynamic-pricing-for-woocommerce'); return $discountTypes; } /** * @param array $discountTypes * * @return array */ public function addExactItemApplicationCouponCartType($discountTypes) { $discountTypes[] = WcCouponFacade::TYPE_ADP_FIXED_CART_ITEM; return $discountTypes; } /** * @param float $discountAmount * @param float $discountingAmount * @param array $cartItem * @param bool $single * @param WC_Coupon $coupon * * @return float */ public function getPercentLimitCouponDiscountAmount( $discountAmount, $discountingAmount, $cartItem, $single, $coupon ) { if ($coupon->get_discount_type() === WcCouponFacade::TYPE_CUSTOM_PERCENT_WITH_LIMIT) { $discountAmount = (float)$coupon->get_amount() * ($discountingAmount / 100); } return $discountAmount; } /** * @param array $couponDiscounts * @param \WC_Coupon $coupon * * @return array */ public function processPercentLimitCoupon($couponDiscounts, $coupon) { if ($coupon->get_discount_type() === WcCouponFacade::TYPE_CUSTOM_PERCENT_WITH_LIMIT) { $coupon_code = $coupon->get_code(); $wdpCoupon = $this->singleCoupons[$coupon_code]; $discountAmount = array_sum($couponDiscounts); $maxDiscount = $wdpCoupon->getMaxDiscount() * pow(10, wc_get_price_decimals()); if ($discountAmount > $maxDiscount) { $itemDiscount = round((float)$maxDiscount / count($couponDiscounts)); $k = 0; foreach ($couponDiscounts as $key => $discount) { if ($k >= count($couponDiscounts) - 1) { $couponDiscounts[$key] = $maxDiscount - $itemDiscount * $k; break; } $couponDiscounts[$key] = $itemDiscount; $k++; } } } return $couponDiscounts; } /** * @param WC_Cart $wcCart */ public function updateTotals(WC_Cart $wcCart) { $this->cartContext->getSession()->insertCouponsData( $this->groupedCoupons, $this->singleCoupons, $this->wcSingleCoupons ); } /** * @param float $discountAmount * @param float $discountingAmount * @param array $cartItem * @param bool $single * @param WC_Coupon $wcCoupon * * @return float */ public function getExactItemApplicationCouponDiscountAmount( $discountAmount, $discountingAmount, $cartItem, $single, $wcCoupon ) { $facade = new WcCartItemFacade($this->context, $cartItem); $couponFacade = new WcCouponFacade($this->context, $wcCoupon); /** @var CouponCartItem[] $coupons */ $coupons = array_filter($couponFacade->getParts(), function ($coupon) { return $coupon instanceof CouponCartItem; }); if (count($coupons) === 0) { return $discountAmount; } $newAmount = floatval(0); $ruleIds = array_unique(array_map(function ($coupon) { return $coupon->getRuleId(); }, $coupons)); $newAmount += array_sum(array_map(function ($ruleId) use ($facade) { return array_sum($facade->getHistory()[$ruleId] ?? []); }, $ruleIds)); if ($newAmount > 0) { $priceFunctions = new PriceFunctions($this->context); $args = array('price' => $newAmount); if ($this->context->getIsPricesIncludeTax()) { $newAmount = $priceFunctions->getPriceIncludingTax($facade->getProduct(), $args); } else { $newAmount = $priceFunctions->getPriceExcludingTax($facade->getProduct(), $args); } $newAmount = round($newAmount, wc_get_rounding_precision()); } return $newAmount; } /** * @param bool $valid * @param \WC_Coupon $coupon * @param \WC_Discounts $wcDiscounts * * @return bool */ public function hookDisabledCouponsIsValidForCart($valid, $coupon, $wcDiscounts) { $customAdpCoupons = (new WcCustomerSessionFacade(WC()->session))->getCustomAdpCoupons(); if ($this->disableAllWcCoupons === true && ! in_array($coupon->get_code(), $customAdpCoupons, true)) { throw new \Exception( esc_html( WcUtils::getCouponNotApplicableMessage($coupon->get_code()) ) ); } if (in_array($coupon->get_code(), $this->disabledWcCoupons, true)) { throw new \Exception( esc_html( WcUtils::getCouponNotApplicableMessage($coupon->get_code()) ) ); } return $valid; } /** * @param WC_Cart $wcCart */ protected function prepareCouponsData($wcCart) { foreach ($this->groupedCoupons as $couponCode => $coupons) { $coupon_type = WcCouponFacade::TYPE_FIXED_CART; $amount = floatval(0); $appliedCoupons = array(); foreach ($coupons as $coupon) { if ($coupon instanceof CouponCart) { if ($coupon->isType($coupon::TYPE_FIXED_VALUE)) { $amount += $coupon->getValue(); $appliedCoupons[] = $coupon; } } elseif ($coupon instanceof CouponCartItem) { $amount += $coupon->getValue() * $coupon->getAffectedCartItemQty(); if ($this->context->isAllowExactApplicationOfReplacementCoupon()) { $coupon_type = WcCouponFacade::TYPE_ADP_FIXED_CART_ITEM; } $appliedCoupons[] = $coupon; } } if ($amount > 0) { $this->addReadyCouponData($couponCode, $coupon_type, $amount, $appliedCoupons); } } foreach ($this->singleCoupons as $coupon) { $coupon_type = WcCouponFacade::TYPE_FIXED_CART; if ($coupon instanceof CouponCart) { if ($coupon->isType($coupon::TYPE_PERCENTAGE)) { $coupon_type = WcCouponFacade::TYPE_PERCENT; } } elseif ($coupon instanceof CouponCartItem) { $coupon_type = WcCouponFacade::TYPE_FIXED_PRODUCT; } $this->addReadyCouponData($coupon->getCode(), $coupon_type, $coupon->getValue(), array($coupon)); } foreach ($this->wcSingleCoupons as $coupon) { $code = $coupon->getCode(); if (isset($this->readyCouponData[$code])) { continue; } $couponFacade = new WcCouponFacade($this->context, new \WC_Coupon($code)); $couponFacade->setParts([$coupon]); $this->readyCouponData[$code] = $couponFacade; } } /** * @param string $code * @param string $type * @param float $amount * @param array $parts */ protected function addReadyCouponData($code, $type, $amount, $parts) { if (isset($this->readyCouponData[$code])) { return; } $couponFacade = new WcCouponFacade($this->context, new \WC_Coupon()); $couponFacade->coupon->set_virtual(true); $couponFacade->coupon->set_code($code); $couponFacade->coupon->set_discount_type($type); $couponFacade->coupon->set_amount($amount); $couponFacade->setParts($parts); $this->readyCouponData[$code] = $couponFacade; } /** * @param CouponInterface $coupon */ protected function addGroupCoupon($coupon) { if ( ! isset($this->groupedCoupons[$coupon->getCode()])) { $this->groupedCoupons[$coupon->getCode()] = array(); } $this->groupedCoupons[$coupon->getCode()][] = $coupon; } /** * @param CouponInterface $coupon */ protected function addSingleCoupon($coupon) { if ( ! isset($this->singleCoupons[$coupon->getCode()])) { $this->singleCoupons[$coupon->getCode()] = $coupon; return; } // add "#1" to the end of the coupon label once $firstCoupon = $this->singleCoupons[$coupon->getCode()]; if (strpos($firstCoupon->getLabel(), "#1") === false) { $firstCoupon->setLabel(sprintf("%s #%s", $coupon->getCode(), 1)); } $count = 1; do { $couponCode = sprintf("%s_%s", $coupon->getCode(), $count); $couponLabel = sprintf("%s #%s", $coupon->getCode(), $count + 1); $count++; } while (isset($this->singleCoupons[$couponCode])); $coupon->setCode($couponCode); $coupon->setLabel($couponLabel); $this->singleCoupons[$coupon->getCode()] = $coupon; } protected function purge() { $this->groupedCoupons = array(); $this->singleCoupons = array(); $this->readyCouponData = array(); $this->wcSingleCoupons = array(); $this->disabledWcCoupons = array(); $this->disableAllWcCoupons = false; } public function replaceNotices() { $context = $this->context; // Replace notice in case of removing coupons later // If coupons won't be removed, notice will be replaced back if ( $context->getOption('external_product_coupons_behavior') === 'disable_if_any_rule_applied' || $context->getOption('external_product_coupons_behavior') === 'disable_if_any_of_cart_items_updated' || $context->getOption('external_cart_coupons_behavior') === 'disable_if_any_rule_applied' || $context->getOption('external_cart_coupons_behavior') === 'disable_if_any_of_cart_items_updated' ) { $this->replaceWcNotice( array( //phpcs:ignore WordPress.WP.I18n.TextDomainMismatch 'text' => __('Coupon code applied successfully.', 'woocommerce'), 'type' => 'success', ), array( 'text' => __('Sorry, coupons are disabled for these products.', 'advanced-dynamic-pricing-for-woocommerce'), 'type' => 'error', ) ); } } /** * @param array $needleNotice * @param array $newNotice */ protected function replaceWcNotice($needleNotice, $newNotice) { if ( ! is_array($needleNotice) || ! is_array($newNotice)) { return; } if ( ! function_exists("wc_get_notices") ) { return; } $needleNotice = array( 'type' => isset($needleNotice['type']) ? $needleNotice['type'] : null, 'text' => isset($needleNotice['text']) ? $needleNotice['text'] : "", ); $newNotice = array( 'type' => isset($newNotice['type']) ? $newNotice['type'] : null, 'text' => isset($newNotice['text']) ? $newNotice['text'] : "", ); $newNotices = array(); foreach (wc_get_notices() as $type => $notices) { if ( ! isset($newNotices[$type])) { $newNotices[$type] = array(); } foreach ($notices as $loopNotice) { if ( ! empty($loopNotice['notice']) && $needleNotice['text'] === $loopNotice['notice'] && ( ! $needleNotice['type'] || $needleNotice['type'] === $type) ) { if ($newNotice['type'] === null) { $newNotice['type'] = $type; } if ( ! isset($newNotices[$newNotice['type']])) { $newNotices[$newNotice['type']] = array(); } $newNotices[$newNotice['type']][] = array( 'notice' => $newNotice['text'], 'data' => array(), ); continue; } else { $newNotices[$type][] = $loopNotice; } } } wc_set_notices($newNotices); } /** * @param WC_Cart $wcCart */ protected function purgeAppliedCoupons($wcCart) { $wcCart->applied_coupons = array(); } /** * @param Cart $cart * @param WC_Cart $wcCart */ protected function addOriginCoupons(&$cart, &$wcCart) { $wcCart->applied_coupons = array_merge($wcCart->applied_coupons, $cart->getOriginCoupons()); } /** * @param Cart $cart * @param WC_Cart $wcCart */ protected function addRuleTriggerCoupons(&$cart, &$wcCart) { $wcCart->applied_coupons = array_merge($wcCart->applied_coupons, $cart->getRuleTriggerCoupons()); } /** * @param Cart $cart * @param WC_Cart $wcCart */ protected function maybeRemoveOriginCoupons($cart, $wcCart) { $externalProductCouponsBehavior = $this->context->getOption('external_product_coupons_behavior'); $externalCartCouponsBehavior = $this->context->getOption('external_cart_coupons_behavior'); $checkIfPriceChanged = function ($wcCart) { $is_price_changed = false; foreach ($wcCart->cart_contents as $cartKey => $wcCartItem) { $wrapper = new WcCartItemFacade($this->context, $wcCartItem, $cartKey); foreach ($wrapper->getDiscounts() as $ruleId => $amounts) { if (array_sum($amounts) > 0) { $is_price_changed = true; break; } } } $is_price_changed = (bool)apply_filters( 'wdp_is_disable_external_coupons_if_items_updated', $is_price_changed, $this, $wcCart ); return $is_price_changed; }; $isPriceChanged = false; $isPriceChangedCalculated = false; $isCouponsRemoved = false; if ($externalProductCouponsBehavior === 'disable_if_any_rule_applied') { if ($cart->removeProductOriginCoupon()) { $isCouponsRemoved = true; } } elseif ($externalProductCouponsBehavior === 'disable_if_any_of_cart_items_updated') { $isPriceChanged = $checkIfPriceChanged($wcCart); $isPriceChangedCalculated = true; if ($isPriceChanged) { if ($cart->removeProductOriginCoupon()) { $isCouponsRemoved = true; } } } if ($externalCartCouponsBehavior === 'disable_if_any_rule_applied') { if ($cart->removeCartOriginCoupon()) { $isCouponsRemoved = true; } } elseif ($externalCartCouponsBehavior === 'disable_if_any_of_cart_items_updated') { if (!$isPriceChangedCalculated) { $isPriceChanged = $checkIfPriceChanged($wcCart); } if ($isPriceChanged) { if ($cart->removeCartOriginCoupon()) { $isCouponsRemoved = true; } } } if (!$isCouponsRemoved) { $this->replaceWcNotice( array( 'text' => __('Sorry, coupons are disabled for these products.', 'advanced-dynamic-pricing-for-woocommerce'), 'type' => 'error', ), array( //phpcs:ignore WordPress.WP.I18n.TextDomainMismatch 'text' => __('Coupon code applied successfully.', 'woocommerce'), 'type' => 'success', ) ); } } public function applyCouponsToWcCart(Cart $cart, WC_Cart $wcCart) { $this->maybeRemoveOriginCoupons($cart, $wcCart); $this->purgeAppliedCoupons($wcCart); $this->addOriginCoupons($cart, $wcCart); $this->addRuleTriggerCoupons($cart, $wcCart); $this->refreshCoupons($cart); $this->applyCoupons($wcCart); } }