custom/plugins/KlarnaPayment/src/Components/EventListener/SessionEventListener.php line 138

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace KlarnaPayment\Components\EventListener;
  4. use KlarnaPayment\Components\CartHasher\CartHasherInterface;
  5. use KlarnaPayment\Components\Client\ClientInterface;
  6. use KlarnaPayment\Components\Client\Hydrator\Request\CreateSession\CreateSessionRequestHydratorInterface;
  7. use KlarnaPayment\Components\Client\Hydrator\Request\UpdateSession\UpdateSessionRequestHydratorInterface;
  8. use KlarnaPayment\Components\Client\Hydrator\Struct\Address\AddressStructHydrator;
  9. use KlarnaPayment\Components\Client\Hydrator\Struct\Address\AddressStructHydratorInterface;
  10. use KlarnaPayment\Components\Client\Hydrator\Struct\Customer\CustomerStructHydratorInterface;
  11. use KlarnaPayment\Components\Client\Response\GenericResponse;
  12. use KlarnaPayment\Components\Client\Struct\Attachment;
  13. use KlarnaPayment\Components\ConfigReader\ConfigReaderInterface;
  14. use KlarnaPayment\Components\Controller\Storefront\KlarnaExpressCheckoutController;
  15. use KlarnaPayment\Components\Converter\CustomOrderConverter;
  16. use KlarnaPayment\Components\Event\OrderCreatedThroughAuthorizationCallback;
  17. use KlarnaPayment\Components\Extension\ErrorMessageExtension;
  18. use KlarnaPayment\Components\Extension\SessionDataExtension;
  19. use KlarnaPayment\Components\Factory\MerchantDataFactoryInterface;
  20. use KlarnaPayment\Components\Helper\OrderFetcherInterface;
  21. use KlarnaPayment\Components\Helper\PaymentHelper\PaymentHelperInterface;
  22. use KlarnaPayment\Installer\Modules\PaymentMethodInstaller;
  23. use Shopware\Core\Checkout\Cart\Cart;
  24. use Shopware\Core\Checkout\Cart\Event\CheckoutOrderPlacedEvent;
  25. use Shopware\Core\Checkout\Customer\Event\CustomerLoginEvent;
  26. use Shopware\Core\Checkout\Order\OrderEntity;
  27. use Shopware\Core\Checkout\Payment\PaymentMethodEntity;
  28. use Shopware\Core\Framework\Context;
  29. use Shopware\Core\Framework\Struct\Struct;
  30. use Shopware\Core\System\SalesChannel\SalesChannelContext;
  31. use Shopware\Core\System\SystemConfig\SystemConfigService;
  32. use Shopware\Storefront\Event\RouteRequest\SetPaymentOrderRouteRequestEvent;
  33. use Shopware\Storefront\Page\Account\Order\AccountEditOrderPageLoadedEvent;
  34. use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent;
  35. use Shopware\Storefront\Page\Page;
  36. use Shopware\Storefront\Page\PageLoadedEvent;
  37. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  38. use Symfony\Component\HttpFoundation\RequestStack;
  39. use Symfony\Component\HttpFoundation\Response;
  40. use Symfony\Component\HttpFoundation\Session\SessionInterface;
  41. class SessionEventListener implements EventSubscriberInterface
  42. {
  43.     /** @var PaymentHelperInterface */
  44.     private $paymentHelper;
  45.     /** @var CreateSessionRequestHydratorInterface */
  46.     private $requestHydrator;
  47.     /** @var UpdateSessionRequestHydratorInterface */
  48.     private $requestUpdateHydrator;
  49.     /** @var AddressStructHydratorInterface */
  50.     private $addressHydrator;
  51.     /** @var CustomerStructHydratorInterface */
  52.     private $customerHydrator;
  53.     /** @var ClientInterface */
  54.     private $client;
  55.     /** @var CartHasherInterface */
  56.     private $cartHasher;
  57.     /** @var MerchantDataFactoryInterface */
  58.     private $merchantDataFactory;
  59.     /** @var CustomOrderConverter */
  60.     private $orderConverter;
  61.     /** @var OrderFetcherInterface */
  62.     private $orderFetcher;
  63.     /** @var null|SessionInterface */
  64.     private $session;
  65.     /** @var RequestStack */
  66.     private $requestStack;
  67.     /** @var SystemConfigService */
  68.     private $systemConfigService;
  69.     /** @var ConfigReaderInterface */
  70.     private $configReader;
  71.     /** @var string */
  72.     private $appSecret;
  73.     public function __construct(
  74.         PaymentHelperInterface $paymentHelper,
  75.         CreateSessionRequestHydratorInterface $requestHydrator,
  76.         UpdateSessionRequestHydratorInterface $requestUpdateHydrator,
  77.         AddressStructHydratorInterface $addressHydrator,
  78.         CustomerStructHydratorInterface $customerHydrator,
  79.         ClientInterface $client,
  80.         CartHasherInterface $cartHasher,
  81.         MerchantDataFactoryInterface $merchantDataFactory,
  82.         CustomOrderConverter $orderConverter,
  83.         OrderFetcherInterface $orderFetcher,
  84.         SystemConfigService $systemConfigService,
  85.         ConfigReaderInterface $configReader,
  86.         ?SessionInterface $session,
  87.         RequestStack $requestStack,
  88.         string $appSecret
  89.     ) {
  90.         $this->paymentHelper         $paymentHelper;
  91.         $this->requestHydrator       $requestHydrator;
  92.         $this->requestUpdateHydrator $requestUpdateHydrator;
  93.         $this->addressHydrator       $addressHydrator;
  94.         $this->customerHydrator      $customerHydrator;
  95.         $this->client                $client;
  96.         $this->cartHasher            $cartHasher;
  97.         $this->merchantDataFactory   $merchantDataFactory;
  98.         $this->orderConverter        $orderConverter;
  99.         $this->orderFetcher          $orderFetcher;
  100.         $this->systemConfigService   $systemConfigService;
  101.         $this->configReader          $configReader;
  102.         $this->appSecret             $appSecret;
  103.         $this->session               $session;
  104.         $this->requestStack          $requestStack;
  105.     }
  106.     public static function getSubscribedEvents(): array
  107.     {
  108.         return [
  109.             CheckoutConfirmPageLoadedEvent::class           => 'startKlarnaSession',
  110.             AccountEditOrderPageLoadedEvent::class          => 'startKlarnaSession',
  111.             CheckoutOrderPlacedEvent::class                 => 'resetKlarnaSession',
  112.             CustomerLoginEvent::class                       => 'resetKlarnaSession',
  113.             SetPaymentOrderRouteRequestEvent::class         => 'resetKlarnaSession',
  114.             OrderCreatedThroughAuthorizationCallback::class => 'resetKlarnaSession',
  115.         ];
  116.     }
  117.     public function startKlarnaSession(PageLoadedEvent $event): void
  118.     {
  119.         $context $event->getSalesChannelContext();
  120.         if (!$this->paymentHelper->isKlarnaPaymentsEnabled($context)) {
  121.             return;
  122.         }
  123.         if ($event instanceof CheckoutConfirmPageLoadedEvent) {
  124.             $cart $event->getPage()->getCart();
  125.         } elseif ($event instanceof AccountEditOrderPageLoadedEvent) {
  126.             /** @phpstan-ignore-next-line */
  127.             $cart $this->convertCartFromOrder($event->getPage()->getOrder(), $event->getContext());
  128.         } else {
  129.             return;
  130.         }
  131.         $response $this->getSession()->has(KlarnaExpressCheckoutController::KLARNA_EXPRESS_SESSION_KEY)
  132.             ? (new GenericResponse())->assign(['httpStatus' => Response::HTTP_BAD_REQUEST])
  133.             : $this->createOrUpdateKlarnaSession($event$cart$context);
  134.         if ($this->isValidResponseStatus($response) && !$this->hasValidKlarnaSession($context)) {
  135.             $this->addKlarnaSessionToShopwareSession($response->getResponse(), $context);
  136.         }
  137.         $this->createSessionDataExtension($response$event->getPage(), $cart$context);
  138.         $this->removeDisabledKlarnaPaymentMethods($event->getPage());
  139.         $this->filterPayNowMethods($event->getPage());
  140.         $this->updateGlobalPurchaseFlowSystemConfig($event->getPage(), $context);
  141.     }
  142.     public function resetKlarnaSession(): void
  143.     {
  144.         $this->getSession()->remove(UpdateSessionRequestHydratorInterface::KLARNA_SESSION_ID);
  145.         $this->getSession()->remove(UpdateSessionRequestHydratorInterface::KLARNA_CLIENT_TOKEN);
  146.         $this->getSession()->remove(UpdateSessionRequestHydratorInterface::KLARNA_PAYMENT_METHOD_CATEGORIES);
  147.         $this->getSession()->remove(UpdateSessionRequestHydratorInterface::KLARNA_ADDRESS_HASH);
  148.     }
  149.     private function filterPayNowMethods(Struct $page): void
  150.     {
  151.         if (!($page instanceof Page)) {
  152.             return;
  153.         }
  154.         /** @var null|SessionDataExtension $sessionData */
  155.         $sessionData $page->getExtension(SessionDataExtension::EXTENSION_NAME);
  156.         if ($sessionData === null) {
  157.             return;
  158.         }
  159.         foreach ($sessionData->getPaymentMethodCategories() as $paymentCategory) {
  160.             if ($paymentCategory['identifier'] === PaymentMethodInstaller::KLARNA_PAYMENTS_PAY_NOW_CODE) {
  161.                 $this->removeSeparatePayNowKlarnaPaymentMethods($page);
  162.                 return;
  163.             }
  164.         }
  165.         $this->removeCombinedKlarnaPaymentPayNowMethod($page);
  166.     }
  167.     private function createErrorMessageExtension(PageLoadedEvent $event): void
  168.     {
  169.         $errorMessageExtension = new ErrorMessageExtension(ErrorMessageExtension::GENERIC_ERROR);
  170.         $event->getPage()->addExtension(ErrorMessageExtension::EXTENSION_NAME$errorMessageExtension);
  171.     }
  172.     private function createSessionDataExtension(GenericResponse $responseStruct $pageCart $cartSalesChannelContext $context): void
  173.     {
  174.         if (!($page instanceof Page)) {
  175.             return;
  176.         }
  177.         $config $this->configReader->read($context->getSalesChannel()->getId());
  178.         $sessionData = new SessionDataExtension();
  179.         $sessionData->assign([
  180.             'selectedPaymentMethodCategory' => $this->getKlarnaCodeFromPaymentMethod($context),
  181.             'cartHash'                      => $this->cartHasher->generate($cart$context),
  182.             'useAuthorizationCallback'      => $config->get('kpUseAuthorizationCallback'),
  183.         ]);
  184.         if ($this->hasValidKlarnaSession($context)) {
  185.             $sessionData->assign([
  186.                 'sessionId'               => $this->getSession()->get(UpdateSessionRequestHydratorInterface::KLARNA_SESSION_ID),
  187.                 'clientToken'             => $this->getSession()->get(UpdateSessionRequestHydratorInterface::KLARNA_CLIENT_TOKEN),
  188.                 'paymentMethodCategories' => $this->getSession()->get(UpdateSessionRequestHydratorInterface::KLARNA_PAYMENT_METHOD_CATEGORIES),
  189.             ]);
  190.         } elseif ($this->getSession()->has(KlarnaExpressCheckoutController::KLARNA_EXPRESS_SESSION_KEY)) {
  191.             $sessionData->assign([
  192.                 'isKlarnaExpress' => true,
  193.                 'clientToken'     => $this->getSession()->get(UpdateSessionRequestHydratorInterface::KLARNA_CLIENT_TOKEN),
  194.             ]);
  195.         } elseif ($this->isValidResponseStatus($response)) {
  196.             $sessionData->assign([
  197.                 'sessionId'               => $response->getResponse()['session_id'],
  198.                 'clientToken'             => $response->getResponse()['client_token'],
  199.                 'paymentMethodCategories' => $response->getResponse()['payment_method_categories'],
  200.             ]);
  201.         }
  202.         if ($this->paymentHelper->isKlarnaPaymentsSelected($context)) {
  203.             $extraMerchantData $this->merchantDataFactory->getExtraMerchantData($sessionData$cart$context);
  204.             if (!empty($extraMerchantData->getAttachment())) {
  205.                 $attachment = new Attachment();
  206.                 $attachment->assign([
  207.                     'data' => $extraMerchantData->getAttachment(),
  208.                 ]);
  209.             } else {
  210.                 $attachment null;
  211.             }
  212.             $sessionData->assign([
  213.                 'customerData' => [
  214.                     'billing_address'  => $this->addressHydrator->hydrateFromContext($contextAddressStructHydrator::TYPE_BILLING),
  215.                     'shipping_address' => $this->addressHydrator->hydrateFromContext($contextAddressStructHydrator::TYPE_SHIPPING),
  216.                     'customer'         => $this->customerHydrator->hydrate($context),
  217.                     'merchant_data'    => $extraMerchantData->getMerchantData(),
  218.                     'attachment'       => $attachment,
  219.                 ],
  220.             ]);
  221.         }
  222.         $page->addExtension(SessionDataExtension::EXTENSION_NAME$sessionData);
  223.     }
  224.     private function removeDisabledKlarnaPaymentMethods(Struct $page): void
  225.     {
  226.         if (!($page instanceof Page)) {
  227.             return;
  228.         }
  229.         /** @var null|SessionDataExtension $sessionData */
  230.         $sessionData $page->getExtension(SessionDataExtension::EXTENSION_NAME);
  231.         if ($sessionData === null) {
  232.             return;
  233.         }
  234.         if (!method_exists($page'setPaymentMethods') || !method_exists($page'getPaymentMethods')) {
  235.             return;
  236.         }
  237.         $isKlarnaExpress $this->getSession()->has(KlarnaExpressCheckoutController::KLARNA_EXPRESS_SESSION_KEY);
  238.         $page->setPaymentMethods(
  239.             $page->getPaymentMethods()->filter(
  240.                 static function (PaymentMethodEntity $paymentMethod) use ($sessionData$isKlarnaExpress) {
  241.                     if (array_key_exists($paymentMethod->getId(), PaymentMethodInstaller::KLARNA_EXPRESS_CHECKOUT_CODES) && !$isKlarnaExpress) {
  242.                         return false;
  243.                     }
  244.                     if (!array_key_exists($paymentMethod->getId(), PaymentMethodInstaller::KLARNA_PAYMENTS_CODES)) {
  245.                         return true;
  246.                     }
  247.                     foreach ($sessionData->getPaymentMethodCategories() as $paymentCategory) {
  248.                         if ($paymentCategory['identifier'] === PaymentMethodInstaller::KLARNA_PAYMENTS_CODES[$paymentMethod->getId()]) {
  249.                             $paymentMethod->setName($paymentCategory['name']);
  250.                             $paymentMethod->setTranslated(
  251.                                 array_merge(
  252.                                     $paymentMethod->getTranslated(),
  253.                                     ['name' => $paymentCategory['name']]
  254.                                 )
  255.                             );
  256.                             return true;
  257.                         }
  258.                     }
  259.                     return false;
  260.                 }
  261.             )
  262.         );
  263.     }
  264.     private function removeSeparatePayNowKlarnaPaymentMethods(Page $page): void
  265.     {
  266.         if (!method_exists($page'setPaymentMethods') || !method_exists($page'getPaymentMethods')) {
  267.             return;
  268.         }
  269.         $page->setPaymentMethods(
  270.             $page->getPaymentMethods()->filter(
  271.                 static function (PaymentMethodEntity $paymentMethod) {
  272.                     if (!array_key_exists($paymentMethod->getId(), PaymentMethodInstaller::KLARNA_PAYMENTS_CODES)) {
  273.                         return true;
  274.                     }
  275.                     return in_array($paymentMethod->getId(), PaymentMethodInstaller::KLARNA_PAYMENTS_CODES_WITH_PAY_NOW_COMBINEDtrue);
  276.                 }
  277.             )
  278.         );
  279.     }
  280.     private function removeCombinedKlarnaPaymentPayNowMethod(Page $page): void
  281.     {
  282.         if (!method_exists($page'setPaymentMethods') || !method_exists($page'getPaymentMethods')) {
  283.             return;
  284.         }
  285.         $page->setPaymentMethods(
  286.             $page->getPaymentMethods()->filter(
  287.                 static function (PaymentMethodEntity $paymentMethod) {
  288.                     if (!array_key_exists($paymentMethod->getId(), PaymentMethodInstaller::KLARNA_PAYMENTS_CODES)) {
  289.                         return true;
  290.                     }
  291.                     return $paymentMethod->getId() !== PaymentMethodInstaller::KLARNA_PAY_NOW;
  292.                 }
  293.             )
  294.         );
  295.     }
  296.     private function removeAllKlarnaPaymentMethods(Struct $page): void
  297.     {
  298.         if (!($page instanceof Page) || !method_exists($page'setPaymentMethods') || !method_exists($page'getPaymentMethods')) {
  299.             return;
  300.         }
  301.         $page->setPaymentMethods(
  302.             $page->getPaymentMethods()->filter(
  303.                 static function (PaymentMethodEntity $paymentMethod) {
  304.                     if (array_key_exists($paymentMethod->getId(), PaymentMethodInstaller::KLARNA_PAYMENTS_CODES)) {
  305.                         return false;
  306.                     }
  307.                     return true;
  308.                 }
  309.             )
  310.         );
  311.     }
  312.     private function createOrUpdateKlarnaSession(PageLoadedEvent $eventCart $cartSalesChannelContext $context): GenericResponse
  313.     {
  314.         $response $this->hasValidKlarnaSession($context)
  315.             ? $this->updateKlarnaSession($this->getSession()->get(UpdateSessionRequestHydratorInterface::KLARNA_SESSION_ID), $cart$context)
  316.             : $this->createKlarnaSession($cart$context);
  317.         if ($this->isValidResponseStatus($response)) {
  318.             return $response;
  319.         }
  320.         if ($this->paymentHelper->isKlarnaPaymentsSelected($context)) {
  321.             $this->createErrorMessageExtension($event);
  322.         }
  323.         $this->removeAllKlarnaPaymentMethods($event->getPage());
  324.         return $response;
  325.     }
  326.     private function isValidResponseStatus(GenericResponse $response): bool
  327.     {
  328.         return in_array($response->getHttpStatus(), [Response::HTTP_OKResponse::HTTP_NO_CONTENT], true);
  329.     }
  330.     private function createKlarnaSession(Cart $cartSalesChannelContext $context): GenericResponse
  331.     {
  332.         $request $this->requestHydrator->hydrate($cart$context);
  333.         return $this->client->request($request$context->getContext());
  334.     }
  335.     private function updateKlarnaSession(string $sessionIdCart $cartSalesChannelContext $context): GenericResponse
  336.     {
  337.         $request $this->requestUpdateHydrator->hydrate($sessionId$cart$context);
  338.         return $this->client->request($request$context->getContext());
  339.     }
  340.     private function getKlarnaCodeFromPaymentMethod(SalesChannelContext $context): string
  341.     {
  342.         if (!array_key_exists($context->getPaymentMethod()->getId(), PaymentMethodInstaller::KLARNA_PAYMENTS_CODES)) {
  343.             return '';
  344.         }
  345.         return PaymentMethodInstaller::KLARNA_PAYMENTS_CODES[$context->getPaymentMethod()->getId()];
  346.     }
  347.     private function convertCartFromOrder(OrderEntity $orderEntityContext $context): Cart
  348.     {
  349.         $order $this->orderFetcher->getOrderFromOrder($orderEntity->getId(), $context);
  350.         if ($order === null) {
  351.             throw new \LogicException('could not find order via id');
  352.         }
  353.         return $this->orderConverter->convertOrderToCart($order$context);
  354.     }
  355.     /**
  356.      * @param array<string,mixed> $klarnaSession
  357.      */
  358.     private function addKlarnaSessionToShopwareSession(array $klarnaSessionSalesChannelContext $context): void
  359.     {
  360.         $this->getSession()->set(UpdateSessionRequestHydratorInterface::KLARNA_SESSION_ID$klarnaSession['session_id']);
  361.         $this->getSession()->set(UpdateSessionRequestHydratorInterface::KLARNA_CLIENT_TOKEN$klarnaSession['client_token']);
  362.         $this->getSession()->set(UpdateSessionRequestHydratorInterface::KLARNA_PAYMENT_METHOD_CATEGORIES$klarnaSession['payment_method_categories']);
  363.         $this->getSession()->set(UpdateSessionRequestHydratorInterface::KLARNA_ADDRESS_HASH$this->getAddressHash($context));
  364.     }
  365.     private function hasValidKlarnaSession(SalesChannelContext $context): bool
  366.     {
  367.         return $this->getSession()->has(UpdateSessionRequestHydratorInterface::KLARNA_SESSION_ID)
  368.             && $this->getSession()->has(UpdateSessionRequestHydratorInterface::KLARNA_CLIENT_TOKEN)
  369.             && $this->getSession()->has(UpdateSessionRequestHydratorInterface::KLARNA_PAYMENT_METHOD_CATEGORIES)
  370.             && $this->getSession()->has(UpdateSessionRequestHydratorInterface::KLARNA_ADDRESS_HASH) && $this->getSession()->get(UpdateSessionRequestHydratorInterface::KLARNA_ADDRESS_HASH) === $this->getAddressHash($context);
  371.     }
  372.     private function getAddressHash(SalesChannelContext $context): ?string
  373.     {
  374.         $customer $this->addressHydrator->hydrateFromContext($context);
  375.         if ($customer === null) {
  376.             return null;
  377.         }
  378.         $json json_encode($customerJSON_PRESERVE_ZERO_FRACTION);
  379.         if (empty($json)) {
  380.             throw new \LogicException('could not generate hash');
  381.         }
  382.         if (empty($this->appSecret)) {
  383.             throw new \LogicException('empty app secret');
  384.         }
  385.         return hash_hmac('sha256'$json$this->appSecret);
  386.     }
  387.     // TODO: Remove me if compatibility is at least 6.4.2.0
  388.     private function getSession(): SessionInterface
  389.     {
  390.         /** @phpstan-ignore-next-line */
  391.         return $this->session ?? $this->requestStack->getSession();
  392.     }
  393.     private function updateGlobalPurchaseFlowSystemConfig(Struct $pageSalesChannelContext $context): void
  394.     {
  395.         if (!($page instanceof Page)) {
  396.             return;
  397.         }
  398.         if (!method_exists($page'getPaymentMethods')) {
  399.             return;
  400.         }
  401.         $paymentMethodIds $page->getPaymentMethods()->getIds();
  402.         if (!$this->hasKlarnaPayment($paymentMethodIds)) {
  403.             return;
  404.         }
  405.         $configRelatedSalesChannelId $context->getSalesChannel()->getId();
  406.         $currentConfig $this->configReader->read($configRelatedSalesChannelIdfalse)
  407.             ->get(ConfigReaderInterface::CONFIG_ACTIVE_GLOBALPURCHASEFLOWnull);
  408.         if ($currentConfig === null) {
  409.             $currentConfig               $this->configReader->read()->get(ConfigReaderInterface::CONFIG_ACTIVE_GLOBALPURCHASEFLOW);
  410.             $configRelatedSalesChannelId null;
  411.         }
  412.         $newConfig = \in_array(PaymentMethodInstaller::KLARNA_PAY$paymentMethodIdstrue);
  413.         if ($currentConfig === $newConfig) {
  414.             return;
  415.         }
  416.         $settingKey = \sprintf('%s%s'ConfigReaderInterface::SYSTEM_CONFIG_DOMAINConfigReaderInterface::CONFIG_ACTIVE_GLOBALPURCHASEFLOW);
  417.         $this->systemConfigService->set($settingKey$newConfig$configRelatedSalesChannelId);
  418.     }
  419.     private function hasKlarnaPayment(array $paymentMethodIds): bool
  420.     {
  421.         $klarnaPaymentIds = \array_keys(PaymentMethodInstaller::KLARNA_PAYMENTS_CODES);
  422.         foreach ($klarnaPaymentIds as $klarnaPaymentId) {
  423.             if (\in_array($klarnaPaymentId$paymentMethodIdstrue)) {
  424.                 return true;
  425.             }
  426.         }
  427.         return false;
  428.     }
  429. }