<?php
namespace App\EventSubscribers\Main;
use App\Entity\Main\Cart;
use App\Entity\Main\RebillManager;
use App\Entity\Main\Transaction;
use App\Events\Main\Cart\CartChargedBackEvent;
use App\Events\Main\Cart\CartRefundedEvent;
use App\Events\Main\Coaching\CoachingCancelledEvent;
use App\Events\Main\Order\OrderPaidByClientEvent;
use App\Events\Main\Paypal\PaypalCaptureCallbackReceivedEvent;
use App\Events\Main\Paypal\PaypalRefundCallbackReceivedEvent;
use App\Events\Main\Paypal\PaypalReverseCallbackReceivedEvent;
use App\Events\Main\User\UserMadeChargebackEvent;
use App\Events\Main\User\UserMadeRefundEvent;
use App\Exceptions\Main\Paypal\PaypalCartMismatchException;
use App\Exceptions\Main\Paypal\PaypalCartNotFoundException;
use App\Exceptions\Main\Paypal\PaypalOrderNotCompletelyPaidException;
use App\Exceptions\Main\Paypal\PaypalOrderNotFoundException;
use App\Exceptions\Main\Paypal\PaypalTransactionNotFoundException;
use App\Repository\Main\TransactionRepository;
use App\Services\BillingManager;
use App\Services\CartManager;
use App\Services\EmailManager;
use App\Services\PayPalManager;
use App\Services\ReferralProgramManager;
use App\Tools\Paypal\PaypalOrder;
use App\Tools\ShortId;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ObjectRepository;
use Psr\Log\LoggerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Class PaypalSubscriber
* @package App\EventSubscribers\Main
*/
class PaypalSubscriber implements EventSubscriberInterface
{
const
PAYPAL_REFUND_CALLBACK = 'paypal.callback.refund',
PAYPAL_REVERSE_CALLBACK = 'paypal.callback.reverse',
PAYPAL_CAPTURE_CALLBACK = 'paypal.callback.capture'
;
/**
* @var EntityManagerInterface
*/
private $entityManager;
/**
* @var TransactionRepository|ObjectRepository
*/
private $transactionRepository;
/**
* @var LoggerInterface
*/
private $logger;
/**
* @var ShortId
*/
private $shortId;
/**
* @var BillingManager
*/
private $billingFactory;
/**
* @var CartManager
*/
private $cartFactory;
/**
* @var PayPalManager
*/
private $payPalManager;
/**
* @var EventDispatcherInterface
*/
private $eventDispatcher;
/**
* @var EmailManager
*/
private $emailFactory;
/**
* @var ReferralProgramManager
*/
private $referralProgramManager;
private $transactionDescriptor;
/**
* PaypalSubscriber constructor.
* @param EntityManagerInterface $entityManager
* @param LoggerInterface $logger
* @param ShortId $shortId
* @param CartManager $cartFactory
* @param BillingManager $billingFactory
* @param PayPalManager $payPalManager
* @param EventDispatcherInterface $eventDispatcher
* @param EmailManager $emailFactory
* @param $transactionDescriptor
*/
public function __construct(
EntityManagerInterface $entityManager,
LoggerInterface $logger,
ShortId $shortId,
CartManager $cartFactory,
BillingManager $billingFactory,
PayPalManager $payPalManager,
EventDispatcherInterface $eventDispatcher,
EmailManager $emailFactory,
$transactionDescriptor
) {
$this->entityManager = $entityManager;
$this->transactionRepository = $entityManager->getRepository(Transaction::class);
$this->logger = $logger;
$this->shortId = $shortId;
$this->cartFactory = $cartFactory;
$this->billingFactory = $billingFactory;
$this->payPalManager = $payPalManager;
$this->eventDispatcher = $eventDispatcher;
$this->emailFactory = $emailFactory;
$this->transactionDescriptor = $transactionDescriptor;
}
/**
* @return array
*/
public static function getSubscribedEvents()
{
return array(
self::PAYPAL_CAPTURE_CALLBACK => "paypalCallbackCapture",
self::PAYPAL_REFUND_CALLBACK => "paypalCallbackRefund",
self::PAYPAL_REVERSE_CALLBACK => "paypalCallbackReverse"
);
}
/**
* @param PaypalCaptureCallbackReceivedEvent $event
* @throws \Exception
*/
public function paypalCallbackCapture(PaypalCaptureCallbackReceivedEvent $event)
{
// check instantly if transaction exists without to query paypal api.
$transaction = $this->transactionRepository->getTransactionForPaypalId($event->getRawEventData()['resource']['id']);
// if transaction is here and with OK status, then we can ignore this callBack and give paypal the response.
if ($transaction instanceof Transaction && $transaction->isSuccessful()) {
return;
}
// Ok the transaction doesn't exists, validate it with their api and load all data.
$paypalCapture = $this->payPalManager->getPaypalCapture($event->getRawEventData()['resource']['id']);
$paypalOrder = $paypalCapture->getOrder();
// If we don't have an order for this event, we can't handle it
if (!$paypalOrder instanceof PaypalOrder || !$paypalOrder->isInitialised() || $paypalOrder->getApiError()) {
// failed to get the order, we can't do anything with this notification.
$this->logger->critical("Receiving paypal callback that can't be exploited (no corresponding order. Data : " . print_r($event->getRawEventData(), true));
throw new PaypalOrderNotFoundException("Couldn't find paypal order for capture. Data : {$paypalCapture->getId()}");
}
// avoid concurrency problems with customer checkout if transaction is not found
sleep(2);
// After waiting we recheck
$transaction = $this->transactionRepository->getTransactionForPaypalId($paypalCapture->getId());
// if transaction is here and with OK status, then we can ignore this callBack
if ($transaction instanceof Transaction && $transaction->isSuccessful()) {
return;
}
// get the cart from the order
$cartId = $this->shortId->decode($paypalOrder->getCustomOrderId());
$cart = $this->entityManager->getRepository(Cart::class)->findOneByCartId($cartId);
if (!$cart instanceof Cart) {
// We have a transaction that we don't know to which cart it applies
$this->logger->critical("Receiving paypal callback that can't be exploited (no corresponding cart. Data : " . print_r($event->getRawEventData(), true));
throw new PaypalCartNotFoundException("Couldn't find the cart for a paypal order. Data : {$paypalCapture->getId()}");
}
// Transaction doesn't exist create it.
if (!$transaction instanceof Transaction) {
$this->billingFactory->setCartFactory($this->cartFactory);
$this->cartFactory->transitState($cart, 'paymentReady');
$this->billingFactory->processPaypalPurchase($cart, null, $paypalOrder);
return;
}
// transaction exists but is was marked as failed, we need to update that.
if ($paypalOrder->getEarnedAmount() != ($cart->getCartTotalAmount() / 100) ||
$paypalOrder->getOrderCurrency() != $cart->getCartCurrency()
) {
$this->logger->critical("Error in Paypal transaction return - Amount paid is not correct or currency is wrong. Paypal Order : {$paypalOrder->getId()}, cartId : {$cart->getCartId()}, return result : " . print_r($paypalOrder->getRawData() . " paypal currency : {$paypalOrder->getOrderCurrency()}, cart currency : {$cart->getCartCurrency()}", true));
throw new PaypalOrderNotCompletelyPaidException("Receiving callback info for a capture, but amount is not sufficient to pay the cart or currency is wrong. Data : {$paypalCapture->getId()}");
}
// Update the payment status and put the cart as paid.
$this->billingFactory->setCartFactory($this->cartFactory);
$this->cartFactory->transitState($cart, 'paymentReady');
$this->billingFactory->updateTransactionStatusToPaid($transaction, " -> Paypal Callback confirming capture -> {$paypalCapture->getId()}");
// dispatch an order paid event
$event = new OrderPaidByClientEvent($cart);
$this->eventDispatcher->dispatch($event, OrderSubscriber::ORDER_PAID_BY_CLIENT);
}
/**
* @param PaypalRefundCallbackReceivedEvent $event
* @return bool
* @throws \Exception
*/
public function paypalCallbackRefund(PaypalRefundCallbackReceivedEvent $event)
{
// check instantly if transaction exists without to query paypal api (this means double callback).
$refundTransaction = $this->transactionRepository->getTransactionForPaypalId($event->getRawEventData()['resource']['id']);
// if transaction is here and with OK status, then we can ignore this callBack and give paypal the response.
if ($refundTransaction instanceof Transaction) {
return;
}
// Ok get the refund confirmation from payapal api and the paypal order info.
$paypalRefund = $this->payPalManager->getPaypalRefund($event->getRawEventData()['resource']['id']);
$paypalOrder = $paypalRefund->getOrder();
$cartId = $this->shortId->decode($paypalOrder->getCustomOrderId());
// Now, get the original transaction
$originalTransaction = $this->transactionRepository->getTransactionForPaypalId($paypalRefund->getCapture()->getId());
if (!$originalTransaction instanceof Transaction) {
$this->logger->critical("Unable to find original transaction for a refund : original paypal transaction {$paypalRefund->getCapture()->getId()}, refund id {$paypalRefund->getId()}, data : " . print_r($event->getRawEventData(), true));
throw new PaypalTransactionNotFoundException("Unable to find original transaction for a refund : original paypal transaction {$paypalRefund->getCapture()->getId()}, refund id {$paypalRefund->getId()}");
}
if ($originalTransaction->getCart()->getCartId() != $cartId) {
$this->logger->critical("Receiving a refund for a non corresponding cart : original paypal transaction {$paypalRefund->getCapture()->getId()}, refund id {$paypalRefund->getId()}, cart : {$cartId}, data : " . print_r($event->getRawEventData(), true));
throw new PaypalCartMismatchException("Receiving a refund for a non corresponding cart : original paypal transaction {$paypalRefund->getCapture()->getId()}, refund id {$paypalRefund->getId()}");
}
// Ok, create refund transaction
$t = new Transaction(
$originalTransaction->getCart(),
$originalTransaction->getBillingAccount(),
$paypalRefund->getTotalValue() * 100,
"Paypal Refund (callback) : {$paypalRefund->getId()}",
$originalTransaction->getUserPaymentToken(),
$paypalRefund->getCurrency(),
$originalTransaction->getTransactionIp(),
$originalTransaction->getTransactionUseragent()
);
$rebillManager = $originalTransaction->getRebillManager();
if (!is_null($rebillManager)) {
$t->setRebillManager($rebillManager);
}
$t->setTransactionStamp($paypalRefund->getTransactionStamp());
$t->setTransactionType(Transaction::REFUND);
$t->setTransactionStatus(Transaction::OK);
$t->setTransactionReason('Transaction refunded by paypal callback');
$t->setTransactionMerkavId(0);
$t->setTransactionAquirerId($paypalRefund->getId());
$t->setTransactionAquirerReason("paypal order : {$paypalOrder->getId()}, paypal capture : {$paypalRefund->getCapture()->getId()}");
$t->setTransactionBankDescriptor($this->transactionDescriptor);
$t->setTransactionIp($originalTransaction->getTransactionIp());
$t->setTransactionUserAgent($originalTransaction->getTransactionUserAgent());
$t->setParent($originalTransaction);
// If the total order was refunded, this cart should be cancelled.
if ($t->getTransactionAmount() == $originalTransaction->getTransactionAmount()) {
$cart = $originalTransaction->getCart();
$cart->setCartState(Cart::STATE_CANCELLED);
$this->emailFactory->sendRefundMail($originalTransaction);
} else {
$this->emailFactory->sendPartialRefundEmail($originalTransaction);
}
$this->entityManager->persist($t);
$this->entityManager->flush();
$event = new UserMadeRefundEvent($originalTransaction->getCart()->getUser());
$this->eventDispatcher->dispatch($event, UserSubscriber::USER_MAKE_REFUND);
$event = new CartRefundedEvent($originalTransaction->getCart());
$this->eventDispatcher->dispatch($event, CartSubscriber::CART_REFUND);
return true;
}
/**
* @param PaypalReverseCallbackReceivedEvent $event
* @throws \Exception
*/
public function paypalCallbackReverse(PaypalReverseCallbackReceivedEvent $event)
{
// check instantly if transaction exists without to query paypal api (this means double callback).
$chargebackedTransaction = $this->transactionRepository->getTransactionForPaypalId($event->getRawEventData()['resource']['id']);
// if transaction is here and with OK status, then we can ignore this callBack and give paypal the response.
if ($chargebackedTransaction instanceof Transaction) {
return;
}
// Ok get the refund confirmation from payapal api and the paypal order info.
$paypalChargeback = $this->payPalManager->getPaypalRefund($event->getRawEventData()['resource']['id']);
$paypalOrder = $paypalChargeback->getOrder();
$cartId = $this->shortId->decode($paypalOrder->getCustomOrderId());
// Now, get the original transaction
$originalTransaction = $this->transactionRepository->getTransactionForPaypalId($paypalChargeback->getCapture()->getId());
if (!$originalTransaction instanceof Transaction) {
$this->logger->critical("Unable to find original transaction for a refund : original paypal transaction {$paypalChargeback->getCapture()->getId()}, refund id {$paypalChargeback->getId()}, data : " . print_r($event->getRawEventData(), true));
throw new PaypalTransactionNotFoundException("Unable to find original transaction for a refund : original paypal transaction {$paypalChargeback->getCapture()->getId()}, refund id {$paypalChargeback->getId()}");
}
if ($originalTransaction->getCart()->getCartId() != $cartId) {
$this->logger->critical("Receiving a refund for a non corresponding cart : original paypal transaction {$paypalChargeback->getCapture()->getId()}, refund id {$paypalChargeback->getId()}, cart : {$cartId}, data : " . print_r($event->getRawEventData(), true));
throw new PaypalCartMismatchException("Receiving a refund for a non corresponding cart : original paypal transaction {$paypalChargeback->getCapture()->getId()}, refund id {$paypalChargeback->getId()}");
}
$chargebackTransaction = clone $originalTransaction;
$chargebackTransaction->setTransactionId(null);
$chargebackTransaction->setTransactionType(Transaction::CHARGEBACK);
$chargebackTransaction->setTransactionStamp($paypalChargeback->getTransactionStamp());
$chargebackTransaction->setTransactionMerkavId($paypalChargeback->getId());
$chargebackTransaction->setParent($originalTransaction);
$chargebackTransaction->setTransactionDescription("Paypal Chargeback (callback) : {$paypalChargeback->getId()}");
$this->entityManager->persist($chargebackTransaction);
$this->entityManager->flush();
// Cut the rebill if there is one:
/** @var Cart $cart */
$cart = $originalTransaction->getCart();
$rebillManagerList = $this->entityManager->getRepository(RebillManager::class)
->findBy(['cart' => $cart]);
foreach ($rebillManagerList as $rebillManager) {
$this->billingFactory->stopRebillManager($rebillManager, RebillManager::CANCEL_BY_CHARGEBACK);
}
$event = new CoachingCancelledEvent($cart->getUser(), $cart, $originalTransaction);
$this->eventDispatcher->dispatch($event, CoachingSubscriber::COACHING_CANCELLED);
$event = new UserMadeChargebackEvent($originalTransaction->getCart()->getUser());
$this->eventDispatcher->dispatch($event, UserSubscriber::USER_MAKE_CHARGEBACK);
$event = new CartChargedBackEvent($originalTransaction->getCart());
$this->eventDispatcher->dispatch($event, CartSubscriber::CART_CHARGEBACK);
}
}