modules/CDev/Coupons/src/Model/Coupon.php line 57

Open in your IDE?
  1. <?php
  2. /**
  3.  * Copyright (c) 2001-present X-Cart Holdings LLC. All rights reserved.
  4.  * See https://www.x-cart.com/license-agreement.html for license details.
  5.  */
  6. namespace CDev\Coupons\Model;
  7. use ApiPlatform\Core\Annotation as ApiPlatform;
  8. use CDev\Coupons\API\Endpoint\Coupon\DTO\CouponInput;
  9. use CDev\Coupons\API\Endpoint\Coupon\DTO\CouponOutput;
  10. use Doctrine\Common\Collections\ArrayCollection;
  11. use Doctrine\Common\Collections\Collection;
  12. use Doctrine\ORM\Mapping as ORM;
  13. /**
  14.  * Coupon
  15.  *
  16.  * @ORM\Entity
  17.  * @ORM\Table  (name="coupons",
  18.  *      indexes={
  19.  *          @ORM\Index (name="ce", columns={"code", "enabled"})
  20.  *      }
  21.  * )
  22.  *
  23.  * @ApiPlatform\ApiResource(
  24.  *     input=CouponInput::class,
  25.  *     output=CouponOutput::class,
  26.  *     itemOperations={
  27.  *          "get"={
  28.  *              "method"="GET",
  29.  *              "path"="/coupons/{id}.{_format}",
  30.  *              "identifiers"={"id"},
  31.  *              "requirements"={"id"="\d+"}
  32.  *          },
  33.  *          "put"={
  34.  *              "method"="PUT",
  35.  *              "path"="/coupons/{id}.{_format}",
  36.  *              "identifiers"={"id"},
  37.  *              "requirements"={"id"="\d+"}
  38.  *          },
  39.  *          "delete"={
  40.  *              "method"="DELETE",
  41.  *              "path"="/coupons/{id}.{_format}",
  42.  *              "identifiers"={"id"},
  43.  *              "requirements"={"id"="\d+"}
  44.  *          }
  45.  *     },
  46.  *     collectionOperations={
  47.  *          "get"={
  48.  *              "method"="GET",
  49.  *              "identifiers"={},
  50.  *              "path"="/coupons.{_format}"
  51.  *          },
  52.  *          "post"={
  53.  *              "method"="POST",
  54.  *              "identifiers"={},
  55.  *              "path"="/coupons.{_format}"
  56.  *          }
  57.  *     }
  58.  * )
  59.  */
  60. class Coupon extends \XLite\Model\AEntity
  61. {
  62.     /**
  63.      * Coupon types
  64.      */
  65.     public const TYPE_PERCENT  '%';
  66.     public const TYPE_ABSOLUTE '$';
  67.     /**
  68.      * Coupon validation error codes
  69.      */
  70.     public const ERROR_DISABLED      'disabled';
  71.     public const ERROR_EXPIRED       'expired';
  72.     public const ERROR_USES          'uses';
  73.     public const ERROR_TOTAL         'total';
  74.     public const ERROR_PRODUCT_CLASS 'product_class';
  75.     public const ERROR_MEMBERSHIP    'membership';
  76.     public const ERROR_SINGLE_USE    'singleUse';
  77.     public const ERROR_SINGLE_USE2   'singleUse2';
  78.     public const ERROR_CATEGORY      'category';
  79.     public const TYPE_FREESHIP                 'S';
  80.     public const APPLY_FREESHIP_TO_CHEAPEST    'C';
  81.     public const APPLY_FREESHIP_TO_FREE_METHOD 'F';
  82.     /**
  83.      * Product unique ID
  84.      *
  85.      * @var   integer
  86.      *
  87.      * @ORM\Id
  88.      * @ORM\GeneratedValue (strategy="AUTO")
  89.      * @ORM\Column         (type="integer", options={ "unsigned": true })
  90.      */
  91.     protected $id;
  92.     /**
  93.      * Code
  94.      *
  95.      * @var   string
  96.      *
  97.      * @ORM\Column (type="string", options={ "fixed": true }, length=16)
  98.      */
  99.     protected $code;
  100.     /**
  101.      * Enabled status
  102.      *
  103.      * @var   boolean
  104.      *
  105.      * @ORM\Column (type="boolean")
  106.      */
  107.     protected $enabled true;
  108.     /**
  109.      * Value
  110.      *
  111.      * @var   float
  112.      *
  113.      * @ORM\Column (type="decimal", precision=14, scale=4)
  114.      */
  115.     protected $value 0.0000;
  116.     /**
  117.      * Type
  118.      *
  119.      * @var   string
  120.      *
  121.      * @ORM\Column (type="string", options={ "fixed": true }, length=1)
  122.      */
  123.     protected $type self::TYPE_PERCENT;
  124.     /**
  125.      * Comment
  126.      *
  127.      * @var   string
  128.      *
  129.      * @ORM\Column (type="string", length=64)
  130.      */
  131.     protected $comment '';
  132.     /**
  133.      * Uses count
  134.      *
  135.      * @var   integer
  136.      *
  137.      * @ORM\Column (type="integer", options={ "unsigned": true })
  138.      */
  139.     protected $uses 0;
  140.     /**
  141.      * Date range (begin)
  142.      *
  143.      * @var   integer
  144.      *
  145.      * @ORM\Column (type="integer", options={ "unsigned": true })
  146.      */
  147.     protected $dateRangeBegin 0;
  148.     /**
  149.      * Date range (end)
  150.      *
  151.      * @var   integer
  152.      *
  153.      * @ORM\Column (type="integer", options={ "unsigned": true })
  154.      */
  155.     protected $dateRangeEnd 0;
  156.     /**
  157.      * Total range (begin)
  158.      *
  159.      * @var   float
  160.      *
  161.      * @ORM\Column (type="decimal", precision=14, scale=4)
  162.      */
  163.     protected $totalRangeBegin 0;
  164.     /**
  165.      * Total range (end)
  166.      *
  167.      * @var   float
  168.      *
  169.      * @ORM\Column (type="decimal", precision=14, scale=4)
  170.      */
  171.     protected $totalRangeEnd 0;
  172.     /**
  173.      * Uses limit
  174.      *
  175.      * @var   integer
  176.      *
  177.      * @ORM\Column (type="integer", options={ "unsigned": true })
  178.      */
  179.     protected $usesLimit 0;
  180.     /**
  181.      * Uses limit per user
  182.      *
  183.      * @var   integer
  184.      *
  185.      * @ORM\Column (type="integer", options={ "unsigned": true })
  186.      */
  187.     protected $usesLimitPerUser 0;
  188.     /**
  189.      * Flag: Can a coupon be used together with other coupons (false) or no (true)
  190.      *
  191.      * @var boolean
  192.      *
  193.      * @ORM\Column (type="boolean")
  194.      */
  195.     protected $singleUse false;
  196.     /**
  197.      * Flag: Coupon is used for specific products or not
  198.      *
  199.      * @var boolean
  200.      *
  201.      * @ORM\Column (type="boolean")
  202.      */
  203.     protected $specificProducts false;
  204.     /**
  205.      * Product classes
  206.      *
  207.      * @var   ArrayCollection
  208.      *
  209.      * @ORM\ManyToMany (targetEntity="XLite\Model\ProductClass", inversedBy="coupons")
  210.      * @ORM\JoinTable (name="product_class_coupons",
  211.      *      joinColumns={@ORM\JoinColumn (name="coupon_id", referencedColumnName="id", onDelete="CASCADE")},
  212.      *      inverseJoinColumns={@ORM\JoinColumn (name="class_id", referencedColumnName="id", onDelete="CASCADE")}
  213.      * )
  214.      */
  215.     protected $productClasses;
  216.     /**
  217.      * Memberships
  218.      *
  219.      * @var   ArrayCollection
  220.      *
  221.      * @ORM\ManyToMany (targetEntity="XLite\Model\Membership", inversedBy="coupons")
  222.      * @ORM\JoinTable (name="membership_coupons",
  223.      *      joinColumns={@ORM\JoinColumn (name="coupon_id", referencedColumnName="id", onDelete="CASCADE")},
  224.      *      inverseJoinColumns={@ORM\JoinColumn (name="membership_id", referencedColumnName="membership_id", onDelete="CASCADE")}
  225.      * )
  226.      */
  227.     protected $memberships;
  228.     /**
  229.      * Zones
  230.      *
  231.      * @var   ArrayCollection
  232.      *
  233.      * @ORM\ManyToMany (targetEntity="XLite\Model\Zone", inversedBy="coupons")
  234.      * @ORM\JoinTable (name="zone_coupons",
  235.      *      joinColumns={@ORM\JoinColumn (name="coupon_id", referencedColumnName="id", onDelete="CASCADE")},
  236.      *      inverseJoinColumns={@ORM\JoinColumn (name="zone_id", referencedColumnName="zone_id", onDelete="CASCADE")}
  237.      * )
  238.      */
  239.     protected $zones;
  240.     /**
  241.      * Coupon products
  242.      *
  243.      * @var   ArrayCollection
  244.      *
  245.      * @ORM\OneToMany (targetEntity="CDev\Coupons\Model\CouponProduct", mappedBy="coupon", cascade={"persist"})
  246.      */
  247.     protected $couponProducts;
  248.     /**
  249.      * Used coupons
  250.      *
  251.      * @var   Collection
  252.      *
  253.      * @ORM\OneToMany (targetEntity="CDev\Coupons\Model\UsedCoupon", mappedBy="coupon")
  254.      */
  255.     protected $usedCoupons;
  256.     /**
  257.      * Categories
  258.      *
  259.      * @var   ArrayCollection
  260.      *
  261.      * @ORM\ManyToMany (targetEntity="XLite\Model\Category", inversedBy="coupons")
  262.      * @ORM\JoinTable (name="coupon_categories",
  263.      *      joinColumns={@ORM\JoinColumn (name="coupon_id", referencedColumnName="id", onDelete="CASCADE")},
  264.      *      inverseJoinColumns={@ORM\JoinColumn (name="category_id", referencedColumnName="category_id", onDelete="CASCADE")}
  265.      * )
  266.      */
  267.     protected $categories;
  268.     /**
  269.      * @ORM\Column (type="string", options={ "fixed": true }, length=1)
  270.      */
  271.     protected string $applyFreeShippingTo self::APPLY_FREESHIP_TO_CHEAPEST;
  272.     protected static $runtimeCacheForUsedCouponsCount = [];
  273.     /**
  274.      * Constructor
  275.      *
  276.      * @param array $data Entity properties OPTIONAL
  277.      */
  278.     public function __construct(array $data = [])
  279.     {
  280.         $this->productClasses = new ArrayCollection();
  281.         $this->memberships    = new ArrayCollection();
  282.         $this->zones          = new ArrayCollection();
  283.         $this->couponProducts = new ArrayCollection();
  284.         $this->usedCoupons    = new ArrayCollection();
  285.         $this->categories     = new ArrayCollection();
  286.         parent::__construct($data);
  287.     }
  288.     /**
  289.      * Check - discount is absolute or not
  290.      *
  291.      * @return boolean
  292.      */
  293.     public function isAbsolute()
  294.     {
  295.         return $this->getType() === static::TYPE_ABSOLUTE;
  296.     }
  297.     /**
  298.      * Check - coupon is started
  299.      *
  300.      * @return boolean
  301.      */
  302.     public function isStarted()
  303.     {
  304.         return $this->getDateRangeBegin() === || $this->getDateRangeBegin() < \XLite\Core\Converter::time();
  305.     }
  306.     /**
  307.      * Check - coupon is expired or not
  308.      *
  309.      * @return boolean
  310.      */
  311.     public function isExpired()
  312.     {
  313.         return $this->getDateRangeEnd() && $this->getDateRangeEnd() < \XLite\Core\Converter::time();
  314.     }
  315.     /**
  316.      * Check coupon activity
  317.      *
  318.      * @param \XLite\Model\Order $order Order OPTIONAL
  319.      *
  320.      * @return boolean
  321.      */
  322.     public function isActive(\XLite\Model\Order $order null)
  323.     {
  324.         try {
  325.             $result $this->checkCompatibility($order);
  326.         } catch (\CDev\Coupons\Core\CompatibilityException $exception) {
  327.             $result false;
  328.         }
  329.         return $result;
  330.     }
  331.     /**
  332.      * Get public code
  333.      *
  334.      * @return string
  335.      */
  336.     public function getPublicCode()
  337.     {
  338.         $code $this->getCode();
  339.         if ($this->isFreeShipping()) {
  340.             $code sprintf('%s (%s)'$code, static::t('[coupons] Free shipping'));
  341.         }
  342.         return $code;
  343.     }
  344.     /**
  345.      * Get coupon public name
  346.      *
  347.      * @return string
  348.      */
  349.     public function getPublicName()
  350.     {
  351.         $suffix '';
  352.         if ($this->getType() === \CDev\Coupons\Model\Coupon::TYPE_PERCENT) {
  353.             $suffix sprintf('(%s%%)'$this->getValue());
  354.         }
  355.         return $this->getPublicCode() . ' ' $suffix;
  356.     }
  357.     // {{{ Amount
  358.     /**
  359.      * Get amount
  360.      *
  361.      * @param \XLite\Model\Order $order Order
  362.      *
  363.      * @return float
  364.      */
  365.     public function getAmount(\XLite\Model\Order $order)
  366.     {
  367.         if ($this->isFreeShipping()) {
  368.             return 0;
  369.         }
  370.         $total $this->getOrderTotal($order);
  371.         return $this->isAbsolute()
  372.             ? min($total$this->getValue())
  373.             : ($total $this->getValue() / 100);
  374.     }
  375.     /**
  376.      * Get order total
  377.      *
  378.      * @param \XLite\Model\Order $order Order
  379.      *
  380.      * @return float
  381.      */
  382.     protected function getOrderTotal(\XLite\Model\Order $order)
  383.     {
  384.         return array_reduce($this->getValidOrderItems($order), static function ($carry$item) {
  385.             return $carry $item->getSubtotal();
  386.         }, 0);
  387.     }
  388.     /**
  389.      * Get order items which are valid for the coupon
  390.      *
  391.      * @param \XLite\Model\Order $order Order
  392.      *
  393.      * @return array
  394.      */
  395.     protected function getValidOrderItems($order)
  396.     {
  397.         return $order->getValidItemsByCoupon($this);
  398.     }
  399.     /**
  400.      * Is coupon valid for product
  401.      *
  402.      * @param \XLite\Model\Product $product Product
  403.      *
  404.      * @return boolean
  405.      */
  406.     public function isValidForProduct(\XLite\Model\Product $product)
  407.     {
  408.         $result true;
  409.         if (count($this->getProductClasses())) {
  410.             // Check product class
  411.             $result $product->getProductClass()
  412.                 && $this->getProductClasses()->contains($product->getProductClass());
  413.         }
  414.         if ($result && count($this->getCategories())) {
  415.             // Check categories
  416.             $result false;
  417.             foreach ($product->getCategories() as $category) {
  418.                 if ($this->getCategories()->contains($category)) {
  419.                     $result true;
  420.                     break;
  421.                 }
  422.             }
  423.         }
  424.         if ($result && $this->getSpecificProducts()) {
  425.             // Check product
  426.             $result in_array($product->getProductId(), $this->getApplicableProductIds());
  427.         }
  428.         return $result;
  429.     }
  430.     // }}}
  431.     /**
  432.      * Check coupon compatibility
  433.      *
  434.      * @param \XLite\Model\Order $order Order
  435.      *
  436.      * @throws \CDev\Coupons\Core\CompatibilityException
  437.      *
  438.      * @return boolean
  439.      */
  440.     public function checkCompatibility(\XLite\Model\Order $order null)
  441.     {
  442.         if (!$this->getEnabled()) {
  443.             $this->throwCompatibilityException(
  444.                 '',
  445.                 'Sorry, the coupon you entered is invalid. Make sure the coupon code is spelled correctly'
  446.             );
  447.         }
  448.         $this->checkDate();
  449.         $this->checkUsage();
  450.         if ($order) {
  451.             if ($order->getProfile()) {
  452.                 $this->checkPerUserUsage($order->getProfile(), $order->containsCoupon($this));
  453.             }
  454.             $this->checkConflictsWithCoupons($order);
  455.             $this->checkMembership($order);
  456.             if ($this->isFreeShipping()) {
  457.                 $this->checkAllItemsForCategory($order);
  458.                 $this->checkAllItemsForProducts($order);
  459.                 $this->checkAllItemForProductClass($order);
  460.             } else {
  461.                 $this->checkCategory($order);
  462.                 $this->checkProductClass($order);
  463.                 $this->checkProducts($order);
  464.             }
  465.             $this->checkOrderTotal($order);
  466.             $this->checkZone($order);
  467.         }
  468.         return true;
  469.     }
  470.     // {{{ Date
  471.     /**
  472.      * Check coupon dates
  473.      *
  474.      * @throws \CDev\Coupons\Core\CompatibilityException
  475.      *
  476.      * @return void
  477.      */
  478.     protected function checkDate()
  479.     {
  480.         if (!$this->isStarted()) {
  481.             $this->throwCompatibilityException(
  482.                 '',
  483.                 'Sorry, the coupon you entered is invalid. Make sure the coupon code is spelled correctly'
  484.             );
  485.         }
  486.         if ($this->isExpired()) {
  487.             $this->throwCompatibilityException(
  488.                 '',
  489.                 'Sorry, the coupon has expired'
  490.             );
  491.         }
  492.     }
  493.     // }}}
  494.     // {{{ Usage
  495.     /**
  496.      * Check coupon usages
  497.      *
  498.      * @throws \CDev\Coupons\Core\CompatibilityException
  499.      *
  500.      * @return void
  501.      */
  502.     protected function checkUsage()
  503.     {
  504.         if ($this->getUsesLimit() && $this->getUsesLimit() <= $this->getUses()) {
  505.             $this->throwCompatibilityException(
  506.                 '',
  507.                 'Sorry, the coupon use limit has been reached'
  508.             );
  509.         }
  510.     }
  511.     /**
  512.      * Check coupon usages per user
  513.      *
  514.      * @throws \CDev\Coupons\Core\CompatibilityException
  515.      *
  516.      * @return void
  517.      */
  518.     protected function checkPerUserUsage(\XLite\Model\Profile $profile$inOrder)
  519.     {
  520.         if (>= $this->getUsesLimitPerUser()) {
  521.             return;
  522.         }
  523.         $profileUsesCount null;
  524.         if (array_key_exists($profile->getLogin(), static::$runtimeCacheForUsedCouponsCount)) {
  525.             $profileUsesCount = static::$runtimeCacheForUsedCouponsCount[$profile->getLogin()];
  526.         } else {
  527.             $profileUsesCount $this->calculatePerUserUsage($profile);
  528.             static::$runtimeCacheForUsedCouponsCount[$profile->getLogin()] = $profileUsesCount;
  529.         }
  530.         if ($inOrder) {
  531.             $profileUsesCount -= 1;
  532.         }
  533.         if ($this->getUsesLimitPerUser() <= $profileUsesCount) {
  534.             $this->throwCompatibilityException(
  535.                 '',
  536.                 'Sorry, the coupon use limit has been reached'
  537.             );
  538.         }
  539.     }
  540.     /**
  541.      * @param \XLite\Model\Profile $profile
  542.      *
  543.      * @return int
  544.      */
  545.     protected function calculatePerUserUsage(\XLite\Model\Profile $profile)
  546.     {
  547.         return $this->getUsedCoupons()->filter(
  548.             static function ($usedCoupon) use ($profile) {
  549.                 /** @var UsedCoupon $usedCoupon */
  550.                 $orderProfileIdentificator $usedCoupon->getOrder()->getProfile()
  551.                     ? $usedCoupon->getOrder()->getProfile()->getLogin()
  552.                     : null;
  553.                 $currentProfileIdentificator $profile->getLogin();
  554.                 return $orderProfileIdentificator
  555.                     && $currentProfileIdentificator
  556.                     && $orderProfileIdentificator === $currentProfileIdentificator;
  557.             }
  558.         )->count();
  559.     }
  560.     // }}}
  561.     // {{{ Coupons conflicts
  562.     /**
  563.      * Check if coupon is unique within an order
  564.      *
  565.      * @param \XLite\Model\Order $order Order
  566.      *
  567.      * @throws \CDev\Coupons\Core\CompatibilityException
  568.      *
  569.      * @return boolean
  570.      */
  571.     public function checkUnique(\XLite\Model\Order $order)
  572.     {
  573.         if ($order->containsCoupon($this)) {
  574.             $this->throwCompatibilityException(
  575.                 '',
  576.                 'You have already used the coupon'
  577.             );
  578.         }
  579.         return true;
  580.     }
  581.     /**
  582.      * Check coupon usages
  583.      *
  584.      * @param \XLite\Model\Order $order Order
  585.      *
  586.      * @throws \CDev\Coupons\Core\CompatibilityException
  587.      *
  588.      * @return void
  589.      */
  590.     protected function checkConflictsWithCoupons(\XLite\Model\Order $order)
  591.     {
  592.         if (!$order->containsCoupon($this)) {
  593.             if ($this->getSingleUse() && count($this->getOrderUsedCoupons($order))) {
  594.                 $this->throwCompatibilityException(
  595.                     static::ERROR_SINGLE_USE,
  596.                     'This coupon cannot be combined with other coupons'
  597.                 );
  598.             }
  599.             if (!$this->getSingleUse() && $this->hasOrderSingleCoupon($order)) {
  600.                 $this->throwCompatibilityException(
  601.                     static::ERROR_SINGLE_USE2,
  602.                     'Sorry, this coupon cannot be combined with the coupon already applied. Revome the previously applied coupon and try again.'
  603.                 );
  604.             }
  605.         }
  606.     }
  607.     /**
  608.      * @param \XLite\Model\Order $order
  609.      *
  610.      * @return array
  611.      */
  612.     protected function getOrderUsedCoupons($order)
  613.     {
  614.         return $order->getUsedCoupons();
  615.     }
  616.     /**
  617.      * @param \XLite\Model\Order $order
  618.      *
  619.      * @return bool
  620.      */
  621.     protected function hasOrderSingleCoupon($order)
  622.     {
  623.         return $order->hasSingleUseCoupon();
  624.     }
  625.     // }}}
  626.     // {{{ Total
  627.     /**
  628.      * Check order total
  629.      *
  630.      * @param \XLite\Model\Order $order Order
  631.      *
  632.      * @throws \CDev\Coupons\Core\CompatibilityException
  633.      *
  634.      * @return void
  635.      */
  636.     protected function checkOrderTotal(\XLite\Model\Order $order)
  637.     {
  638.         $total $this->getOrderTotal($order);
  639.         $currency $order->getCurrency();
  640.         $rangeBegin $this->getTotalRangeBegin();
  641.         $rangeEnd $this->getTotalRangeEnd();
  642.         $betweenTotalCoupon $rangeBegin && $rangeEnd 0;
  643.         $rangeBeginValid $rangeBegin === 0.0 || $rangeBegin <= $total;
  644.         $rangeEndValid $rangeEnd === 0.0 || $rangeEnd >= $total;
  645.         $hasProductsConditions $this->getSpecificProducts()
  646.             || count($this->getCategories()) > 0
  647.             || count($this->getProductClasses()) > 0;
  648.         if ($betweenTotalCoupon && (!$rangeBeginValid || !$rangeEndValid)) {
  649.             $text $hasProductsConditions
  650.                 $this->getBetweenSubtotalConditionalExceptionText()
  651.                 : $this->getBetweenSubtotalExceptionText();
  652.             $this->throwCompatibilityException(
  653.                 static::ERROR_TOTAL,
  654.                 $text,
  655.                 [
  656.                     'min' => implode(''$currency->formatParts($rangeBegin)),
  657.                     'max' => implode(''$currency->formatParts($rangeEnd)),
  658.                 ]
  659.             );
  660.         } elseif (!$rangeBeginValid) {
  661.             $text $hasProductsConditions
  662.                 $this->getLeastSubtotalConditionalExceptionText()
  663.                 : $this->getLeastSubtotalExceptionText();
  664.             $this->throwCompatibilityException(
  665.                 static::ERROR_TOTAL,
  666.                 $text,
  667.                 [
  668.                     'min' => implode(''$currency->formatParts($rangeBegin))
  669.                 ]
  670.             );
  671.         } elseif (!$rangeEndValid) {
  672.             $text $hasProductsConditions
  673.                 $this->getExceedSubtotalConditionalExceptionText()
  674.                 : $this->getExceedSubtotalExceptionText();
  675.             $this->throwCompatibilityException(
  676.                 static::ERROR_TOTAL,
  677.                 $text,
  678.                 [
  679.                     'max' => implode(''$currency->formatParts($rangeEnd))
  680.                 ]
  681.             );
  682.         }
  683.     }
  684.     /**
  685.      * Return text of exception
  686.      *
  687.      * @return void
  688.      */
  689.     protected function getBetweenSubtotalExceptionText()
  690.     {
  691.         return 'To use the coupon, your order subtotal must be between X and Y';
  692.     }
  693.     /**
  694.      * Return text of exception
  695.      *
  696.      * @return void
  697.      */
  698.     protected function getLeastSubtotalExceptionText()
  699.     {
  700.         return 'To use the coupon, your order subtotal must be at least X';
  701.     }
  702.     /**
  703.      * Return text of exception
  704.      *
  705.      * @return void
  706.      */
  707.     protected function getExceedSubtotalExceptionText()
  708.     {
  709.         return 'To use the coupon, your order subtotal must not exceed Y';
  710.     }
  711.     /**
  712.      * Return text of exception
  713.      *
  714.      * @return void
  715.      */
  716.     protected function getBetweenSubtotalConditionalExceptionText()
  717.     {
  718.         return 'To use the coupon, your order subtotal must be between X and Y for specific products';
  719.     }
  720.     /**
  721.      * Return text of exception
  722.      *
  723.      * @return void
  724.      */
  725.     protected function getLeastSubtotalConditionalExceptionText()
  726.     {
  727.         return 'To use the coupon, your order subtotal must be at least X for specific products';
  728.     }
  729.     /**
  730.      * Return text of exception
  731.      *
  732.      * @return void
  733.      */
  734.     protected function getExceedSubtotalConditionalExceptionText()
  735.     {
  736.         return 'To use the coupon, your order subtotal must not exceed Y for specific products';
  737.     }
  738.     // }}}
  739.     // {{{ Category
  740.     /**
  741.      * Check coupon category
  742.      *
  743.      * @param \XLite\Model\Order $order Order
  744.      *
  745.      * @throws \CDev\Coupons\Core\CompatibilityException
  746.      *
  747.      * @return void
  748.      */
  749.     protected function checkCategory(\XLite\Model\Order $order)
  750.     {
  751.         if ($this->getCategories()->count()) {
  752.             $found false;
  753.             foreach ($order->getItems() as $item) {
  754.                 foreach ($item->getProduct()->getCategories() as $category) {
  755.                     if ($this->getCategories()->contains($category)) {
  756.                         $found true;
  757.                         break;
  758.                     }
  759.                 }
  760.                 if ($found) {
  761.                     break;
  762.                 }
  763.             }
  764.             if (!$found) {
  765.                 $this->throwCompatibilityException(
  766.                     '',
  767.                     'Sorry, the coupon you entered cannot be applied to the items in your cart'
  768.                 );
  769.             }
  770.         }
  771.     }
  772.     // }}}
  773.     // {{{ Products
  774.     /**
  775.      * Check coupon products
  776.      *
  777.      * @param \XLite\Model\Order $order Order
  778.      *
  779.      * @throws \CDev\Coupons\Core\CompatibilityException
  780.      *
  781.      * @return void
  782.      */
  783.     protected function checkProducts(\XLite\Model\Order $order)
  784.     {
  785.         if ($this->getSpecificProducts()) {
  786.             $applicableProductIds $this->getApplicableProductIds();
  787.             $found false;
  788.             foreach ($order->getItems() as $item) {
  789.                 if (in_array($item->getProduct()->getProductId(), $applicableProductIds)) {
  790.                     $found true;
  791.                     break;
  792.                 }
  793.             }
  794.             if (!$found) {
  795.                 $this->throwCompatibilityException(
  796.                     '',
  797.                     'Sorry, the coupon you entered cannot be applied to the items in your cart'
  798.                 );
  799.             }
  800.         }
  801.     }
  802.     // }}}
  803.     // {{{ Membership
  804.     /**
  805.      * Check coupon membership
  806.      *
  807.      * @param \XLite\Model\Order $order Order
  808.      *
  809.      * @throws \CDev\Coupons\Core\CompatibilityException
  810.      *
  811.      * @return void
  812.      */
  813.     protected function checkMembership(\XLite\Model\Order $order)
  814.     {
  815.         if (
  816.             $this->getMemberships()->count()
  817.             && (!$order->getProfile()
  818.                 || !$this->getMemberships()->contains($order->getProfile()->getMembership())
  819.             )
  820.         ) {
  821.             $this->throwCompatibilityException(
  822.                 '',
  823.                 'Sorry, the coupon you entered is not valid for your membership level. Contact the administrator'
  824.             );
  825.         }
  826.     }
  827.     // }}}
  828.     // {{{ Zone
  829.     /**
  830.      * Check coupon zone
  831.      *
  832.      * @param \XLite\Model\Order $order Order
  833.      *
  834.      * @throws \CDev\Coupons\Core\CompatibilityException
  835.      *
  836.      * @return void
  837.      */
  838.     protected function checkZone(\XLite\Model\Order $order)
  839.     {
  840.         $profile $order->getProfile();
  841.         $shippingAddress $profile $profile->getShippingAddress() : null;
  842.         if (!$shippingAddress) {
  843.             $shippingAddress \XLite\Model\Address::createDefaultShippingAddress();
  844.         }
  845.         if ($shippingAddress && !$this->getZones()->isEmpty()) {
  846.             $applicableZones \XLite\Core\Database::getRepo('XLite\Model\Zone')->findApplicableZones($shippingAddress->toArray());
  847.             $couponZoneIds array_map(static function ($zone) {
  848.                 return $zone->getZoneId();
  849.             }, $this->getZones()->toArray());
  850.             $isApplicable false;
  851.             foreach ($applicableZones as $zone) {
  852.                 if (in_array($zone->getZoneId(), $couponZoneIds)) {
  853.                     $isApplicable true;
  854.                     break;
  855.                 }
  856.             }
  857.             if (!$isApplicable) {
  858.                 $this->throwCompatibilityException(
  859.                     '',
  860.                     'Sorry, the coupon you entered cannot be applied to this delivery address'
  861.                 );
  862.             }
  863.         }
  864.     }
  865.     // }}}
  866.     // {{{ Product class
  867.     /**
  868.      * Check coupon product class
  869.      *
  870.      * @param \XLite\Model\Order $order Order
  871.      *
  872.      * @throws \CDev\Coupons\Core\CompatibilityException
  873.      *
  874.      * @return void
  875.      */
  876.     protected function checkProductClass(\XLite\Model\Order $order)
  877.     {
  878.         if ($this->getProductClasses()->count()) {
  879.             $found false;
  880.             foreach ($order->getItems() as $item) {
  881.                 if (
  882.                     $item->getProduct()->getProductClass()
  883.                     && $this->getProductClasses()->contains($item->getProduct()->getProductClass())
  884.                 ) {
  885.                     $found true;
  886.                     break;
  887.                 }
  888.             }
  889.             if (!$found) {
  890.                 $this->throwCompatibilityException(
  891.                     '',
  892.                     'Sorry, the coupon you entered cannot be applied to the items in your cart'
  893.                 );
  894.             }
  895.         }
  896.     }
  897.     // }}}
  898.     /**
  899.      * @throws \CDev\Coupons\Core\CompatibilityException
  900.      * @return void
  901.      */
  902.     protected function checkAllItemsForCategory(\XLite\Model\Order $order)
  903.     {
  904.         if ($this->getCategories()->count()) {
  905.             $isAll true;
  906.             foreach ($order->getItems() as $item) {
  907.                 if (!$this->isItemApplyCategoryCondition($item)) {
  908.                     $isAll false;
  909.                     break;
  910.                 }
  911.             }
  912.             if (!$isAll) {
  913.                 $this->throwCompatibilityException(
  914.                     '',
  915.                     'Sorry, the coupon you entered cannot be applied to the items in your cart'
  916.                 );
  917.             }
  918.         }
  919.     }
  920.     protected function isItemApplyCategoryCondition(\XLite\Model\OrderItem $item): bool
  921.     {
  922.         $found false;
  923.         foreach ($item->getProduct()->getCategories() as $category) {
  924.             if ($this->getCategories()->contains($category)) {
  925.                 $found true;
  926.                 break;
  927.             }
  928.         }
  929.         return $found;
  930.     }
  931.     /**
  932.      * @throws \CDev\Coupons\Core\CompatibilityException
  933.      * @return void
  934.      */
  935.     protected function checkAllItemsForProducts(\XLite\Model\Order $order)
  936.     {
  937.         if ($this->getSpecificProducts()) {
  938.             $applicableProductIds $this->getApplicableProductIds();
  939.             $isAll true;
  940.             foreach ($order->getItems() as $item) {
  941.                 if (!in_array($item->getProduct()->getProductId(), $applicableProductIds)) {
  942.                     $isAll false;
  943.                     break;
  944.                 }
  945.             }
  946.             if (!$isAll) {
  947.                 $this->throwCompatibilityException(
  948.                     '',
  949.                     'Sorry, the coupon you entered cannot be applied to the items in your cart'
  950.                 );
  951.             }
  952.         }
  953.     }
  954.     /**
  955.      * @throws \CDev\Coupons\Core\CompatibilityException
  956.      * @return void
  957.      */
  958.     protected function checkAllItemForProductClass(\XLite\Model\Order $order)
  959.     {
  960.         if ($this->getProductClasses()->count()) {
  961.             $isAll true;
  962.             foreach ($order->getItems() as $item) {
  963.                 $itemProductClass $item->getProduct()->getProductClass();
  964.                 if (
  965.                     !$itemProductClass
  966.                     || !$this->getProductClasses()->contains($itemProductClass)
  967.                 ) {
  968.                     $isAll false;
  969.                     break;
  970.                 }
  971.             }
  972.             if (!$isAll) {
  973.                 $this->throwCompatibilityException(
  974.                     '',
  975.                     'Sorry, the coupon you entered cannot be applied to the items in your cart'
  976.                 );
  977.             }
  978.         }
  979.     }
  980.     /**
  981.      * Throws exception
  982.      *
  983.      * @param string $code    Message params
  984.      * @param string $message Message text
  985.      * @param array  $params  Message params
  986.      *
  987.      * @throws \CDev\Coupons\Core\CompatibilityException
  988.      *
  989.      * @return void
  990.      */
  991.     protected function throwCompatibilityException($code ''$message null, array $params = [])
  992.     {
  993.         throw new \CDev\Coupons\Core\CompatibilityException($message$params$this$code);
  994.     }
  995.     /**
  996.      * Get id
  997.      *
  998.      * @return integer
  999.      */
  1000.     public function getId()
  1001.     {
  1002.         return $this->id;
  1003.     }
  1004.     /**
  1005.      * Set code
  1006.      *
  1007.      * @param string $code
  1008.      * @return Coupon
  1009.      */
  1010.     public function setCode($code)
  1011.     {
  1012.         $this->code $code;
  1013.         return $this;
  1014.     }
  1015.     /**
  1016.      * Get code
  1017.      *
  1018.      * @return string
  1019.      */
  1020.     public function getCode()
  1021.     {
  1022.         return $this->code;
  1023.     }
  1024.     /**
  1025.      * Set enabled
  1026.      *
  1027.      * @param boolean $enabled
  1028.      * @return Coupon
  1029.      */
  1030.     public function setEnabled($enabled)
  1031.     {
  1032.         $this->enabled = (bool)$enabled;
  1033.         return $this;
  1034.     }
  1035.     /**
  1036.      * Get enabled
  1037.      *
  1038.      * @return boolean
  1039.      */
  1040.     public function getEnabled()
  1041.     {
  1042.         return $this->enabled;
  1043.     }
  1044.     /**
  1045.      * Set value
  1046.      *
  1047.      * @param float $value
  1048.      * @return Coupon
  1049.      */
  1050.     public function setValue($value)
  1051.     {
  1052.         $this->value $value;
  1053.         return $this;
  1054.     }
  1055.     /**
  1056.      * Get value
  1057.      *
  1058.      * @return float
  1059.      */
  1060.     public function getValue()
  1061.     {
  1062.         return $this->value;
  1063.     }
  1064.     /**
  1065.      * Set type
  1066.      *
  1067.      * @param string $type
  1068.      * @return Coupon
  1069.      */
  1070.     public function setType($type)
  1071.     {
  1072.         $this->type $type;
  1073.         return $this;
  1074.     }
  1075.     /**
  1076.      * Get type
  1077.      *
  1078.      * @return string
  1079.      */
  1080.     public function getType()
  1081.     {
  1082.         return $this->type;
  1083.     }
  1084.     /**
  1085.      * Set comment
  1086.      *
  1087.      * @param string $comment
  1088.      * @return Coupon
  1089.      */
  1090.     public function setComment($comment)
  1091.     {
  1092.         $this->comment $comment;
  1093.         return $this;
  1094.     }
  1095.     /**
  1096.      * Get comment
  1097.      *
  1098.      * @return string
  1099.      */
  1100.     public function getComment()
  1101.     {
  1102.         return $this->comment;
  1103.     }
  1104.     /**
  1105.      * Set uses
  1106.      *
  1107.      * @param integer $uses
  1108.      * @return Coupon
  1109.      */
  1110.     public function setUses($uses)
  1111.     {
  1112.         $this->uses $uses;
  1113.         return $this;
  1114.     }
  1115.     /**
  1116.      * Get uses
  1117.      *
  1118.      * @return integer
  1119.      */
  1120.     public function getUses()
  1121.     {
  1122.         return $this->uses;
  1123.     }
  1124.     /**
  1125.      * Set dateRangeBegin
  1126.      *
  1127.      * @param integer $dateRangeBegin
  1128.      * @return Coupon
  1129.      */
  1130.     public function setDateRangeBegin($dateRangeBegin)
  1131.     {
  1132.         $this->dateRangeBegin $dateRangeBegin;
  1133.         return $this;
  1134.     }
  1135.     /**
  1136.      * Get dateRangeBegin
  1137.      *
  1138.      * @return integer
  1139.      */
  1140.     public function getDateRangeBegin()
  1141.     {
  1142.         return $this->dateRangeBegin;
  1143.     }
  1144.     /**
  1145.      * Set dateRangeEnd
  1146.      *
  1147.      * @param integer $dateRangeEnd
  1148.      * @return Coupon
  1149.      */
  1150.     public function setDateRangeEnd($dateRangeEnd)
  1151.     {
  1152.         $this->dateRangeEnd $dateRangeEnd;
  1153.         return $this;
  1154.     }
  1155.     /**
  1156.      * Get dateRangeEnd
  1157.      *
  1158.      * @return integer
  1159.      */
  1160.     public function getDateRangeEnd()
  1161.     {
  1162.         return $this->dateRangeEnd;
  1163.     }
  1164.     /**
  1165.      * Set totalRangeBegin
  1166.      *
  1167.      * @param float $totalRangeBegin
  1168.      * @return Coupon
  1169.      */
  1170.     public function setTotalRangeBegin($totalRangeBegin)
  1171.     {
  1172.         $this->totalRangeBegin $totalRangeBegin;
  1173.         return $this;
  1174.     }
  1175.     /**
  1176.      * Get totalRangeBegin
  1177.      *
  1178.      * @return float
  1179.      */
  1180.     public function getTotalRangeBegin()
  1181.     {
  1182.         return $this->totalRangeBegin;
  1183.     }
  1184.     /**
  1185.      * Set totalRangeEnd
  1186.      *
  1187.      * @param float $totalRangeEnd
  1188.      * @return Coupon
  1189.      */
  1190.     public function setTotalRangeEnd($totalRangeEnd)
  1191.     {
  1192.         $this->totalRangeEnd $totalRangeEnd;
  1193.         return $this;
  1194.     }
  1195.     /**
  1196.      * Get totalRangeEnd
  1197.      *
  1198.      * @return float
  1199.      */
  1200.     public function getTotalRangeEnd()
  1201.     {
  1202.         return $this->totalRangeEnd;
  1203.     }
  1204.     /**
  1205.      * Set usesLimit
  1206.      *
  1207.      * @param integer $usesLimit
  1208.      * @return Coupon
  1209.      */
  1210.     public function setUsesLimit($usesLimit)
  1211.     {
  1212.         $this->usesLimit $usesLimit;
  1213.         return $this;
  1214.     }
  1215.     /**
  1216.      * Get usesLimit
  1217.      *
  1218.      * @return integer
  1219.      */
  1220.     public function getUsesLimit()
  1221.     {
  1222.         return $this->usesLimit;
  1223.     }
  1224.     /**
  1225.      * Set usesLimitPerUser
  1226.      *
  1227.      * @param integer $usesLimitPerUser
  1228.      * @return Coupon
  1229.      */
  1230.     public function setUsesLimitPerUser($usesLimitPerUser)
  1231.     {
  1232.         $this->usesLimitPerUser $usesLimitPerUser;
  1233.         return $this;
  1234.     }
  1235.     /**
  1236.      * Get usesLimitPerUser
  1237.      *
  1238.      * @return integer
  1239.      */
  1240.     public function getUsesLimitPerUser()
  1241.     {
  1242.         return $this->usesLimitPerUser;
  1243.     }
  1244.     /**
  1245.      * Set singleUse
  1246.      *
  1247.      * @param boolean $singleUse
  1248.      * @return Coupon
  1249.      */
  1250.     public function setSingleUse($singleUse)
  1251.     {
  1252.         $this->singleUse $singleUse;
  1253.         return $this;
  1254.     }
  1255.     /**
  1256.      * Get singleUse
  1257.      *
  1258.      * @return boolean
  1259.      */
  1260.     public function getSingleUse()
  1261.     {
  1262.         return $this->singleUse;
  1263.     }
  1264.     /**
  1265.      * Set specificProducts
  1266.      *
  1267.      * @param boolean $specificProducts
  1268.      * @return Coupon
  1269.      */
  1270.     public function setSpecificProducts($specificProducts)
  1271.     {
  1272.         $this->specificProducts $specificProducts;
  1273.         return $this;
  1274.     }
  1275.     /**
  1276.      * Get specificProducts
  1277.      *
  1278.      * @return boolean
  1279.      */
  1280.     public function getSpecificProducts()
  1281.     {
  1282.         return $this->specificProducts;
  1283.     }
  1284.     /**
  1285.      * Add productClasses
  1286.      *
  1287.      * @param \XLite\Model\ProductClass $productClasses
  1288.      * @return Coupon
  1289.      */
  1290.     public function addProductClasses(\XLite\Model\ProductClass $productClasses)
  1291.     {
  1292.         $this->productClasses[] = $productClasses;
  1293.         return $this;
  1294.     }
  1295.     /**
  1296.      * Get productClasses
  1297.      *
  1298.      * @return Collection
  1299.      */
  1300.     public function getProductClasses()
  1301.     {
  1302.         return $this->productClasses;
  1303.     }
  1304.     /**
  1305.      * Clear product classes
  1306.      */
  1307.     public function clearProductClasses()
  1308.     {
  1309.         foreach ($this->getProductClasses()->getKeys() as $key) {
  1310.             $this->getProductClasses()->remove($key);
  1311.         }
  1312.     }
  1313.     /**
  1314.      * Add memberships
  1315.      *
  1316.      * @param \XLite\Model\Membership $memberships
  1317.      * @return Coupon
  1318.      */
  1319.     public function addMemberships(\XLite\Model\Membership $memberships)
  1320.     {
  1321.         $this->memberships[] = $memberships;
  1322.         return $this;
  1323.     }
  1324.     /**
  1325.      * Get memberships
  1326.      *
  1327.      * @return Collection
  1328.      */
  1329.     public function getMemberships()
  1330.     {
  1331.         return $this->memberships;
  1332.     }
  1333.     /**
  1334.      * Add coupon products
  1335.      *
  1336.      * @param \CDev\Coupons\Model\CouponProduct $couponProduct
  1337.      * @return Coupon
  1338.      */
  1339.     public function addCouponProducts(\CDev\Coupons\Model\CouponProduct $couponProduct)
  1340.     {
  1341.         $this->couponProducts[] = $couponProduct;
  1342.         return $this;
  1343.     }
  1344.     /**
  1345.      * Get product ids if coupon is specificProducts
  1346.      *
  1347.      * @return array
  1348.      */
  1349.     public function getApplicableProductIds()
  1350.     {
  1351.         $ids = [];
  1352.         if ($this->isPersistent() && $this->getSpecificProducts()) {
  1353.             $ids \XLite\Core\Database::getRepo('CDev\Coupons\Model\CouponProduct')
  1354.                 ->getCouponProductIds($this->getId());
  1355.         }
  1356.         return $ids;
  1357.     }
  1358.     /**
  1359.      * Get coupon products
  1360.      *
  1361.      * @return Collection
  1362.      */
  1363.     public function getCouponProducts()
  1364.     {
  1365.         return $this->couponProducts;
  1366.     }
  1367.     /**
  1368.      * Clear memberships
  1369.      */
  1370.     public function clearMemberships()
  1371.     {
  1372.         foreach ($this->getMemberships()->getKeys() as $key) {
  1373.             $this->getMemberships()->remove($key);
  1374.         }
  1375.     }
  1376.     /**
  1377.      * Add zones
  1378.      *
  1379.      * @param \XLite\Model\Zone $zone
  1380.      * @return Coupon
  1381.      */
  1382.     public function addZones(\XLite\Model\Zone $zone)
  1383.     {
  1384.         $this->zones[] = $zone;
  1385.         return $this;
  1386.     }
  1387.     /**
  1388.      * Get zones
  1389.      *
  1390.      * @return ArrayCollection
  1391.      */
  1392.     public function getZones()
  1393.     {
  1394.         return $this->zones;
  1395.     }
  1396.     /**
  1397.      * Clear zones
  1398.      */
  1399.     public function clearZones()
  1400.     {
  1401.         foreach ($this->getZones()->getKeys() as $key) {
  1402.             $this->getZones()->remove($key);
  1403.         }
  1404.     }
  1405.     /**
  1406.      * Add usedCoupons
  1407.      *
  1408.      * @param UsedCoupon $usedCoupons
  1409.      *
  1410.      * @return Coupon
  1411.      */
  1412.     public function addUsedCoupons(UsedCoupon $usedCoupons)
  1413.     {
  1414.         $this->usedCoupons[] = $usedCoupons;
  1415.         return $this;
  1416.     }
  1417.     /**
  1418.      * Get usedCoupons
  1419.      *
  1420.      * @return ArrayCollection|Collection
  1421.      */
  1422.     public function getUsedCoupons()
  1423.     {
  1424.         return $this->usedCoupons;
  1425.     }
  1426.     /**
  1427.      * Add categories
  1428.      *
  1429.      * @param \XLite\Model\Category $categories
  1430.      * @return Coupon
  1431.      */
  1432.     public function addCategories(\XLite\Model\Category $categories)
  1433.     {
  1434.         $this->getCategories()->add($categories);
  1435.         return $this;
  1436.     }
  1437.     /**
  1438.      * Get categories
  1439.      *
  1440.      * @return Collection
  1441.      */
  1442.     public function getCategories()
  1443.     {
  1444.         return $this->categories;
  1445.     }
  1446.     /**
  1447.      * Clear categories
  1448.      */
  1449.     public function clearCategories()
  1450.     {
  1451.         foreach ($this->getCategories()->getKeys() as $key) {
  1452.             $this->getCategories()->remove($key);
  1453.         }
  1454.     }
  1455.     public function getApplyFreeShippingTo(): string
  1456.     {
  1457.         return $this->applyFreeShippingTo;
  1458.     }
  1459.     public function setApplyFreeShippingTo(string $applyFreeShippingTo): void
  1460.     {
  1461.         $this->applyFreeShippingTo $applyFreeShippingTo;
  1462.     }
  1463.     public static function getReadableApplyFreeShippingTo(): array
  1464.     {
  1465.         return [
  1466.             static::APPLY_FREESHIP_TO_CHEAPEST    => static::t('[coupons] The cheapest shipping option'),
  1467.             static::APPLY_FREESHIP_TO_FREE_METHOD => static::t('[coupons] Free shipping method'),
  1468.         ];
  1469.     }
  1470.     public function isFreeShippingCheapestType(): bool
  1471.     {
  1472.         return $this->isFreeShipping()
  1473.             && $this->applyFreeShippingTo === static::APPLY_FREESHIP_TO_CHEAPEST;
  1474.     }
  1475.     public function isFreeShippingFreeMethodType(): bool
  1476.     {
  1477.         return $this->isFreeShipping()
  1478.             && $this->applyFreeShippingTo === static::APPLY_FREESHIP_TO_FREE_METHOD;
  1479.     }
  1480.     public function isFreeShipping(): bool
  1481.     {
  1482.         return $this->getType() === static::TYPE_FREESHIP;
  1483.     }
  1484. }