src/App/Subscriber/ManageCreditsSubscriber.php line 29

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. namespace App\Subscriber;
  3. use App\Entity\CreditHistory;
  4. use App\Entity\PaymentVxsOrder;
  5. use App\Entity\User;
  6. use App\Event\CreditsEvent;
  7. use Doctrine\DBAL\Connection;
  8. use Doctrine\Persistence\ManagerRegistry;
  9. use LogicException;
  10. use Psr\Log\LoggerInterface;
  11. use RuntimeException;
  12. use Symfony\Component\EventDispatcher\EventSubscriberInterface;
  13. class ManageCreditsSubscriber implements EventSubscriberInterface
  14. {
  15.     private ManagerRegistry $doctrine;
  16.     private LoggerInterface $logger;
  17.     public function __construct(ManagerRegistry $doctrineLoggerInterface $paymentLogger)
  18.     {
  19.         $this->doctrine $doctrine;
  20.         $this->logger $paymentLogger;
  21.     }
  22.     public function onCredits(CreditsEvent $event): void
  23.     {
  24.         /** @var Connection $connection */
  25.         $connection $this->doctrine->getConnection();
  26.         if (!$connection->isTransactionActive()) {
  27.             throw new RuntimeException('You must start DB transaction in order to use this event');
  28.         }
  29.         if (!($event->getUser() instanceof User)) {
  30.             throw new RuntimeException('The event must have a user.');
  31.         }
  32.         $eventLog 'CreditsEvent: user: ' $event->getUser()->getId();
  33.         $eventLog .= ' action: ' $event->getAction();
  34.         $this->logger->info($eventLog ' start');
  35.         $creditsRelated $event->getRelatedEntity();
  36.         if ($creditsRelated instanceof PaymentVxsOrder) {
  37.             $eventLog .= ' ' CreditsEvent::PROVIDER_VXS;
  38.         }
  39.         if (null !== $event->getBillAt()) {
  40.             $eventLog .= ' bill at: ' $event->getBillAt()->format('Y-m-d H:i:s');
  41.         }
  42.         $credits $event->getCredits();
  43.         if (in_array($event->getAction(), [CreditsEvent::ACTION_CHARGEBACKCreditsEvent::ACTION_REFUND], true)) {
  44.             $credits *= -1;
  45.             $eventLog .= ' deduct';
  46.         }
  47.         $this->manageCredits($event$credits);
  48.         $this->logger->info($eventLog ' end');
  49.         $this->doctrine->getManager()->flush();
  50.     }
  51.     private function manageCredits(CreditsEvent $eventint $creditsChange): void
  52.     {
  53.         if ($creditsChange === 0) {
  54.             return;
  55.         }
  56.         $user $event->getUser();
  57.         if (null === $user || !($user instanceof User)) {
  58.             throw new RuntimeException('User must be present in the credits event.');
  59.         }
  60.         $userCredits $user->getCredits();
  61.         $this->doctrine->getRepository(User::class)->addCredits($user$creditsChange);
  62.         if ($user->getCredits() >= 0) {
  63.             return;
  64.         }
  65.         if (!in_array($event->getAction(), [CreditsEvent::ACTION_CHARGEBACKCreditsEvent::ACTION_REFUND], true)) {
  66.             $this->logger->error('User ' $user->getId() . ' performed a transaction with type "' $event->getAction() . '" that lead to negative balance.');
  67.             throw new LogicException('User will have negative credits after the operation "' $event->getAction() . '" which is not VXS chargeback/refund.');
  68.         }
  69.         $this->logger->warning(
  70.             sprintf(
  71.                 'User with id %s had low credits problem. His credits were %s adding %s',
  72.                 $user->getId(),
  73.                 $userCredits,
  74.                 $creditsChange
  75.             ),
  76.             ['Payment']
  77.         );
  78.         $refundedCredits $this->refundPurchases($user$user->getCredits());
  79.         if (=== $refundedCredits) {
  80.             return;
  81.         }
  82.         $this->doctrine->getRepository(User::class)->addCredits($user$refundedCredits);
  83.     }
  84.     private function refundPurchases(User $userint $missingCredits): int
  85.     {
  86.         $refundedCredits 0;
  87.         /** @var CreditHistory[] $creditCharges */
  88.         $creditCharges = [];
  89.         $page 0;
  90.         while ($missingCredits 0) {
  91.             if (=== count($creditCharges)) {
  92.                 /** @var CreditHistory[] $creditCharges */
  93.                 $creditCharges $this->doctrine->getRepository(CreditHistory::class)
  94.                     // select charges of this user
  95.                     ->filter([
  96.                         'user' => $user->getId(),
  97.                         'operation' => [
  98.                             CreditHistory::OPERATIONS[CreditsEvent::ACTION_DEAL_BID],
  99.                             CreditHistory::OPERATIONS[CreditsEvent::ACTION_PROJECT_BID],
  100.                         ],
  101.                         'refunded' => 0,
  102.                         'credits' => '!=0',
  103.                     ])
  104.                     ->addOrderBy('e.lastModified''DESC')
  105.                     ->addOrderBy('e.id''DESC')
  106.                     ->setFirstResult($page++)
  107.                     ->setMaxResults(40)->getQuery()->getResult();
  108.             }
  109.             $creditCharge array_shift($creditCharges);
  110.             if (null === $creditCharge) {
  111.                 // There are no credit charges to refund.
  112.                 // Then there is the only way: remove all missing from an account.
  113.                 return -$missingCredits;
  114.             }
  115.             $proposalBid $creditCharge->getProjectBid() ?? $creditCharge->getDealBid();
  116.             if (null === $proposalBid || $proposalBid->isCanceled() || $proposalBid->isAccessed()) {
  117.                 continue;
  118.             }
  119.             // charge is the negative number
  120.             $missingCredits -= $creditCharge->getCredits();
  121.             $refundedCredits += -$creditCharge->getCredits();
  122.             $creditCharge->setRefunded(true);
  123.             $proposalBid->setRefunded(true);
  124.         }
  125.         return $refundedCredits;
  126.     }
  127.     /**
  128.      * @return array<string, array<int|string, array<int|string, int|string>|int|string>|string>
  129.      */
  130.     public static function getSubscribedEvents(): array
  131.     {
  132.         return [
  133.             CreditsEvent::class => 'onCredits',
  134.         ];
  135.     }
  136. }