feat: implement product controller, service, and feature tests with updated API collection.
This commit is contained in:
@@ -15,19 +15,10 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|||||||
|
|
||||||
class ProductController extends Controller
|
class ProductController extends Controller
|
||||||
{
|
{
|
||||||
public function index(Tenant $tenant): JsonResponse
|
public function index(Tenant $tenant, ProductService $productService): JsonResponse
|
||||||
{
|
{
|
||||||
return ProductResource::collection(
|
return ProductResource::collection(
|
||||||
Product::query()
|
$productService->getProductos($tenant)
|
||||||
->where('tenant_codigo', $tenant->codigo)
|
|
||||||
->with([
|
|
||||||
'attachments' => fn ($query) => $query->orderBy('attachments.id')->limit(1),
|
|
||||||
'brand',
|
|
||||||
'category',
|
|
||||||
'attributes.options',
|
|
||||||
])
|
|
||||||
->latest()
|
|
||||||
->paginateFromRequest()
|
|
||||||
)->response();
|
)->response();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -244,4 +244,37 @@ class ProductService
|
|||||||
Product::deleteAttribute($attribute);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,4 +90,137 @@ class ProductVariantAttachmentTest extends TestCase
|
|||||||
$this->assertTrue($variant->attachments->contains($attachment1));
|
$this->assertTrue($variant->attachments->contains($attachment1));
|
||||||
$this->assertTrue($variant->attachments->contains($attachment2));
|
$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']);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user