diff --git a/app/Jobs/ProcessPaymentGatewayRefundJob.php b/app/Jobs/ProcessPaymentGatewayRefundJob.php new file mode 100644 index 000000000..ebef5e11b --- /dev/null +++ b/app/Jobs/ProcessPaymentGatewayRefundJob.php @@ -0,0 +1,77 @@ +request_id = $request->getId(); + $this->order_id = $order->getId(); + } + + /** + * @param ISummitOrderService $service + * @return void + */ + public function handle(ISummitOrderService $service):void + { + Log::debug + ( + sprintf + ( + "ProcessPaymentGatewayRefundJob::handle order id %s request id %s", + $this->order_id, + $this->request_id + ) + ); + + $service->processPaymentGatewayRefundRequest($this->order_id, $this->request_id); + } + + public function failed(\Throwable $exception) + { + Log::error($exception); + } + +} \ No newline at end of file diff --git a/app/Services/Model/ISummitOrderService.php b/app/Services/Model/ISummitOrderService.php index 99c6bc7a0..c16b67385 100644 --- a/app/Services/Model/ISummitOrderService.php +++ b/app/Services/Model/ISummitOrderService.php @@ -436,4 +436,11 @@ public function delegateTicket(Summit $summit, int $order_id, int $ticket_id,Mem * @return void */ public function ingestPaymentInfoForRegistrationOrders():void; + + /** + * @param int $order_id + * @param int $request_id + * @return void + */ + public function processPaymentGatewayRefundRequest(int $order_id, int $request_id):void; } \ No newline at end of file diff --git a/app/Services/Model/Imp/SummitOrderService.php b/app/Services/Model/Imp/SummitOrderService.php index 6841ace0c..48f05f3e3 100644 --- a/app/Services/Model/Imp/SummitOrderService.php +++ b/app/Services/Model/Imp/SummitOrderService.php @@ -22,14 +22,17 @@ use App\Jobs\Emails\Registration\Reminders\SummitTicketReminderEmail; use App\Jobs\Emails\UnregisteredMemberOrderPaidMail; use App\Jobs\IngestSummitExternalRegistrationData; +use App\Jobs\ProcessPaymentGatewayRefundJob; use App\Jobs\ProcessTicketDataImport; use App\Jobs\SendAttendeeInvitationEmail; +use App\Jobs\Utils\JobDispatcher; use App\Models\Foundation\Summit\Factories\SummitOrderFactory; use App\Models\Foundation\Summit\Registration\IBuildDefaultPaymentGatewayProfileStrategy; use App\Models\Foundation\Summit\Registration\PromoCodes\PromoCodesUtils; use App\Models\Foundation\Summit\Repositories\ISummitAttendeeBadgePrintRuleRepository; use App\Models\Foundation\Summit\Repositories\ISummitAttendeeBadgeRepository; use App\Models\Foundation\Summit\Repositories\ISummitOrderRepository; +use App\Models\Foundation\Summit\Repositories\ISummitRefundRequestRepository; use App\Services\FileSystem\IFileDownloadStrategy; use App\Services\FileSystem\IFileUploadStrategy; use App\Services\Model\dto\ExternalUserDTO; @@ -63,6 +66,7 @@ use models\summit\SummitAttendee; use models\summit\SummitAttendeeBadge; use models\summit\SummitAttendeeTicket; +use models\summit\SummitAttendeeTicketRefundRequest; use models\summit\SummitBadgeType; use models\summit\SummitBadgeViewType; use models\summit\SummitOrder; @@ -1537,6 +1541,11 @@ final class SummitOrderService */ private $tags_repository; + /** + * @var ISummitRefundRequestRepository + */ + private $refund_request_repository; + /** * @param ISummitTicketTypeRepository $ticket_type_repository * @param IMemberRepository $member_repository @@ -1553,6 +1562,7 @@ final class SummitOrderService * @param IFileDownloadStrategy $download_strategy * @param ICompanyRepository $company_repository * @param ITagRepository $tags_repository + * @param ISummitRefundRequestRepository $refund_request_repository * @param ICompanyService $company_service * @param ITicketFinderStrategyFactory $ticket_finder_strategy_factory * @param ITransactionService $tx_service @@ -1575,6 +1585,7 @@ public function __construct IFileDownloadStrategy $download_strategy, ICompanyRepository $company_repository, ITagRepository $tags_repository, + ISummitRefundRequestRepository $refund_request_repository, ICompanyService $company_service, ITicketFinderStrategyFactory $ticket_finder_strategy_factory, ITransactionService $tx_service, @@ -1600,6 +1611,7 @@ public function __construct $this->company_service = $company_service; $this->ticket_finder_strategy_factory = $ticket_finder_strategy_factory; $this->tags_repository = $tags_repository; + $this->refund_request_repository = $refund_request_repository; } /** @@ -1853,11 +1865,11 @@ public function revokeTicket(Member $current_user, int $order_id, int $ticket_id throw new ValidationException("Ticket can not be revoked due badge its already printed."); } - if(!$ticket->getTicketType()->isAllowsToReassignRelatedTickets()){ + if (!$ticket->getTicketType()->isAllowsToReassignRelatedTickets()) { throw new ValidationException("You cannot reassign this ticket. Please contact support."); } - if($ticket->hasPromoCode() && !$ticket->getPromoCode()->isAllowsToReassign()){ + if ($ticket->hasPromoCode() && !$ticket->getPromoCode()->isAllowsToReassign()) { throw new ValidationException("You cannot reassign this ticket. Please contact support."); } @@ -1950,12 +1962,11 @@ public function ownerAssignTicket(Member $current_user, int $order_id, int $tick $member = $this->member_repository->getByEmail($email); $attendee = $summit->getAttendeeByEmail($email); - if(is_null($member)){ + if (is_null($member)) { $user = $this->member_service->checkExternalUser($email); //If user exists => update it - if (!is_null($user)) - { + if (!is_null($user)) { $member = $this->member_service->registerExternalUserByPayload($user); } } @@ -2027,9 +2038,9 @@ public function ownerAssignTicket(Member $current_user, int $order_id, int $tick $ticket->generateHash(); $attendee->updateStatus(); $member_company = !is_null($member) ? $member->getCompany() : null; - if(!$attendee->hasCompany() && !empty($member_company)){ + if (!$attendee->hasCompany() && !empty($member_company)) { $company = $this->company_repository->getByName($member_company); - if(!is_null($company)){ + if (!is_null($company)) { $attendee->setCompany($company); } else { $company = $this->company_service->addCompany(['name' => $member_company]); @@ -2329,16 +2340,26 @@ public function requestRefundTicket(Member $current_user, int $order_id, int $ti /** * @param Summit $summit * @param Member $currentUser - * @param int|string $ticket_id + * @param $ticket_id * @param float $amount_2_refund * @param string|null $notes * @return SummitAttendeeTicket - * @throws EntityNotFoundException - * @throws ValidationException + * @throws \Throwable */ public function refundTicket(Summit $summit, Member $currentUser, $ticket_id, float $amount_2_refund, ?string $notes): SummitAttendeeTicket { - return $this->tx_service->transaction(function () use ($summit, $currentUser, $ticket_id, $amount_2_refund, $notes) { + $res = $this->tx_service->transaction(function () use ($summit, $currentUser, $ticket_id, $amount_2_refund, $notes) { + + Log::debug + ( + sprintf + ( + "SummitOrderService::refundTicket summit %s current user %s amount_2_refund %s", + $summit->getId(), + $currentUser->getId(), + $amount_2_refund + ) + ); $ticket = $this->ticket_repository->getByIdExclusiveLock(intval($ticket_id)); @@ -2368,11 +2389,55 @@ public function refundTicket(Summit $summit, Member $currentUser, $ticket_id, fl if ($order->getSummitId() != $summit->getId()) throw new EntityNotFoundException('Ticket not found.'); - $paymentGatewayRes = null; - $request = $ticket->refund($currentUser, $amount_2_refund, $paymentGatewayRes, $notes); + $request = $ticket->refund($currentUser, $amount_2_refund, null, $notes); + + return [$ticket, $order, $request]; + }); + + list($ticket, $order, $request) = $res; + + $job = new ProcessPaymentGatewayRefundJob($order, $request); + JobDispatcher::withDbFallback( + job: $job, + logContext: ['order_id' => $order->getId(), 'ticket_id' => $ticket->getId()] + ); + return $ticket; + } + + /** + * @param int $order_id + * @param int $request_id + * @return void + * @throws \Exception + */ + public function processPaymentGatewayRefundRequest(int $order_id, int $request_id): void + { + + Log::debug + ( + sprintf + ( + "SummitOrderService::processPaymentGatewayRefundRequest order id %s request id %s", + $order_id, + $request_id + ) + ); + + $this->tx_service->transaction(function () use ($order_id, $request_id) { + + $order = $this->order_repository->getById($order_id); + if (!$order instanceof SummitOrder) + throw new EntityNotFoundException('Order not found.'); + + $request = $this->refund_request_repository->getByIdExclusiveLock(intval($request_id)); + if (!$request instanceof SummitAttendeeTicketRefundRequest) + throw new EntityNotFoundException('Refund Request not found.'); if ($order->hasPaymentInfo()) { + $ticket = $request->getTicket(); + $summit = $order->getSummit(); + try { $payment_gateway = $summit->getPaymentGateWayPerApp ( @@ -2388,7 +2453,7 @@ public function refundTicket(Summit $summit, Member $currentUser, $ticket_id, fl ( sprintf ( - "SummitOrderService::refundTicket trying to refund on payment gateway cart id %s final amount %s", + "SummitOrderService::processPaymentGatewayRefundRequest trying to refund on payment gateway cart id %s final amount %s", $order->getPaymentGatewayCartId(), $request->getTotalRefundedAmount() ) @@ -2405,7 +2470,7 @@ public function refundTicket(Summit $summit, Member $currentUser, $ticket_id, fl ( sprintf ( - "SummitOrderService::refundTicket refunded payment gateway cart id %s payment gateway response %s", + "SummitOrderService::processPaymentGatewayRefundRequest refunded payment gateway cart id %s payment gateway response %s", $order->getPaymentGatewayCartId(), $paymentGatewayRes ) @@ -2418,11 +2483,37 @@ public function refundTicket(Summit $summit, Member $currentUser, $ticket_id, fl throw new ValidationException($ex->getMessage()); } } - - return $ticket; }); } + /** + * @param Summit $summit + * @param $ticket_id + * @return SummitAttendeeTicket|null + * @throws \Exception + */ + public function getTicket(Summit $summit, $ticket_id): ?SummitAttendeeTicket + { + + Log::debug(sprintf("SummitOrderService::getTicket summit %s ticket id %s", $summit->getId(), $ticket_id)); + + $strategy = $this->ticket_finder_strategy_factory->build($summit, $ticket_id); + if (is_null($strategy)) + throw new EntityNotFoundException("Ticket not found."); + + $ticket = $strategy->find(); + + if (!$ticket instanceof SummitAttendeeTicket) + throw new EntityNotFoundException("Ticket not found."); + + if ($ticket->getOrder()->getSummitId() != $summit->getId()) { + throw new ValidationException("Ticket does not belong to summit."); + } + + return $ticket; + + } + /** * @param Summit $summit * @param string $order_hash @@ -2589,7 +2680,7 @@ public function confirmOrdersOlderThanNMinutes(int $minutes, int $max = 100): vo ) ); - if($payment_gateway->isSucceeded($status)) { + if ($payment_gateway->isSucceeded($status)) { Log::debug ( sprintf @@ -2602,8 +2693,7 @@ public function confirmOrdersOlderThanNMinutes(int $minutes, int $max = 100): vo $order->setPaid($payment_gateway->getPaymentDetailsInfo($cart_id)); // invoke now to avoid delays $this->processInvitation($order); - } - else if($payment_gateway->isDeclined($status)) { + } else if ($payment_gateway->isDeclined($status)) { Log::warning ( sprintf @@ -3207,34 +3297,6 @@ private function checkPrintingRights(Member $requestor, SummitAttendeeBadge $bad return true; } - /** - * @param Summit $summit - * @param $ticket_id - * @return SummitAttendeeTicket|null - * @throws \Exception - */ - public function getTicket(Summit $summit, $ticket_id): ?SummitAttendeeTicket - { - - Log::debug(sprintf("SummitOrderService::getTicket summit %s ticket id %s", $summit->getId(), $ticket_id)); - - $strategy = $this->ticket_finder_strategy_factory->build($summit, $ticket_id); - if (is_null($strategy)) - throw new EntityNotFoundException("Ticket not found."); - - $ticket = $strategy->find(); - - if (!$ticket instanceof SummitAttendeeTicket) - throw new EntityNotFoundException("Ticket not found."); - - if ($ticket->getOrder()->getSummitId() != $summit->getId()) { - throw new ValidationException("Ticket does not belong to summit."); - } - - return $ticket; - - } - /** * @param Summit $summit * @param int|string $ticket_id @@ -3934,7 +3996,7 @@ public function importTicketData(Summit $summit, UploadedFile $csv_file): void { Log::debug(sprintf("SummitOrderService::importTicketData - summit %s", $summit->getId())); - $allowed_extensions = ['txt','csv']; + $allowed_extensions = ['txt', 'csv']; if (!in_array($csv_file->extension(), $allowed_extensions)) { throw new ValidationException("file does not has a valid extension ('csv')."); @@ -4177,7 +4239,7 @@ public function processTicketData(int $summit_id, string $filename) if ($reader->hasColumn('promo_code_id')) { Log::debug(sprintf("SummitOrderService::processTicketData trying to get promo code by id %s", $row['promo_code_id'])); $promo_code = $this->promo_code_repository->getById(intval($row['promo_code_id'])); - if($promo_code instanceof SummitRegistrationPromoCode && $promo_code->getSummitId() != $summit->getId()){ + if ($promo_code instanceof SummitRegistrationPromoCode && $promo_code->getSummitId() != $summit->getId()) { Log::debug ( sprintf @@ -4200,7 +4262,7 @@ public function processTicketData(int $summit_id, string $filename) if (is_null($ticket_type) && $reader->hasColumn('ticket_type_id')) { Log::debug(sprintf("SummitOrderService::processTicketData trying to get ticket type by id %s", $row['ticket_type_id'])); $ticket_type = $this->ticket_type_repository->getById(intval($row['ticket_type_id'])); - if($ticket_type instanceof SummitTicketType && $ticket_type->getSummitId() != $summit->getId()){ + if ($ticket_type instanceof SummitTicketType && $ticket_type->getSummitId() != $summit->getId()) { Log::debug( sprintf ( @@ -4549,8 +4611,8 @@ public function processAllOrderReminder(): void /** * @param Summit $summit - * @throws \Exception * @return array + * @throws \Exception */ public function processSummitOrderReminders(Summit $summit): array { @@ -4559,7 +4621,7 @@ public function processSummitOrderReminders(Summit $summit): array if ($summit->isEnded()) { Log::warning(sprintf("SummitOrderService::processSummitOrderReminders - summit %s has ended already", $summit->getId())); - return [0,0]; + return [0, 0]; } $page = 1; @@ -4578,7 +4640,7 @@ public function processSummitOrderReminders(Summit $summit): array Log::debug(sprintf("SummitOrderService::processSummitOrderReminders processing summit %s order %s", $summit->getId(), $order_id)); try { - if($this->processOrderReminder($order_id)) + if ($this->processOrderReminder($order_id)) ++$qty_order_sent; } catch (\Exception $ex) { Log::error($ex); @@ -4604,7 +4666,7 @@ public function processSummitOrderReminders(Summit $summit): array foreach ($tickets_ids as $ticket_id) { try { - if($this->processTicketReminder($ticket_id)) + if ($this->processTicketReminder($ticket_id)) ++$qty_ticket_sent; } catch (\Exception $ex) { Log::error($ex); @@ -4637,8 +4699,8 @@ public function processSummitOrderReminders(Summit $summit): array /** * @param int $order_id - * @throws \Exception * @return bool + * @throws \Exception */ public function processOrderReminder(int $order_id): bool { @@ -4647,8 +4709,8 @@ public function processOrderReminder(int $order_id): bool Log::debug(sprintf("SummitOrderService::processOrderReminder order %s", $order_id)); $order = $this->order_repository->getById($order_id); - if(!$order instanceof SummitOrder) { - return null; + if (!$order instanceof SummitOrder) { + return null; } $order_tickets = $order->getTickets(); //specific case check: don't send order reminder if there is one ticket per order and is the same owner @@ -4756,7 +4818,7 @@ public function processOrderReminder(int $order_id): bool return null; }); - if(!is_null($order)){ + if (!is_null($order)) { Log::debug(sprintf("SummitOrderService::processOrderReminder sending reminder email for order %s", $order->getId())); SummitOrderReminderEmail::dispatch($order); return true; @@ -4766,8 +4828,8 @@ public function processOrderReminder(int $order_id): bool /** * @param int $ticket_id - * @throws \Exception * @return bool + * @throws \Exception */ public function processTicketReminder(int $ticket_id): bool { @@ -4776,7 +4838,7 @@ public function processTicketReminder(int $ticket_id): bool Log::debug(sprintf("SummitOrderService::processTicketReminder processing ticket %s", $ticket_id)); $ticket = $this->ticket_repository->getById($ticket_id); - if(!$ticket instanceof SummitAttendeeTicket) return null; + if (!$ticket instanceof SummitAttendeeTicket) return null; if (!$ticket->isActive()) { Log::warning(sprintf("SummitOrderService::processTicketReminder %s is not active.", $ticket_id)); @@ -4864,7 +4926,7 @@ public function processTicketReminder(int $ticket_id): bool return null; }); - if(!is_null($ticket)) { + if (!is_null($ticket)) { Log::debug(sprintf("SummitOrderService::processTicketReminder sending reminder email for ticket %s", $ticket->getId())); SummitTicketReminderEmail::dispatch($ticket); return true; @@ -5269,13 +5331,13 @@ public function delegateTicket(Summit $summit, int $order_id, int $ticket_id, Me $ticket_owner = $ticket->getOwner(); $former_manager = !is_null($ticket_owner) ? $ticket_owner->getManager() : null; // check current manager against new manager - if(!is_null($former_manager) && $current_user->getEmail() !== $former_manager->getEmail()){ + if (!is_null($former_manager) && $current_user->getEmail() !== $former_manager->getEmail()) { throw new ValidationException("You can not delegate this ticket ( it has already assigned a different Manager)."); } // try to get attendee for current user $manager = $this->attendee_repository->getBySummitAndEmail($summit, $current_user->getEmail()); - if(is_null($manager)){ + if (is_null($manager)) { // create the manager attendee Log::debug ( @@ -5293,7 +5355,7 @@ public function delegateTicket(Summit $summit, int $order_id, int $ticket_id, Me 'email' => $current_user->getEmail(), ], $current_user); } - if($manager->hasManager()){ + if ($manager->hasManager()) { // if proposed manager already has a manager then short circuit throw new ValidationException("You can not delegate this ticket ( proposed manager already has a manager)."); } @@ -5304,7 +5366,7 @@ public function delegateTicket(Summit $summit, int $order_id, int $ticket_id, Me Log::debug(sprintf("SummitOrderService::delegateTicket - delegate email to manager %s (%s)", $manager->getId(), $manager->getEmail())); // check if the attendee exists already $attendee = null; - if($manager->getId() > 0) { + if ($manager->getId() > 0) { Log::debug ( sprintf @@ -5324,7 +5386,7 @@ public function delegateTicket(Summit $summit, int $order_id, int $ticket_id, Me $manager ); } - if(is_null($attendee)) { + if (is_null($attendee)) { Log::debug ( sprintf @@ -5421,8 +5483,9 @@ public function ingestPaymentInfoForRegistrationOrders(): void * @return void * @throws \Exception */ - private function getOrderPaymentInfo(int $order_id):void{ - $this->tx_service->transaction(function() use($order_id){ + private function getOrderPaymentInfo(int $order_id): void + { + $this->tx_service->transaction(function () use ($order_id) { $order = $this->order_repository->getByIdExclusiveLock($order_id); if (!$order instanceof SummitOrder) return; diff --git a/tests/SummitOrderServiceTest.php b/tests/SummitOrderServiceTest.php index 61fcbf7d9..3cd2829a3 100644 --- a/tests/SummitOrderServiceTest.php +++ b/tests/SummitOrderServiceTest.php @@ -19,6 +19,7 @@ use App\Models\Foundation\Summit\Repositories\ISummitAttendeeBadgePrintRuleRepository; use App\Models\Foundation\Summit\Repositories\ISummitAttendeeBadgeRepository; use App\Models\Foundation\Summit\Repositories\ISummitOrderRepository; +use App\Models\Foundation\Summit\Repositories\ISummitRefundRequestRepository; use App\Services\FileSystem\IFileDownloadStrategy; use App\Services\FileSystem\IFileUploadStrategy; use App\Services\Model\ICompanyService; @@ -116,6 +117,7 @@ public function testProcessSummitOrderRemindersSendsReminders() $download_strategy = Mockery::mock(IFileDownloadStrategy::class); $company_repository = Mockery::mock(ICompanyRepository::class); $tags_repository = Mockery::mock(ITagRepository::class); + $refund_request_repository = Mockery::mock(ISummitRefundRequestRepository::class)->makePartial(); $company_service = Mockery::mock(ICompanyService::class); $ticket_finder_strategy_factory = Mockery::mock(ITicketFinderStrategyFactory::class); $tx_service = Mockery::mock(ITransactionService::class); @@ -229,6 +231,7 @@ public function testProcessSummitOrderRemindersSendsReminders() $download_strategy, $company_repository, $tags_repository, + $refund_request_repository, $company_service, $ticket_finder_strategy_factory, $tx_service, @@ -368,4 +371,4 @@ public function testAutoAssignDifferentPrePaidTicketsUntilEmpty() { $this->assertTrue(str_starts_with($ex->getMessage(), 'No more available PrePaid Tickets for Promo Code')); } } -} \ No newline at end of file +}