custom/plugins/PremsIndividualOffer6/src/Core/Offer/Storefront/OfferService.php line 1007

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. /**
  3.  * PremSoft
  4.  * Copyright © 2020 Premsoft - Sven Mittreiter
  5.  *
  6.  * @copyright  Copyright (c) 2020, premsoft - Sven Mittreiter (http://www.premsoft.de)
  7.  * @author     Sven Mittreiter <info@premsoft.de>
  8.  */
  9. namespace Prems\Plugin\PremsIndividualOffer6\Core\Offer\Storefront;
  10. use Prems\Plugin\PremsIndividualOffer6\Core\Entity\Offer\Aggregate\OfferItem\OfferItemCollection;
  11. use Prems\Plugin\PremsIndividualOffer6\Core\Entity\Offer\Aggregate\OfferItem\OfferItemEntity;
  12. use Prems\Plugin\PremsIndividualOffer6\Core\Entity\Offer\Aggregate\OfferMessages\OfferMessageCollection;
  13. use Prems\Plugin\PremsIndividualOffer6\Core\Entity\Offer\OfferDefinition;
  14. use Prems\Plugin\PremsIndividualOffer6\Core\Entity\Offer\OfferEntity;
  15. use Prems\Plugin\PremsIndividualOffer6\Core\Media\MediaService;
  16. use Prems\Plugin\PremsIndividualOffer6\Event\OfferCreatedEvent;
  17. use Prems\Plugin\PremsIndividualOffer6\PremsIndividualOffer6;
  18. use Shopware\Core\Checkout\Cart\Cart;
  19. use Shopware\Core\Checkout\Cart\CartPersisterInterface;
  20. use Shopware\Core\Checkout\Cart\Error\ErrorCollection;
  21. use Shopware\Core\Checkout\Cart\LineItem\LineItem;
  22. use Shopware\Core\Checkout\Cart\Price\AbsolutePriceCalculator;
  23. use Shopware\Core\Checkout\Cart\Price\AmountCalculator;
  24. use Shopware\Core\Checkout\Cart\Price\PercentagePriceCalculator;
  25. use Shopware\Core\Checkout\Cart\Price\QuantityPriceCalculator;
  26. use Shopware\Core\Checkout\Cart\Price\Struct\AbsolutePriceDefinition;
  27. use Shopware\Core\Checkout\Cart\Price\Struct\CartPrice;
  28. use Shopware\Core\Checkout\Cart\Price\Struct\CalculatedPrice;
  29. use Shopware\Core\Checkout\Cart\Price\Struct\PercentagePriceDefinition;
  30. use Shopware\Core\Checkout\Cart\Price\Struct\PriceCollection;
  31. use Shopware\Core\Checkout\Cart\Price\Struct\QuantityPriceDefinition;
  32. use Shopware\Core\Checkout\Cart\Price\Struct\ReferencePriceDefinition;
  33. use Shopware\Core\Checkout\Cart\SalesChannel\CartService;
  34. use Shopware\Core\Checkout\Cart\Tax\PercentageTaxRuleBuilder;
  35. use Shopware\Core\Checkout\Cart\Tax\Struct\TaxRule;
  36. use Shopware\Core\Checkout\Cart\Tax\Struct\TaxRuleCollection;
  37. use Shopware\Core\Checkout\Customer\CustomerEntity;
  38. use Shopware\Core\Checkout\Shipping\ShippingMethodEntity;
  39. use Shopware\Core\Content\Product\Cart\ProductLineItemFactory;
  40. use Shopware\Core\Content\Product\SalesChannel\Detail\AbstractProductDetailRoute;
  41. use Shopware\Core\Content\Product\SalesChannel\SalesChannelProductEntity;
  42. use Shopware\Core\Content\Product\ProductEntity;
  43. use Shopware\Core\Defaults;
  44. use Shopware\Core\Framework\Context;
  45. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
  46. use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenContainerEvent;
  47. use Shopware\Core\Framework\DataAbstractionLayer\Exception\InconsistentCriteriaIdsException;
  48. use Shopware\Core\Framework\DataAbstractionLayer\Pricing\Price;
  49. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Bucket\FilterAggregation;
  50. use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\CountAggregation;
  51. use Shopware\Core\Framework\DataAbstractionLayer\Search\AggregationResult\AggregationResultCollection;
  52. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  53. use Shopware\Core\Framework\DataAbstractionLayer\Search\EntitySearchResult;
  54. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\AndFilter;
  55. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  56. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
  57. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\NotFilter;
  58. use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
  59. use Shopware\Core\Framework\Struct\ArrayEntity;
  60. use Shopware\Core\Framework\Uuid\Uuid;
  61. use Shopware\Core\System\NumberRange\ValueGenerator\NumberRangeValueGeneratorInterface;
  62. use Shopware\Core\System\SalesChannel\Entity\SalesChannelRepositoryInterface;
  63. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  64. use Shopware\Core\Checkout\Customer\Aggregate\CustomerGroup\CustomerGroupEntity;
  65. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  66. use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
  67. use Symfony\Component\HttpFoundation\Request;
  68. use Shopware\Core\Framework\Struct\ArrayStruct;
  69. use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
  70. use Shopware\Core\Framework\DataAbstractionLayer\Pricing\PriceCollection as DALPriceCollection;
  71. class OfferService
  72. {
  73.     /**
  74.      * @var EntityRepositoryInterface
  75.      */
  76.     private $offerRepository;
  77.     /**
  78.      * @var EntityRepositoryInterface
  79.      */
  80.     private $offerItemRepository;
  81.     /**
  82.      * @var EntityRepositoryInterface
  83.      */
  84.     private $offerMessageRepository;
  85.     /**
  86.      * @var SalesChannelRepositoryInterface
  87.      */
  88.     private $productRepository;
  89.     /**
  90.      * @var CartService
  91.      */
  92.     private $cartService;
  93.     /**
  94.      * @var NumberRangeValueGeneratorInterface
  95.      */
  96.     private $numberRangeValueGenerator;
  97.     /**
  98.      * @var ProductLineItemFactory
  99.      */
  100.     private $productLineItemFactory;
  101.     /**
  102.      * @var ConfigService
  103.      */
  104.     private $configService;
  105.     /**
  106.      * @var AbstractProductDetailRoute
  107.      */
  108.     private $productLoader;
  109.     /** @var MediaService $mediaService */
  110.     private $mediaService;
  111.     private EventDispatcherInterface $eventDispatcher;
  112.     /**
  113.      * @var CartPersisterInterface
  114.      */
  115.     private CartPersisterInterface $cartPersister;
  116.     /**
  117.      * @var AmountCalculator
  118.      */
  119.     private AmountCalculator $amountCalculator;
  120.     /**
  121.      * @var QuantityPriceCalculator
  122.      */
  123.     private QuantityPriceCalculator $quantityPriceCalculator;
  124.     /**
  125.      * @var PercentagePriceCalculator
  126.      */
  127.     private PercentagePriceCalculator $percentagePriceCalculator;
  128.     /**
  129.      * @var AbsolutePriceCalculator
  130.      */
  131.     private AbsolutePriceCalculator $absolutePriceCalculator;
  132.     /**
  133.      * @var PercentageTaxRuleBuilder
  134.      */
  135.     private PercentageTaxRuleBuilder $percentageTaxRuleBuilder;
  136.     public function __construct(
  137.         EntityRepositoryInterface $offerRepository,
  138.         EntityRepositoryInterface $offerItemRepository,
  139.         EntityRepositoryInterface $offerMessageRepository,
  140.         SalesChannelRepositoryInterface $productRepository,
  141.         CartService $cartService,
  142.         NumberRangeValueGeneratorInterface $numberRangeValueGenerator,
  143.         ProductLineItemFactory $productLineItemFactory,
  144.         ConfigService $configService,
  145.         AbstractProductDetailRoute $productLoader,
  146.         MediaService $mediaService,
  147.         EventDispatcherInterface $eventDispatcher,
  148.         CartPersisterInterface $cartPersister,
  149.         AmountCalculator $amountCalculator,
  150.         QuantityPriceCalculator $quantityPriceCalculator,
  151.         PercentagePriceCalculator $percentagePriceCalculator,
  152.         AbsolutePriceCalculator $absolutePriceCalculator,
  153.         PercentageTaxRuleBuilder $percentageTaxRuleBuilder
  154.     ) {
  155.         $this->offerRepository $offerRepository;
  156.         $this->offerItemRepository $offerItemRepository;
  157.         $this->offerMessageRepository $offerMessageRepository;
  158.         $this->productRepository $productRepository;
  159.         $this->cartService $cartService;
  160.         $this->numberRangeValueGenerator $numberRangeValueGenerator;
  161.         $this->productLineItemFactory $productLineItemFactory;
  162.         $this->configService $configService;
  163.         $this->productLoader $productLoader;
  164.         $this->mediaService $mediaService;
  165.         $this->eventDispatcher $eventDispatcher;
  166.         $this->cartPersister $cartPersister;
  167.         $this->amountCalculator $amountCalculator;
  168.         $this->quantityPriceCalculator $quantityPriceCalculator;
  169.         $this->percentagePriceCalculator $percentagePriceCalculator;
  170.         $this->absolutePriceCalculator $absolutePriceCalculator;
  171.         $this->percentageTaxRuleBuilder $percentageTaxRuleBuilder;
  172.     }
  173.     /**
  174.      * Return all offers for an user
  175.      *
  176.      * @param SalesChannelContext $salesChannelContext
  177.      * @param Request $request
  178.      *
  179.      * @return EntitySearchResult
  180.      *
  181.      * @throws InconsistentCriteriaIdsException
  182.      */
  183.     public function getOffersForUser(SalesChannelContext $salesChannelContextRequest $request)
  184.     {
  185.         $criteria = (new Criteria())
  186.             ->addFilter(new AndFilter([
  187.                 new EqualsFilter('prems_individual_offer.customerId'$salesChannelContext->getCustomer()->getId()),
  188.                 new EqualsFilter('prems_individual_offer.salesChannelId'$salesChannelContext->getSalesChannelId())
  189.             ]))
  190.             ->addAssociation('items')
  191.             ->addAssociation('items.product')
  192.             ->addAssociation('items.product.cover')
  193.             ->addAssociation('items.product.prices')
  194.             ->addSorting(new FieldSorting('createdAt'FieldSorting::DESCENDING));
  195.         $filters = [];
  196.         if ($request->query->get('openRequest')) {
  197.             $filters[] = new MultiFilter(
  198.                 MultiFilter::CONNECTION_AND,
  199.                 [
  200.                     new EqualsFilter('prems_individual_offer.offered'0),
  201.                     new EqualsFilter('prems_individual_offer.accepted'0),
  202.                     new EqualsFilter('prems_individual_offer.declined'0)
  203.                 ]
  204.             );
  205.         }
  206.         if ($request->query->get('received')) {
  207.             $filters[] = new MultiFilter(
  208.                 MultiFilter::CONNECTION_AND,
  209.                 [
  210.                     new EqualsFilter('prems_individual_offer.offered'1),
  211.                     new EqualsFilter('prems_individual_offer.accepted'0),
  212.                     new EqualsFilter('prems_individual_offer.declined'0)
  213.                 ]
  214.             );
  215.         }
  216.         if ($request->query->get('ordered')) {
  217.             $filters[] = new MultiFilter(
  218.                 MultiFilter::CONNECTION_AND,
  219.                 [
  220.                     new EqualsFilter('prems_individual_offer.offered'1),
  221.                     new EqualsFilter('prems_individual_offer.accepted'1),
  222.                     new EqualsFilter('prems_individual_offer.declined'0)
  223.                 ]
  224.             );
  225.         }
  226.         if ($request->query->get('declined')) {
  227.             $filters[] = new EqualsFilter('prems_individual_offer.declined'1);
  228.         }
  229.         if (!empty($filters)) {
  230.             $criteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_OR$filters));
  231.         }
  232.         $offers $this->offerRepository->search($criteria$salesChannelContext->getContext());
  233.         if (!empty($offers)) {
  234.             /** @var OfferEntity $offer */
  235.             foreach ($offers as $offer) {
  236.                 $currentDate date('Y-m-d H:i:s');
  237.                 $offerDays = (int) $offer->getOfferDaysValid();
  238.                 $offerDate date('Y-m-d H:i:s'strtotime($offer->getUpdatedAtDateTime() . " +{$offerDays} day"));
  239.                 if (($offer->getOfferDaysValid() !== null && $offer->getOfferDaysValid() !== '') &&
  240.                     $offer->getUpdatedAtDateTime() !== null &&
  241.                     $offerDate <= $currentDate &&
  242.                     ($offer->isOffered() && !$offer->isAccepted() && !$offer->isDeclined())
  243.                 ) {
  244.                     $offer->setDeclined(true);
  245.                     $this->offerRepository->update([[
  246.                         'id' => $offer->getId(),
  247.                         'declined' => $offer->isDeclined(),
  248.                         'updated_at' => $offer->getUpdatedAtDateTime()
  249.                     ]], $salesChannelContext->getContext());
  250.                 }
  251.             }
  252.         }
  253.         // skip offers without items
  254.         $criteria->addFilter(
  255.             new NotFilter(
  256.                 NotFilter::CONNECTION_AND,
  257.                 [
  258.                     new EqualsFilter('prems_individual_offer.items.id'null)
  259.                 ]
  260.             )
  261.         );
  262.         $offers $this->offerRepository->search($criteria$salesChannelContext->getContext());
  263.         if (!empty($offers)) {
  264.             /** @var OfferEntity $offer */
  265.             foreach ($offers as $offer) {
  266.                 $criteriaMessages = (new Criteria())
  267.                     ->addFilter(new EqualsFilter('prems_individual_offer_messages.offerId'$offer->getId()))
  268.                     ->addFilter(new EqualsFilter('prems_individual_offer_messages.customerId'$salesChannelContext->getCustomer()->getId()))
  269.                     ->addAssociation('user')
  270.                     ->addAssociation('customer')
  271.                     ->addSorting(new FieldSorting('prems_individual_offer_messages.createdAt'FieldSorting::DESCENDING));
  272.                 /** @var OfferMessageCollection $messages */
  273.                 $messages $this->offerMessageRepository->search($criteriaMessages$salesChannelContext->getContext())->getEntities();
  274.                 $offer->setMessages($messages);
  275.             }
  276.         }
  277.         return $offers;
  278.     }
  279.     /**
  280.      * @param Context $context
  281.      * @return EntitySearchResult
  282.      */
  283.     public function updateOffersStatus(Context $context): EntitySearchResult
  284.     {
  285.         $criteria = new Criteria();
  286.         $offers $this->offerRepository->search($criteria$context);
  287.         if (!empty($offers)) {
  288.             /** @var OfferEntity $offer */
  289.             foreach ($offers as $offer) {
  290.                 $currentDate date('Y-m-d H:i:s');
  291.                 $offerDays = (int) $offer->getOfferDaysValid();
  292.                 $offerDate date('Y-m-d H:i:s'strtotime($offer->getUpdatedAtDateTime() . " +{$offerDays} day"));
  293.                 if(($offer->getOfferDaysValid() !== null && $offer->getOfferDaysValid() !== '') &&
  294.                     $offer->getUpdatedAtDateTime() !== null &&
  295.                     $offerDate <= $currentDate &&
  296.                     ($offer->isOffered() && !$offer->isAccepted() && !$offer->isDeclined())
  297.                 ) {
  298.                     $offer->setDeclined(true);
  299.                     $this->offerRepository->update([[
  300.                         'id' => $offer->getId(),
  301.                         'declined' => $offer->isDeclined(),
  302.                         'updated_at' => $offer->getUpdatedAtDateTime()
  303.                     ]], $context);
  304.                 }
  305.             }
  306.         }
  307.         return $offers;
  308.     }
  309.     /**
  310.      * @param CustomerEntity $customer
  311.      * @param Context $context
  312.      * @return AggregationResultCollection
  313.      */
  314.     public function getOfferStatusAggregations(CustomerEntity $customerContext $context): AggregationResultCollection
  315.     {
  316.         $criteria = (new Criteria())
  317.             ->addFilter(new EqualsFilter('prems_individual_offer.customerId'$customer->getId()))
  318.             ->addAggregation(new FilterAggregation('offered', new CountAggregation('offeredCount''prems_individual_offer.id'), [
  319.                 new MultiFilter(
  320.                     MultiFilter::CONNECTION_AND,
  321.                     [
  322.                         new EqualsFilter('prems_individual_offer.offered'0),
  323.                         new EqualsFilter('prems_individual_offer.accepted'0),
  324.                         new EqualsFilter('prems_individual_offer.declined'0),
  325.                         new NotFilter(
  326.                             NotFilter::CONNECTION_AND,
  327.                             [
  328.                                 new EqualsFilter('prems_individual_offer.items.id'null)
  329.                             ]
  330.                         )
  331.                     ]
  332.                 )
  333.             ]))
  334.             ->addAggregation(new FilterAggregation('declined', new CountAggregation('declinedCount''prems_individual_offer.id'), [
  335.                   new MultiFilter(
  336.                       MultiFilter::CONNECTION_AND,
  337.                       [
  338.                           new EqualsFilter('prems_individual_offer.declined'1),
  339.                           new NotFilter(
  340.                               NotFilter::CONNECTION_AND,
  341.                               [
  342.                                   new EqualsFilter('prems_individual_offer.items.id'null)
  343.                               ]
  344.                           )
  345.                       ]
  346.                   )
  347.             ]))
  348.             ->addAggregation(new FilterAggregation('received', new CountAggregation('receivedCount''prems_individual_offer.id'), [
  349.                 new MultiFilter(
  350.                     MultiFilter::CONNECTION_AND,
  351.                     [
  352.                         new EqualsFilter('prems_individual_offer.offered'1),
  353.                         new EqualsFilter('prems_individual_offer.accepted'0),
  354.                         new EqualsFilter('prems_individual_offer.declined'0),
  355.                         new NotFilter(
  356.                             NotFilter::CONNECTION_AND,
  357.                             [
  358.                                 new EqualsFilter('prems_individual_offer.items.id'null)
  359.                             ]
  360.                         )
  361.                     ]
  362.                 )
  363.             ]))
  364.             ->addAggregation(new FilterAggregation('ordered', new CountAggregation('orderedCount''prems_individual_offer.id'), [
  365.                 new MultiFilter(
  366.                     MultiFilter::CONNECTION_AND,
  367.                     [
  368.                         new EqualsFilter('prems_individual_offer.offered'1),
  369.                         new EqualsFilter('prems_individual_offer.accepted'1),
  370.                         new EqualsFilter('prems_individual_offer.declined'0),
  371.                         new NotFilter(
  372.                             NotFilter::CONNECTION_AND,
  373.                             [
  374.                                 new EqualsFilter('prems_individual_offer.items.id'null)
  375.                             ]
  376.                         )
  377.                     ]
  378.                 )
  379.             ]));
  380.         return $this->offerRepository->search($criteria$context)->getAggregations();
  381.     }
  382.     /**
  383.      * Get on offer by offer ID
  384.      * @param string $offerId
  385.      * @param Context $context
  386.      * @param CustomerEntity|null $customer
  387.      * @return OfferEntity
  388.      */
  389.     public function getOfferById(string $offerIdContext $contextCustomerEntity $customer null)
  390.     {
  391.         $criteria = new Criteria([ $offerId ]);
  392.         if ($customer) {
  393.             $criteria->addFilter(new EqualsFilter('prems_individual_offer.customerId'$customer->getId()));
  394.         }
  395.         $criteria
  396.             ->addAssociation('items')
  397.             ->addAssociation('items.product')
  398.             ->addAssociation('items.product.prices')
  399.             ->addAssociation('items.product.cover')
  400.             ->addAssociation('items.product.options.group')
  401.             ->addAssociation('customer')
  402.             ->addAssociation('customer.addresses')
  403.             ->addAssociation('customer.salutation')
  404.             ->addAssociation('customer.defaultBillingAddress')
  405.             ->addAssociation('customer.activeBillingAddress')
  406.             ->addAssociation('customer.defaultBillingAddress.country')
  407.             ->addAssociation('customer.activeBillingAddress.country')
  408.             ->addAssociation('shippingMethod.tax')
  409.             ->addAssociation('currency')
  410.             ->addAssociation('salesChannel.domains')
  411.             ->addAssociation('salesChannel.mailHeaderFooter')
  412.             ->addAssociation('items.product.deliveryTime')
  413.             ->addAssociation('language.locale');
  414.         $criteria
  415.             ->getAssociation('items')
  416.             ->addSorting(new FieldSorting('position'));
  417.         $context->setConsiderInheritance(true);
  418.         $offer $this->offerRepository->search($criteria$context)->first();
  419.         if ($offer && $customer != null) {
  420.             $criteriaMessages = (new Criteria())
  421.                 ->addFilter(new EqualsFilter('prems_individual_offer_messages.offerId'$offer->getId()))
  422.                 ->addFilter(new EqualsFilter('prems_individual_offer_messages.customerId'$customer->getId()))
  423.                 ->addAssociation('user')
  424.                 ->addAssociation('customer')
  425.                 ->addSorting(new FieldSorting('prems_individual_offer_messages.createdAt'FieldSorting::DESCENDING));
  426.             $messages $this->offerMessageRepository->search($criteriaMessages$context)->getEntities();
  427.             $offer->setMessages($messages);
  428.         }
  429.         return $offer;
  430.     }
  431.     /**
  432.      * Returns the offer language ID
  433.      *
  434.      * @param string $offerId
  435.      * @param Context $context
  436.      * @return string|null
  437.      */
  438.     public function getOfferLanguageId(string $offerIdContext $context): ?string
  439.     {
  440.         $criteria = new Criteria([$offerId]);
  441.         /** @var OfferEntity $offer */
  442.         if ($offer $this->offerRepository->search($criteria$context)->getEntities()->first()) {
  443.             return $offer->getLanguageId();
  444.         }
  445.         return null;
  446.     }
  447.     /**
  448.      * Get on product by product ID
  449.      * @param string $productId
  450.      * @param SalesChannelContext $context
  451.      * @return ProductEntity
  452.      */
  453.     public function getProductById(string $productIdSalesChannelContext $context): ProductEntity
  454.     {
  455.         $criteria = new Criteria([$productId]);
  456.         $criteria->addAssociation('cover');
  457.         $criteria->addAssociation('prices');
  458.         return $this->productRepository->search($criteria$context)->first();
  459.     }
  460.     /**
  461.      * Add product to the offer
  462.      *
  463.      * @param array $item
  464.      * @param SalesChannelProductEntity $product
  465.      * @param SalesChannelContext $salesChannelContext
  466.      * @return void
  467.      */
  468.     public function addProductOfferItem(array $itemSalesChannelProductEntity $productSalesChannelContext $salesChannelContext): void
  469.     {
  470.         $customTaxRules null;
  471.         // use custom tax rules if offer item is not new
  472.         if (isset($item['id']) && isset($item['priceDefinition']) && !empty($item['priceDefinition']['taxRules'])) {
  473.             $customPriceDefinition QuantityPriceDefinition::fromArray($item['priceDefinition']);
  474.             $customTaxRules $customPriceDefinition->getTaxRules();
  475.         }
  476.         // new price definition
  477.         $priceDefinition $this->getPriceDefinition($product$item['quantity'], $item['price'], $customTaxRules);
  478.         $calculatedPrice $this->quantityPriceCalculator->calculate($priceDefinition$salesChannelContext);
  479.         // old price definition with original price
  480.         $oldPriceDefinition $this->getPriceDefinition($product$item['quantity']);
  481.         // if offer item is new
  482.         if (!isset($item['id'])) {
  483.             $offerItemsCount $this->getOfferItemsCount($item['offerId'], $salesChannelContext->getContext());
  484.             $item['position'] = ++$offerItemsCount;
  485.         }
  486.         $item array_merge(
  487.             $item,
  488.             [
  489.                 'price'           => $priceDefinition->getPrice(),
  490.                 'oldPrice'        => $oldPriceDefinition->getPrice(),
  491.                 'totalPrice'      => $calculatedPrice->getTotalPrice(),
  492.                 'itemOption'      => null,
  493.                 'priceDefinition' => $priceDefinition
  494.             ]
  495.         );
  496.         $this->offerItemRepository->upsert([$item], $salesChannelContext->getContext());
  497.     }
  498.     /**
  499.      * Add custom item to the offer
  500.      *
  501.      * custom type or credit type
  502.      *
  503.      * @param array $item
  504.      * @param Context $context
  505.      * @return EntityWrittenContainerEvent
  506.      */
  507.     public function addCustomOfferItem(array $itemContext $context): EntityWrittenContainerEvent
  508.     {
  509.         // if offer item is new
  510.         if (!isset($item['id'])) {
  511.             $offerItemsCount $this->getOfferItemsCount($item['offerId'], $context);
  512.             $item['position'] = ++$offerItemsCount;
  513.         }
  514.         $priceDefinition QuantityPriceDefinition::fromArray($item['priceDefinition']);
  515.         $item array_merge(
  516.             $item,
  517.             [
  518.                 'oldPrice' => $item['price'],
  519.                 'totalPrice' => $item['price'],
  520.                 'priceDefinition' => (new QuantityPriceDefinition($item['price'], $priceDefinition->getTaxRules(), $item['quantity']))
  521.             ]
  522.         );
  523.         return $this->offerItemRepository->upsert([$item], $context);
  524.     }
  525.     /**
  526.      * Remove items from the offer
  527.      *
  528.      * @param array $items
  529.      * @param Context $context
  530.      * @return EntityWrittenContainerEvent
  531.      */
  532.     public function removeOfferItems(array $itemsContext $context): EntityWrittenContainerEvent
  533.     {
  534.         return $this->offerItemRepository->delete([$items], $context);
  535.     }
  536.     /**
  537.      * Recalculate the offer
  538.      *
  539.      * @param OfferEntity $offer
  540.      * @param SalesChannelContext $context
  541.      * @return void
  542.      */
  543.     public function recalculateOffer(OfferEntity $offerSalesChannelContext $context): void
  544.     {
  545.         $prices = new PriceCollection();
  546.         $originalPrices = new PriceCollection();
  547.         /** @var OfferItemEntity $offerItem */
  548.         foreach ($offer->getItems() as $offerItem) {
  549.             $priceDefinition $offerItem->getPriceDefinition();
  550.             if (!$priceDefinition || $offerItem->getItemType() === LineItem::PROMOTION_LINE_ITEM_TYPE) {
  551.                 $taxRate 0;
  552.                 if ($offerItem->getProduct() && $productTax $offerItem->getProduct()->getTax()) {
  553.                     $taxRate $productTax->getTaxRate();
  554.                     if ($taxRule $context->getTaxRules()->get($productTax->getId())->getRules()->first()) {
  555.                         $taxRate $taxRule->getTaxRate();
  556.                     }
  557.                 }
  558.                 $taxRules = new TaxRuleCollection([new TaxRule($taxRate ?? 0)]);
  559.                 $priceDefinition = new QuantityPriceDefinition($offerItem->getPrice(), $taxRules$offerItem->getQuantity());
  560.             }
  561.             $prices->add($this->quantityPriceCalculator->calculate($priceDefinition$context));
  562.             $originalPrices->add($this->getOfferOriginalQuantityPrice($offerItem$priceDefinition->getTaxRules(), $context));
  563.         }
  564.         $shippingCosts $this->getOfferShippingCosts($offer$context);
  565.         $price $this->amountCalculator->calculate($prices$shippingCosts$context);
  566.         $originalPrice $this->amountCalculator->calculate($originalPrices, new PriceCollection(), $context);
  567.         $data = [
  568.             'id' => $offer->getId(),
  569.             'price' => $price,
  570.             'totalDiscount' => $this->calculateOfferTotalDiscount($originalPrice$price)
  571.         ];
  572.         $this->offerRepository->update([$data], $context->getContext());
  573.     }
  574.     /**
  575.      * Returns the offer shipping costs
  576.      *
  577.      * @param OfferEntity $offer
  578.      * @param SalesChannelContext $salesChannelContext
  579.      * @return PriceCollection
  580.      */
  581.     public function getOfferShippingCosts(OfferEntity $offerSalesChannelContext $salesChannelContext): PriceCollection
  582.     {
  583.         $shippingCosts = new PriceCollection();
  584.         if ($offer->getShippingCostsCalculationType() === OfferEntity::SHIPPING_COST_CALCULATION_TYPE_FIXED && $offer->getShippingCosts()) {
  585.             $prices = new DALPriceCollection([
  586.                 new Price(
  587.                     Defaults::CURRENCY,
  588.                     $offer->getShippingCosts(),
  589.                     $offer->getShippingCosts(),
  590.                     false
  591.                 ),
  592.             ]);
  593.             $shippingCosts->add(
  594.                 $this->calculateShippingCosts(
  595.                     $offer->getShippingMethod(),
  596.                     $prices,
  597.                     $offer->getItems(),
  598.                     $salesChannelContext
  599.                 )
  600.             );
  601.         }
  602.         return $shippingCosts;
  603.     }
  604.     /**
  605.      * Returns the offer original quantity price
  606.      *
  607.      * @param OfferItemEntity $item
  608.      * @param TaxRuleCollection $taxRules
  609.      * @param SalesChannelContext $salesChannelContext
  610.      * @return CalculatedPrice
  611.      */
  612.     protected function getOfferOriginalQuantityPrice(OfferItemEntity $itemTaxRuleCollection $taxRulesSalesChannelContext $salesChannelContext): CalculatedPrice
  613.     {
  614.         $quantityPriceDefinition = new QuantityPriceDefinition($item->getOldPrice(), $taxRules$item->getQuantity());
  615.         return $this->quantityPriceCalculator->calculate($quantityPriceDefinition$salesChannelContext);
  616.     }
  617.     /**
  618.      * Calculate total offer discount percentage and price
  619.      *
  620.      * @param CartPrice $originalPrice
  621.      * @param CartPrice $price
  622.      * @return array
  623.      */
  624.     protected function calculateOfferTotalDiscount(CartPrice $originalPriceCartPrice $price): array
  625.     {
  626.         $price $price->getPositionPrice();
  627.         $originalPrice $originalPrice->getPositionPrice();
  628.         return [
  629.             'price' => $originalPrice $price,
  630.             'percentage' => $originalPrice ? (($originalPrice $price) * 100) / $originalPrice $originalPrice
  631.         ];
  632.     }
  633.     /**
  634.      * Get and return items of an offer
  635.      * @param string $offerId
  636.      * @param string $customerId
  637.      * @param SalesChannelContext $context
  638.      * @return null|OfferItemCollection
  639.      * @throws InconsistentCriteriaIdsException
  640.      */
  641.     public function getItemsFromOffer(string $offerIdstring $customerIdSalesChannelContext $context): ?OfferItemCollection
  642.     {
  643.         $criteria = new Criteria();
  644.         $criteria->addFilter(new EqualsFilter('id'$offerId))
  645.             ->addFilter(new EqualsFilter('prems_individual_offer.customerId'$customerId))
  646.             ->addAssociation('items')
  647.             ->addAssociation('customer')
  648.             //->addAssociation('items.product.prices')
  649.             /**->addAssociation('items.product')
  650.             ->addAssociation('items.product.cover')
  651.             ->addAssociation('items.product.prices')*/;
  652.         /** @var OfferEntity|null $offer */
  653.         $offer $this->offerRepository->search($criteria$context->getContext())->first();
  654.         if (!$offer instanceof OfferEntity) {
  655.             throw new NotFoundHttpException();
  656.         }
  657.         $nestedItems $offer->getNestedItems();
  658.         $elements $nestedItems->getElements();
  659.         $productCriteria = new Criteria();
  660.         $productCriteria->addAssociation('options.group');
  661.         /** @var OfferItemEntity $element */
  662.         foreach($elements as $element) {
  663.             $clonedCriteria = clone $productCriteria;
  664.             /** @var SalesChannelProductEntity|null $product */
  665.             if (!$element->getProductId()) {
  666.                 continue;
  667.             }
  668.             $result $this->productLoader->load($element->getProductId(), new Request(), $context$clonedCriteria);
  669.             /** @var SalesChannelProductEntity|null $product */
  670.             $product $result->getProduct();
  671.             if ($product) {
  672.                 $element->setProduct($product);
  673.                 $element->setPriceNet();
  674.             }
  675.         }
  676.         return $nestedItems;
  677.     }
  678.     /**
  679.      * Determine if a offer request is generally allowed (Plugin activated for saleschannel, cart amount high enough)
  680.      * @param SalesChannelContext $context
  681.      *
  682.      * @return bool
  683.      */
  684.     public function isOfferRequestAllowed(SalesChannelContext $context)
  685.     {
  686.         $offerSettings $this->configService->getConfig($context);
  687.         $settings $offerSettings->getVars();
  688.         if ($settings['disallowedCustomerGroups'] && is_array($settings['disallowedCustomerGroups'])) {
  689.             $customerGroup $context->getCurrentCustomerGroup();
  690.             if (in_array($customerGroup->getId(), $settings['disallowedCustomerGroups'])) {
  691.                 return false;
  692.             }
  693.         }
  694.         if ($settings['offerMinValueReached'] == true) {
  695.             return true;
  696.         }
  697.         return false;
  698.     }
  699.     /**
  700.      * Determine if the account offer nav item is allowed (Plugin activated for saleschannel, customer group allowed)
  701.      * @param SalesChannelContext $context
  702.      *
  703.      * @return bool
  704.      */
  705.     public function isOfferNavAllowed(SalesChannelContext $context)
  706.     {
  707.         $offerSettings $this->configService->getConfig($context);
  708.         $settings $offerSettings->getVars();
  709.         if ($settings['disallowedCustomerGroups'] && is_array($settings['disallowedCustomerGroups'])) {
  710.             $customerGroup $context->getCurrentCustomerGroup();
  711.             if (in_array($customerGroup->getId(), $settings['disallowedCustomerGroups'])) {
  712.                 return false;
  713.             }
  714.         }
  715.         return true;
  716.     }
  717.     /**
  718.      * Creates an offer request for a customer
  719.      * @param CustomerEntity $customer
  720.      * @param CustomerGroupEntity $customerGroup
  721.      * @param SalesChannelContext $context
  722.      *
  723.      * @return EntityWrittenContainerEvent
  724.      * @throws \Exception
  725.      */
  726.     public function createOfferRequest(CustomerEntity $customerCustomerGroupEntity $customerGroupSalesChannelContext $context): EntityWrittenContainerEvent
  727.     {
  728.         $items = [];
  729.         $cart $this->cartService->getCart($context->getToken(), $context);
  730.         $lineItems $cart->getLineItems()->filter(
  731.             function (LineItem $lineItem) {
  732.                 return in_array($lineItem->getType(), [LineItem::PRODUCT_LINE_ITEM_TYPE'customized-products'LineItem::PROMOTION_LINE_ITEM_TYPE]);
  733.             }
  734.         );
  735.         $currencySymbol $context->getCurrency()->getSymbol();
  736.         if ($lineItems) {
  737.             $i 0;
  738.             foreach($lineItems as $lineItem) {
  739.                 $i++;
  740.                 $priceDefinition $lineItem->getPriceDefinition() ?: $this->buildPriceDefinition($lineItem->getPrice(), $lineItem->getQuantity());
  741.                 //$products[] = ['id' => $lineItem->getId()];
  742.                 $data = [
  743.                     'productId' => $lineItem->getId(),
  744.                     'oldPrice' => $lineItem->getPrice()->getUnitPrice(),
  745.                     'price' => $lineItem->getPrice()->getUnitPrice(),
  746.                     'totalPrice' => $lineItem->getPrice()->getTotalPrice(),
  747.                     'quantity' => $lineItem->getQuantity(),
  748.                     'position' => $i,
  749.                     'lineItem' => \base64_encode(\serialize($lineItem)),
  750.                     'priceDefinition' => $priceDefinition,
  751.                     'itemType' => LineItem::PRODUCT_LINE_ITEM_TYPE
  752.                 ];
  753.                 // Possibility to use referencedID instead of productId for third party vendors
  754.                 if ($lineItem->hasPayloadValue('prems_individual_offer_use_reference_id_instead_of_product_id')) {
  755.                     $data['productId'] = $lineItem->getReferencedId();
  756.                 }
  757.                 if($lineItem->getType() == LineItem::PROMOTION_LINE_ITEM_TYPE) {
  758.                     $data['productId'] = null;
  759.                     $data['itemType'] = LineItem::PROMOTION_LINE_ITEM_TYPE;
  760.                 }
  761.                 if ($lineItem->getType() == 'customized-products') {
  762.                     $productId 0;
  763.                     $children $lineItem->getChildren();
  764.                     $itemOption = [];
  765.                     foreach($children as $child) {
  766.                         if ($child->getType() == LineItem::PRODUCT_LINE_ITEM_TYPE) {
  767.                             $productId $child->getReferencedId();
  768.                         }
  769.                         if ($child->getType() == 'customized-products-option') {
  770.                             $quantity '';
  771.                             $price '';
  772.                             $payloadValue '';
  773.                             if ($child->getQuantity() > 1) {
  774.                                 $quantity $child->getQuantity() . ' x ';
  775.                             }
  776.                             if ($child->hasChildren()) {
  777.                                 $subChildren $child->getChildren();
  778.                                 $optionValues = [];
  779.                                 foreach($subChildren as $subChild) {
  780.                                     if ($subChild->getPrice()->getTotalPrice() > 0) {
  781.                                         $price ' '.$subChild->getPrice()->getTotalPrice() . ' ' $currencySymbol;
  782.                                     }
  783.                                     $optionValues[] = $subChild->getLabel();
  784.                                 }
  785.                                 $itemOption[] = $quantity.$child->getLabel() . ' (' implode('; '$optionValues) . ')'.$payloadValue.$price;
  786.                             } else {
  787.                                 if ($child->getPrice()->getTotalPrice() > 0) {
  788.                                     $price ' '.$child->getPrice()->getTotalPrice() . ' ' $currencySymbol;
  789.                                 }
  790.                                 $payload $child->getPayload();
  791.                                 if ($payload && array_key_exists('value'$payload) && !is_array($payload['value'])) {
  792.                                     $payloadValue ': '.$payload['value'];
  793.                                 }
  794.                                 if ($payload && array_key_exists('media'$payload) && is_array($payload['media'])) {
  795.                                     $media $this->mediaService->getMediaById($payload['media']['0']['mediaId'], $context->getContext());
  796.                                     $payloadValue ': '.$media->getUrl();
  797.                                 }
  798.                                 $itemOption[] = $quantity.$child->getLabel().$payloadValue.$price ;
  799.                             }
  800.                         }
  801.                     }
  802.                     //$data['itemOption'] = implode('<br>',$itemOption);
  803.                     $data['itemOption'] = json_encode($itemOption);
  804.                     $data['productId'] = $productId;
  805.                     $data['itemType'] = 'swag-customized-products';
  806.                 }
  807.                 $items[] = $data;
  808.             }
  809.         }
  810.         //$this->numberRangeValueGenerator->previewPattern(OfferDefinition::ENTITY_NAME, '{n}', 1);
  811.         if ($customerGroup->getDisplayGross()) {
  812.             $taxStatus 'gross';
  813.         } else {
  814.             $taxStatus 'net';
  815.         }
  816.         $cart->setErrors(new ErrorCollection());
  817.         $cart->setData(null);
  818.         $offerId Uuid::randomHex();
  819.         $offerData =             [
  820.             'id' => $offerId,
  821.             'customerId' => $customer->getId(),
  822.             'taxStatus' => $taxStatus,
  823.             'offerNumber' => $this->numberRangeValueGenerator->getValue(
  824.                 OfferDefinition::ENTITY_NAME,
  825.                 $context->getContext(),
  826.                 $context->getSalesChannel()->getId()
  827.             ),
  828.             'pdfComment' => $this->configService->getConfig($context)->getDefaultPdfComment(),
  829.             'currencyFactor' => $context->getContext()->getCurrencyFactor(),
  830.             'currencyId' => $context->getContext()->getCurrencyId(),
  831.             'languageId' => $context->getLanguageId() ?: $context->getContext()->getLanguageId(),
  832.             'salesChannelId' => $context->getSalesChannel()->getId(),
  833.             'cart' => \base64_encode(\serialize($cart)),
  834.             'items' => $items,
  835.             'price' => $this->amountCalculator->calculate($lineItems->getPrices(), new PriceCollection(), $context)
  836.         ];
  837.         if ($this->configService->getConfig($context)->getDefaultOfferDaysValid() > 0) {
  838.             $offerData['offer_days_valid'] = (int) $this->configService->getConfig($context)->getDefaultOfferDaysValid();
  839.             if ($offerData['offer_days_valid']) {
  840.                 $offerData['offerExpiration'] = $this->calculateOfferExpirationDate($offerData['offer_days_valid']);
  841.             }
  842.         }
  843.         $offerCreation $this->offerRepository->create([
  844.             $offerData
  845.         ], $context->getContext());
  846.         $this->eventDispatcher->dispatch(new OfferCreatedEvent($offerId$context));
  847.         return $offerCreation;
  848.     }
  849.     /**
  850.      * Determine if an offer is orderable
  851.      * @param OfferEntity $offer
  852.      * @return bool
  853.      */
  854.     public function isOfferOrderable(OfferEntity $offer): bool
  855.     {
  856.         if ($offer->isOffered() && !$offer->isAccepted() && !$offer->isDeclined()) {
  857.             return true;
  858.         }
  859.         return false;
  860.     }
  861.     /**
  862.      * Determine if an offer is declineable
  863.      * @param OfferEntity $offer
  864.      * @return bool
  865.      */
  866.     public function isOfferDeclineable(OfferEntity $offer): bool
  867.     {
  868.         if ($offer->isOffered() && !$offer->isAccepted() && !$offer->isDeclined()) {
  869.             return true;
  870.         }
  871.         return false;
  872.     }
  873.     /**
  874.      * Determine if customer is allowed to access an offer
  875.      * @param OfferEntity $offer
  876.      * @param CustomerEntity $customer
  877.      * @return bool
  878.      */
  879.     public function hasAccessToOffer(OfferEntity $offerCustomerEntity $customer): bool
  880.     {
  881.         $customerIsOwner $customer->getId() === $offer->getCustomer()->getId();
  882.         return $customerIsOwner;
  883.     }
  884.     /**
  885.      * Determine if basket is in offer mode and then return offer mode
  886.      *
  887.      * @param SalesChannelContext $context
  888.      * @param SessionInterface $session
  889.      * @return string
  890.      */
  891.     public function getOfferMode(SalesChannelContext $contextSessionInterface $session): string
  892.     {
  893.         // compatibility mode for older version with session value. If there is an entry then cancel offer mode.
  894.         // Code will be removed in newer versions
  895.         if ($session->has('prems_individual_offer')) {
  896.             $session->remove('prems_individual_offer');
  897.             $this->cancelOfferMode($context$session);
  898.         }
  899.         $cart $this->cartService->getCart($context->getToken(), $context);
  900.         if (!$cart->hasExtension('offer_mode_active')) {
  901.             return '';
  902.         }
  903.         $offerMode $cart->getExtension('offer_mode_active');
  904.         if (!$offerMode || !$offerMode->getId()) {
  905.             return '';
  906.         }
  907.         return $offerMode->getId();
  908.     }
  909.     /**
  910.      * Set flag that basket is in offer mode
  911.      *
  912.      * @param Cart $cart
  913.      * @param string $offerId
  914.      */
  915.     protected function activateOfferMode($cart$offerId):void
  916.     {
  917.         $cart->addExtension(PremsIndividualOffer6::OFFER_MODE_EXTENSION_NAME, new ArrayEntity(['id' => $offerId]));
  918.     }
  919.     /**
  920.      * Set flag that basket is no longer in offer mode
  921.      */
  922.     public function disableOfferMode(SalesChannelContext $context):void
  923.     {
  924.         $cart $this->cartService->getCart($context->getToken(), $context);
  925.         // compatibility mode for older version with session value.
  926.         if ($cart->hasExtension(PremsIndividualOffer6::OFFER_MODE_EXTENSION_NAME)) {
  927.             $cart->removeExtension(PremsIndividualOffer6::OFFER_MODE_EXTENSION_NAME);
  928.         }
  929.     }
  930.     /**
  931.      * Set flag that basket is no longer in offer mode
  932.      *
  933.      * @param SalesChannelContext $context
  934.      * @param SessionInterface $session
  935.      * @return void
  936.      */
  937.     public function restoreBackupCart(SalesChannelContext $contextSessionInterface $session):void
  938.     {
  939.         $this->cartService->deleteCart($context);
  940.         $backupCartToken $session->remove('premsoftIndividualOffer6.backupCart');
  941.         if ($backupCartToken != null) {
  942.             $backupCart $this->cartService->getCart($backupCartToken$context);
  943.             $backupCart->setToken($context->getToken());
  944.             $backupCart->setName($this->cartService::SALES_CHANNEL);
  945.             $this->cartService->setCart($backupCart);
  946.             $this->cartPersister->save($backupCart$context);
  947.             $this->cartPersister->delete($backupCartToken$context);
  948.         }
  949.     }
  950.     /**
  951.      * @param SalesChannelContext $context
  952.      * @param SessionInterface $session
  953.      */
  954.     public function cancelOfferMode(SalesChannelContext $contextSessionInterface $session): void
  955.     {
  956.         $this->disableOfferMode($context);
  957.         $context->removeState(PremsIndividualOffer6::OFFER_MODE_STATE);
  958.         $this->restoreBackupCart($context$session);
  959.     }
  960.     /**
  961.      * Add an offer to basket and set basket to offer mode
  962.      *
  963.      * @param OfferEntity $offer
  964.      * @param SalesChannelContext $context
  965.      * @param SessionInterface $session
  966.      * @return void
  967.      */
  968.     public function addOfferToBasket(OfferEntity $offerSalesChannelContext $contextSessionInterface $session)
  969.     {
  970.         // Backup current cart
  971.         $backupCart $this->cartService->getCart($context->getToken(), $context);
  972.         $backupCartToken $session->get('premsoftIndividualOffer6.backupCart');
  973.         if ($backupCartToken == null) {
  974.             $backupCartToken Uuid::randomHex();
  975.             $session->set('premsoftIndividualOffer6.backupCart'$backupCartToken);
  976.         }
  977.         $backupCart->setToken($backupCartToken);
  978.         $backupCart->setName('premsoftIndividualOffer6.backupCart');
  979.         $this->cartService->setCart($backupCart);
  980.         $this->cartPersister->save($backupCart$context);
  981.         // Create new Cart
  982.         $cart $this->cartService->createNew($context->getToken());
  983.         $this->activateOfferMode($cart$offer->getId());
  984.         $products = [];
  985.         /** @var OfferItemEntity $item */
  986.         foreach ($offer->getItems() as $item) {
  987.             // Swag Custom Product found?
  988.             if ($item->getItemType() == 'swag-customized-products') {
  989.                 $product \unserialize(base64_decode((string)$item->getLineItem()));
  990.                 if ($product && $product instanceof LineItem) {
  991.                     $product->setStackable(true);
  992.                     $product->setQuantity($item->getQuantity());
  993.                     $product->setRemovable(false);
  994.                     $product->setStackable(false);
  995.                 } else {
  996.                     continue;
  997.                 }
  998.             } elseif($item->getItemType() == LineItem::PROMOTION_LINE_ITEM_TYPE) {
  999.                 $row \unserialize(base64_decode((string) $item->getLineItem()));
  1000.                 $product = new LineItem(Uuid::randomHex(), LineItem::CUSTOM_LINE_ITEM_TYPE);
  1001.                 $product->assign($row->getVars());
  1002.                 $product->setType(LineItem::CUSTOM_LINE_ITEM_TYPE);
  1003.                 $product->setPriceDefinition(new QuantityPriceDefinition($product->getPrice()->getTotalPrice(), $product->getPrice()->getTaxRules()));
  1004.                 $product->setGood(true);
  1005.                 $product->setRemovable(false);
  1006.             } elseif($item->getItemType() === LineItem::CREDIT_LINE_ITEM_TYPE || $item->getItemType() === LineItem::CUSTOM_LINE_ITEM_TYPE) {
  1007.                 $product = new LineItem(Uuid::randomHex(), $item->getItemType());
  1008.                 $product->setLabel($item->getName());
  1009.                 $product->setStackable(true);
  1010.                 $product->setRemovable(false);
  1011.                 $product->setQuantity($item->getQuantity());
  1012.                 $product->setReferencedId(Uuid::randomHex());
  1013.                 $product->setPriceDefinition($item->getPriceDefinition());
  1014.                 if ($item->getItemType() === LineItem::CREDIT_LINE_ITEM_TYPE) {
  1015.                     $product->setPriceDefinition(new AbsolutePriceDefinition($item->getPriceDefinition()->getPrice()));
  1016.                 }
  1017.             } else {
  1018.                 $product \unserialize(base64_decode((string) $item->getLineItem()));
  1019.                 if (!$product || !is_object($product)) {
  1020.                     $product $this->productLineItemFactory->create($item->getProductId());
  1021.                 }
  1022.                 $product->setId(Uuid::randomHex());
  1023.                 $product->setStackable(true);
  1024.                 $product->setRemovable(false);
  1025.                 $product->setQuantity($item->getQuantity());
  1026.                 $product->setStackable(false);
  1027.             }
  1028.             $product->addExtension('prems', new ArrayStruct(
  1029.                 [
  1030.                     'price'                 => $product->getPrice(),
  1031.                     'originalId'            => $item->getId(),
  1032.                     'customPriceDefinition' => $item->getPriceDefinition()
  1033.                 ]
  1034.             ));
  1035.             $products[] = $product;
  1036.         }
  1037.         $this->cartService->add($cart$products$context);
  1038.     }
  1039.     /**
  1040.      * Declines an offer
  1041.      * @param OfferEntity $offer
  1042.      * @param Context $context
  1043.      * @param int|null $offer_days_valid
  1044.      *
  1045.      * @return EntityWrittenContainerEvent
  1046.      * @throws \Exception
  1047.      */
  1048.     public function declineOffer(OfferEntity $offerContext $context$offer_days_valid null)
  1049.     {
  1050.         $offer->setDeclined(true);
  1051.         $data = [
  1052.             'id' => $offer->getId(),
  1053.             'declined' => $offer->isDeclined(),
  1054.             'offer_days_valid' => $offer_days_valid,
  1055.             'offerExpiration' => null
  1056.         ];
  1057.         if ($offer_days_valid) {
  1058.             $data['offerExpiration'] = $this->calculateOfferExpirationDate($offer_days_valid$offer->getUpdatedAt());
  1059.         }
  1060.         return $this->offerRepository->update([$data], $context);
  1061.     }
  1062.     /**
  1063.      * Reset an offer
  1064.      * @param OfferEntity $offer
  1065.      * @param Context $context
  1066.      * @param int|null $offer_days_valid
  1067.      *
  1068.      * @return EntityWrittenContainerEvent
  1069.      * @throws \Exception
  1070.      */
  1071.     public function resetOffer(OfferEntity $offerContext $context$offer_days_valid null)
  1072.     {
  1073.         $offer->setDeclined(true);
  1074.         $data = [
  1075.             'id' => $offer->getId(),
  1076.             'declined' => false,
  1077.             'accepted' => false,
  1078.             'offered' => false,
  1079.             'offer_days_valid' => $offer_days_valid,
  1080.             'offerExpiration' => null
  1081.         ];
  1082.         if ($offer_days_valid) {
  1083.             $data['offerExpiration'] = $this->calculateOfferExpirationDate($offer_days_valid$offer->getUpdatedAt());
  1084.         }
  1085.         return $this->offerRepository->update([$data], $context);
  1086.     }
  1087.     /**
  1088.      * Accept an offer
  1089.      * @param OfferEntity $offer
  1090.      * @param Context $context
  1091.      *
  1092.      * @return EntityWrittenContainerEvent
  1093.      */
  1094.     public function acceptOffer(OfferEntity $offerContext $context)
  1095.     {
  1096.         $offer->setAccepted(true);
  1097.         return $this->offerRepository->update([[
  1098.             'id' => $offer->getId(),
  1099.             'accepted' => $offer->isAccepted()
  1100.         ]], $context);
  1101.     }
  1102.     /**
  1103.      * Creates an offer
  1104.      * @param OfferEntity $offer
  1105.      * @param Context $context
  1106.      * @param int|null $offer_days_valid
  1107.      *
  1108.      * @return EntityWrittenContainerEvent
  1109.      * @throws \Exception
  1110.      */
  1111.     public function createOffer(OfferEntity $offerContext $context$offer_days_valid null)
  1112.     {
  1113.         $offer->setOffered(true);
  1114.         $offer->setAccepted(false);
  1115.         $offer->setDeclined(false);
  1116.         $data = [
  1117.             'id' => $offer->getId(),
  1118.             'offered' => $offer->isOffered(),
  1119.             'accepted' => $offer->isAccepted(),
  1120.             'declined' => $offer->isDeclined(),
  1121.             'offer_days_valid' => $offer_days_valid,
  1122.             'offerExpiration' => null,
  1123.             'admin_updated_at' => date("Y-m-d H:i:s")
  1124.         ];
  1125.         if ($offer_days_valid) {
  1126.             $data['offerExpiration'] = $this->calculateOfferExpirationDate($offer_days_valid$offer->getUpdatedAt());
  1127.         }
  1128.         return $this->offerRepository->update([$data], $context);
  1129.     }
  1130.     /**
  1131.      * Change days an offer
  1132.      * @param OfferEntity $offer
  1133.      * @param Context $context
  1134.      * @param int|null $offer_days_valid
  1135.      *
  1136.      * @return EntityWrittenContainerEvent
  1137.      * @throws \Exception
  1138.      */
  1139.     public function changeOfferDays(OfferEntity $offerContext $context$offer_days_valid null)
  1140.     {
  1141.         $offer->setOffered(true);
  1142.         $data = [
  1143.             'id' => $offer->getId(),
  1144.             'offer_days_valid' => $offer_days_valid ?? null,
  1145.             'offerExpiration' => null
  1146.         ];
  1147.         if ($offer_days_valid) {
  1148.             $data['offerExpiration'] = $this->calculateOfferExpirationDate($offer_days_valid$offer->getUpdatedAt());
  1149.         }
  1150.         return $this->offerRepository->update([$data], $context);
  1151.     }
  1152.     public function setOffered(string $offerIdContext $context): bool
  1153.     {
  1154.         $this->offerRepository->update([['id' => $offerId'offered' => true]], $context);
  1155.         return true;
  1156.     }
  1157.     /**
  1158.      * Adds Line items to cart
  1159.      * @param Cart $cart
  1160.      * @param LineItem|LineItem[] $items
  1161.      * @param SalesChannelContext $context
  1162.      * @return Cart
  1163.      */
  1164.     public function addCartItems(Cart $cart$itemsSalesChannelContext $context): Cart
  1165.     {
  1166.         $cart $this->cartService->add($cart$items$context);
  1167.         return $cart;
  1168.     }
  1169.     /**
  1170.      * @param Cart $cart
  1171.      * @param string $id
  1172.      * @param SalesChannelContext $context
  1173.      * @return Cart
  1174.      */
  1175.     public function removeCartItem(Cart $cartstring $idSalesChannelContext $context): Cart
  1176.     {
  1177.         $cart $this->cartService->remove($cart$id$context);
  1178.         return $cart;
  1179.     }
  1180.     /**
  1181.      * @param array $items
  1182.      * @param Context $context
  1183.      * @return EntityWrittenContainerEvent
  1184.      */
  1185.     public function updateOfferItems(array $itemsContext $context): EntityWrittenContainerEvent
  1186.     {
  1187.         return $this->offerItemRepository->update($items$context);
  1188.     }
  1189.     /**
  1190.      * @param array $data
  1191.      * @param Context $context
  1192.      * @return EntityWrittenContainerEvent
  1193.      */
  1194.     public function updateOfferShipping(array $dataContext $context): EntityWrittenContainerEvent
  1195.     {
  1196.         return $this->offerRepository->update([$data], $context);
  1197.     }
  1198.     /**
  1199.      * @param $days
  1200.      * @param $date
  1201.      * @throws \Exception
  1202.      * @return \DateTimeImmutable
  1203.      */
  1204.     public function calculateOfferExpirationDate($days$date null): \DateTimeImmutable
  1205.     {
  1206.         if (!$date) {
  1207.             $date = new \DateTimeImmutable(date("Y-m-d H:i:s"time()));
  1208.         }
  1209.         if (is_string($date)) {
  1210.             $date = new \DateTimeImmutable($date);
  1211.         }
  1212.         $interval \DateInterval::createFromDateString("{$days} day");
  1213.         return $date->add($interval);
  1214.     }
  1215.     /**
  1216.      * Returns the offer items count
  1217.      *
  1218.      * @param string $offerId
  1219.      * @param Context $context
  1220.      * @return int
  1221.      */
  1222.     private function getOfferItemsCount(string $offerIdContext $context): int
  1223.     {
  1224.         $criteria = new Criteria();
  1225.         $criteria->addFilter(new EqualsFilter('offerId'$offerId));
  1226.         return $this->offerItemRepository->search($criteria$context)->getTotal();
  1227.     }
  1228.     /**
  1229.      * @param SalesChannelProductEntity $product
  1230.      * @param int $quantity
  1231.      * @param float $customPrice
  1232.      * @param null $customTaxRules
  1233.      * @return QuantityPriceDefinition
  1234.      */
  1235.     private function getPriceDefinition(SalesChannelProductEntity $productint $quantityfloat $customPrice 0$customTaxRules null): QuantityPriceDefinition
  1236.     {
  1237.         if ($product->getCalculatedPrices()->count() === 0) {
  1238.             return $this->buildPriceDefinition($product->getCalculatedPrice(), $quantity$customPrice$customTaxRules);
  1239.         }
  1240.         // keep loop reference to $price variable to get last quantity price in case of "null"
  1241.         $price $product->getCalculatedPrice();
  1242.         foreach ($product->getCalculatedPrices() as $price) {
  1243.             if ($quantity <= $price->getQuantity()) {
  1244.                 break;
  1245.             }
  1246.         }
  1247.         return $this->buildPriceDefinition($price$quantity$customPrice$customTaxRules);
  1248.     }
  1249.     /**
  1250.      * @param CalculatedPrice $price
  1251.      * @param int $quantity
  1252.      * @param float $customPrice
  1253.      * @param null $customTaxRules
  1254.      * @return QuantityPriceDefinition
  1255.      */
  1256.     private function buildPriceDefinition(CalculatedPrice $priceint $quantityfloat $customPrice 0$customTaxRules null): QuantityPriceDefinition
  1257.     {
  1258.         $unitPrice $customPrice ?: $price->getUnitPrice();
  1259.         $taxRules $customTaxRules ?: $price->getTaxRules();
  1260.         $definition = new QuantityPriceDefinition($unitPrice$taxRules$quantity);
  1261.         if ($price->getListPrice() !== null) {
  1262.             $definition->setListPrice($price->getListPrice()->getPrice());
  1263.         }
  1264.         if ($price->getReferencePrice() !== null) {
  1265.             $definition->setReferencePriceDefinition(
  1266.                 new ReferencePriceDefinition(
  1267.                     $price->getReferencePrice()->getPurchaseUnit(),
  1268.                     $price->getReferencePrice()->getReferenceUnit(),
  1269.                     $price->getReferencePrice()->getUnitName()
  1270.                 )
  1271.             );
  1272.         }
  1273.         return $definition;
  1274.     }
  1275.     /**
  1276.      * @param ShippingMethodEntity $shippingMethod
  1277.      * @param DALPriceCollection $priceCollection
  1278.      * @param OfferItemCollection $offerItems
  1279.      * @param SalesChannelContext $context
  1280.      * @return CalculatedPrice
  1281.      */
  1282.     private function calculateShippingCosts(ShippingMethodEntity $shippingMethodDALPriceCollection $priceCollectionOfferItemCollection $offerItemsSalesChannelContext $context): CalculatedPrice
  1283.     {
  1284.         switch ($shippingMethod->getTaxType()) {
  1285.             case ShippingMethodEntity::TAX_TYPE_HIGHEST:
  1286.                 $rules $this->getOfferItemPrices($offerItems$context)->getHighestTaxRule();
  1287.                 break;
  1288.             case ShippingMethodEntity::TAX_TYPE_FIXED:
  1289.                 $tax $shippingMethod->getTax();
  1290.                 if ($tax !== null) {
  1291.                     $rules $context->buildTaxRules($tax->getId());
  1292.                     break;
  1293.                 }
  1294.             // no break
  1295.             default:
  1296.                 $rules $this->percentageTaxRuleBuilder->buildRules(
  1297.                     $this->getOfferItemPrices($offerItems$context)->sum()
  1298.                 );
  1299.         }
  1300.         $price $this->getCurrencyPrice($priceCollection$context);
  1301.         $definition = new QuantityPriceDefinition($price$rules1);
  1302.         return $this->quantityPriceCalculator->calculate($definition$context);
  1303.     }
  1304.     /**
  1305.      * @param DALPriceCollection $priceCollection
  1306.      * @param SalesChannelContext $context
  1307.      * @return float
  1308.      */
  1309.     private function getCurrencyPrice(DALPriceCollection $priceCollectionSalesChannelContext $context): float
  1310.     {
  1311.         /** @var Price $price */
  1312.         $price $priceCollection->getCurrencyPrice($context->getCurrency()->getId());
  1313.         $value $this->getPriceForTaxState($price$context);
  1314.         if ($price->getCurrencyId() === Defaults::CURRENCY) {
  1315.             $value *= $context->getContext()->getCurrencyFactor();
  1316.         }
  1317.         return $value;
  1318.     }
  1319.     /**
  1320.      * @param Price $price
  1321.      * @param SalesChannelContext $context
  1322.      * @return float
  1323.      */
  1324.     private function getPriceForTaxState(Price $priceSalesChannelContext $context): float
  1325.     {
  1326.         if ($context->getTaxState() === CartPrice::TAX_STATE_GROSS) {
  1327.             return $price->getGross();
  1328.         }
  1329.         return $price->getNet();
  1330.     }
  1331.     /**
  1332.      * @param OfferItemCollection $offerItems
  1333.      * @param SalesChannelContext $salesChannelContext
  1334.      * @return PriceCollection
  1335.      */
  1336.     private function getOfferItemPrices(OfferItemCollection $offerItemsSalesChannelContext $salesChannelContext): PriceCollection
  1337.     {
  1338.         $self $this;
  1339.         return new PriceCollection(
  1340.             array_filter(array_map(static function (OfferItemEntity $offerItem) use ($self$salesChannelContext) {
  1341.                 return $self->calculateOfferItemPrice($offerItem->getPriceDefinition(), $salesChannelContext);
  1342.             }, array_values($offerItems->getElements())))
  1343.         );
  1344.     }
  1345.     /**
  1346.      * Return the offer item calculated price
  1347.      *
  1348.      * @param $definition
  1349.      * @param SalesChannelContext $salesChannelContext
  1350.      * @return CalculatedPrice
  1351.      */
  1352.     private function calculateOfferItemPrice($definitionSalesChannelContext $salesChannelContext): CalculatedPrice
  1353.     {
  1354.         $prices = new PriceCollection();
  1355.         if ($definition instanceof AbsolutePriceDefinition) {
  1356.             return $this->absolutePriceCalculator->calculate($definition->getPrice(), $prices$salesChannelContext);
  1357.         }
  1358.         if ($definition instanceof PercentagePriceDefinition) {
  1359.             return $this->percentagePriceCalculator->calculate($definition->getPercentage(), $prices$salesChannelContext);
  1360.         }
  1361.         return $this->quantityPriceCalculator->calculate($definition$salesChannelContext);
  1362.     }
  1363. }