diff --git a/backend/app/DomainObjects/Generated/EventSettingDomainObjectAbstract.php b/backend/app/DomainObjects/Generated/EventSettingDomainObjectAbstract.php
index e9b7b492e5..c571b2c67d 100644
--- a/backend/app/DomainObjects/Generated/EventSettingDomainObjectAbstract.php
+++ b/backend/app/DomainObjects/Generated/EventSettingDomainObjectAbstract.php
@@ -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';
@@ -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;
@@ -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,
@@ -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;
diff --git a/backend/app/Http/Request/EventSettings/UpdateEventSettingsRequest.php b/backend/app/Http/Request/EventSettings/UpdateEventSettingsRequest.php
index beb64f2b7c..d99533f0c1 100644
--- a/backend/app/Http/Request/EventSettings/UpdateEventSettingsRequest.php
+++ b/backend/app/Http/Request/EventSettings/UpdateEventSettingsRequest.php
@@ -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'],
diff --git a/backend/app/Resources/Event/EventSettingsResource.php b/backend/app/Resources/Event/EventSettingsResource.php
index b61c69bf09..c10b065be9 100644
--- a/backend/app/Resources/Event/EventSettingsResource.php
+++ b/backend/app/Resources/Event/EventSettingsResource.php
@@ -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(),
diff --git a/backend/app/Resources/Event/EventSettingsResourcePublic.php b/backend/app/Resources/Event/EventSettingsResourcePublic.php
index 02ed37b5c2..822d6a0cc3 100644
--- a/backend/app/Resources/Event/EventSettingsResourcePublic.php
+++ b/backend/app/Resources/Event/EventSettingsResourcePublic.php
@@ -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(),
diff --git a/backend/app/Services/Application/Handlers/EventSettings/DTO/UpdateEventSettingsDTO.php b/backend/app/Services/Application/Handlers/EventSettings/DTO/UpdateEventSettingsDTO.php
index ca73dec5a3..86ce00afdc 100644
--- a/backend/app/Services/Application/Handlers/EventSettings/DTO/UpdateEventSettingsDTO.php
+++ b/backend/app/Services/Application/Handlers/EventSettings/DTO/UpdateEventSettingsDTO.php
@@ -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,
@@ -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,
diff --git a/backend/app/Services/Application/Handlers/EventSettings/PartialUpdateEventSettingsHandler.php b/backend/app/Services/Application/Handlers/EventSettings/PartialUpdateEventSettingsHandler.php
index cbad542a81..f873196507 100644
--- a/backend/app/Services/Application/Handlers/EventSettings/PartialUpdateEventSettingsHandler.php
+++ b/backend/app/Services/Application/Handlers/EventSettings/PartialUpdateEventSettingsHandler.php
@@ -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(),
diff --git a/backend/app/Services/Application/Handlers/EventSettings/UpdateEventSettingsHandler.php b/backend/app/Services/Application/Handlers/EventSettings/UpdateEventSettingsHandler.php
index de98ee5862..953d819401 100644
--- a/backend/app/Services/Application/Handlers/EventSettings/UpdateEventSettingsHandler.php
+++ b/backend/app/Services/Application/Handlers/EventSettings/UpdateEventSettingsHandler.php
@@ -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,
diff --git a/backend/app/Services/Domain/Event/CreateEventService.php b/backend/app/Services/Domain/Event/CreateEventService.php
index 76e08b974d..a963784ee3 100644
--- a/backend/app/Services/Domain/Event/CreateEventService.php
+++ b/backend/app/Services/Domain/Event/CreateEventService.php
@@ -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' => [
diff --git a/backend/database/migrations/2026_06_04_120000_add_allow_copy_details_to_all_attendees_to_event_settings.php b/backend/database/migrations/2026_06_04_120000_add_allow_copy_details_to_all_attendees_to_event_settings.php
new file mode 100644
index 0000000000..d508f44bf3
--- /dev/null
+++ b/backend/database/migrations/2026_06_04_120000_add_allow_copy_details_to_all_attendees_to_event_settings.php
@@ -0,0 +1,22 @@
+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');
+ });
+ }
+};
diff --git a/backend/tests/Unit/Http/Request/EventSettings/UpdateEventSettingsRequestTest.php b/backend/tests/Unit/Http/Request/EventSettings/UpdateEventSettingsRequestTest.php
index 0e66005956..66d8ffa3f2 100644
--- a/backend/tests/Unit/Http/Request/EventSettings/UpdateEventSettingsRequestTest.php
+++ b/backend/tests/Unit/Http/Request/EventSettings/UpdateEventSettingsRequestTest.php
@@ -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'));
+ }
}
diff --git a/backend/tests/Unit/Resources/Event/EventSettingsResourcePublicTest.php b/backend/tests/Unit/Resources/Event/EventSettingsResourcePublicTest.php
new file mode 100644
index 0000000000..96fa0dccaa
--- /dev/null
+++ b/backend/tests/Unit/Resources/Event/EventSettingsResourcePublicTest.php
@@ -0,0 +1,34 @@
+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']);
+ }
+}
diff --git a/backend/tests/Unit/Services/Application/Handlers/EventSettings/PartialUpdateEventSettingsHandlerTest.php b/backend/tests/Unit/Services/Application/Handlers/EventSettings/PartialUpdateEventSettingsHandlerTest.php
new file mode 100644
index 0000000000..55d841ed00
--- /dev/null
+++ b/backend/tests/Unit/Services/Application/Handlers/EventSettings/PartialUpdateEventSettingsHandlerTest.php
@@ -0,0 +1,76 @@
+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;
+ }
+}
diff --git a/backend/tests/Unit/Services/Application/Handlers/EventSettings/UpdateEventSettingsHandlerTest.php b/backend/tests/Unit/Services/Application/Handlers/EventSettings/UpdateEventSettingsHandlerTest.php
index 57d97eebc6..9b364bfd04 100644
--- a/backend/tests/Unit/Services/Application/Handlers/EventSettings/UpdateEventSettingsHandlerTest.php
+++ b/backend/tests/Unit/Services/Application/Handlers/EventSettings/UpdateEventSettingsHandlerTest.php
@@ -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,
@@ -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,
]);
diff --git a/frontend/src/components/routes/event/Settings/Sections/HomepageAndCheckoutSettings/index.tsx b/frontend/src/components/routes/event/Settings/Sections/HomepageAndCheckoutSettings/index.tsx
index 76fba0c3c2..a62306b6dc 100644
--- a/frontend/src/components/routes/event/Settings/Sections/HomepageAndCheckoutSettings/index.tsx
+++ b/frontend/src/components/routes/event/Settings/Sections/HomepageAndCheckoutSettings/index.tsx
@@ -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,
@@ -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]);
@@ -128,6 +130,13 @@ export const HomepageAndCheckoutSettings = () => {
{...form.getInputProps('show_marketing_opt_in', {type: 'checkbox'})}
/>
+
+
diff --git a/frontend/src/components/routes/product-widget/CollectInformation/index.tsx b/frontend/src/components/routes/product-widget/CollectInformation/index.tsx
index 671cf19412..a0777f36ba 100644
--- a/frontend/src/components/routes/product-widget/CollectInformation/index.tsx
+++ b/frontend/src/components/routes/product-widget/CollectInformation/index.tsx
@@ -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) => {
@@ -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'}] : []),
]}
/>
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index 039bc3e615..707ddcabc6 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -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;