diff --git a/backend/app/Services/Application/Handlers/Order/GetOrderPublicHandler.php b/backend/app/Services/Application/Handlers/Order/GetOrderPublicHandler.php index 0672a0e995..d6eecedef3 100644 --- a/backend/app/Services/Application/Handlers/Order/GetOrderPublicHandler.php +++ b/backend/app/Services/Application/Handlers/Order/GetOrderPublicHandler.php @@ -20,14 +20,16 @@ use HiEvents\Repository\Eloquent\Value\Relationship; use HiEvents\Repository\Interfaces\OrderRepositoryInterface; use HiEvents\Services\Application\Handlers\Order\DTO\GetOrderPublicDTO; +use HiEvents\Services\Domain\Order\OfflinePaymentInstructionsRenderService; use HiEvents\Services\Infrastructure\Session\CheckoutSessionManagementService; use Symfony\Component\Routing\Exception\ResourceNotFoundException; class GetOrderPublicHandler { public function __construct( - private readonly OrderRepositoryInterface $orderRepository, - private readonly CheckoutSessionManagementService $sessionIdentifierService + private readonly OrderRepositoryInterface $orderRepository, + private readonly CheckoutSessionManagementService $sessionIdentifierService, + private readonly OfflinePaymentInstructionsRenderService $offlinePaymentInstructionsRenderService, ) { } @@ -49,6 +51,8 @@ public function handle(GetOrderPublicDTO $getOrderData): OrderDomainObject $this->verifySessionId($order->getSessionId()); } + $this->offlinePaymentInstructionsRenderService->renderForOrder($order); + return $order; } diff --git a/backend/app/Services/Application/Handlers/TicketLookup/GetOrdersByLookupTokenHandler.php b/backend/app/Services/Application/Handlers/TicketLookup/GetOrdersByLookupTokenHandler.php index 0030f48c60..5e7aa59b44 100644 --- a/backend/app/Services/Application/Handlers/TicketLookup/GetOrdersByLookupTokenHandler.php +++ b/backend/app/Services/Application/Handlers/TicketLookup/GetOrdersByLookupTokenHandler.php @@ -23,6 +23,7 @@ use HiEvents\Repository\Interfaces\OrderRepositoryInterface; use HiEvents\Repository\Interfaces\TicketLookupTokenRepositoryInterface; use HiEvents\Services\Application\Handlers\TicketLookup\DTO\GetOrdersByLookupTokenDTO; +use HiEvents\Services\Domain\Order\OfflinePaymentInstructionsRenderService; use Illuminate\Support\Collection; class GetOrdersByLookupTokenHandler @@ -30,6 +31,7 @@ class GetOrdersByLookupTokenHandler public function __construct( private readonly TicketLookupTokenRepositoryInterface $ticketLookupTokenRepository, private readonly OrderRepositoryInterface $orderRepository, + private readonly OfflinePaymentInstructionsRenderService $offlinePaymentInstructionsRenderService, ) { } @@ -41,7 +43,8 @@ public function handle(GetOrdersByLookupTokenDTO $dto): Collection { $tokenRecord = $this->validateAndFetchToken($dto->token); - return $this->getOrdersForEmail($tokenRecord->getEmail()); + return $this->getOrdersForEmail($tokenRecord->getEmail()) + ->each(fn(OrderDomainObject $order) => $this->offlinePaymentInstructionsRenderService->renderForOrder($order)); } /** diff --git a/backend/app/Services/Domain/Email/EmailTokenContextBuilder.php b/backend/app/Services/Domain/Email/EmailTokenContextBuilder.php index c0f71aa1a1..b5e33ae202 100644 --- a/backend/app/Services/Domain/Email/EmailTokenContextBuilder.php +++ b/backend/app/Services/Domain/Email/EmailTokenContextBuilder.php @@ -16,9 +16,19 @@ use HiEvents\Helper\IdHelper; use HiEvents\Helper\Url; use HiEvents\Locale; +use HiEvents\Services\Infrastructure\Email\LiquidTemplateRenderer; +use HiEvents\Services\Infrastructure\HtmlPurifier\HtmlPurifierService; +use Throwable; class EmailTokenContextBuilder { + public function __construct( + private readonly LiquidTemplateRenderer $liquidTemplateRenderer, + private readonly HtmlPurifierService $htmlPurifierService, + ) + { + } + public function buildOrderConfirmationContext( OrderDomainObject $order, EventDomainObject $event, @@ -29,7 +39,7 @@ public function buildOrderConfirmationContext( $eventStartDate = new Carbon(DateHelper::convertFromUTC($event->getStartDate(), $event->getTimezone())); $eventEndDate = $event->getEndDate() ? new Carbon(DateHelper::convertFromUTC($event->getEndDate(), $event->getTimezone())) : null; - return [ + $context = [ // Event object 'event' => [ 'title' => $event->getTitle(), @@ -75,6 +85,27 @@ public function buildOrderConfirmationContext( 'post_checkout_message' => $eventSettings->getPostCheckoutMessage() ?? '', ], ]; + + $context['settings']['offline_payment_instructions'] = $this->renderOfflinePaymentInstructions($context); + + return $context; + } + + private function renderOfflinePaymentInstructions(array $context): string + { + $instructions = $context['settings']['offline_payment_instructions']; + + if ($instructions === '') { + return $instructions; + } + + try { + $rendered = $this->liquidTemplateRenderer->render($instructions, $context); + + return $this->htmlPurifierService->purify($rendered) ?? $instructions; + } catch (Throwable) { + return $instructions; + } } public function buildAttendeeTicketContext( diff --git a/backend/app/Services/Domain/Email/MailBuilderService.php b/backend/app/Services/Domain/Email/MailBuilderService.php index 59ef8806b3..5cea766a80 100644 --- a/backend/app/Services/Domain/Email/MailBuilderService.php +++ b/backend/app/Services/Domain/Email/MailBuilderService.php @@ -12,12 +12,14 @@ use HiEvents\Mail\Attendee\AttendeeTicketMail; use HiEvents\Mail\Order\OrderSummary; use HiEvents\Services\Domain\Email\DTO\RenderedEmailTemplateDTO; +use HiEvents\Services\Domain\Order\OfflinePaymentInstructionsRenderService; class MailBuilderService { public function __construct( private readonly EmailTemplateService $emailTemplateService, private readonly EmailTokenContextBuilder $tokenContextBuilder, + private readonly OfflinePaymentInstructionsRenderService $offlinePaymentInstructionsRenderService, ) { } @@ -60,6 +62,10 @@ public function buildOrderSummaryMail( $organizer ); + if (!$renderedTemplate) { + $this->offlinePaymentInstructionsRenderService->render($order, $event, $organizer, $eventSettings); + } + return new OrderSummary( order: $order, event: $event, diff --git a/backend/app/Services/Domain/Order/OfflinePaymentInstructionsRenderService.php b/backend/app/Services/Domain/Order/OfflinePaymentInstructionsRenderService.php new file mode 100644 index 0000000000..33cea1191e --- /dev/null +++ b/backend/app/Services/Domain/Order/OfflinePaymentInstructionsRenderService.php @@ -0,0 +1,61 @@ +getEvent(); + $eventSettings = $event?->getEventSettings(); + $organizer = $event?->getOrganizer(); + + if (!$event || !$eventSettings || !$organizer) { + return; + } + + $this->render($order, $event, $organizer, $eventSettings); + } + + public function render( + OrderDomainObject $order, + EventDomainObject $event, + OrganizerDomainObject $organizer, + EventSettingDomainObject $eventSettings, + ): void + { + if (!$eventSettings->getOfflinePaymentInstructions()) { + return; + } + + try { + $context = $this->tokenContextBuilder->buildOrderConfirmationContext( + order: $order, + event: $event, + organizer: $organizer, + eventSettings: $eventSettings, + ); + + $eventSettings->setOfflinePaymentInstructions( + $context['settings']['offline_payment_instructions'] + ); + } catch (Throwable) { + return; + } + } +} diff --git a/backend/tests/Unit/Services/Application/Handlers/TicketLookup/GetOrdersByLookupTokenHandlerTest.php b/backend/tests/Unit/Services/Application/Handlers/TicketLookup/GetOrdersByLookupTokenHandlerTest.php index a738bc6823..79909fbda0 100644 --- a/backend/tests/Unit/Services/Application/Handlers/TicketLookup/GetOrdersByLookupTokenHandlerTest.php +++ b/backend/tests/Unit/Services/Application/Handlers/TicketLookup/GetOrdersByLookupTokenHandlerTest.php @@ -10,6 +10,7 @@ use HiEvents\Repository\Interfaces\TicketLookupTokenRepositoryInterface; use HiEvents\Services\Application\Handlers\TicketLookup\DTO\GetOrdersByLookupTokenDTO; use HiEvents\Services\Application\Handlers\TicketLookup\GetOrdersByLookupTokenHandler; +use HiEvents\Services\Domain\Order\OfflinePaymentInstructionsRenderService; use Illuminate\Support\Collection; use Mockery as m; use Tests\TestCase; @@ -30,6 +31,7 @@ protected function setUp(): void $this->handler = new GetOrdersByLookupTokenHandler( $this->ticketLookupTokenRepository, $this->orderRepository, + app(OfflinePaymentInstructionsRenderService::class), ); } @@ -46,6 +48,7 @@ public function testHandleSuccessfullyReturnsOrdersWhenTokenIsValid(): void ->andReturn($email); $order = m::mock(OrderDomainObject::class); + $order->shouldReceive('getEvent')->andReturn(null); $orders = new Collection([$order]); $this->ticketLookupTokenRepository diff --git a/backend/tests/Unit/Services/Domain/Email/EmailTokenContextBuilderTest.php b/backend/tests/Unit/Services/Domain/Email/EmailTokenContextBuilderTest.php index 37ff777ca2..1ff9ced8a2 100644 --- a/backend/tests/Unit/Services/Domain/Email/EmailTokenContextBuilderTest.php +++ b/backend/tests/Unit/Services/Domain/Email/EmailTokenContextBuilderTest.php @@ -21,7 +21,7 @@ class EmailTokenContextBuilderTest extends TestCase protected function setUp(): void { parent::setUp(); - $this->contextBuilder = new EmailTokenContextBuilder(); + $this->contextBuilder = app(EmailTokenContextBuilder::class); } public function test_builds_order_confirmation_context(): void @@ -101,6 +101,70 @@ public function test_builds_attendee_ticket_context(): void $this->assertEquals('Great Organizer', $context['organizer']['name']); } + public function test_offline_payment_instructions_tokens_are_rendered_into_context(): void + { + $order = $this->createMockOrder(); + $event = $this->createMockEvent(); + $organizer = $this->createMockOrganizer(); + $eventSettings = $this->createMockEventSettings( + offlinePaymentInstructions: '
Use {{ order.number }} for {{ event.title }}
', + ); + + $context = $this->contextBuilder->buildOrderConfirmationContext( + $order, + $event, + $organizer, + $eventSettings + ); + + $this->assertSame( + 'Use ORD-123456 for Amazing Event
', + $context['settings']['offline_payment_instructions'], + ); + } + + public function test_rendered_offline_payment_instructions_are_purified(): void + { + $order = $this->createMockOrder(firstName: 'John'); + $event = $this->createMockEvent(); + $organizer = $this->createMockOrganizer(); + $eventSettings = $this->createMockEventSettings( + offlinePaymentInstructions: 'Reference {{ order.first_name }}
', + ); + + $context = $this->contextBuilder->buildOrderConfirmationContext( + $order, + $event, + $organizer, + $eventSettings + ); + + $this->assertStringNotContainsString('Jane', + ); + + $this->service()->renderForOrder($order); + + $instructions = $order->getEvent()->getEventSettings()->getOfflinePaymentInstructions(); + + $this->assertStringNotContainsString('