feat: implement product controller, service, and feature tests with updated API collection.

This commit is contained in:
ncoronel 2026-06-29 17:01:21 -03:00
parent 14f3906ef0
commit 46e7c27ae7
3 changed files with 168 additions and 11 deletions

View File

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

View File

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

View File

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