custom/plugins/FourtwosixShippingCostsCalculator/src/Decorator/Core/System/SalesChannel/Context/BaseContextFactoryDecorator.php line 184

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace FourtwosixShippingCostsCalculator\Decorator\Core\System\SalesChannel\Context;
  3. use Doctrine\DBAL\Connection;
  4. use Shopware\Core\Checkout\Cart\Delivery\Struct\ShippingLocation;
  5. use Shopware\Core\Checkout\Cart\Price\Struct\CartPrice;
  6. use Shopware\Core\Checkout\Customer\Aggregate\CustomerAddress\CustomerAddressEntity;
  7. use Shopware\Core\Checkout\Payment\PaymentMethodEntity;
  8. use Shopware\Core\Checkout\Shipping\ShippingMethodEntity;
  9. use Shopware\Core\Defaults;
  10. use Shopware\Core\Framework\Api\Context\AdminSalesChannelApiSource;
  11. use Shopware\Core\Framework\Api\Context\SalesChannelApiSource;
  12. use Shopware\Core\Framework\Context;
  13. use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
  14. use Shopware\Core\Framework\DataAbstractionLayer\Pricing\CashRoundingConfig;
  15. use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
  16. use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
  17. use Shopware\Core\Framework\Feature;
  18. use Shopware\Core\Framework\Uuid\Uuid;
  19. use Shopware\Core\System\Country\Aggregate\CountryState\CountryStateEntity;
  20. use Shopware\Core\System\Country\CountryEntity;
  21. use Shopware\Core\System\Currency\Aggregate\CurrencyCountryRounding\CurrencyCountryRoundingEntity;
  22. use Shopware\Core\System\Currency\CurrencyEntity;
  23. use Shopware\Core\System\SalesChannel\BaseContext;
  24. use Shopware\Core\System\SalesChannel\Context\BaseContextFactory;
  25. use Shopware\Core\System\SalesChannel\Context\SalesChannelContextService;
  26. use Shopware\Core\System\SalesChannel\SalesChannelEntity;
  27. use Shopware\Core\System\SalesChannel\SalesChannelException;
  28. use Shopware\Core\System\Tax\TaxCollection;
  29. class BaseContextFactoryDecorator extends BaseContextFactory
  30. {
  31.     public function __construct(private EntityRepository $salesChannelRepository,
  32.                                 private EntityRepository $currencyRepository,
  33.                                 private EntityRepository $customerGroupRepository,
  34.                                 private EntityRepository $countryRepository,
  35.                                 private EntityRepository $taxRepository,
  36.                                 private $paymentMethodRepository,
  37.                                 private EntityRepository $shippingMethodRepository,
  38.                                 private Connection       $connection,
  39.                                 private EntityRepository $countryStateRepository,
  40.                                 private EntityRepository $currencyCountryRepository)
  41.     {
  42.         parent::__construct($salesChannelRepository$currencyRepository$customerGroupRepository$countryRepository$taxRepository$paymentMethodRepository$shippingMethodRepository$connection$countryStateRepository$currencyCountryRepository);
  43.     }
  44.     public function create(string $salesChannelId, array $options = []): BaseContext
  45.     {
  46.         $context $this->getContext($salesChannelId$options);
  47.         $criteria = new Criteria([$salesChannelId]);
  48.         $criteria->setTitle('base-context-factory::sales-channel');
  49.         $criteria->addAssociation('currency');
  50.         $criteria->addAssociation('domains');
  51.         /** @var SalesChannelEntity|null $salesChannel */
  52.         $salesChannel $this->salesChannelRepository->search($criteria$context)
  53.             ->get($salesChannelId);
  54.         if (!$salesChannel) {
  55.             throw new \RuntimeException(sprintf('Sales channel with id %s not found or not valid!'$salesChannelId));
  56.         }
  57.         if (!Feature::isActive('FEATURE_NEXT_17276')) {
  58.             /*
  59.              * @deprecated tag:v6.5.0 - Overriding the languageId of the SalesChannel is deprecated and will be removed in v6.5.0
  60.              * use `$salesChannelContext->getLanguageId()` instead
  61.              */
  62.             if (\array_key_exists(SalesChannelContextService::LANGUAGE_ID$options)) {
  63.                 $salesChannel->setLanguageId($options[SalesChannelContextService::LANGUAGE_ID]);
  64.             }
  65.         }
  66.         //load active currency, fallback to shop currency
  67.         $currency $salesChannel->getCurrency();
  68.         if (\array_key_exists(SalesChannelContextService::CURRENCY_ID$options)) {
  69.             $currencyId $options[SalesChannelContextService::CURRENCY_ID];
  70.             $criteria = new Criteria([$currencyId]);
  71.             $criteria->setTitle('base-context-factory::currency');
  72.             $currency $this->currencyRepository->search($criteria$context)->get($currencyId);
  73.         }
  74.         //load not logged in customer with default shop configuration or with provided checkout scopes
  75.         $shippingLocation $this->loadShippingLocation($options$context$salesChannel);
  76.         $groupId $salesChannel->getCustomerGroupId();
  77.         /** @deprecated tag:v6.5.0 - Fallback customer group is deprecated and will be removed */
  78.         $groupIds array_unique([$salesChannel->getCustomerGroupId(), Defaults::FALLBACK_CUSTOMER_GROUP]);
  79.         $criteria = new Criteria($groupIds);
  80.         $criteria->setTitle('base-context-factory::customer-group');
  81.         $customerGroups $this->customerGroupRepository->search($criteria$context);
  82.         /** @deprecated tag:v6.5.0 - Fallback customer group is deprecated and will be removed */
  83.         $fallbackGroup $customerGroups->has(Defaults::FALLBACK_CUSTOMER_GROUP) ? $customerGroups->get(Defaults::FALLBACK_CUSTOMER_GROUP) : $customerGroups->get($salesChannel->getCustomerGroupId());
  84.         $customerGroup $customerGroups->get($groupId);
  85.         //loads tax rules based on active customer and delivery address
  86.         $taxRules $this->getTaxRules($context);
  87.         //detect active payment method, first check if checkout defined other payment method, otherwise validate if customer logged in, at least use shop default
  88.         $payment $this->getPaymentMethod($options$context$salesChannel);
  89.         //detect active delivery method, at first checkout scope, at least shop default method
  90.         $shippingMethod $this->getShippingMethod($options$context$salesChannel);
  91.         [$itemRounding$totalRounding] = $this->getCashRounding($currency$shippingLocation$context);
  92.         $context = new Context(
  93.             $context->getSource(),
  94.             [],
  95.             $currency->getId(),
  96.             $context->getLanguageIdChain(),
  97.             $context->getVersionId(),
  98.             $currency->getFactor(),
  99.             true,
  100.             CartPrice::TAX_STATE_GROSS,
  101.             $itemRounding
  102.         );
  103.         return new BaseContext(
  104.             $context,
  105.             $salesChannel,
  106.             $currency,
  107.             $customerGroup,
  108.             $fallbackGroup,
  109.             $taxRules,
  110.             $payment,
  111.             $shippingMethod,
  112.             $shippingLocation,
  113.             $itemRounding,
  114.             $totalRounding
  115.         );
  116.     }
  117.     /**
  118.      * @param array<string, mixed> $options
  119.      * @fourtwosix-edit
  120.      */
  121.     private function loadShippingLocation(array $optionsContext $contextSalesChannelEntity $salesChannel): ShippingLocation
  122.     {
  123.         $customerAddress null;
  124.         if (isset($options["zipcode"])) {
  125.             $customerAddress = new CustomerAddressEntity();
  126.             $customerAddress->setZipcode($options["zipcode"]);
  127.         }
  128.         // allows previewing cart calculation for a specify state for not logged in customers
  129.         if (isset($options[SalesChannelContextService::COUNTRY_STATE_ID])) {
  130.             $countryStateId $options[SalesChannelContextService::COUNTRY_STATE_ID];
  131.             \assert(\is_string($countryStateId) && Uuid::isValid($countryStateId));
  132.             $criteria = new Criteria([$countryStateId]);
  133.             $criteria->addAssociation('country');
  134.             $criteria->setTitle('base-context-factory::country');
  135.             $state $this->countryStateRepository->search($criteria$context)->get($countryStateId);
  136.             if (!$state instanceof CountryStateEntity) {
  137.                 throw SalesChannelException::countryStateNotFound($countryStateId);
  138.             }
  139.             /** @var CountryEntity $country */
  140.             $country $state->getCountry();
  141.             if (isset($options["zipcode"])) {
  142.                 $customerAddress->setCountry($country);
  143.             }
  144.             return new ShippingLocation($country$state$customerAddress);
  145.         }
  146.         $countryId $options[SalesChannelContextService::COUNTRY_ID] ?? $salesChannel->getCountryId();
  147.         \assert(\is_string($countryId) && Uuid::isValid($countryId));
  148.         $criteria = new Criteria([$countryId]);
  149.         $criteria->setTitle('base-context-factory::country');
  150.         $country $this->countryRepository->search($criteria$context)->get($countryId);
  151.         if (!$country instanceof CountryEntity) {
  152.             throw SalesChannelException::countryNotFound($countryId);
  153.         }
  154.         if (isset($options["zipcode"])) {
  155.             $customerAddress->setCountry($country);
  156.         }
  157.         return new ShippingLocation($countrynull$customerAddress);
  158.     }
  159.     private function getTaxRules(Context $context): TaxCollection
  160.     {
  161.         $criteria = new Criteria();
  162.         $criteria->setTitle('base-context-factory::taxes');
  163.         $criteria->addAssociation('rules.type');
  164.         /** @var TaxCollection $taxes */
  165.         $taxes $this->taxRepository->search($criteria$context)->getEntities();
  166.         return $taxes;
  167.     }
  168.     /**
  169.      * @param array<string, mixed> $options
  170.      */
  171.     private function getPaymentMethod(array $optionsContext $contextSalesChannelEntity $salesChannel): PaymentMethodEntity
  172.     {
  173.         $id $options[SalesChannelContextService::PAYMENT_METHOD_ID] ?? $salesChannel->getPaymentMethodId();
  174.         $criteria = (new Criteria([$id]))->addAssociation('media');
  175.         $criteria->setTitle('base-context-factory::payment-method');
  176.         $paymentMethod $this->paymentMethodRepository
  177.             ->search($criteria$context)
  178.             ->get($id);
  179.         if (!$paymentMethod instanceof PaymentMethodEntity) {
  180.             throw SalesChannelException::unknownPaymentMethod($id);
  181.         }
  182.         return $paymentMethod;
  183.     }
  184.     /**
  185.      * @param array<string, mixed> $options
  186.      */
  187.     private function getShippingMethod(array $optionsContext $contextSalesChannelEntity $salesChannel): ShippingMethodEntity
  188.     {
  189.         $id $options[SalesChannelContextService::SHIPPING_METHOD_ID] ?? $salesChannel->getShippingMethodId();
  190.         $ids \array_unique(array_filter([$id$salesChannel->getShippingMethodId()]));
  191.         $criteria = new Criteria($ids);
  192.         $criteria->addAssociation('media');
  193.         $criteria->setTitle('base-context-factory::shipping-method');
  194.         $shippingMethods $this->shippingMethodRepository->search($criteria$context);
  195.         /** @var ShippingMethodEntity $shippingMethod */
  196.         $shippingMethod $shippingMethods->get($id) ?? $shippingMethods->get($salesChannel->getShippingMethodId());
  197.         return $shippingMethod;
  198.     }
  199.     /**
  200.      * @return CashRoundingConfig[]
  201.      */
  202.     private function getCashRounding(CurrencyEntity $currencyShippingLocation $shippingLocationContext $context): array
  203.     {
  204.         $criteria = new Criteria();
  205.         $criteria->setTitle('base-context-factory::cash-rounding');
  206.         $criteria->setLimit(1);
  207.         $criteria->addFilter(new EqualsFilter('currencyId'$currency->getId()));
  208.         $criteria->addFilter(new EqualsFilter('countryId'$shippingLocation->getCountry()->getId()));
  209.         $countryConfig $this->currencyCountryRepository->search($criteria$context)->first();
  210.         if ($countryConfig instanceof CurrencyCountryRoundingEntity) {
  211.             return [$countryConfig->getItemRounding(), $countryConfig->getTotalRounding()];
  212.         }
  213.         return [$currency->getItemRounding(), $currency->getTotalRounding()];
  214.     }
  215.     /**
  216.      * @param array<string, mixed> $session
  217.      */
  218.     private function getContext(string $salesChannelId, array $session): Context
  219.     {
  220.         $sql '
  221.         # context-factory::base-context
  222.         SELECT
  223.           sales_channel.id as sales_channel_id,
  224.           sales_channel.language_id as sales_channel_default_language_id,
  225.           sales_channel.currency_id as sales_channel_currency_id,
  226.           currency.factor as sales_channel_currency_factor,
  227.           GROUP_CONCAT(LOWER(HEX(sales_channel_language.language_id))) as sales_channel_language_ids
  228.         FROM sales_channel
  229.             INNER JOIN currency
  230.                 ON sales_channel.currency_id = currency.id
  231.             LEFT JOIN sales_channel_language
  232.                 ON sales_channel_language.sales_channel_id = sales_channel.id
  233.         WHERE sales_channel.id = :id
  234.         GROUP BY sales_channel.id, sales_channel.language_id, sales_channel.currency_id, currency.factor';
  235.         $data $this->connection->fetchAssociative($sql, [
  236.             'id' => Uuid::fromHexToBytes($salesChannelId),
  237.         ]);
  238.         if ($data === false) {
  239.             throw SalesChannelException::noContextData($salesChannelId);
  240.         }
  241.         if (isset($session[SalesChannelContextService::ORIGINAL_CONTEXT])) {
  242.             $origin = new AdminSalesChannelApiSource($salesChannelId$session[SalesChannelContextService::ORIGINAL_CONTEXT]);
  243.         } else {
  244.             $origin = new SalesChannelApiSource($salesChannelId);
  245.         }
  246.         // explode all available languages for the provided sales channel
  247.         $languageIds $data['sales_channel_language_ids'] ? explode(',', (string)$data['sales_channel_language_ids']) : [];
  248.         $languageIds array_keys(array_flip($languageIds));
  249.         // check which language should be used in the current request (request header set, or context already contains a language - stored in `sales_channel_api_context`)
  250.         $defaultLanguageId Uuid::fromBytesToHex($data['sales_channel_default_language_id']);
  251.         $languageChain $this->buildLanguageChain($session$defaultLanguageId$languageIds);
  252.         $versionId Defaults::LIVE_VERSION;
  253.         if (isset($session[SalesChannelContextService::VERSION_ID])) {
  254.             $versionId $session[SalesChannelContextService::VERSION_ID];
  255.         }
  256.         return new Context(
  257.             $origin,
  258.             [],
  259.             Uuid::fromBytesToHex($data['sales_channel_currency_id']),
  260.             $languageChain,
  261.             $versionId,
  262.             (float)$data['sales_channel_currency_factor'],
  263.             true
  264.         );
  265.     }
  266.     /**
  267.      * @param array<string, mixed> $sessionOptions
  268.      * @param array<string> $availableLanguageIds
  269.      *
  270.      * @return non-empty-array<string>
  271.      */
  272.     private function buildLanguageChain(array $sessionOptionsstring $defaultLanguageId, array $availableLanguageIds): array
  273.     {
  274.         $current $sessionOptions[SalesChannelContextService::LANGUAGE_ID] ?? $defaultLanguageId;
  275.         if (!\is_string($current) || !Uuid::isValid($current)) {
  276.             throw SalesChannelException::invalidLanguageId();
  277.         }
  278.         // check provided language is part of the available languages
  279.         if (!\in_array($current$availableLanguageIdstrue)) {
  280.             throw SalesChannelException::providedLanguageNotAvailable($current$availableLanguageIds);
  281.         }
  282.         if ($current === Defaults::LANGUAGE_SYSTEM) {
  283.             return [Defaults::LANGUAGE_SYSTEM];
  284.         }
  285.         // provided language can be a child language
  286.         return array_filter([$current$this->getParentLanguageId($current), Defaults::LANGUAGE_SYSTEM]);
  287.     }
  288.     private function getParentLanguageId(string $languageId): ?string
  289.     {
  290.         $data $this->connection->createQueryBuilder()
  291.             ->select(['LOWER(HEX(language.parent_id))'])
  292.             ->from('language')
  293.             ->where('language.id = :id')
  294.             ->setParameter('id'Uuid::fromHexToBytes($languageId))
  295.             ->executeQuery()
  296.             ->fetchOne();
  297.         if ($data === false) {
  298.             throw SalesChannelException::languageNotFound($languageId);
  299.         }
  300.         return $data;
  301.     }
  302. }