vendor/thelia/core/lib/Thelia/Action/Sale.php line 211

Open in your IDE?
  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.      * @param $eventName
  180.      *
  181.      * @throws PropelException
  182.      */
  183.     public function update(SaleUpdateEvent $event$eventNameEventDispatcherInterface $dispatcher): void
  184.     {
  185.         if (null !== $sale SaleQuery::create()->findPk($event->getSaleId())) {
  186.             $con Propel::getWriteConnection(SaleTableMap::DATABASE_NAME);
  187.             $con->beginTransaction();
  188.             try {
  189.                 // Disable all promo flag on sale's currently selected products,
  190.                 // to reset promo status of the products that may have been removed from the selection.
  191.                 $sale->setActive(false);
  192.                 $dispatcher->dispatch(
  193.                     new ProductSaleStatusUpdateEvent($sale),
  194.                     TheliaEvents::UPDATE_PRODUCT_SALE_STATUS
  195.                 );
  196.                 $sale
  197.                     ->setActive($event->getActive())
  198.                     ->setStartDate($event->getStartDate())
  199.                     ->setEndDate($event->getEndDate())
  200.                     ->setPriceOffsetType($event->getPriceOffsetType())
  201.                     ->setDisplayInitialPrice($event->getDisplayInitialPrice())
  202.                     ->setLocale($event->getLocale())
  203.                     ->setSaleLabel($event->getSaleLabel())
  204.                     ->setTitle($event->getTitle())
  205.                     ->setDescription($event->getDescription())
  206.                     ->setChapo($event->getChapo())
  207.                     ->setPostscriptum($event->getPostscriptum())
  208.                     ->save($con)
  209.                 ;
  210.                 $event->setSale($sale);
  211.                 // Update price offsets
  212.                 SaleOffsetCurrencyQuery::create()->filterBySaleId($sale->getId())->delete($con);
  213.                 foreach ($event->getPriceOffsets() as $currencyId => $priceOffset) {
  214.                     $saleOffset = new SaleOffsetCurrency();
  215.                     $saleOffset
  216.                         ->setCurrencyId($currencyId)
  217.                         ->setSaleId($sale->getId())
  218.                         ->setPriceOffsetValue($priceOffset)
  219.                         ->save($con)
  220.                     ;
  221.                 }
  222.                 // Update products
  223.                 SaleProductQuery::create()->filterBySaleId($sale->getId())->delete($con);
  224.                 $productAttributesArray $event->getProductAttributes();
  225.                 foreach ($event->getProducts() as $productId) {
  226.                     if (isset($productAttributesArray[$productId])) {
  227.                         foreach ($productAttributesArray[$productId] as $attributeId) {
  228.                             $saleProduct = new SaleProduct();
  229.                             $saleProduct
  230.                                 ->setSaleId($sale->getId())
  231.                                 ->setProductId($productId)
  232.                                 ->setAttributeAvId($attributeId)
  233.                                 ->save($con)
  234.                             ;
  235.                         }
  236.                     } else {
  237.                         $saleProduct = new SaleProduct();
  238.                         $saleProduct
  239.                             ->setSaleId($sale->getId())
  240.                             ->setProductId($productId)
  241.                             ->setAttributeAvId(null)
  242.                             ->save($con)
  243.                         ;
  244.                     }
  245.                 }
  246.                 // Update related products sale status if the Sale is active. This is not required if the sale is
  247.                 // not active, as we de-activated promotion for this sale at the beginning ofd this method
  248.                 if ($sale->getActive()) {
  249.                     $dispatcher->dispatch(
  250.                         new ProductSaleStatusUpdateEvent($sale),
  251.                         TheliaEvents::UPDATE_PRODUCT_SALE_STATUS
  252.                     );
  253.                 }
  254.                 $con->commit();
  255.             } catch (PropelException $e) {
  256.                 $con->rollback();
  257.                 throw $e;
  258.             }
  259.         }
  260.     }
  261.     /**
  262.      * Toggle Sale activity.
  263.      *
  264.      * @param $eventName
  265.      *
  266.      * @throws \Propel\Runtime\Exception\PropelException
  267.      */
  268.     public function toggleActivity(SaleToggleActivityEvent $event$eventNameEventDispatcherInterface $dispatcher): void
  269.     {
  270.         $sale $event->getSale();
  271.         $con Propel::getWriteConnection(SaleTableMap::DATABASE_NAME);
  272.         $con->beginTransaction();
  273.         try {
  274.             $sale
  275.             ->setActive(!$sale->getActive())
  276.             ->save($con);
  277.             // Update related products sale status
  278.             $dispatcher->dispatch(
  279.                 new ProductSaleStatusUpdateEvent($sale),
  280.                 TheliaEvents::UPDATE_PRODUCT_SALE_STATUS
  281.             );
  282.             $event->setSale($sale);
  283.             $con->commit();
  284.         } catch (PropelException $e) {
  285.             $con->rollback();
  286.             throw $e;
  287.         }
  288.     }
  289.     /**
  290.      * Delete a sale.
  291.      *
  292.      * @param $eventName
  293.      *
  294.      * @throws \Propel\Runtime\Exception\PropelException
  295.      */
  296.     public function delete(SaleDeleteEvent $event$eventNameEventDispatcherInterface $dispatcher): void
  297.     {
  298.         if (null !== $sale SaleQuery::create()->findPk($event->getSaleId())) {
  299.             $con Propel::getWriteConnection(SaleTableMap::DATABASE_NAME);
  300.             $con->beginTransaction();
  301.             try {
  302.                 // Update related products sale status, if required
  303.                 if ($sale->getActive()) {
  304.                     $sale->setActive(false);
  305.                     // Update related products sale status
  306.                     $dispatcher->dispatch(
  307.                         new ProductSaleStatusUpdateEvent($sale),
  308.                         TheliaEvents::UPDATE_PRODUCT_SALE_STATUS
  309.                     );
  310.                 }
  311.                 $sale->delete($con);
  312.                 $event->setSale($sale);
  313.                 $con->commit();
  314.             } catch (PropelException $e) {
  315.                 $con->rollback();
  316.                 throw $e;
  317.             }
  318.         }
  319.     }
  320.     /**
  321.      * Clear all sales.
  322.      *
  323.      * @throws \Exception
  324.      */
  325.     public function clearStatus(/* @noinspection PhpUnusedParameterInspection */ SaleClearStatusEvent $event): void
  326.     {
  327.         $con Propel::getWriteConnection(SaleTableMap::DATABASE_NAME);
  328.         $con->beginTransaction();
  329.         try {
  330.             // Set the active status of all Sales to false
  331.             SaleQuery::create()
  332.                 ->filterByActive(true)
  333.                 ->update(['Active' => false], $con)
  334.             ;
  335.             // Reset all sale status on PSE
  336.             ProductSaleElementsQuery::create()
  337.                 ->filterByPromo(true)
  338.                 ->update(['Promo' => false], $con)
  339.             ;
  340.             $con->commit();
  341.         } catch (PropelException $e) {
  342.             $con->rollback();
  343.             throw $e;
  344.         }
  345.     }
  346.     /**
  347.      * This method check the activation and deactivation dates of sales, and perform
  348.      * the required action depending on the current date.
  349.      *
  350.      * @param $eventName
  351.      *
  352.      * @throws \Propel\Runtime\Exception\PropelException
  353.      */
  354.     public function checkSaleActivation(SaleActiveStatusCheckEvent $event$eventNameEventDispatcherInterface $dispatcher): void
  355.     {
  356.         $con Propel::getWriteConnection(SaleTableMap::DATABASE_NAME);
  357.         $con->beginTransaction();
  358.         try {
  359.             $now time();
  360.             // Disable expired sales
  361.             if (null !== $salesToDisable SaleQuery::create()
  362.                     ->filterByActive(true)
  363.                     ->filterByEndDate($nowCriteria::LESS_THAN)
  364.                     ->find()) {
  365.                 /** @var SaleModel $sale */
  366.                 foreach ($salesToDisable as $sale) {
  367.                     $sale->setActive(false)->save();
  368.                     // Update related products sale status
  369.                     $dispatcher->dispatch(
  370.                         new ProductSaleStatusUpdateEvent($sale),
  371.                         TheliaEvents::UPDATE_PRODUCT_SALE_STATUS
  372.                     );
  373.                 }
  374.             }
  375.             // Enable sales that should be enabled.
  376.             if (null !== $salesToEnable SaleQuery::create()
  377.                     ->filterByActive(false)
  378.                     ->filterByStartDate($nowCriteria::LESS_EQUAL)
  379.                     ->filterByEndDate($nowCriteria::GREATER_EQUAL)
  380.                     ->find()) {
  381.                 /** @var SaleModel $sale */
  382.                 foreach ($salesToEnable as $sale) {
  383.                     $sale->setActive(true)->save();
  384.                     // Update related products sale status
  385.                     $dispatcher->dispatch(
  386.                         new ProductSaleStatusUpdateEvent($sale),
  387.                         TheliaEvents::UPDATE_PRODUCT_SALE_STATUS
  388.                     );
  389.                 }
  390.             }
  391.             $con->commit();
  392.         } catch (PropelException $e) {
  393.             $con->rollback();
  394.             throw $e;
  395.         }
  396.     }
  397.     /**
  398.      * {@inheritdoc}
  399.      */
  400.     public static function getSubscribedEvents()
  401.     {
  402.         return [
  403.             TheliaEvents::SALE_CREATE => ['create'128],
  404.             TheliaEvents::SALE_UPDATE => ['update'128],
  405.             TheliaEvents::SALE_DELETE => ['delete'128],
  406.             TheliaEvents::SALE_TOGGLE_ACTIVITY => ['toggleActivity'128],
  407.             TheliaEvents::SALE_CLEAR_SALE_STATUS => ['clearStatus'128],
  408.             TheliaEvents::UPDATE_PRODUCT_SALE_STATUS => ['updateProductsSaleStatus'128],
  409.             TheliaEvents::CHECK_SALE_ACTIVATION_EVENT => ['checkSaleActivation'128],
  410.         ];
  411.     }
  412. }