src/Core/System/SalesChannel/Context/SalesChannelContextFactory.php line 52

  1. <?php declare(strict_types=1);
  2. namespace Shopware\Core\System\SalesChannel\Context;
  3. use Shopware\Core\Checkout\Cart\Delivery\Struct\ShippingLocation;
  4. use Shopware\Core\Checkout\Cart\Price\Struct\CartPrice;
  5. use Shopware\Core\Checkout\Cart\Tax\TaxDetector;
  6. use Shopware\Core\Checkout\Customer\Aggregate\CustomerAddress\CustomerAddressEntity;
  7. use Shopware\Core\Checkout\Customer\CustomerEntity;
  8. use Shopware\Core\Checkout\Payment\Exception\UnknownPaymentMethodException;
  9. use Shopware\Core\Checkout\Payment\PaymentMethodEntity;
  10. use Shopware\Core\Framework\Api\Context\SalesChannelApiSource;
  11. use Shopware\Core\Framework\Context;
  12. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  13. use Shopware\Core\Framework\DataAbstractionLayer\Pricing\CashRoundingConfig;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\MultiFilter;
  17. use Shopware\Core\Framework\Log\Package;
  18. use Shopware\Core\Framework\Plugin\Exception\DecorationPatternException;
  19. use Shopware\Core\System\Currency\Aggregate\CurrencyCountryRounding\CurrencyCountryRoundingEntity;
  20. use Shopware\Core\System\SalesChannel\BaseContext;
  21. use Shopware\Core\System\SalesChannel\Event\SalesChannelContextPermissionsChangedEvent;
  22. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  23. use Shopware\Core\System\Tax\Aggregate\TaxRule\TaxRuleCollection;
  24. use Shopware\Core\System\Tax\Aggregate\TaxRule\TaxRuleEntity;
  25. use Shopware\Core\System\Tax\TaxCollection;
  26. use Shopware\Core\System\Tax\TaxRuleType\TaxRuleTypeFilterInterface;
  27. use Symfony\Component\EventDispatcher\EventDispatcherInterface;
  28. use function array_unique;
  29. #[Package('sales-channel')]
  30. class SalesChannelContextFactory extends AbstractSalesChannelContextFactory
  31. {
  32.     /**
  33.      * @internal
  34.      *
  35.      * @param iterable<TaxRuleTypeFilterInterface> $taxRuleTypeFilter
  36.      */
  37.     public function __construct(private readonly EntityRepository $customerRepository, private readonly EntityRepository $customerGroupRepository, private readonly EntityRepository $addressRepository, private readonly EntityRepository $paymentMethodRepository, private readonly TaxDetector $taxDetector, private readonly iterable $taxRuleTypeFilter, private readonly EventDispatcherInterface $eventDispatcher, private readonly EntityRepository $currencyCountryRepository, private readonly AbstractBaseContextFactory $baseContextFactory)
  38.     {
  39.     }
  40.     public function getDecorated(): AbstractSalesChannelContextFactory
  41.     {
  42.         throw new DecorationPatternException(self::class);
  43.     }
  44.     public function create(string $tokenstring $salesChannelId, array $options = []): SalesChannelContext
  45.     {
  46.         // we split the context generation to allow caching of the base context
  47.         $base $this->baseContextFactory->create($salesChannelId$options);
  48.         // customer
  49.         $customer null;
  50.         if (\array_key_exists(SalesChannelContextService::CUSTOMER_ID$options) && $options[SalesChannelContextService::CUSTOMER_ID] !== null) {
  51.             //load logged in customer and set active addresses
  52.             $customer $this->loadCustomer($options$base->getContext());
  53.         }
  54.         $shippingLocation $base->getShippingLocation();
  55.         if ($customer) {
  56.             /** @var CustomerAddressEntity $activeShippingAddress */
  57.             $activeShippingAddress $customer->getActiveShippingAddress();
  58.             $shippingLocation ShippingLocation::createFromAddress($activeShippingAddress);
  59.         }
  60.         $customerGroup $base->getCurrentCustomerGroup();
  61.         if ($customer) {
  62.             $criteria = new Criteria([$customer->getGroupId()]);
  63.             $criteria->setTitle('context-factory::customer-group');
  64.             $customerGroup $this->customerGroupRepository->search($criteria$base->getContext())->first() ?? $customerGroup;
  65.         }
  66.         //loads tax rules based on active customer and delivery address
  67.         $taxRules $this->getTaxRules($base$customer$shippingLocation);
  68.         //detect active payment method, first check if checkout defined other payment method, otherwise validate if customer logged in, at least use shop default
  69.         $payment $this->getPaymentMethod($options$base$customer);
  70.         [$itemRounding$totalRounding] = $this->getCashRounding($base$shippingLocation);
  71.         $context = new Context(
  72.             $base->getContext()->getSource(),
  73.             [],
  74.             $base->getCurrencyId(),
  75.             $base->getContext()->getLanguageIdChain(),
  76.             $base->getContext()->getVersionId(),
  77.             $base->getCurrency()->getFactor(),
  78.             true,
  79.             CartPrice::TAX_STATE_GROSS,
  80.             $itemRounding
  81.         );
  82.         $salesChannelContext = new SalesChannelContext(
  83.             $context,
  84.             $token,
  85.             $options[SalesChannelContextService::DOMAIN_ID] ?? null,
  86.             $base->getSalesChannel(),
  87.             $base->getCurrency(),
  88.             $customerGroup,
  89.             $taxRules,
  90.             $payment,
  91.             $base->getShippingMethod(),
  92.             $shippingLocation,
  93.             $customer,
  94.             $itemRounding,
  95.             $totalRounding
  96.         );
  97.         if (\array_key_exists(SalesChannelContextService::PERMISSIONS$options)) {
  98.             $salesChannelContext->setPermissions($options[SalesChannelContextService::PERMISSIONS]);
  99.             $event = new SalesChannelContextPermissionsChangedEvent($salesChannelContext$options[SalesChannelContextService::PERMISSIONS]);
  100.             $this->eventDispatcher->dispatch($event);
  101.             $salesChannelContext->lockPermissions();
  102.         }
  103.         $salesChannelContext->setTaxState($this->taxDetector->getTaxState($salesChannelContext));
  104.         return $salesChannelContext;
  105.     }
  106.     private function getTaxRules(BaseContext $context, ?CustomerEntity $customerShippingLocation $shippingLocation): TaxCollection
  107.     {
  108.         $taxes $context->getTaxRules()->getElements();
  109.         foreach ($taxes as $tax) {
  110.             $taxRules $tax->getRules();
  111.             if ($taxRules === null) {
  112.                 continue;
  113.             }
  114.             $taxRules $taxRules->filter(function (TaxRuleEntity $taxRule) use ($customer$shippingLocation) {
  115.                 foreach ($this->taxRuleTypeFilter as $ruleTypeFilter) {
  116.                     if ($ruleTypeFilter->match($taxRule$customer$shippingLocation)) {
  117.                         return true;
  118.                     }
  119.                 }
  120.                 return false;
  121.             });
  122.             $taxRules->sortByTypePosition();
  123.             $taxRule $taxRules->first();
  124.             $matchingRules = new TaxRuleCollection();
  125.             if ($taxRule) {
  126.                 $matchingRules->add($taxRule);
  127.             }
  128.             $tax->setRules($matchingRules);
  129.         }
  130.         return new TaxCollection($taxes);
  131.     }
  132.     /**
  133.      * @group not-deterministic
  134.      * NEXT-21735 - This is covered randomly
  135.      * @codeCoverageIgnore
  136.      *
  137.      * @param array<string, mixed> $options
  138.      */
  139.     private function getPaymentMethod(array $optionsBaseContext $context, ?CustomerEntity $customer): PaymentMethodEntity
  140.     {
  141.         if ($customer === null || isset($options[SalesChannelContextService::PAYMENT_METHOD_ID])) {
  142.             return $context->getPaymentMethod();
  143.         }
  144.         $id $customer->getLastPaymentMethodId() ?? $customer->getDefaultPaymentMethodId();
  145.         if ($id === $context->getPaymentMethod()->getId()) {
  146.             // NEXT-21735 - does not execute on every test run
  147.             return $context->getPaymentMethod();
  148.         }
  149.         $criteria = new Criteria([$id]);
  150.         $criteria->addAssociation('media');
  151.         $criteria->setTitle('context-factory::payment-method');
  152.         /** @var PaymentMethodEntity|null $paymentMethod */
  153.         $paymentMethod $this->paymentMethodRepository->search($criteria$context->getContext())->get($id);
  154.         if (!$paymentMethod) {
  155.             throw new UnknownPaymentMethodException($id);
  156.         }
  157.         return $paymentMethod;
  158.     }
  159.     /**
  160.      * @param array<string, mixed> $options
  161.      */
  162.     private function loadCustomer(array $optionsContext $context): ?CustomerEntity
  163.     {
  164.         $addressIds = [];
  165.         $customerId $options[SalesChannelContextService::CUSTOMER_ID];
  166.         $criteria = new Criteria([$customerId]);
  167.         $criteria->setTitle('context-factory::customer');
  168.         $criteria->addAssociation('salutation');
  169.         $criteria->addAssociation('defaultPaymentMethod');
  170.         /** @var SalesChannelApiSource $source */
  171.         $source $context->getSource();
  172.         $criteria->addFilter(new MultiFilter(MultiFilter::CONNECTION_OR, [
  173.             new EqualsFilter('customer.boundSalesChannelId'null),
  174.             new EqualsFilter('customer.boundSalesChannelId'$source->getSalesChannelId()),
  175.         ]));
  176.         /** @var CustomerEntity|null $customer */
  177.         $customer $this->customerRepository->search($criteria$context)->get($customerId);
  178.         if (!$customer) {
  179.             return null;
  180.         }
  181.         $activeBillingAddressId $options[SalesChannelContextService::BILLING_ADDRESS_ID] ?? $customer->getDefaultBillingAddressId();
  182.         $activeShippingAddressId $options[SalesChannelContextService::SHIPPING_ADDRESS_ID] ?? $customer->getDefaultShippingAddressId();
  183.         $addressIds[] = $activeBillingAddressId;
  184.         $addressIds[] = $activeShippingAddressId;
  185.         $addressIds[] = $customer->getDefaultBillingAddressId();
  186.         $addressIds[] = $customer->getDefaultShippingAddressId();
  187.         $criteria = new Criteria(array_unique($addressIds));
  188.         $criteria->setTitle('context-factory::addresses');
  189.         $criteria->addAssociation('salutation');
  190.         $criteria->addAssociation('country');
  191.         $criteria->addAssociation('countryState');
  192.         $addresses $this->addressRepository->search($criteria$context);
  193.         /** @var CustomerAddressEntity $activeBillingAddress */
  194.         $activeBillingAddress $addresses->get($activeBillingAddressId);
  195.         $customer->setActiveBillingAddress($activeBillingAddress);
  196.         /** @var CustomerAddressEntity $activeShippingAddress */
  197.         $activeShippingAddress $addresses->get($activeShippingAddressId);
  198.         $customer->setActiveShippingAddress($activeShippingAddress);
  199.         /** @var CustomerAddressEntity $defaultBillingAddress */
  200.         $defaultBillingAddress $addresses->get($customer->getDefaultBillingAddressId());
  201.         $customer->setDefaultBillingAddress($defaultBillingAddress);
  202.         /** @var CustomerAddressEntity $defaultShippingAddress */
  203.         $defaultShippingAddress $addresses->get($customer->getDefaultShippingAddressId());
  204.         $customer->setDefaultShippingAddress($defaultShippingAddress);
  205.         return $customer;
  206.     }
  207.     /**
  208.      * @return CashRoundingConfig[]
  209.      *
  210.      * @group not-deterministic
  211.      * NEXT-21735 - This is covered randomly
  212.      * @codeCoverageIgnore
  213.      */
  214.     private function getCashRounding(BaseContext $contextShippingLocation $shippingLocation): array
  215.     {
  216.         if ($context->getShippingLocation()->getCountry()->getId() === $shippingLocation->getCountry()->getId()) {
  217.             return [$context->getItemRounding(), $context->getTotalRounding()];
  218.         }
  219.         $criteria = new Criteria();
  220.         $criteria->setTitle('context-factory::cash-rounding');
  221.         $criteria->setLimit(1);
  222.         $criteria->addFilter(new EqualsFilter('currencyId'$context->getCurrencyId()));
  223.         $criteria->addFilter(new EqualsFilter('countryId'$shippingLocation->getCountry()->getId()));
  224.         /** @var CurrencyCountryRoundingEntity|null $countryConfig */
  225.         $countryConfig $this->currencyCountryRepository
  226.             ->search($criteria$context->getContext())
  227.             ->first();
  228.         if ($countryConfig) {
  229.             return [$countryConfig->getItemRounding(), $countryConfig->getTotalRounding()];
  230.         }
  231.         return [$context->getCurrency()->getItemRounding(), $context->getCurrency()->getTotalRounding()];
  232.     }
  233. }