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
33 changes: 33 additions & 0 deletions backend/app/DomainObjects/Enums/TrackingPixelProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

namespace HiEvents\DomainObjects\Enums;

enum TrackingPixelProvider: string
{
use BaseEnum;

case FACEBOOK_PIXEL = 'facebook_pixel';
case GOOGLE_ANALYTICS_4 = 'google_analytics_4';
case GOOGLE_TAG_MANAGER = 'google_tag_manager';
case TIKTOK_PIXEL = 'tiktok_pixel';

public function pixelIdPattern(): string
{
return match ($this) {
self::FACEBOOK_PIXEL => '/^\d{9,20}$/',
self::GOOGLE_ANALYTICS_4 => '/^G-[a-zA-Z0-9]{6,20}$/',
self::GOOGLE_TAG_MANAGER => '/^GTM-[a-zA-Z0-9]{4,20}$/',
self::TIKTOK_PIXEL => '/^[a-zA-Z0-9]{6,30}$/',
};
}

public function pixelIdFormatDescription(): string
{
return match ($this) {
self::FACEBOOK_PIXEL => __('Must be 9-20 digits (e.g., 1234567890)'),
self::GOOGLE_ANALYTICS_4 => __('Must start with G- followed by 6-20 characters (e.g., G-XXXXXXXXXX)'),
self::GOOGLE_TAG_MANAGER => __('Must start with GTM- followed by 4-20 characters (e.g., GTM-XXXXXXX)'),
self::TIKTOK_PIXEL => __('Must be 6-30 alphanumeric characters'),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ abstract class OrganizerSettingDomainObjectAbstract extends \HiEvents\DomainObje
final public const DEFAULT_SHOW_MARKETING_OPT_IN = 'default_show_marketing_opt_in';
final public const DEFAULT_PASS_PLATFORM_FEE_TO_BUYER = 'default_pass_platform_fee_to_buyer';
final public const DEFAULT_ALLOW_ATTENDEE_SELF_EDIT = 'default_allow_attendee_self_edit';
final public const TRACKING_PIXELS = 'tracking_pixels';
final public const TRACKING_CONSENT_ACKNOWLEDGED = 'tracking_consent_acknowledged';

protected int $id;
protected int $organizer_id;
Expand All @@ -49,6 +51,8 @@ abstract class OrganizerSettingDomainObjectAbstract extends \HiEvents\DomainObje
protected bool $default_show_marketing_opt_in = true;
protected bool $default_pass_platform_fee_to_buyer = false;
protected bool $default_allow_attendee_self_edit = true;
protected array|string|null $tracking_pixels = null;
protected bool $tracking_consent_acknowledged = false;

public function toArray(): array
{
Expand All @@ -72,6 +76,8 @@ public function toArray(): array
'default_show_marketing_opt_in' => $this->default_show_marketing_opt_in ?? null,
'default_pass_platform_fee_to_buyer' => $this->default_pass_platform_fee_to_buyer ?? null,
'default_allow_attendee_self_edit' => $this->default_allow_attendee_self_edit ?? null,
'tracking_pixels' => $this->tracking_pixels ?? null,
'tracking_consent_acknowledged' => $this->tracking_consent_acknowledged ?? null,
];
}

Expand Down Expand Up @@ -284,4 +290,26 @@ public function getDefaultAllowAttendeeSelfEdit(): bool
{
return $this->default_allow_attendee_self_edit;
}

public function setTrackingPixels(array|string|null $tracking_pixels): self
{
$this->tracking_pixels = $tracking_pixels;
return $this;
}

public function getTrackingPixels(): array|string|null
{
return $this->tracking_pixels;
}

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

public function getTrackingConsentAcknowledged(): bool
{
return $this->tracking_consent_acknowledged;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,40 @@
use HiEvents\DomainObjects\Enums\AttendeeDetailsCollectionMethod;
use HiEvents\DomainObjects\Enums\HomepageBackgroundType;
use HiEvents\DomainObjects\Enums\OrganizerHomepageVisibility;
use HiEvents\DomainObjects\Enums\TrackingPixelProvider;
use HiEvents\Http\Request\BaseRequest;
use HiEvents\Validators\Rules\RulesHelper;
use Illuminate\Validation\Rule;

class PartialUpdateOrganizerSettingsRequest extends BaseRequest
{
public function after(): array
{
return [
function ($validator) {
$pixels = $this->input('tracking_pixels', []);
if (!is_array($pixels)) {
return;
}

foreach ($pixels as $index => $pixel) {
$providerValue = $pixel['provider'] ?? null;
$pixelId = $pixel['pixel_id'] ?? '';
$provider = TrackingPixelProvider::tryFrom($providerValue);

if ($provider && $pixelId !== '') {
if (!preg_match($provider->pixelIdPattern(), $pixelId)) {
$validator->errors()->add(
"tracking_pixels.{$index}.pixel_id",
$provider->pixelIdFormatDescription()
);
}
}
}
},
];
}

public static function rules(): array
{
return [
Expand Down Expand Up @@ -73,6 +101,13 @@ public static function rules(): array

// Password
'homepage_password' => ['sometimes', 'nullable', 'string', 'max:100'],

// Tracking pixels
'tracking_pixels' => ['sometimes', 'nullable', 'array', 'max:10'],
'tracking_pixels.*.provider' => ['required', 'string', Rule::in(TrackingPixelProvider::valuesArray())],
'tracking_pixels.*.pixel_id' => ['required', 'string', 'max:50', 'regex:/^[a-zA-Z0-9\-_]+$/'],
'tracking_pixels.*.enabled' => ['required', 'boolean'],
'tracking_consent_acknowledged' => ['sometimes', 'nullable', 'boolean'],
];
}
}
1 change: 1 addition & 0 deletions backend/app/Models/OrganizerSetting.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public function getCastMap(): array
'social_media_handles' => 'array',
'homepage_theme_settings' => 'array',
'location_details' => 'array',
'tracking_pixels' => 'array',
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,19 @@
use HiEvents\DomainObjects\OrganizerSettingDomainObject;

/**
* We can extend the OrganizerSettingsResource for now
*
* @mixin OrganizerSettingDomainObject
*/
class OrganizerSettingsPublicResource extends OrganizerSettingsResource
{
public function toArray($request): array
{
$data = parent::toArray($request);

unset(
$data['tracking_consent_acknowledged'],
$data['homepage_password'],
);

return $data;
}
}
2 changes: 2 additions & 0 deletions backend/app/Resources/Organizer/OrganizerSettingsResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public function toArray($request): array
'seo_description' => $this->getSeoDescription(),
'allow_search_engine_indexing' => $this->getAllowSearchEngineIndexing(),
'location_details' => $this->getLocationDetails(),
'tracking_pixels' => $this->getTrackingPixels(),
'tracking_consent_acknowledged' => $this->getTrackingConsentAcknowledged(),
];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ public function __construct(

// Password
public readonly string|Optional|null $homepagePassword,

// Tracking pixels
public readonly array|Optional|null $trackingPixels,
public readonly bool|Optional|null $trackingConsentAcknowledged,
)
{
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,9 @@ public function handle(PartialUpdateOrganizerSettingsDTO $dto): OrganizerSetting
'allow_search_engine_indexing' => $dto->getProvided('allowSearchEngineIndexing', $organizerSettings->getAllowSearchEngineIndexing()),

'homepage_password' => $dto->getProvided('homepagePassword', $organizerSettings->getHomepagePassword()),

'tracking_pixels' => $dto->getProvided('trackingPixels', $organizerSettings->getTrackingPixels()),
'tracking_consent_acknowledged' => $dto->getProvided('trackingConsentAcknowledged', $organizerSettings->getTrackingConsentAcknowledged()),
], [
'organizer_id' => $dto->organizerId,
'id' => $organizerSettings->getId(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?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('organizer_settings', function (Blueprint $table) {
$table->jsonb('tracking_pixels')->nullable();
$table->boolean('tracking_consent_acknowledged')->default(false);
});
}

public function down(): void
{
Schema::table('organizer_settings', function (Blueprint $table) {
$table->dropColumn('tracking_pixels');
$table->dropColumn('tracking_consent_acknowledged');
});
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
<?php

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

use HiEvents\DomainObjects\OrganizerDomainObject;
use HiEvents\DomainObjects\OrganizerSettingDomainObject;
use HiEvents\Repository\Interfaces\OrganizerRepositoryInterface;
use HiEvents\Repository\Interfaces\OrganizerSettingsRepositoryInterface;
use HiEvents\Services\Application\Handlers\Organizer\DTO\PartialUpdateOrganizerSettingsDTO;
use HiEvents\Services\Application\Handlers\Organizer\Settings\PartialUpdateOrganizerSettingsHandler;
use Mockery;
use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
use Tests\TestCase;

class PartialUpdateOrganizerSettingsHandlerTest extends TestCase
{
use MockeryPHPUnitIntegration;

private OrganizerSettingsRepositoryInterface $settingsRepository;
private OrganizerRepositoryInterface $organizerRepository;
private PartialUpdateOrganizerSettingsHandler $handler;

protected function setUp(): void
{
parent::setUp();

$this->settingsRepository = Mockery::mock(OrganizerSettingsRepositoryInterface::class);
$this->organizerRepository = Mockery::mock(OrganizerRepositoryInterface::class);

$this->handler = new PartialUpdateOrganizerSettingsHandler(
organizerSettingsRepository: $this->settingsRepository,
organizerRepository: $this->organizerRepository,
);
}

public function testTrackingPixelsArePersisted(): void
{
$organizer = new OrganizerDomainObject();
$organizer->setId(1);

$existingSettings = new OrganizerSettingDomainObject();
$existingSettings->setId(10);
$existingSettings->setOrganizerId(1);

$this->organizerRepository
->shouldReceive('findFirstWhere')
->with(['id' => 1, 'account_id' => '100'])
->once()
->andReturn($organizer);

$this->settingsRepository
->shouldReceive('findFirstWhere')
->with(['organizer_id' => 1])
->once()
->andReturn($existingSettings);

$trackingPixels = [
['provider' => 'facebook_pixel', 'pixel_id' => '1234567890', 'enabled' => true],
['provider' => 'google_tag_manager', 'pixel_id' => 'GTM-XXXXXXX', 'enabled' => true],
];

$this->settingsRepository
->shouldReceive('updateWhere')
->once()
->with(
Mockery::on(function (array $attributes) use ($trackingPixels) {
return $attributes['tracking_pixels'] === $trackingPixels
&& $attributes['tracking_consent_acknowledged'] === true;
}),
Mockery::any()
);

$updatedSettings = new OrganizerSettingDomainObject();
$updatedSettings->setId(10);

$this->settingsRepository
->shouldReceive('findFirst')
->with(10)
->once()
->andReturn($updatedSettings);

$dto = PartialUpdateOrganizerSettingsDTO::from([
'organizer_id' => 1,
'account_id' => '100',
'tracking_pixels' => $trackingPixels,
'tracking_consent_acknowledged' => true,
]);

$result = $this->handler->handle($dto);

$this->assertInstanceOf(OrganizerSettingDomainObject::class, $result);
}

public function testTrackingPixelsDefaultToExistingWhenNotProvided(): void
{
$organizer = new OrganizerDomainObject();
$organizer->setId(1);

$existingPixels = [
['provider' => 'facebook_pixel', 'pixel_id' => '9999999', 'enabled' => true],
];

$existingSettings = new OrganizerSettingDomainObject();
$existingSettings->setId(10);
$existingSettings->setOrganizerId(1);
$existingSettings->setTrackingPixels($existingPixels);
$existingSettings->setTrackingConsentAcknowledged(true);

$this->organizerRepository
->shouldReceive('findFirstWhere')
->once()
->andReturn($organizer);

$this->settingsRepository
->shouldReceive('findFirstWhere')
->once()
->andReturn($existingSettings);

$this->settingsRepository
->shouldReceive('updateWhere')
->once()
->with(
Mockery::on(function (array $attributes) use ($existingPixels) {
return $attributes['tracking_pixels'] === $existingPixels
&& $attributes['tracking_consent_acknowledged'] === true;
}),
Mockery::any()
);

$this->settingsRepository
->shouldReceive('findFirst')
->with(10)
->once()
->andReturn($existingSettings);

// Only update SEO title, not tracking pixels
$dto = PartialUpdateOrganizerSettingsDTO::from([
'organizer_id' => 1,
'account_id' => '100',
'seo_title' => 'New Title',
]);

$this->handler->handle($dto);
}
}
Loading
Loading