core/lib/Thelia/Action/Sale.php line 113

  1. <?php
  2. /*
  3.  * This file is part of the Thelia package.
  4.  * http://www.thelia.net
  5.  *
  6.  * (c) OpenStudio <info@thelia.net>
  7.  *
  8.  * For the full copyright and license information, please view the LICENSE
  9.  * file that was distributed with this source code.
  10.  */
  11. namespace Thelia\Action;
  12. use Propel\Runtime\ActiveQuery\Criteria;
  13. use Propel\Runtime\Connection\ConnectionInterface;
  14. use Propel\Runtime\Exception\PropelException;
  15. use Propel\Runtime\Propel;
  16. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  17. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  18. use Thelia\Core\Event\Sale\ProductSaleStatusUpdateEvent;
  19. use Thelia\Core\Event\Sale\SaleActiveStatusCheckEvent;
  20. use Thelia\Core\Event\Sale\SaleClearStatusEvent;
  21. use Thelia\Core\Event\Sale\SaleCreateEvent;
  22. use Thelia\Core\Event\Sale\SaleDeleteEvent;
  23. use Thelia\Core\Event\Sale\SaleToggleActivityEvent;
  24. use Thelia\Core\Event\Sale\SaleUpdateEvent;
  25. use Thelia\Core\Event\TheliaEvents;
  26. use Thelia\Model\Country as CountryModel;
  27. use Thelia\Model\Map\SaleTableMap;
  28. use Thelia\Model\ProductPriceQuery;
  29. use Thelia\Model\ProductSaleElements;
  30. use Thelia\Model\ProductSaleElementsQuery;
  31. use Thelia\Model\Sale as SaleModel;
  32. use Thelia\Model\SaleOffsetCurrency;
  33. use Thelia\Model\SaleOffsetCurrencyQuery;
  34. use Thelia\Model\SaleProduct;
  35. use Thelia\Model\SaleProductQuery;
  36. use Thelia\Model\SaleQuery;
  37. use Thelia\TaxEngine\Calculator;
  38. /**
  39.  * Class Sale.
  40.  *
  41.  * @author  Franck Allimant <franck@cqfdev.fr>
  42.  */
  43. class Sale extends BaseAction implements EventSubscriberInterface
  44. {
  45.     /**
  46.      * Update PSE for a given product.
  47.      *
  48.      * @param array      $pseList              an array of priduct sale elements
  49.      * @param bool       $promoStatus          true if the PSEs are on sale, false otherwise
  50.      * @param int        $offsetType           the offset type, see SaleModel::OFFSET_* constants
  51.      * @param Calculator $taxCalculator        the tax calculator
  52.      * @param array      $saleOffsetByCurrency an array of price offset for each currency (currency ID => offset_amount)
  53.      *
  54.      * @throws PropelException
  55.      */
  56.     protected function updateProductSaleElementsPrices($pseList$promoStatus$offsetTypeCalculator $taxCalculator$saleOffsetByCurrencyConnectionInterface $con): void
  57.     {
  58.         /** @var ProductSaleElements $pse */
  59.         foreach ($pseList as $pse) {
  60.             if ($pse->getPromo() != $promoStatus) {
  61.                 $pse
  62.                     ->setPromo($promoStatus)
  63.                     ->save($con)
  64.                 ;
  65.             }
  66.             /** @var SaleOffsetCurrency $offsetByCurrency */
  67.             foreach ($saleOffsetByCurrency as $currencyId => $offset) {
  68.                 $productPrice ProductPriceQuery::create()
  69.                     ->filterByProductSaleElementsId($pse->getId())
  70.                     ->filterByCurrencyId($currencyId)
  71.                     ->findOne($con);
  72.                 if (null !== $productPrice) {
  73.                     // Get the taxed price
  74.                     $priceWithTax $taxCalculator->getTaxedPrice($productPrice->getPrice());
  75.                     // Remove the price offset to get the taxed promo price
  76.                     switch ($offsetType) {
  77.                         case SaleModel::OFFSET_TYPE_AMOUNT:
  78.                             $promoPrice max(0$priceWithTax $offset);
  79.                             break;
  80.                         case SaleModel::OFFSET_TYPE_PERCENTAGE:
  81.                             $promoPrice $priceWithTax * ($offset 100);
  82.                             break;
  83.                         default:
  84.                             $promoPrice $priceWithTax;
  85.                     }
  86.                     // and then get the untaxed promo price.
  87.                     $promoPrice $taxCalculator->getUntaxedPrice($promoPrice);
  88.                     $productPrice
  89.                         ->setPromoPrice($promoPrice)
  90.                         ->save($con)
  91.                     ;
  92.                 }
  93.             }
  94.         }
  95.     }
  96.     /**
  97.      * Update the promo status of the sale's selected products and combinations.
  98.      *
  99.      * @throws \RuntimeException
  100.      * @throws \Exception
  101.      * @throws \Propel\Runtime\Exception\PropelException
  102.      */
  103.     public function updateProductsSaleStatus(ProductSaleStatusUpdateEvent $event): void
  104.     {
  105.         $taxCalculator = new Calculator();
  106.         $sale $event->getSale();
  107.         // Get all selected product sale elements for this sale
  108.         if (null !== $saleProducts SaleProductQuery::create()->filterBySale($sale)->orderByProductId()) {
  109.             $saleOffsetByCurrency $sale->getPriceOffsets();
  110.             $offsetType $sale->getPriceOffsetType();
  111.             $con Propel::getWriteConnection(SaleTableMap::DATABASE_NAME);
  112.             $con->beginTransaction();
  113.             try {
  114.                 /** @var SaleProduct $saleProduct */
  115.                 foreach ($saleProducts as $saleProduct) {
  116.                     // Reset all sale status on product's PSE
  117.                     ProductSaleElementsQuery::create()
  118.                         ->filterByProductId($saleProduct->getProductId())
  119.                         ->update(['Promo' => false], $con)
  120.                     ;
  121.                     $taxCalculator->load(
  122.                         $saleProduct->getProduct($con),
  123.                         CountryModel::getShopLocation()
  124.                     );
  125.                     $attributeAvId $saleProduct->getAttributeAvId();
  126.                     $pseRequest ProductSaleElementsQuery::create()
  127.                         ->filterByProductId($saleProduct->getProductId())
  128.                     ;
  129.                     // If no attribute AV id is defined, consider ALL product combinations
  130.                     if (null !== $attributeAvId) {
  131.                         // Find PSE attached to combination containing this attribute av :
  132.                         // SELECT * from product_sale_elements pse
  133.                         // left join attribute_combination ac on ac.product_sale_elements_id = pse.id
  134.                         // where pse.product_id=363
  135.                         // and ac.attribute_av_id = 7
  136.                         // group by pse.id
  137.                         $pseRequest
  138.                             ->useAttributeCombinationQuery(nullCriteria::LEFT_JOIN)
  139.                                 ->filterByAttributeAvId($attributeAvId)
  140.                             ->endUse()
  141.                         ;
  142.                     }
  143.                     $pseList $pseRequest->find();
  144.                     if (null !== $pseList) {
  145.                         $this->updateProductSaleElementsPrices(
  146.                             $pseList,
  147.                             $sale->getActive(),
  148.                             $offsetType,
  149.                             $taxCalculator,
  150.                             $saleOffsetByCurrency,
  151.                             $con
  152.                         );
  153.                     }
  154.                 }
  155.                 $con->commit();
  156.             } catch (PropelException $e) {
  157.                 $con->rollback();
  158.                 throw $e;
  159.             }
  160.         }
  161.     }
  162.     /**
  163.      * Create a new Sale.
  164.      */
  165.     public function create(SaleCreateEvent $event): void
  166.     {
  167.         $sale = new SaleModel();
  168.         $sale
  169.             ->setLocale($event->getLocale())
  170.             ->setTitle($event->getTitle())
  171.             ->setSaleLabel($event->getSaleLabel())
  172.             ->save()
  173.         ;
  174.         $event->setSale($sale);
  175.     }
  176.     /**
  177.      * Process update sale.
  178.      *
  179.      * @throws PropelException
  180.      */
  181.     public function update(SaleUpdateEvent $event$eventNameEventDispatcherInterface $dispatcher): void
  182.     {
  183.         if (null !== $sale SaleQuery::create()->findPk($event->getSaleId())) {
  184.             $con Propel::getWriteConnection(SaleTableMap::DATABASE_NAME);
  185.             $con->beginTransaction();
  186.             try {
  187.                 // Disable all promo flag on sale's currently selected products,
  188.                 // to reset promo status of the products that may have been removed from the selection.
  189.                 $sale->setActive(false);
  190.                 $dispatcher->dispatch(
  191.                     new ProductSaleStatusUpdateEvent($sale),
  192.                     TheliaEvents::UPDATE_PRODUCT_SALE_STATUS
  193.                 );
  194.                 $sale
  195.                     ->setActive($event->getActive())
  196.                     ->setStartDate($event->getStartDate())
  197.                     ->setEndDate($event->getEndDate())
  198.                     ->setPriceOffsetType($event->getPriceOffsetType())
  199.                     ->setDisplayInitialPrice($event->getDisplayInitialPrice())
  200.                     ->setLocale($event->getLocale())
  201.                     ->setSaleLabel($event->getSaleLabel())
  202.                     ->setTitle($event->getTitle())
  203.                     ->setDescription($event->getDescription())
  204.                     ->setChapo($event->getChapo())
  205.                     ->setPostscriptum($event->getPostscriptum())
  206.                     ->save($con)
  207.                 ;
  208.                 $event->setSale($sale);
  209.                 // Update price offsets
  210.                 SaleOffsetCurrencyQuery::create()->filterBySaleId($sale->getId())->delete($con);
  211.                 foreach ($event->getPriceOffsets() as $currencyId => $priceOffset) {
  212.                     $saleOffset = new SaleOffsetCurrency();
  213.                     $saleOffset
  214.                         ->setCurrencyId($currencyId)
  215.                         ->setSaleId($sale->getId())
  216.                         ->setPriceOffsetValue($priceOffset)
  217.                         ->save($con)
  218.                     ;
  219.                 }
  220.                 // Update products
  221.                 SaleProductQuery::create()->filterBySaleId($sale->getId())->delete($con);
  222.                 $productAttributesArray $event->getProductAttributes();
  223.                 foreach ($event->getProducts() as $productId) {
  224.                     if (isset($productAttributesArray[$productId])) {
  225.                         foreach ($productAttributesArray[$productId] as $attributeId) {
  226.                             $saleProduct = new SaleProduct();
  227.                             $saleProduct
  228.                                 ->setSaleId($sale->getId())
  229.                                 ->setProductId($productId)
  230.                                 ->setAttributeAvId($attributeId)
  231.                                 ->save($con)
  232.                             ;
  233.                         }
  234.                     } else {
  235.                         $saleProduct = new SaleProduct();
  236.                         $saleProduct
  237.                             ->setSaleId($sale->getId())
  238.                             ->setProductId($productId)
  239.                             ->setAttributeAvId(null)
  240.                             ->save($con)
  241.                         ;
  242.                     }
  243.                 }
  244.                 // Update related products sale status if the Sale is active. This is not required if the sale is
  245.                 // not active, as we de-activated promotion for this sale at the beginning ofd this method
  246.                 if ($sale->getActive()) {
  247.                     $dispatcher->dispatch(
  248.                         new ProductSaleStatusUpdateEvent($sale),
  249.                         TheliaEvents::UPDATE_PRODUCT_SALE_STATUS
  250.                     );
  251.                 }
  252.                 $con->commit();
  253.             } catch (PropelException $e) {
  254.                 $con->rollback();
  255.                 throw $e;
  256.             }
  257.         }
  258.     }
  259.     /**
  260.      * Toggle Sale activity.
  261.      *
  262.      * @throws \Propel\Runtime\Exception\PropelException
  263.      */
  264.     public function toggleActivity(SaleToggleActivityEvent $event$eventNameEventDispatcherInterface $dispatcher): void
  265.     {
  266.         $sale $event->getSale();
  267.         $con Propel::getWriteConnection(SaleTableMap::DATABASE_NAME);
  268.         $con->beginTransaction();
  269.         try {
  270.             $sale
  271.             ->setActive(!$sale->getActive())
  272.             ->save($con);
  273.             // Update related products sale status
  274.             $dispatcher->dispatch(
  275.                 new ProductSaleStatusUpdateEvent($sale),
  276.                 TheliaEvents::UPDATE_PRODUCT_SALE_STATUS
  277.             );
  278.             $event->setSale($sale);
  279.             $con->commit();
  280.         } catch (PropelException $e) {
  281.             $con->rollback();
  282.             throw $e;
  283.         }
  284.     }
  285.     /**
  286.      * Delete a sale.
  287.      *
  288.      * @throws \Propel\Runtime\Exception\PropelException
  289.      */
  290.     public function delete(SaleDeleteEvent $event$eventNameEventDispatcherInterface $dispatcher): void
  291.     {
  292.         if (null !== $sale SaleQuery::create()->findPk($event->getSaleId())) {
  293.             $con Propel::getWriteConnection(SaleTableMap::DATABASE_NAME);
  294.             $con->beginTransaction();
  295.             try {
  296.                 // Update related products sale status, if required
  297.                 if ($sale->getActive()) {
  298.                     $sale->setActive(false);
  299.                     // Update related products sale status
  300.                     $dispatcher->dispatch(
  301.                         new ProductSaleStatusUpdateEvent($sale),
  302.                         TheliaEvents::UPDATE_PRODUCT_SALE_STATUS
  303.                     );
  304.                 }
  305.                 $sale->delete($con);
  306.                 $event->setSale($sale);
  307.                 $con->commit();
  308.             } catch (PropelException $e) {
  309.                 $con->rollback();
  310.                 throw $e;
  311.             }
  312.         }
  313.     }
  314.     /**
  315.      * Clear all sales.
  316.      *
  317.      * @throws \Exception
  318.      */
  319.     public function clearStatus(/* @noinspection PhpUnusedParameterInspection */ SaleClearStatusEvent $event): void
  320.     {
  321.         $con Propel::getWriteConnection(SaleTableMap::DATABASE_NAME);
  322.         $con->beginTransaction();
  323.         try {
  324.             // Set the active status of all Sales to false
  325.             SaleQuery::create()
  326.                 ->filterByActive(true)
  327.                 ->update(['Active' => false], $con)
  328.             ;
  329.             // Reset all sale status on PSE
  330.             ProductSaleElementsQuery::create()
  331.                 ->filterByPromo(true)
  332.                 ->update(['Promo' => false], $con)
  333.             ;
  334.             $con->commit();
  335.         } catch (PropelException $e) {
  336.             $con->rollback();
  337.             throw $e;
  338.         }
  339.     }
  340.     /**
  341.      * This method check the activation and deactivation dates of sales, and perform
  342.      * the required action depending on the current date.
  343.      *
  344.      * @throws \Propel\Runtime\Exception\PropelException
  345.      */
  346.     public function checkSaleActivation(SaleActiveStatusCheckEvent $event$eventNameEventDispatcherInterface $dispatcher): void
  347.     {
  348.         $con Propel::getWriteConnection(SaleTableMap::DATABASE_NAME);
  349.         $con->beginTransaction();
  350.         try {
  351.             $now time();
  352.             // Disable expired sales
  353.             if (null !== $salesToDisable SaleQuery::create()
  354.                     ->filterByActive(true)
  355.                     ->filterByEndDate($nowCriteria::LESS_THAN)
  356.                     ->find()) {
  357.                 /** @var SaleModel $sale */
  358.                 foreach ($salesToDisable as $sale) {
  359.                     $sale->setActive(false)->save();
  360.                     // Update related products sale status
  361.                     $dispatcher->dispatch(
  362.                         new ProductSaleStatusUpdateEvent($sale),
  363.                         TheliaEvents::UPDATE_PRODUCT_SALE_STATUS
  364.                     );
  365.                 }
  366.             }
  367.             // Enable sales that should be enabled.
  368.             if (null !== $salesToEnable SaleQuery::create()
  369.                     ->filterByActive(false)
  370.                     ->filterByStartDate($nowCriteria::LESS_EQUAL)
  371.                     ->filterByEndDate($nowCriteria::GREATER_EQUAL)
  372.                     ->find()) {
  373.                 /** @var SaleModel $sale */
  374.                 foreach ($salesToEnable as $sale) {
  375.                     $sale->setActive(true)->save();
  376.                     // Update related products sale status
  377.                     $dispatcher->dispatch(
  378.                         new ProductSaleStatusUpdateEvent($sale),
  379.                         TheliaEvents::UPDATE_PRODUCT_SALE_STATUS
  380.                     );
  381.                 }
  382.             }
  383.             $con->commit();
  384.         } catch (PropelException $e) {
  385.             $con->rollback();
  386.             throw $e;
  387.         }
  388.     }
  389.     /**
  390.      * {@inheritdoc}
  391.      */
  392.     public static function getSubscribedEvents()
  393.     {
  394.         return [
  395.             TheliaEvents::SALE_CREATE => ['create'128],
  396.             TheliaEvents::SALE_UPDATE => ['update'128],
  397.             TheliaEvents::SALE_DELETE => ['delete'128],
  398.             TheliaEvents::SALE_TOGGLE_ACTIVITY => ['toggleActivity'128],
  399.             TheliaEvents::SALE_CLEAR_SALE_STATUS => ['clearStatus'128],
  400.             TheliaEvents::UPDATE_PRODUCT_SALE_STATUS => ['updateProductsSaleStatus'128],
  401.             TheliaEvents::CHECK_SALE_ACTIVATION_EVENT => ['checkSaleActivation'128],
  402.         ];
  403.     }
  404. }