diff --git a/core/lib/Thelia/Config/Resources/coupon.xml b/core/lib/Thelia/Config/Resources/coupon.xml index 530a36d56..06e8f7bb6 100644 --- a/core/lib/Thelia/Config/Resources/coupon.xml +++ b/core/lib/Thelia/Config/Resources/coupon.xml @@ -66,9 +66,10 @@ - - - + + + + diff --git a/core/lib/Thelia/Coupon/Type/FreeProduct.php b/core/lib/Thelia/Coupon/Type/FreeProduct.php new file mode 100644 index 000000000..7b06f5dd9 --- /dev/null +++ b/core/lib/Thelia/Coupon/Type/FreeProduct.php @@ -0,0 +1,347 @@ + + */ +class FreeProduct extends AbstractRemoveOnProducts +{ + const OFFERED_PRODUCT_ID = 'offered_product_id'; + const OFFERED_CATEGORY_ID = 'offered_category_id'; + + /** @var string Service Id */ + protected $serviceId = 'thelia.coupon.type.free_product'; + + protected $offeredProductId; + protected $offeredCategoryId; + + /** + * This constant is user to mark a free product as in the process of being added to the cart, + * but the CartItem ID is not yet been defined. + */ + const ADD_TO_CART_IN_PROCESS = -1; + + /** + * @inheritdoc + */ + public function setFieldsValue($effects) + { + $this->offeredProductId = $effects[self::OFFERED_PRODUCT_ID]; + $this->offeredCategoryId = $effects[self::OFFERED_CATEGORY_ID]; + } + + /** + * @inheritdoc + */ + public function getCartItemDiscount($cartItem) + { + // This method is not used, we use our own implementation of exec(); + return 0; + } + + /** + * @return string The session variable where the cart item IDs for the free products are stored + */ + protected function getSessionVarName() { + return "coupon.free_product.cart_items." . $this->getCode(); + } + /** + * Return the cart item id which contains the free product related to a given product + * + * @param Product $product the product in the cart which triggered the discount + * + * @return bool|int|CartItem the cart item which contains the free product, or false if the product is no longer in the cart, or ADD_TO_CART_IN_PROCESS if the adding process is not finished + */ + protected function getRelatedCartItem($product) { + + $cartItemIdList = $this->facade->getRequest()->getSession()->get( + $this->getSessionVarName(), + array() + ); + + if (isset($cartItemIdList[$product->getId()])) { + + $cartItemId = $cartItemIdList[$product->getId()]; + + if ($cartItemId == self::ADD_TO_CART_IN_PROCESS) { + return self::ADD_TO_CART_IN_PROCESS; + } + else if (null !== $cartItem = CartItemQuery::create()->findPk($cartItemId)) { + return $cartItem; + } + } + else { + // Maybe the product we're offering is already in the cart ? Search it. + $cartItems = $this->facade->getCart()->getCartItems(); + + /** @var CartItem $cartItem */ + foreach ($cartItems as $cartItem) { + if ($cartItem->getProduct()->getId() == $this->offeredProductId) { + + // We found the product. Store its cart item as the free product container. + $this->setRelatedCartItem($product, $cartItem->getId()); + + return $cartItem; + } + } + + } + + return false; + } + + /** + * Set the cart item id which contains the free product related to a given product + * + * @param Product $product the product in the cart which triggered the discount + * @param bool|int $cartItemId the cart item ID which contains the free product, or just true if the free product is not yet added. + */ + protected function setRelatedCartItem($product, $cartItemId) { + + $cartItemIdList = $this->facade->getRequest()->getSession()->get( + $this->getSessionVarName(), + array() + ); + + if (! is_array($cartItemIdList)) $cartItemIdList = array(); + + $cartItemIdList[$product->getId()] = $cartItemId; + + $this->facade->getRequest()->getSession()->set( + $this->getSessionVarName(), + $cartItemIdList + ); + } + + /** + * Get the product id / cart item id list. + * + * @return array an array where the free product ID is the key, and the related cart item id the value. + */ + protected function getFreeProductsCartItemIds() { + return $this->facade->getRequest()->getSession()->get( + $this->getSessionVarName(), + array() + ); + } + + /** + * Clear the session variable. + */ + protected function clearFreeProductsCartItemIds() { + return $this->facade->getRequest()->getSession()->remove($this->getSessionVarName()); + } + + /** + * We overload this method here to remove the free products when the + * coupons conditions are no longer met. + * + * @inheritdoc + */ + public function isMatching() { + $match = parent::isMatching(); + + if (! $match) { + // Cancel coupon effect (but no not remove the product) + $this->clearFreeProductsCartItemIds(); + } + + return $match; + } + + /** + * @inheritdoc + */ + public function exec() + { + $discount = 0; + + $cartItems = $this->facade->getCart()->getCartItems(); + + /** @var Product $eligibleProduct */ + $eligibleProduct = null; + + /** @var CartItem $cartItem */ + foreach ($cartItems as $cartItem) { + if (in_array($cartItem->getProduct()->getId(), $this->product_list)) { + if (! $cartItem->getPromo() || $this->isAvailableOnSpecialOffers()) { + $eligibleProduct = $cartItem; + break; + } + } + } + + if ($eligibleProduct !== null) { + + // Get the cart item for the eligible product + $freeProductCartItem = $this->getRelatedCartItem($eligibleProduct); + + // We add the free product it only if it not yet in the cart. + if ($freeProductCartItem === false) { + + if (null !== $freeProduct = ProductQuery::create()->findPk($this->offeredProductId)) { + + // Store in the session that the free product is added to the cart, + // so that we don't enter the following infinite loop : + // + // 1) exec() adds a product by firing a CART_ADDITEM event, + // 2) the event is processed by Action\Coupon::updateOrderDiscount(), + // 3) Action\Coupon::updateOrderDiscount() calls CouponManager::getDiscount() + // 4) CouponManager::getDiscount() calls exec() -> Infinite loop !! + + // Store a marker first, we do not have the cart item id yet. + $this->setRelatedCartItem($eligibleProduct, self::ADD_TO_CART_IN_PROCESS); + + $cartEvent = new CartEvent($this->facade->getCart()); + + $cartEvent->setNewness(true); + $cartEvent->setAppend(false); + $cartEvent->setQuantity(1); + $cartEvent->setProductSaleElementsId($freeProduct->getDefaultSaleElements()->getId()); + $cartEvent->setProduct($this->offeredProductId); + + $this->facade->getDispatcher()->dispatch(TheliaEvents::CART_ADDITEM, $cartEvent); + + // Store the final cart item ID. + $this->setRelatedCartItem($eligibleProduct, $cartEvent->getCartItem()->getId()); + + $freeProductCartItem = $cartEvent->getCartItem(); + + // Setting product price is dangerous, as the customer could change the ordered quantity of this product. + // We will instead add the product price to the order discount. + // $cartEvent->getCartItem()->setPrice(0)->save(); + } + } + + if ($freeProductCartItem instanceof CartItem) { + + $taxCountry = $this->facade->getDeliveryCountry(); + + // The discount is the product price. + $discount = $freeProductCartItem->getPromo() ? + $freeProductCartItem->getPromoPrice() : $freeProductCartItem->getPrice(); + } + } + // No eligible product was found ! + else { + // Remove all free products for this coupon, but no not remove the product from the cart. + $this->clearFreeProductsCartItemIds(); + } + + return $discount; + } + + /** + * @inheritdoc + */ + protected function getFieldList() + { + return $this->getBaseFieldList([self::OFFERED_CATEGORY_ID, self::OFFERED_PRODUCT_ID]); + } + + /** + * @inheritdoc + */ + protected function checkCouponFieldValue($fieldName, $fieldValue) + { + $this->checkBaseCouponFieldValue($fieldName, $fieldValue); + + if ($fieldName === self::OFFERED_PRODUCT_ID) { + + if (floatval($fieldValue) < 0) { + throw new \InvalidArgumentException( + Translator::getInstance()->trans( + 'Please select the offered product' + ) + ); + } + } + else if ($fieldName === self::OFFERED_CATEGORY_ID) { + if (empty($fieldValue)) { + throw new \InvalidArgumentException( + Translator::getInstance()->trans( + 'Please select the category of the offred product' + ) + ); + } + } + + return $fieldValue; + } + + + /** + * Get I18n name + * + * @return string + */ + public function getName() + { + return $this->facade + ->getTranslator() + ->trans('Free product when buying one or more selected products', array(), 'coupon'); + } + + /** + * @inheritdoc + */ + public function getToolTip() + { + $toolTip = $this->facade + ->getTranslator() + ->trans( + 'This coupon adds a free product to the cart if one of the selected products is in the cart.', + array(), + 'coupon' + ); + + return $toolTip; + } + + /** + * @inheritdoc + */ + public function drawBackOfficeInputs() + { + return $this->drawBaseBackOfficeInputs("coupon/type-fragments/free-product.html", [ + 'offered_category_field_name' => $this->makeCouponFieldName(self::OFFERED_CATEGORY_ID), + 'offered_category_value' => $this->offeredCategoryId, + + 'offered_product_field_name' => $this->makeCouponFieldName(self::OFFERED_PRODUCT_ID), + 'offered_product_value' => $this->offeredProductId + ]); + } + + /** + * @inheritdoc + */ + public function clear() + { + // Clear the session variable when the coupon is cleared. + $this->clearFreeProductsCartItemIds(); + } +} \ No newline at end of file