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
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ abstract class EventSettingDomainObjectAbstract extends \HiEvents\DomainObjects\
final public const TICKET_DESIGN_SETTINGS = 'ticket_design_settings';
final public const ATTENDEE_DETAILS_COLLECTION_METHOD = 'attendee_details_collection_method';
final public const SHOW_MARKETING_OPT_IN = 'show_marketing_opt_in';
final public const ALLOW_COPY_DETAILS_TO_ALL_ATTENDEES = 'allow_copy_details_to_all_attendees';
final public const HOMEPAGE_THEME_SETTINGS = 'homepage_theme_settings';
final public const PASS_PLATFORM_FEE_TO_BUYER = 'pass_platform_fee_to_buyer';
final public const ALLOW_ATTENDEE_SELF_EDIT = 'allow_attendee_self_edit';
Expand Down Expand Up @@ -119,6 +120,7 @@ abstract class EventSettingDomainObjectAbstract extends \HiEvents\DomainObjects\
protected array|string|null $ticket_design_settings = null;
protected string $attendee_details_collection_method = 'PER_TICKET';
protected bool $show_marketing_opt_in = true;
protected bool $allow_copy_details_to_all_attendees = true;
protected array|string|null $homepage_theme_settings = null;
protected bool $pass_platform_fee_to_buyer = false;
protected bool $allow_attendee_self_edit = true;
Expand Down Expand Up @@ -180,6 +182,7 @@ public function toArray(): array
'ticket_design_settings' => $this->ticket_design_settings ?? null,
'attendee_details_collection_method' => $this->attendee_details_collection_method ?? null,
'show_marketing_opt_in' => $this->show_marketing_opt_in ?? null,
'allow_copy_details_to_all_attendees' => $this->allow_copy_details_to_all_attendees ?? null,
'homepage_theme_settings' => $this->homepage_theme_settings ?? null,
'pass_platform_fee_to_buyer' => $this->pass_platform_fee_to_buyer ?? null,
'allow_attendee_self_edit' => $this->allow_attendee_self_edit ?? null,
Expand Down Expand Up @@ -751,6 +754,17 @@ public function getShowMarketingOptIn(): bool
return $this->show_marketing_opt_in;
}

public function setAllowCopyDetailsToAllAttendees(bool $allow_copy_details_to_all_attendees): self
{
$this->allow_copy_details_to_all_attendees = $allow_copy_details_to_all_attendees;
return $this;
}

public function getAllowCopyDetailsToAllAttendees(): bool
{
return $this->allow_copy_details_to_all_attendees;
}

