From 185333ede948f77ed8ebbe1a1ca01b5d710c288d Mon Sep 17 00:00:00 2001 From: Ridwan Aguda <59691595+realicon23@users.noreply.github.com> Date: Wed, 1 Jul 2026 19:49:16 +0100 Subject: [PATCH] Render offline payment instruction tokens Offline payment instructions can include order-specific references in organizer configuration, but public checkout resources were returning the stored template verbatim. That left buyers seeing unreplaced Liquid tokens and prevented organizers from matching bank transfers to orders. Render the instructions only when the public event settings resource has both event and order context, reusing the existing order-confirmation token builder so checkout and email references stay consistent. Purify the rendered HTML before returning it, and fall back to the stored instructions if rendering fails. Also prefer the order-loaded event payload on the payment page so the offline payment method receives the resource that contains order-aware settings. --- .../Resources/Event/EventResourcePublic.php | 9 ++- .../Event/EventSettingsResourcePublic.php | 38 ++++++++- .../Resources/Order/OrderResourcePublic.php | 1 + .../Event/EventSettingsResourcePublicTest.php | 81 +++++++++++++++++++ .../routes/product-widget/Payment/index.tsx | 7 +- 5 files changed, 129 insertions(+), 7 deletions(-) diff --git a/backend/app/Resources/Event/EventResourcePublic.php b/backend/app/Resources/Event/EventResourcePublic.php index da969e58f5..d505aa830d 100644 --- a/backend/app/Resources/Event/EventResourcePublic.php +++ b/backend/app/Resources/Event/EventResourcePublic.php @@ -3,6 +3,7 @@ namespace HiEvents\Resources\Event; use HiEvents\DomainObjects\EventDomainObject; +use HiEvents\DomainObjects\OrderDomainObject; use HiEvents\Resources\BaseResource; use HiEvents\Resources\Image\ImageResource; use HiEvents\Resources\Organizer\OrganizerResourcePublic; @@ -17,9 +18,12 @@ class EventResourcePublic extends BaseResource { private readonly bool $includePostCheckoutData; + private readonly ?OrderDomainObject $orderContext; + public function __construct( mixed $resource, mixed $includePostCheckoutData = false, + ?OrderDomainObject $orderContext = null, ) { // This is a hacky workaround to handle when this resource is instantiated @@ -28,6 +32,7 @@ public function __construct( $this->includePostCheckoutData = is_bool($includePostCheckoutData) ? $includePostCheckoutData : false; + $this->orderContext = $orderContext; parent::__construct($resource); } @@ -56,7 +61,9 @@ public function toArray(Request $request): array condition: !is_null($this->getEventSettings()), value: fn() => new EventSettingsResourcePublic( $this->getEventSettings(), - $this->includePostCheckoutData + $this->includePostCheckoutData, + $this->resource instanceof EventDomainObject ? $this->resource : null, + $this->orderContext, ), ), // @TODO - public question resource diff --git a/backend/app/Resources/Event/EventSettingsResourcePublic.php b/backend/app/Resources/Event/EventSettingsResourcePublic.php index 822d6a0cc3..c5144e6d45 100644 --- a/backend/app/Resources/Event/EventSettingsResourcePublic.php +++ b/backend/app/Resources/Event/EventSettingsResourcePublic.php @@ -2,8 +2,14 @@ namespace HiEvents\Resources\Event; +use HiEvents\DomainObjects\EventDomainObject; use HiEvents\DomainObjects\EventSettingDomainObject; +use HiEvents\DomainObjects\OrderDomainObject; +use HiEvents\Services\Domain\Email\EmailTokenContextBuilder; +use HiEvents\Services\Infrastructure\Email\LiquidTemplateRenderer; +use HiEvents\Services\Infrastructure\HtmlPurifier\HtmlPurifierService; use Illuminate\Http\Resources\Json\JsonResource; +use RuntimeException; /** * @mixin EventSettingDomainObject @@ -11,8 +17,10 @@ class EventSettingsResourcePublic extends JsonResource { public function __construct( - mixed $resource, - private readonly bool $includePostCheckoutData = false, + mixed $resource, + private readonly bool $includePostCheckoutData = false, + private readonly ?EventDomainObject $eventContext = null, + private readonly ?OrderDomainObject $orderContext = null, ) { parent::__construct($resource); @@ -67,7 +75,7 @@ public function toArray($request): array // Payment settings 'payment_providers' => $this->getPaymentProviders(), - 'offline_payment_instructions' => $this->getOfflinePaymentInstructions(), + 'offline_payment_instructions' => $this->getOfflinePaymentInstructionsForOrder(), 'allow_orders_awaiting_offline_payment_to_check_in' => $this->getAllowOrdersAwaitingOfflinePaymentToCheckIn(), // Invoice settings @@ -94,4 +102,28 @@ public function toArray($request): array 'waitlist_offer_timeout_minutes' => $this->getWaitlistOfferTimeoutMinutes(), ]; } + + private function getOfflinePaymentInstructionsForOrder(): ?string + { + $instructions = $this->getOfflinePaymentInstructions(); + $organizer = $this->eventContext?->getOrganizer(); + + if (!$instructions || !$this->eventContext || !$this->orderContext || !$organizer) { + return $instructions; + } + + try { + $context = app(EmailTokenContextBuilder::class)->buildOrderConfirmationContext( + order: $this->orderContext, + event: $this->eventContext, + organizer: $organizer, + eventSettings: $this->resource, + ); + $rendered = app(LiquidTemplateRenderer::class)->render($instructions, $context); + + return app(HtmlPurifierService::class)->purify($rendered); + } catch (RuntimeException) { + return $instructions; + } + } } diff --git a/backend/app/Resources/Order/OrderResourcePublic.php b/backend/app/Resources/Order/OrderResourcePublic.php index 7880d22ed5..f6973c7fdf 100644 --- a/backend/app/Resources/Order/OrderResourcePublic.php +++ b/backend/app/Resources/Order/OrderResourcePublic.php @@ -45,6 +45,7 @@ public function toArray(Request $request): array fn() => new EventResourcePublic( resource: $this->getEvent(), includePostCheckoutData: $this->getStatus() === OrderStatus::COMPLETED->name, + orderContext: $this->resource instanceof OrderDomainObject ? $this->resource : null, ), ), 'latest_invoice' => $this->when( diff --git a/backend/tests/Unit/Resources/Event/EventSettingsResourcePublicTest.php b/backend/tests/Unit/Resources/Event/EventSettingsResourcePublicTest.php index 96fa0dccaa..4257f9715e 100644 --- a/backend/tests/Unit/Resources/Event/EventSettingsResourcePublicTest.php +++ b/backend/tests/Unit/Resources/Event/EventSettingsResourcePublicTest.php @@ -2,7 +2,13 @@ namespace Tests\Unit\Resources\Event; +use HiEvents\DomainObjects\Enums\PaymentProviders; +use HiEvents\DomainObjects\EventDomainObject; use HiEvents\DomainObjects\EventSettingDomainObject; +use HiEvents\DomainObjects\OrderDomainObject; +use HiEvents\DomainObjects\OrganizerDomainObject; +use HiEvents\DomainObjects\Status\OrderPaymentStatus; +use HiEvents\DomainObjects\Status\OrderStatus; use HiEvents\Resources\Event\EventSettingsResourcePublic; use Illuminate\Http\Request; use Tests\TestCase; @@ -31,4 +37,79 @@ public function test_public_resource_exposes_allow_copy_details_when_disabled(): $this->assertFalse($resource['allow_copy_details_to_all_attendees']); } + + public function test_offline_payment_instructions_render_order_tokens(): void + { + $resource = $this->makeResource( + '

Use {{ order.number }} for {{ event.title }}

', + orderFirstName: 'Jane', + ); + + $data = $resource->toArray(Request::create('/')); + + $this->assertSame( + '

Use ORD-12345 for Summer Session

', + $data['offline_payment_instructions'], + ); + } + + public function test_rendered_offline_payment_instructions_are_purified(): void + { + $resource = $this->makeResource( + '

Reference {{ order.first_name }}

', + orderFirstName: 'Jane', + ); + + $data = $resource->toArray(Request::create('/')); + + $this->assertStringNotContainsString('