Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion backend/app/Resources/Event/EventResourcePublic.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -28,6 +32,7 @@ public function __construct(
$this->includePostCheckoutData = is_bool($includePostCheckoutData)
? $includePostCheckoutData
: false;
$this->orderContext = $orderContext;

parent::__construct($resource);
}
Expand Down Expand Up @@ -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
Expand Down
38 changes: 35 additions & 3 deletions backend/app/Resources/Event/EventSettingsResourcePublic.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,25 @@

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
*/
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);
Expand Down Expand Up @@ -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
Expand All @@ -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;
}
}
}
1 change: 1 addition & 0 deletions backend/app/Resources/Order/OrderResourcePublic.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(
'<p>Use {{ order.number }} for {{ event.title }}</p>',
orderFirstName: 'Jane',
);

$data = $resource->toArray(Request::create('/'));

$this->assertSame(
'<p>Use ORD-12345 for Summer Session</p>',
$data['offline_payment_instructions'],
);
}

public function test_rendered_offline_payment_instructions_are_purified(): void
{
$resource = $this->makeResource(
'<p>Reference {{ order.first_name }}</p>',
orderFirstName: '<script>alert("xss")</script>Jane',
);

$data = $resource->toArray(Request::create('/'));

$this->assertStringNotContainsString('<script>', $data['offline_payment_instructions']);
$this->assertStringContainsString('Jane', $data['offline_payment_instructions']);
}

private function makeResource(
string $offlinePaymentInstructions,
string $orderFirstName,
): EventSettingsResourcePublic {
$organizer = (new OrganizerDomainObject())
->setId(1)
->setName('Example Organizer')
->setEmail('organizer@example.com');

$event = (new EventDomainObject())
->setId(10)
->setTitle('Summer Session')
->setDescription('An evening event')
->setStartDate('2026-08-15 18:00:00')
->setCurrency('GBP')
->setTimezone('UTC')
->setOrganizer($organizer);

$settings = (new EventSettingDomainObject())
->setId(20)
->setEventId(10)
->setPaymentProviders([PaymentProviders::OFFLINE->value])
->setSupportEmail('support@example.com')
->setOfflinePaymentInstructions($offlinePaymentInstructions);

$order = (new OrderDomainObject())
->setId(30)
->setEventId(10)
->setShortId('order-short-id')
->setPublicId('ORD-12345')
->setFirstName($orderFirstName)
->setLastName('Buyer')
->setEmail('buyer@example.com')
->setTotalGross(125.50)
->setCurrency('GBP')
->setCreatedAt('2026-08-01 12:00:00')
->setStatus(OrderStatus::AWAITING_OFFLINE_PAYMENT->name)
->setPaymentStatus(OrderPaymentStatus::AWAITING_OFFLINE_PAYMENT->name)
->setPaymentProvider(PaymentProviders::OFFLINE->value);

return new EventSettingsResourcePublic(
resource: $settings,
eventContext: $event,
orderContext: $order,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const Payment = () => {
const {data: event, isFetched: isEventFetched} = useGetEventPublic(eventId);
const {data: order, isFetched: isOrderFetched} = useGetOrderPublic(eventId, orderShortId, ['event']);
const isLoading = !isOrderFetched;
const checkoutEvent = order?.event || event;
const [isPaymentLoading, setIsPaymentLoading] = useState(false);
const [activePaymentMethod, setActivePaymentMethod] = useState<'STRIPE' | 'OFFLINE' | null>(null);
const [submitHandler, setSubmitHandler] = useState<(() => Promise<void>) | null>(null);
Expand Down Expand Up @@ -93,8 +94,8 @@ const Payment = () => {
return (
<>
<CheckoutContent>
{(event && order) && (
<InlineOrderSummary event={event} order={order} defaultExpanded={false}/>
{(checkoutEvent && order) && (
<InlineOrderSummary event={checkoutEvent} order={order} defaultExpanded={false}/>
)}
{isStripeEnabled && (
<div style={{display: activePaymentMethod === 'STRIPE' ? 'block' : 'none'}}>
Expand All @@ -104,7 +105,7 @@ const Payment = () => {

{isOfflineEnabled && (
<div style={{display: activePaymentMethod === 'OFFLINE' ? 'block' : 'none'}}>
<OfflinePaymentMethod event={event as Event}/>
<OfflinePaymentMethod event={checkoutEvent as Event}/>
</div>
)}

Expand Down
Loading