Merge branch 'fix/website_types_favicons' into dev

This commit is contained in:
2026-08-26 14:40:46 -03:00
13 changed files with 235 additions and 3 deletions

View File

@@ -30,6 +30,7 @@ class AdminAppBootstrapResource extends JsonResource
'login_header_footer_color' => $websiteType->login_header_footer_color,
'site_logo' => $websiteType->siteLogo?->getTemporaryUrl(1440),
'footer_logo' => $websiteType->footerLogo?->getTemporaryUrl(1440),
'favicon' => $websiteType->favicon?->getTemporaryUrl(1440),
];
}
}

View File

@@ -11,7 +11,7 @@ class AdminAppBootstrapService
{
return [
'website_type' => WebsiteType::query()
->with(['siteLogo', 'footerLogo'])
->with(['siteLogo', 'footerLogo', 'favicon'])
->where('dominio', $domain)
->firstOrFail(),
];

View File

@@ -11,7 +11,7 @@ class ScannerBootstrapService
{
return [
'website_type' => WebsiteType::query()
->with(['siteLogo', 'footerLogo'])
->with(['siteLogo', 'footerLogo', 'favicon'])
->where('scanner_domain', $domain)
->firstOrFail(),
];

View File

@@ -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();
}
}

View File

@@ -0,0 +1,96 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
return new class extends Migration
{
private const FILENAME = 'onticket_favicon.svg';
/** @var list<string> */
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.
}
};

View File

@@ -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,
);
}
}

View File

@@ -0,0 +1,3 @@
<svg width="63" height="36" viewBox="0 0 63 36" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M44.64 0H17.94C8.05 0 0 8.05 0 17.94C0 27.83 8.05 35.88 17.94 35.88H44.64C54.53 35.88 62.58 27.83 62.58 17.94C62.58 8.05 54.53 0 44.64 0ZM44.62 31.78C36.98 31.78 30.79 25.59 30.79 17.95C30.79 10.31 36.98 4.12 44.62 4.12C52.26 4.12 58.45 10.31 58.45 17.95C58.45 25.59 52.26 31.78 44.62 31.78Z" fill="#FF7006"/>
</svg>

After

Width:  |  Height:  |  Size: 422 B

View File

@@ -0,0 +1,61 @@
<?php
namespace Tests\Feature\Migrations;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Tests\TestCase;
class SetWebsiteTypeFaviconTest extends TestCase
{
use RefreshDatabase;
public function test_it_uploads_one_favicon_and_shares_the_attachment_between_website_types(): void
{
Storage::fake('s3');
DB::table('website_type')->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),
);
}
}

View File

@@ -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(),

View File

@@ -14,6 +14,7 @@ class BootstrapAdminAppControllerTest extends TestCase
public function test_it_publicly_bootstraps_the_admin_app_by_domain(): void
{
$footerLogo = Attachment::factory()->create();
$favicon = Attachment::factory()->create();
WebsiteType::query()->create([
'codigo' => 'shopit',
@@ -31,6 +32,7 @@ class BootstrapAdminAppControllerTest extends TestCase
'border_color' => '#eaeaea',
'login_header_footer_color' => '#313131',
'footer_logo' => $footerLogo->id,
'favicon_id' => $favicon->id,
]);
$this->getJson('/api/v1/adminapp/bootstrap/ADMIN.SHOPIT.TEST')
@@ -41,6 +43,7 @@ class BootstrapAdminAppControllerTest extends TestCase
->assertJsonPath('data.login_header_footer_color', '#313131')
->assertJsonPath('data.site_logo', null)
->assertJsonPath('data.footer_logo', $footerLogo->getTemporaryUrl(1440))
->assertJsonPath('data.favicon', $favicon->getTemporaryUrl(1440))
->assertJsonMissingPath('data.forms')
->assertJsonMissingPath('data.codigo')
->assertJsonMissingPath('data.nombre')

View File

@@ -14,6 +14,7 @@ class BootstrapScannerControllerTest extends TestCase
public function test_it_publicly_bootstraps_the_scanner_by_scanner_domain(): void
{
$siteLogo = Attachment::factory()->create();
$favicon = Attachment::factory()->create();
WebsiteType::query()->create([
'codigo' => 'shopit',
@@ -32,6 +33,7 @@ class BootstrapScannerControllerTest extends TestCase
'border_color' => '#eaeaea',
'login_header_footer_color' => '#313131',
'site_logo' => $siteLogo->id,
'favicon_id' => $favicon->id,
]);
$this->getJson('/api/v1/scanner/bootstrap/SCANNER.SHOPIT.TEST')
@@ -40,6 +42,7 @@ class BootstrapScannerControllerTest extends TestCase
->assertJsonPath('data.primary_color', '#112233')
->assertJsonPath('data.site_logo', $siteLogo->getTemporaryUrl(1440))
->assertJsonPath('data.footer_logo', null)
->assertJsonPath('data.favicon', $favicon->getTemporaryUrl(1440))
->assertJsonMissingPath('data.codigo')
->assertJsonMissingPath('data.nombre')
->assertJsonMissingPath('data.dominio')

View File

@@ -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);
}
}

View File

@@ -17,11 +17,13 @@ class AdminAppBootstrapResourceTest extends TestCase
]);
$websiteType->setRelation('siteLogo', null);
$websiteType->setRelation('footerLogo', null);
$websiteType->setRelation('favicon', null);
$data = AdminAppBootstrapResource::make([
'website_type' => $websiteType,
])->resolve(request());
$this->assertSame('shopit', $data['website_type_code']);
$this->assertNull($data['favicon']);
$this->assertArrayNotHasKey('forms', $data);
}
}