diff --git a/app/Domains/Tenant/Services/WebsiteTypeService.php b/app/Domains/Tenant/Services/WebsiteTypeService.php index f89d69f..9c57eba 100644 --- a/app/Domains/Tenant/Services/WebsiteTypeService.php +++ b/app/Domains/Tenant/Services/WebsiteTypeService.php @@ -82,6 +82,7 @@ class WebsiteTypeService && $previousLogo->id !== $websiteType->site_logo && $previousLogo->id !== $websiteType->footer_logo && $previousLogo->id !== $websiteType->favicon_id + && ! $this->isReferencedByWebsiteType($previousLogo) ) { $this->attachmentService->delete($previousLogo); } @@ -90,4 +91,16 @@ class WebsiteTypeService return $websiteType; }); } + + private function isReferencedByWebsiteType(Attachment $attachment): bool + { + return WebsiteType::query() + ->where(function ($query) use ($attachment): void { + $query + ->where('site_logo', $attachment->id) + ->orWhere('footer_logo', $attachment->id) + ->orWhere('favicon_id', $attachment->id); + }) + ->exists(); + } } diff --git a/database/migrations/2026_08_26_000000_set_website_type_favicon.php b/database/migrations/2026_08_26_000000_set_website_type_favicon.php new file mode 100644 index 0000000..e3699ed --- /dev/null +++ b/database/migrations/2026_08_26_000000_set_website_type_favicon.php @@ -0,0 +1,96 @@ + */ + private const WEBSITE_TYPE_CODES = ['shopit', 'onticket']; + + public function up(): void + { + $websiteTypes = DB::table('website_type') + ->whereIn('codigo', self::WEBSITE_TYPE_CODES) + ->get(['codigo', 'favicon_id']); + + if ($websiteTypes->isEmpty()) { + return; + } + + $faviconIds = $websiteTypes + ->pluck('favicon_id') + ->filter() + ->unique() + ->values(); + + if ( + $faviconIds->count() === 1 + && DB::table('attachments') + ->where('id', $faviconIds->first()) + ->where('filename', self::FILENAME) + ->exists() + && $websiteTypes->every( + fn (object $websiteType): bool => $websiteType->favicon_id === $faviconIds->first() + ) + ) { + return; + } + + $sourcePath = public_path('images/website_types/'.self::FILENAME); + + if (! is_file($sourcePath)) { + throw new RuntimeException("Favicon not found at path: {$sourcePath}"); + } + + $contents = file_get_contents($sourcePath); + + if ($contents === false) { + throw new RuntimeException("Could not read favicon at path: {$sourcePath}"); + } + + $key = (string) Str::uuid(); + $storedPath = "website-types/{$key}.svg"; + + if (! Storage::disk('s3')->put($storedPath, $contents)) { + throw new RuntimeException("Could not store favicon at path: {$storedPath}"); + } + + try { + DB::transaction(function () use ($contents, $key, $storedPath): void { + $attachmentId = DB::table('attachments')->insertGetId([ + 'key' => $key, + 'path' => $storedPath, + 'filename' => self::FILENAME, + 'type' => 'image', + 'mime_type' => 'image/svg+xml', + 'extension' => 'svg', + 'size' => strlen($contents), + 'created_at' => now(), + 'updated_at' => now(), + ]); + + DB::table('website_type') + ->whereIn('codigo', self::WEBSITE_TYPE_CODES) + ->update([ + 'favicon_id' => $attachmentId, + 'updated_at' => now(), + ]); + }); + } catch (Throwable $throwable) { + Storage::disk('s3')->delete($storedPath); + + throw $throwable; + } + } + + public function down(): void + { + // The shared attachment may be in use outside these website types. + // Keep this data migration irreversible to avoid deleting an active asset. + } +}; diff --git a/database/seeders/WebsiteTypeSeeder.php b/database/seeders/WebsiteTypeSeeder.php index 3b022a4..7db0556 100644 --- a/database/seeders/WebsiteTypeSeeder.php +++ b/database/seeders/WebsiteTypeSeeder.php @@ -36,6 +36,7 @@ class WebsiteTypeSeeder extends Seeder ...self::PRESENTATION, 'site_logo' => $this->onTicketLogo(), 'footer_logo' => $this->onTicketFooterLogo(), + 'favicon' => $this->onTicketFavicon(), ], ); @@ -70,6 +71,7 @@ class WebsiteTypeSeeder extends Seeder ...self::PRESENTATION, 'site_logo' => $this->onTicketLogo(), 'footer_logo' => $this->onTicketFooterLogo(), + 'favicon' => $shopIt->favicon()->firstOrFail()->key, ], ); @@ -175,4 +177,21 @@ class WebsiteTypeSeeder extends Seeder true, ); } + + private function onTicketFavicon(): UploadedFile + { + $path = public_path('images/website_types/onticket_favicon.svg'); + + if (! file_exists($path)) { + throw new RuntimeException("OnTicket favicon not found at path: {$path}"); + } + + return new UploadedFile( + $path, + 'onticket_favicon.svg', + 'image/svg+xml', + null, + true, + ); + } } diff --git a/public/images/website_types/onticket_favicon.svg b/public/images/website_types/onticket_favicon.svg new file mode 100644 index 0000000..30502c6 --- /dev/null +++ b/public/images/website_types/onticket_favicon.svg @@ -0,0 +1,3 @@ + + + diff --git a/tests/Feature/Migrations/SetWebsiteTypeFaviconTest.php b/tests/Feature/Migrations/SetWebsiteTypeFaviconTest.php new file mode 100644 index 0000000..674130f --- /dev/null +++ b/tests/Feature/Migrations/SetWebsiteTypeFaviconTest.php @@ -0,0 +1,61 @@ +insert([ + [ + 'codigo' => 'shopit', + 'nombre' => 'ShopIt', + 'created_at' => now(), + 'updated_at' => now(), + ], + [ + 'codigo' => 'onticket', + 'nombre' => 'OnTicket', + 'created_at' => now(), + 'updated_at' => now(), + ], + ]); + + $migration = require database_path( + 'migrations/2026_08_26_000000_set_website_type_favicon.php' + ); + + $migration->up(); + $migration->up(); + + $faviconIds = DB::table('website_type') + ->whereIn('codigo', ['shopit', 'onticket']) + ->pluck('favicon_id'); + + $this->assertCount(2, $faviconIds); + $this->assertNotNull($faviconIds->first()); + $this->assertSame(1, $faviconIds->unique()->count()); + + $attachment = DB::table('attachments')->where('id', $faviconIds->first())->first(); + + $this->assertNotNull($attachment); + $this->assertSame('onticket_favicon.svg', $attachment->filename); + $this->assertSame('image/svg+xml', $attachment->mime_type); + $this->assertSame('svg', $attachment->extension); + $this->assertSame(1, DB::table('attachments')->where('filename', 'onticket_favicon.svg')->count()); + Storage::disk('s3')->assertExists($attachment->path); + $this->assertSame( + file_get_contents(public_path('images/website_types/onticket_favicon.svg')), + Storage::disk('s3')->get($attachment->path), + ); + } +} diff --git a/tests/Feature/Seeders/WebsiteTypeSeederTest.php b/tests/Feature/Seeders/WebsiteTypeSeederTest.php index c91c003..d286cf6 100644 --- a/tests/Feature/Seeders/WebsiteTypeSeederTest.php +++ b/tests/Feature/Seeders/WebsiteTypeSeederTest.php @@ -21,7 +21,7 @@ class WebsiteTypeSeederTest extends TestCase $this->seed(WebsiteTypeSeeder::class); $this->assertSame(2, WebsiteType::query()->count()); - $this->assertSame(4, Attachment::query()->count()); + $this->assertSame(5, Attachment::query()->count()); $expectedPresentation = [ 'primary_color' => '#FF7006', @@ -49,6 +49,8 @@ class WebsiteTypeSeederTest extends TestCase Storage::disk('s3')->assertExists($shopIt->siteLogo->path); $this->assertSame('onticket_footer_logo.png', $shopIt->footerLogo->filename); Storage::disk('s3')->assertExists($shopIt->footerLogo->path); + $this->assertSame('onticket_favicon.svg', $shopIt->favicon->filename); + Storage::disk('s3')->assertExists($shopIt->favicon->path); $this->assertSame(['carousel'], $shopIt->extras->pluck('codigo')->all()); $this->assertSame('Carrusel principal', $shopIt->extras->sole()->nombre); $this->assertSame([ @@ -79,6 +81,8 @@ class WebsiteTypeSeederTest extends TestCase Storage::disk('s3')->assertExists($onTicket->footerLogo->path); $this->assertNotSame($shopIt->site_logo, $onTicket->site_logo); $this->assertNotSame($shopIt->footer_logo, $onTicket->footer_logo); + $this->assertSame($shopIt->favicon_id, $onTicket->favicon_id); + $this->assertSame('onticket_favicon.svg', $onTicket->favicon->filename); $this->assertEqualsCanonicalizing( ['heroConfig', 'eventConfig', 'additionalInfoConfig'], $onTicket->extras->pluck('codigo')->all(), diff --git a/tests/Feature/Tenant/WebsiteTypeServiceTest.php b/tests/Feature/Tenant/WebsiteTypeServiceTest.php index 176f6e8..39cd9a2 100644 --- a/tests/Feature/Tenant/WebsiteTypeServiceTest.php +++ b/tests/Feature/Tenant/WebsiteTypeServiceTest.php @@ -69,4 +69,31 @@ class WebsiteTypeServiceTest extends TestCase 'favicon_id' => $favicon->id, ]); } + + public function test_it_keeps_a_shared_favicon_when_one_website_type_replaces_it(): void + { + Storage::fake('s3'); + + $service = app(WebsiteTypeService::class); + $shopIt = $service->create([ + 'codigo' => 'shopit', + 'nombre' => 'ShopIt', + 'favicon' => UploadedFile::fake()->image('shared-favicon.png', 32, 32), + ]); + $sharedFavicon = $shopIt->favicon()->firstOrFail(); + $onTicket = $service->create([ + 'codigo' => 'onticket', + 'nombre' => 'OnTicket', + 'favicon' => $sharedFavicon->key, + ]); + + $service->updateOrCreate( + ['codigo' => 'shopit'], + ['favicon' => UploadedFile::fake()->image('shopit-favicon.png', 32, 32)], + ); + + $this->assertSame($sharedFavicon->id, $onTicket->fresh()->favicon_id); + $this->assertDatabaseHas('attachments', ['id' => $sharedFavicon->id]); + Storage::disk('s3')->assertExists($sharedFavicon->path); + } }