<?php
declare(strict_types=1);
namespace KlarnaPayment\Components\EventListener;
use KlarnaPayment\Components\Client\ClientInterface;
use KlarnaPayment\Components\Client\Hydrator\Request\UpdateAddress\UpdateAddressRequestHydratorInterface;
use KlarnaPayment\Components\Client\Hydrator\Request\UpdateOrder\UpdateOrderRequestHydratorInterface;
use KlarnaPayment\Components\Client\Request\UpdateOrderRequest;
use KlarnaPayment\Components\Helper\OrderFetcherInterface;
use KlarnaPayment\Components\Helper\OrderValidator\OrderValidatorInterface;
use KlarnaPayment\Components\Helper\RequestHasherInterface;
use KlarnaPayment\Components\PaymentHandler\AbstractKlarnaPaymentHandler;
use KlarnaPayment\Core\Framework\ContextScope;
use Shopware\Core\Checkout\Order\Aggregate\OrderAddress\OrderAddressDefinition;
use Shopware\Core\Checkout\Order\Aggregate\OrderLineItem\OrderLineItemDefinition;
use Shopware\Core\Checkout\Order\OrderDefinition;
use Shopware\Core\Checkout\Order\OrderEntity;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Write\Command\UpdateCommand;
use Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PostWriteValidationEvent;
use Shopware\Core\Framework\Validation\WriteConstraintViolationException;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Component\Validator\ConstraintViolationList;
use Symfony\Contracts\Translation\TranslatorInterface;
class OrderChangeEventListener implements EventSubscriberInterface
{
/** @var OrderFetcherInterface */
private $orderFetcher;
/** @var UpdateAddressRequestHydratorInterface */
private $addressRequestHydrator;
/** @var UpdateOrderRequestHydratorInterface */
private $orderRequestHydrator;
/** @var ClientInterface */
private $client;
/** @var EntityRepository */
private $orderRepository;
/** @var TranslatorInterface */
private $translator;
/** @var RequestHasherInterface */
private $updateOrderRequestHasher;
/** @var RequestHasherInterface */
private $updateAddressRequestHasher;
/** @var OrderValidatorInterface */
private $orderValidator;
/** @var string */
private $currentLocale;
public function __construct(
OrderFetcherInterface $orderFetcher,
UpdateAddressRequestHydratorInterface $addressRequestHydrator,
UpdateOrderRequestHydratorInterface $orderRequestHydrator,
ClientInterface $client,
EntityRepository $orderRepository,
TranslatorInterface $translator,
RequestHasherInterface $updateOrderRequestHasher,
RequestHasherInterface $updateAddressRequestHasher,
OrderValidatorInterface $orderValidator
) {
$this->orderFetcher = $orderFetcher;
$this->addressRequestHydrator = $addressRequestHydrator;
$this->orderRequestHydrator = $orderRequestHydrator;
$this->client = $client;
$this->orderRepository = $orderRepository;
$this->translator = $translator;
$this->updateOrderRequestHasher = $updateOrderRequestHasher;
$this->updateAddressRequestHasher = $updateAddressRequestHasher;
$this->orderValidator = $orderValidator;
}
public static function getSubscribedEvents(): array
{
return [
PostWriteValidationEvent::class => 'validateKlarnaOrder',
];
}
/**
* @see \KlarnaPayment\Components\Controller\Administration\OrderUpdateController::update Change accordingly to keep functionality synchronized
*/
public function validateKlarnaOrder(PostWriteValidationEvent $event): void
{
if ($event->getContext()->getScope() === ContextScope::INTERNAL_SCOPE) {
// only check user generated changes
return;
}
if ($event->getContext()->getVersionId() !== Defaults::LIVE_VERSION) {
// No live data change, just draft versions
return;
}
$order = $this->getOrderFromWriteCommands($event);
if ($order === null || !$this->orderValidator->isKlarnaOrder($order)) {
return;
}
$this->setCurrentLocale($event);
$this->validateOrderAddress($order, $event);
$this->validateLineItems($order, $event);
}
private function setCurrentLocale(PostWriteValidationEvent $event): void
{
$languages = $event->getWriteContext()->getLanguages();
$this->currentLocale = $languages[$event->getContext()->getLanguageId()]['code'] ?? 'en-GB';
}
private function validateLineItems(OrderEntity $orderEntity, PostWriteValidationEvent $event): void
{
$request = $this->orderRequestHydrator->hydrate($orderEntity, $event->getContext());
$customFields = $orderEntity->getCustomFields();
$hashVersion = $customFields['klarna_order_cart_hash_version'] ?? AbstractKlarnaPaymentHandler::CART_HASH_DEFAULT_VERSION;
$hash = $this->updateOrderRequestHasher->getHash($request, $hashVersion);
if (!empty($customFields['klarna_order_cart_hash'])) {
$currentHash = $customFields['klarna_order_cart_hash'];
if ($hash === $currentHash) {
if ($hashVersion !== AbstractKlarnaPaymentHandler::CART_HASH_CURRENT_VERSION) {
$this->updateOrderCartHash($request, $orderEntity, $event->getContext());
}
return;
}
}
$response = $this->client->request($request, $event->getContext());
if ($response->getHttpStatus() === 204) {
$this->updateOrderCartHash($request, $orderEntity, $event->getContext());
return;
}
$violation = new ConstraintViolation(
$this->translator->trans('KlarnaPayment.errorMessages.lineItemChangeDeclined', [], null, $this->currentLocale),
'',
[],
'',
'',
''
);
$violations = new ConstraintViolationList([$violation]);
$event->getExceptions()->add(new WriteConstraintViolationException($violations));
}
private function validateOrderAddress(OrderEntity $orderEntity, PostWriteValidationEvent $event): void
{
$request = $this->addressRequestHydrator->hydrate($orderEntity, $event->getContext());
$hash = $this->updateAddressRequestHasher->getHash($request);
if (!empty($orderEntity->getCustomFields()['klarna_order_address_hash'])) {
$currentHash = $orderEntity->getCustomFields()['klarna_order_address_hash'];
if ($hash === $currentHash) {
return;
}
}
$response = $this->client->request($request, $event->getContext());
if ($response->getHttpStatus() === 204) {
$this->saveOrderAddressHash($hash, $orderEntity, $event->getContext());
return;
}
$violation = new ConstraintViolation(
$this->translator->trans('KlarnaPayment.errorMessages.addressChangeDeclined', [], null, $this->currentLocale),
'',
[],
'',
'',
''
);
$violations = new ConstraintViolationList([$violation]);
$event->getExceptions()->add(new WriteConstraintViolationException($violations));
}
private function getOrderFromWriteCommands(PostWriteValidationEvent $event): ?OrderEntity
{
foreach ($event->getCommands() as $command) {
if (!($command instanceof UpdateCommand)) {
continue;
}
$primaryKeys = $command->getPrimaryKey();
if (!array_key_exists('id', $primaryKeys) || empty($primaryKeys['id'])) {
continue;
}
if ($command->getDefinition()->getClass() === OrderAddressDefinition::class) {
return $this->orderFetcher->getOrderFromOrderAddress($primaryKeys['id'], $event->getContext());
}
if ($command->getDefinition()->getClass() === OrderLineItemDefinition::class) {
return $this->orderFetcher->getOrderFromOrderLineItem($primaryKeys['id'], $event->getContext());
}
if ($command->getDefinition()->getClass() === OrderDefinition::class) {
return $this->orderFetcher->getOrderFromOrder($primaryKeys['id'], $event->getContext());
}
}
return null;
}
private function saveOrderAddressHash(string $hash, OrderEntity $orderEntity, Context $context): void
{
$customFields = $orderEntity->getCustomFields();
$customFields['klarna_order_address_hash'] = $hash;
$update = [
'id' => $orderEntity->getId(),
'customFields' => $customFields,
];
$context->scope(ContextScope::INTERNAL_SCOPE, function (Context $context) use ($update): void {
$this->orderRepository->upsert([$update], $context);
});
}
private function updateOrderCartHash(UpdateOrderRequest $request, OrderEntity $orderEntity, Context $context): void
{
$customFields = $orderEntity->getCustomFields();
$newHash = $this->updateOrderRequestHasher->getHash($request, AbstractKlarnaPaymentHandler::CART_HASH_CURRENT_VERSION);
$customFields['klarna_order_cart_hash'] = $newHash;
$customFields['klarna_order_cart_hash_version'] = AbstractKlarnaPaymentHandler::CART_HASH_CURRENT_VERSION;
$update = [
'id' => $orderEntity->getId(),
'customFields' => $customFields,
];
$context->scope(ContextScope::INTERNAL_SCOPE, function (Context $context) use ($update): void {
$this->orderRepository->upsert([$update], $context);
});
}
}