<?php
declare(strict_types=1);
namespace App\Subscriber\Payment;
use App\Action\Query\Order\GetBasketStatus\GetBasketStatusQuery;
use App\Event\Cinema\CinemaContextOnDemandEvent;
use App\Event\Order\OrderPaymentTransactionHandledEvent;
use App\Event\Payment\WebhookNotificationEvent;
use App\Exception\Order\BasketClosedException;
use App\Exception\Order\BasketDoesNotExistException;
use App\Exception\Order\BasketOrderLockedException;
use App\Manager\OrderManager;
use App\Model\Order\BasketStatus;
use App\Model\Order\Order;
use App\Payment\LockablePaymentClientInterface;
use App\Payment\Model\Response as PaymentProviderResponse;
use App\Payment\PaymentFactory;
use App\Service\Order\OrderLockedException;
use App\Service\Order\OrderLockServiceInterface;
use App\Service\Payment\PaymentProviderService;
use App\Tool\Uuid\UuidBuilder;
use Psr\Log\LoggerInterface;
use Ramsey\Uuid\Uuid;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Stamp\HandledStamp;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class WebhookSubscriber implements EventSubscriberInterface
{
private PaymentFactory $paymentFactory;
private PaymentProviderService $paymentProviderService;
private EventDispatcherInterface $eventDispatcher;
private MessageBusInterface $messageBus;
private OrderManager $orderManager;
private OrderLockServiceInterface $lockService;
private LoggerInterface $orderLockLogger;
public function __construct(
PaymentFactory $paymentFactory,
PaymentProviderService $paymentProviderService,
EventDispatcherInterface $eventDispatcher,
MessageBusInterface $messageBus,
OrderManager $orderManager,
OrderLockServiceInterface $lockService,
LoggerInterface $orderLockLogger
)
{
$this->paymentFactory = $paymentFactory;
$this->paymentProviderService = $paymentProviderService;
$this->eventDispatcher = $eventDispatcher;
$this->messageBus = $messageBus;
$this->orderManager = $orderManager;
$this->lockService = $lockService;
$this->orderLockLogger = $orderLockLogger;
}
public static function getSubscribedEvents(): array
{
return [
WebhookNotificationEvent::class => 'onNotification'
];
}
public function onNotification(WebhookNotificationEvent $event): void
{
$client = $this->paymentFactory->get(
$this->paymentProviderService->getPaymentProviderServiceIdentifier($event->getPaymentProvider())
);
if($client instanceof LockablePaymentClientInterface) {
$sessionId = $client->getSessionId($event->getRequest());
if($sessionId !== null) {
try {
$this->processOrderLock($sessionId);
} catch (BasketClosedException) {
return;
} catch (OrderLockedException $e) {
throw $e;
} catch (\Throwable) {}
}
}
$paymentProviderResponse = $client->dispatch($event->getRequest());
if ($paymentProviderResponse !== null) {
$this->eventDispatcher->dispatch(
new CinemaContextOnDemandEvent($paymentProviderResponse->getCinemaId())
);
$order = $this->orderManager->get(Uuid::fromString($paymentProviderResponse->getId()));
$client->flushLogger();
} else {
return;
}
if ($order === null) {
throw new BasketDoesNotExistException();
}
if ($paymentProviderResponse->getStatus() !== PaymentProviderResponse::PENDING_TRANSACTION) {
if ($this->orderManager->isOrderLocked(Uuid::fromString($paymentProviderResponse->getId()))) {
throw new BasketOrderLockedException();
}
$this->orderManager->lockOrder(Uuid::fromString($paymentProviderResponse->getId()));
}
try {
$this->orderManager->handlePaymentProviderResponse(
$paymentProviderResponse,
$order,
$client,
$event->getPaymentChannel(),
['isProcessingExpired' => false, 'isProcessingAllowed' => true],
);
} catch (\Exception $exception) {
throw $exception;
} finally {
$this->eventDispatcher->dispatch(
new OrderPaymentTransactionHandledEvent($order, $paymentProviderResponse,
$client, $event->getPaymentChannel()
)
);
if($client instanceof LockablePaymentClientInterface) {
$this->lockService->unlock(
Uuid::fromString($paymentProviderResponse->getId()),
Uuid::fromString($paymentProviderResponse->getCinemaId())
);
}
}
}
/**
* Locks the order if it isn't closed yet.
*/
private function processOrderLock(array $sessionId): void
{
$orderId = UuidBuilder::build($sessionId['merchantTransactionId']);
$cinemaId = UuidBuilder::build($sessionId['cinemaId']);
if($cinemaId !== null && $orderId !== null) {
$this->eventDispatcher->dispatch(new CinemaContextOnDemandEvent($cinemaId->toString()));
/** @var BasketStatus $basketStatus */
$basketStatus = $this->messageBus->dispatch(new GetBasketStatusQuery($orderId->toString()))
->last(HandledStamp::class)->getResult();
if($basketStatus->getStatus() === Order::CLOSED) {
$context = [
'cinemaId' => $cinemaId->toString(),
'orderId' => $orderId->toString(),
'basketStatus' => $basketStatus->getStatus(),
'channel' => 'webhook'
];
$this->orderLockLogger->error('Webhook received when basket is closed',$context);
throw new BasketClosedException();
}
$this->lockService->lock($orderId, $cinemaId, 'webhook');
}
}
}