public function setHomepageThemeSettings(array|string|null $homepage_theme_settings): self
{
$this->homepage_theme_settings = $homepage_theme_settings;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ public function rules(): array
// Marketing settings
'show_marketing_opt_in' => ['boolean'],

// Attendee detail copy control
'allow_copy_details_to_all_attendees' => ['boolean'],

// Platform fee settings
'pass_platform_fee_to_buyer' => ['boolean'],

Expand Down
3 changes: 3 additions & 0 deletions backend/app/Resources/Event/EventSettingsResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ public function toArray($request): array
// Marketing settings
'show_marketing_opt_in' => $this->getShowMarketingOptIn(),

// Attendee detail copy control
'allow_copy_details_to_all_attendees' => $this->getAllowCopyDetailsToAllAttendees(),

// Platform fee settings
'pass_platform_fee_to_buyer' => $this->getPassPlatformFeeToBuyer(),

Expand Down
3 changes: 3 additions & 0 deletions backend/app/Resources/Event/EventSettingsResourcePublic.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ public function toArray($request): array
// Marketing settings
'show_marketing_opt_in' => $this->getShowMarketingOptIn(),

// Attendee detail copy control
'allow_copy_details_to_all_attendees' => $this->getAllowCopyDetailsToAllAttendees(),

// Platform fee settings
'pass_platform_fee_to_buyer' => $this->getPassPlatformFeeToBuyer(),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ public function __construct(
// Marketing settings
public readonly bool $show_marketing_opt_in = true,

// Attendee detail copy control
public readonly bool $allow_copy_details_to_all_attendees = true,

// Platform fee settings
public readonly bool $pass_platform_fee_to_buyer = false,

Expand Down Expand Up @@ -158,6 +161,9 @@ public static function createWithDefaults(
// Marketing defaults
show_marketing_opt_in: true,

// Attendee detail copy control default
allow_copy_details_to_all_attendees: true,

// Platform fee defaults
pass_platform_fee_to_buyer: false,

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ public function handle(PartialUpdateEventSettingsDTO $eventSettingsDTO): EventSe
// Marketing settings
'show_marketing_opt_in' => $eventSettingsDTO->settings['show_marketing_opt_in'] ?? $existingSettings->getShowMarketingOptIn(),

// Attendee detail copy control
'allow_copy_details_to_all_attendees' => $eventSettingsDTO->settings['allow_copy_details_to_all_attendees'] ?? $existingSettings->getAllowCopyDetailsToAllAttendees(),

// Platform fee settings
'pass_platform_fee_to_buyer' => $eventSettingsDTO->settings['pass_platform_fee_to_buyer'] ?? $existingSettings->getPassPlatformFeeToBuyer(),

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ public function handle(UpdateEventSettingsDTO $settings): EventSettingDomainObje
// Marketing settings
'show_marketing_opt_in' => $settings->show_marketing_opt_in,

// Attendee detail copy control
'allow_copy_details_to_all_attendees' => $settings->allow_copy_details_to_all_attendees,

// Platform fee settings
'pass_platform_fee_to_buyer' => $settings->pass_platform_fee_to_buyer,

Expand Down
1 change: 1 addition & 0 deletions backend/app/Services/Domain/Event/CreateEventService.php
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,7 @@ private function createEventSettings(

'attendee_details_collection_method' => $organizerSettings->getDefaultAttendeeDetailsCollectionMethod(),
'show_marketing_opt_in' => $organizerSettings->getDefaultShowMarketingOptIn(),
'allow_copy_details_to_all_attendees' => true,
'pass_platform_fee_to_buyer' => $organizerSettings->getDefaultPassPlatformFeeToBuyer(),
'allow_attendee_self_edit' => $organizerSettings->getDefaultAllowAttendeeSelfEdit() ?? false,
'ticket_design_settings' => [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up(): void
{
Schema::table('event_settings', function (Blueprint $table) {
$table->boolean('allow_copy_details_to_all_attendees')->default(true)->after('show_marketing_opt_in');
});
}

public function down(): void
{
Schema::table('event_settings', function (Blueprint $table) {
$table->dropColumn('allow_copy_details_to_all_attendees');
});
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,34 @@ public function test_date_display_mode_is_optional(): void

$this->assertFalse($validator->errors()->has('ticket_design_settings.date_display_mode'));
}

public function test_allow_copy_details_to_all_attendees_accepts_boolean(): void
{
$validator = Validator::make(
['allow_copy_details_to_all_attendees' => false],
(new UpdateEventSettingsRequest)->rules()
);

$this->assertFalse($validator->errors()->has('allow_copy_details_to_all_attendees'));
}

public function test_allow_copy_details_to_all_attendees_rejects_non_boolean(): void
{
$validator = Validator::make(
['allow_copy_details_to_all_attendees' => 'not-a-boolean'],
(new UpdateEventSettingsRequest)->rules()
);

$this->assertTrue($validator->errors()->has('allow_copy_details_to_all_attendees'));
}

public function test_allow_copy_details_to_all_attendees_is_optional(): void
{
$validator = Validator::make(
['ticket_design_settings' => ['accent_color' => '#333333']],
(new UpdateEventSettingsRequest)->rules()
);

$this->assertFalse($validator->errors()->has('allow_copy_details_to_all_attendees'));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Tests\Unit\Resources\Event;

use HiEvents\DomainObjects\EventSettingDomainObject;
use HiEvents\Resources\Event\EventSettingsResourcePublic;
use Illuminate\Http\Request;
use Tests\TestCase;

class EventSettingsResourcePublicTest extends TestCase
{
public function test_public_resource_exposes_allow_copy_details_when_enabled(): void
{
$settings = (new EventSettingDomainObject())
->setAllowCopyDetailsToAllAttendees(true);

$resource = (new EventSettingsResourcePublic($settings))->toArray(Request::create('/'));

// Load-bearing: the checkout can only hide the control if this flag is on the
// public payload, so it must always be present (outside the post-checkout block).
$this->assertArrayHasKey('allow_copy_details_to_all_attendees', $resource);
$this->assertTrue($resource['allow_copy_details_to_all_attendees']);
}

public function test_public_resource_exposes_allow_copy_details_when_disabled(): void
{
$settings = (new EventSettingDomainObject())
->setAllowCopyDetailsToAllAttendees(false);

$resource = (new EventSettingsResourcePublic($settings))->toArray(Request::create('/'));

$this->assertFalse($resource['allow_copy_details_to_all_attendees']);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

namespace Tests\Unit\Services\Application\Handlers\EventSettings;

use HiEvents\DomainObjects\EventSettingDomainObject;
use HiEvents\Repository\Interfaces\EventSettingsRepositoryInterface;
use HiEvents\Services\Application\Handlers\EventSettings\DTO\PartialUpdateEventSettingsDTO;
use HiEvents\Services\Application\Handlers\EventSettings\DTO\UpdateEventSettingsDTO;
use HiEvents\Services\Application\Handlers\EventSettings\PartialUpdateEventSettingsHandler;
use HiEvents\Services\Application\Handlers\EventSettings\UpdateEventSettingsHandler;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use Tests\TestCase;

class PartialUpdateEventSettingsHandlerTest extends TestCase
{
use MockeryPHPUnitIntegration;

public function test_explicit_show_copy_details_value_is_passed_through(): void
{
// Existing event has the control ON; the PATCH explicitly turns it OFF.
$dto = $this->runPartialUpdate(
existingValue: true,
settings: ['allow_copy_details_to_all_attendees' => false],
);

$this->assertFalse($dto->allow_copy_details_to_all_attendees);
}

public function test_omitted_show_copy_details_key_falls_back_to_existing_value(): void
{
// Existing event has the control OFF; the PATCH omits the key entirely.
// It must keep the existing value (false), NOT reset to the default (true).
$dto = $this->runPartialUpdate(
existingValue: false,
settings: [],
);

$this->assertFalse($dto->allow_copy_details_to_all_attendees);
}

/**
* Drives the partial handler and returns the UpdateEventSettingsDTO it forwards
* to the (mocked) full handler, so we can assert how the field was resolved.
*/
private function runPartialUpdate(bool $existingValue, array $settings): UpdateEventSettingsDTO
{
$existingSettings = (new EventSettingDomainObject())
->setAllowCopyDetailsToAllAttendees($existingValue)
->setPaymentProviders([]);

$repository = Mockery::mock(EventSettingsRepositoryInterface::class);
$repository->shouldReceive('findFirstWhere')
->with(['event_id' => 1])
->andReturn($existingSettings);

$captured = null;
$fullHandler = Mockery::mock(UpdateEventSettingsHandler::class);
$fullHandler->shouldReceive('handle')
->once()
->andReturnUsing(function (UpdateEventSettingsDTO $dto) use (&$captured, $existingSettings) {
$captured = $dto;
return $existingSettings;
});

$handler = new PartialUpdateEventSettingsHandler($fullHandler, $repository);

$handler->handle(new PartialUpdateEventSettingsDTO(
account_id: 1,
event_id: 1,
settings: array_merge(['location_details' => []], $settings),
));

return $captured;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,43 @@ public function testDoesNotDispatchEventWhenAutoProcessDisabled(): void
Event::assertNotDispatched(CapacityChangedEvent::class);
}

private function createDTO(?bool $waitlist_auto_process = null): UpdateEventSettingsDTO
public function testPersistsAllowCopyDetailsToAllAttendees(): void
{
Event::fake();

$existingSettings = new EventSettingDomainObject();

$this->eventSettingsRepository
->shouldReceive('findFirstWhere')
->with(['event_id' => 1])
->twice()
->andReturn($existingSettings);

$captured = null;
$this->eventSettingsRepository
->shouldReceive('updateWhere')
->once()
->andReturnUsing(function (...$args) use (&$captured) {
foreach ($args as $arg) {
if (is_array($arg) && array_key_exists('allow_copy_details_to_all_attendees', $arg)) {
$captured = $arg['allow_copy_details_to_all_attendees'];
}
}
return 1;
});

$this->handler->handle($this->createDTO(allow_copy_details_to_all_attendees: false));

$this->assertFalse(
$captured,
'Expected allow_copy_details_to_all_attendees=false to be persisted via updateWhere'
);
}

private function createDTO(
?bool $waitlist_auto_process = null,
bool $allow_copy_details_to_all_attendees = true,
): UpdateEventSettingsDTO
{
return UpdateEventSettingsDTO::fromArray([
'account_id' => 1,
Expand All @@ -143,6 +179,7 @@ private function createDTO(?bool $waitlist_auto_process = null): UpdateEventSett
'seo_title' => null,
'seo_description' => null,
'seo_keywords' => null,
'allow_copy_details_to_all_attendees' => $allow_copy_details_to_all_attendees,
'waitlist_auto_process' => $waitlist_auto_process,
'waitlist_offer_timeout_minutes' => 60,
]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const HomepageAndCheckoutSettings = () => {
order_timeout_in_minutes: 15,
attendee_details_collection_method: 'PER_TICKET' as 'PER_TICKET' | 'PER_ORDER',
show_marketing_opt_in: true,
allow_copy_details_to_all_attendees: true,
},
transformValues: (values) => ({
...values,
Expand Down Expand Up @@ -58,6 +59,7 @@ export const HomepageAndCheckoutSettings = () => {
order_timeout_in_minutes: eventSettingsQuery.data.order_timeout_in_minutes,
attendee_details_collection_method: eventSettingsQuery.data.attendee_details_collection_method || 'PER_TICKET',
show_marketing_opt_in: eventSettingsQuery.data.show_marketing_opt_in ?? true,
allow_copy_details_to_all_attendees: eventSettingsQuery.data.allow_copy_details_to_all_attendees ?? true,
});
}
}, [eventSettingsQuery.isFetched]);
Expand Down Expand Up @@ -128,6 +130,13 @@ export const HomepageAndCheckoutSettings = () => {
{...form.getInputProps('show_marketing_opt_in', {type: 'checkbox'})}
/>

<Switch
mt="md"
label={t`Allow buyers to copy their details to all attendees`}
description={t`When enabled, buyers can copy their own name and email onto all attendees at once. Turn this off to remove the "All attendees" option; buyers can still copy to the first attendee, and the rest must be entered individually.`}
{...form.getInputProps('allow_copy_details_to_all_attendees', {type: 'checkbox'})}
/>

<Button loading={updateMutation.isPending} type={'submit'}>
{t`Save`}
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export const CollectInformation = () => {
const products = productCategories?.flatMap(category => category.products);
const requireBillingAddress = event?.settings?.require_billing_address;
const isPerOrderCollection = event?.settings?.attendee_details_collection_method === 'PER_ORDER';
const allowCopyToAllAttendees = event?.settings?.allow_copy_details_to_all_attendees ?? true;
const [copyOption, setCopyOption] = useState<'none' | 'first' | 'all'>('none');

const isEmailValid = (email: string) => {
Expand Down Expand Up @@ -498,7 +499,7 @@ export const CollectInformation = () => {
data={[
{label: t`None`, value: 'none'},
{label: t`First attendee`, value: 'first'},
{label: t`All attendees`, value: 'all'},
...(allowCopyToAllAttendees ? [{label: t`All attendees`, value: 'all'}] : []),
]}
/>
</Tooltip>
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,9 @@ export interface EventSettings {
// Marketing settings
show_marketing_opt_in?: boolean;

// Attendee detail copy control
allow_copy_details_to_all_attendees?: boolean;

// Platform fee settings
pass_platform_fee_to_buyer?: boolean;

Expand Down
Loading