diff --git a/app/Domains/Catalog/Controllers/ProductController.php b/app/Domains/Catalog/Controllers/ProductController.php index 9ae9aeb..0eaf2e3 100644 --- a/app/Domains/Catalog/Controllers/ProductController.php +++ b/app/Domains/Catalog/Controllers/ProductController.php @@ -15,19 +15,10 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class ProductController extends Controller { - public function index(Tenant $tenant): JsonResponse + public function index(Tenant $tenant, ProductService $productService): JsonResponse { return ProductResource::collection( - Product::query() - ->where('tenant_codigo', $tenant->codigo) - ->with([ - 'attachments' => fn ($query) => $query->orderBy('attachments.id')->limit(1), - 'brand', - 'category', - 'attributes.options', - ]) - ->latest() - ->paginateFromRequest() + $productService->getProductos($tenant) )->response(); } diff --git a/app/Domains/Catalog/Services/ProductService.php b/app/Domains/Catalog/Services/ProductService.php index df070a2..02c2d85 100644 --- a/app/Domains/Catalog/Services/ProductService.php +++ b/app/Domains/Catalog/Services/ProductService.php @@ -244,4 +244,37 @@ class ProductService Product::deleteAttribute($attribute); }); } + + /** + * Get products for a tenant with resolved first image (with fallback to first variant's first image). + */ + public function getProductos(Tenant $tenant): \Illuminate\Contracts\Pagination\LengthAwarePaginator + { + $products = Product::query() + ->where('tenant_codigo', $tenant->codigo) + ->with([ + 'attachments' => fn ($query) => $query->orderBy('attachments.id'), + 'brand', + 'category', + 'variants.attachments' => fn ($query) => $query->orderBy('attachments.id'), + ]) + ->latest() + ->paginateFromRequest(); + + foreach ($products as $product) { + $resolvedAttachment = null; + if ($product->attachments->isNotEmpty()) { + $resolvedAttachment = $product->attachments->first(); + } else { + $firstVariant = $product->variants->sortBy('id')->first(); + if ($firstVariant && $firstVariant->attachments->isNotEmpty()) { + $resolvedAttachment = $firstVariant->attachments->first(); + } + } + + $product->setRelation('attachments', $resolvedAttachment ? collect([$resolvedAttachment]) : collect()); + } + + return $products; + } } diff --git a/tests/Feature/Catalog/ProductVariantAttachmentTest.php b/tests/Feature/Catalog/ProductVariantAttachmentTest.php index 875bbea..b1e9927 100644 --- a/tests/Feature/Catalog/ProductVariantAttachmentTest.php +++ b/tests/Feature/Catalog/ProductVariantAttachmentTest.php @@ -90,4 +90,137 @@ class ProductVariantAttachmentTest extends TestCase $this->assertTrue($variant->attachments->contains($attachment1)); $this->assertTrue($variant->attachments->contains($attachment2)); } + + public function test_getProductos_listing_image_fallback(): void + { + // 1. Create Tenant + $hdrKey = (string) Str::uuid(); + $ftrKey = (string) Str::uuid(); + $headerAttachment = Attachment::create([ + 'key' => $hdrKey, + 'path' => 'tenants/' . $hdrKey . '.png', + 'filename' => 'logo_header.png', + 'type' => \App\Domains\Attachable\Enums\AttachmentType::Image, + 'mime_type' => 'image/png', + ]); + $footerAttachment = Attachment::create([ + 'key' => $ftrKey, + 'path' => 'tenants/' . $ftrKey . '.png', + 'filename' => 'logo_footer.png', + 'type' => \App\Domains\Attachable\Enums\AttachmentType::Image, + 'mime_type' => 'image/png', + ]); + + $tenant = Tenant::create([ + 'codigo' => 'acme', + 'nombre' => 'Acme Inc.', + 'dominio' => 'acme.com', + 'primary_color' => '#ffffff', + 'secondary_color' => '#ffffff', + 'danger_color' => '#ffffff', + 'header_footer_bg_color' => '#ffffff', + 'header_logo_id' => $headerAttachment->id, + 'footer_logo_id' => $footerAttachment->id, + ]); + + // 2. Create Product 1 (has 2 attachments itself) + $product1 = Product::create([ + 'tenant_codigo' => $tenant->codigo, + 'categoria_id' => 1, + 'slug' => 'product-1', + 'nombre' => 'Product 1', + 'precio' => 10.00, + ]); + $p1Attachment1 = Attachment::create([ + 'key' => (string) Str::uuid(), + 'path' => 'attachments/p1_1.png', + 'filename' => 'p1_1.png', + 'type' => \App\Domains\Attachable\Enums\AttachmentType::Image, + 'mime_type' => 'image/png', + ]); + $p1Attachment2 = Attachment::create([ + 'key' => (string) Str::uuid(), + 'path' => 'attachments/p1_2.png', + 'filename' => 'p1_2.png', + 'type' => \App\Domains\Attachable\Enums\AttachmentType::Image, + 'mime_type' => 'image/png', + ]); + $product1->attachments()->attach([$p1Attachment1->id, $p1Attachment2->id]); + + // 3. Create Product 2 (no attachments itself, has 2 variants: first variant has 2 attachments, second has 1) + $product2 = Product::create([ + 'tenant_codigo' => $tenant->codigo, + 'categoria_id' => 1, + 'slug' => 'product-2', + 'nombre' => 'Product 2', + 'precio' => 20.00, + ]); + $v1 = ProductVariant::create([ + 'producto_id' => $product2->id, + 'slug' => 'p2-v1', + 'nombre' => 'P2 V1', + 'stock' => 10, + 'precio' => 20.00, + ]); + $v2 = ProductVariant::create([ + 'producto_id' => $product2->id, + 'slug' => 'p2-v2', + 'nombre' => 'P2 V2', + 'stock' => 5, + 'precio' => 20.00, + ]); + $v1Attachment1 = Attachment::create([ + 'key' => (string) Str::uuid(), + 'path' => 'attachments/v1_1.png', + 'filename' => 'v1_1.png', + 'type' => \App\Domains\Attachable\Enums\AttachmentType::Image, + 'mime_type' => 'image/png', + ]); + $v1Attachment2 = Attachment::create([ + 'key' => (string) Str::uuid(), + 'path' => 'attachments/v1_2.png', + 'filename' => 'v1_2.png', + 'type' => \App\Domains\Attachable\Enums\AttachmentType::Image, + 'mime_type' => 'image/png', + ]); + $v1->attachments()->attach([$v1Attachment1->id, $v1Attachment2->id]); + + $v2Attachment = Attachment::create([ + 'key' => (string) Str::uuid(), + 'path' => 'attachments/v2_1.png', + 'filename' => 'v2_1.png', + 'type' => \App\Domains\Attachable\Enums\AttachmentType::Image, + 'mime_type' => 'image/png', + ]); + $v2->attachments()->attach([$v2Attachment->id]); + + // 4. Create Product 3 (no attachments, no variants) + $product3 = Product::create([ + 'tenant_codigo' => $tenant->codigo, + 'categoria_id' => 1, + 'slug' => 'product-3', + 'nombre' => 'Product 3', + 'precio' => 30.00, + ]); + + // Call the listing API + $response = $this->getJson("/api/tenants/{$tenant->codigo}/productos"); + $response->assertOk(); + + // Check response data + // Since we order products by latest() (created_at desc), the order is: Product 3, Product 2, Product 1. + $data = $response->json('data'); + $this->assertCount(3, $data); + + // Product 1 (index 0) has attachments -> should have exactly its first attachment (p1Attachment1) + $this->assertCount(1, $data[0]['images']); + $this->assertStringContainsString($p1Attachment1->path, $data[0]['images'][0]); + + // Product 2 (index 1) has no attachments, falls back to first variant (v1) first attachment (v1Attachment1) -> should have exactly 1 image + $this->assertCount(1, $data[1]['images']); + $this->assertStringContainsString($v1Attachment1->path, $data[1]['images'][0]); + + // Product 3 (index 2) has no attachments, no variants -> should have empty images + $this->assertEmpty($data[2]['images']); + } }