Compare commits

..

2 Commits

12 changed files with 240 additions and 39 deletions

View File

@@ -2,8 +2,10 @@
namespace App\Domains\Catalog\Controllers;
use App\Domains\Catalog\Enums\GroupLayout;
use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\FeaturedGroup;
use App\Domains\Catalog\Models\FeaturedItem;
use App\Domains\Catalog\Requests\CatalogItemDetailRequest;
use App\Domains\Catalog\Requests\FeaturedGroupPageRequest;
use App\Domains\Catalog\Requests\StoreCatalogItemRequest;
@@ -14,6 +16,7 @@ use App\Domains\Catalog\Resources\CatalogItemResource;
use App\Domains\Catalog\Services\CatalogService;
use App\Domains\Tenant\Models\Tenant;
use App\Http\Controllers\Controller;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Http\JsonResponse;
use Illuminate\Pagination\LengthAwarePaginator;
@@ -81,9 +84,19 @@ class CatalogController extends Controller
->setStatusCode(201);
}
/** @return array<string, mixed> */
/** @return array<array-key, mixed> */
private function featuredItemsResponse(FeaturedGroup $featuredGroup, int $page): array
{
if ($featuredGroup->group_layout !== GroupLayout::Paginated) {
$featuredItems = $this->featuredItemsQuery($featuredGroup)->get();
$featuredItems->each(
fn ($featuredItem) => $featuredItem->setRelation('featuredGroup', $featuredGroup)
);
return CatalogFeaturedItemResource::collection($featuredItems)->resolve();
}
$paginator = $this->paginateFeaturedItems($featuredGroup, $page);
$paginator->getCollection()->each(
@@ -99,25 +112,29 @@ class CatalogController extends Controller
FeaturedGroup $featuredGroup,
int $page,
): LengthAwarePaginator {
$paginator = $featuredGroup->featuredItems()
->with([
'catalogItem.inventory',
'catalogItem.attachments',
'catalogItem.variants.inventory',
'catalogItem.variants.attachments',
'catalogItem.variants.definitions.itemAttribute.attribute',
'catalogItem.bundleComponents.catalogItem',
'catalogItem.bundleComponents.variant.catalogItem',
])
->paginate(
perPage: self::ITEMS_PER_PAGE,
pageName: 'page',
page: $page,
);
$paginator = $this->featuredItemsQuery($featuredGroup)->paginate(
perPage: self::ITEMS_PER_PAGE,
pageName: 'page',
page: $page,
);
return $paginator->withPath(route('catalog.featured-groups.items.index', [
'tenant' => $featuredGroup->tenant_code,
'featuredGroup' => $featuredGroup->id,
]));
}
/** @return HasMany<FeaturedItem, FeaturedGroup> */
private function featuredItemsQuery(FeaturedGroup $featuredGroup): HasMany
{
return $featuredGroup->featuredItems()->with([
'catalogItem.inventory',
'catalogItem.attachments',
'catalogItem.variants.inventory',
'catalogItem.variants.attachments',
'catalogItem.variants.definitions.itemAttribute.attribute',
'catalogItem.bundleComponents.catalogItem',
'catalogItem.bundleComponents.variant.catalogItem',
]);
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Domains\Catalog\Enums;
enum GroupLayout: string
{
case Paginated = 'paginated';
case Simple = 'simple';
case SimpleVertical = 'simple_vertical';
case Carousel = 'carousel';
/**
* @return list<string>
*/
public static function values(): array
{
return array_column(self::cases(), 'value');
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Domains\Catalog\Models;
use App\Domains\Catalog\Enums\GroupLayout;
use App\Domains\Catalog\Enums\ProductLayout;
use App\Domains\Tenant\Models\Tenant;
use Illuminate\Database\Eloquent\Attributes\Fillable;
@@ -13,6 +14,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
#[Fillable([
'tenant_code',
'product_layout',
'group_layout',
'group_name',
'group_order',
])]
@@ -28,6 +30,7 @@ class FeaturedGroup extends Model
{
return [
'product_layout' => ProductLayout::class,
'group_layout' => GroupLayout::class,
'group_order' => 'integer',
];
}

View File

@@ -9,8 +9,8 @@ use Illuminate\Http\Resources\Json\JsonResource;
/** @mixin FeaturedGroup */
class CatalogFeaturedGroupResource extends JsonResource
{
/** @param array<string, mixed> $itemsPage */
public function __construct($resource, private readonly array $itemsPage)
/** @param array<array-key, mixed> $items */
public function __construct($resource, private readonly array $items)
{
parent::__construct($resource);
}
@@ -22,8 +22,9 @@ class CatalogFeaturedGroupResource extends JsonResource
'id' => $this->id,
'title' => $this->group_name,
'layout' => $this->product_layout->value,
'group_layout' => $this->group_layout->value,
'group_order' => $this->group_order,
'items' => $this->itemsPage,
'items' => $this->items,
];
}
}

View File

@@ -0,0 +1,25 @@
<?php
use App\Domains\Catalog\Enums\GroupLayout;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('featured_groups', function (Blueprint $table): void {
$table->enum('group_layout', GroupLayout::values())
->default(GroupLayout::Paginated->value)
->after('product_layout');
});
}
public function down(): void
{
Schema::table('featured_groups', function (Blueprint $table): void {
$table->dropColumn('group_layout');
});
}
};

View File

@@ -3,6 +3,7 @@
namespace Database\Seeders;
use App\Domains\Catalog\Enums\CatalogItemType;
use App\Domains\Catalog\Enums\GroupLayout;
use App\Domains\Catalog\Enums\InventoryPolicy;
use App\Domains\Catalog\Enums\ProductLayout;
use App\Domains\Catalog\Models\CatalogItem;
@@ -167,6 +168,7 @@ class FiestaFutbolInfantilProductSeeder extends Seeder
$featuredGroup = FeaturedGroup::query()->create([
'tenant_code' => $tenant->codigo,
'product_layout' => ProductLayout::Row,
'group_layout' => GroupLayout::Paginated,
'group_name' => $groupName,
'group_order' => $groupOrder++,
]);

View File

@@ -2,6 +2,7 @@
namespace Database\Seeders;
use App\Domains\Catalog\Enums\GroupLayout;
use App\Domains\Catalog\Enums\InventoryPolicy;
use App\Domains\Catalog\Enums\ProductLayout;
use App\Domains\Catalog\Models\Attribute;
@@ -167,11 +168,12 @@ class ProductCatalogFromImagesSeeder extends Seeder
{
FeaturedGroup::query()->where('tenant_code', $tenant->codigo)->delete();
$group = FeaturedGroup::query()->create([
$paginatedGroup = FeaturedGroup::query()->create([
'tenant_code' => $tenant->codigo,
'product_layout' => ProductLayout::ColumnWithImage,
'group_layout' => GroupLayout::Paginated,
'group_name' => 'Productos',
'group_order' => 0,
'group_order' => 2,
]);
$items = CatalogItem::query()
@@ -179,7 +181,7 @@ class ProductCatalogFromImagesSeeder extends Seeder
->orderBy('id')
->get('id');
$group->featuredItems()->createMany(
$paginatedGroup->featuredItems()->createMany(
$items->values()->map(
fn (CatalogItem $item, int $order): array => [
'catalog_item_id' => $item->id,
@@ -187,6 +189,29 @@ class ProductCatalogFromImagesSeeder extends Seeder
]
)->all()
);
$carouselGroup = FeaturedGroup::query()->create([
'tenant_code' => $tenant->codigo,
'product_layout' => ProductLayout::ColumnWithImage,
'group_layout' => GroupLayout::Carousel,
'group_name' => 'Productos destacados',
'group_order' => 1,
]);
$randomItems = CatalogItem::query()
->where('tenant_code', $tenant->codigo)
->inRandomOrder()
->limit(5)
->get('id');
$carouselGroup->featuredItems()->createMany(
$randomItems->values()->map(
fn (CatalogItem $item, int $order): array => [
'catalog_item_id' => $item->id,
'order' => $order,
]
)->all()
);
}
/**

View File

@@ -4,6 +4,7 @@ namespace Tests\Feature\Catalog;
use App\Domains\Attachable\Enums\AttachmentType;
use App\Domains\Attachable\Models\Attachment;
use App\Domains\Catalog\Enums\GroupLayout;
use App\Domains\Catalog\Enums\ProductLayout;
use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\FeaturedGroup;
@@ -21,7 +22,13 @@ class CatalogControllerTest extends TestCase
{
$tenant = $this->createTenant('catalog-index');
$row = $this->createGroup($tenant, ProductLayout::Row, 'Row', 2);
$cart = $this->createGroup($tenant, ProductLayout::ColumnWithCart, 'Cart', 1);
$cart = $this->createGroup(
$tenant,
ProductLayout::ColumnWithCart,
'Cart',
1,
GroupLayout::SimpleVertical,
);
$directInventory = Inventory::query()->create([
'real_stock' => 10,
@@ -50,14 +57,14 @@ class CatalogControllerTest extends TestCase
->assertJsonCount(2)
->assertJsonPath('0.title', 'Cart')
->assertJsonPath('0.layout', ProductLayout::ColumnWithCart->value)
->assertJsonPath('0.items.meta.current_page', 1)
->assertJsonPath('0.items.data.0.nombre', 'Variants')
->assertJsonPath('0.items.data.0.descripcion', 'Variants description')
->assertJsonPath('0.items.data.0.precio', '100.00')
->assertJsonPath('0.items.data.0.stock_tecnico', 7)
->assertJsonCount(2, '0.items.data.0.variants')
->assertJsonPath('0.items.data.0.variants.0.stock_tecnico', 4)
->assertJsonPath('0.items.data.0.variants.1.stock_tecnico', 3)
->assertJsonPath('0.group_layout', GroupLayout::SimpleVertical->value)
->assertJsonPath('0.items.0.nombre', 'Variants')
->assertJsonPath('0.items.0.descripcion', 'Variants description')
->assertJsonPath('0.items.0.precio', '100.00')
->assertJsonPath('0.items.0.stock_tecnico', 7)
->assertJsonCount(2, '0.items.0.variants')
->assertJsonPath('0.items.0.variants.0.stock_tecnico', 4)
->assertJsonPath('0.items.0.variants.1.stock_tecnico', 3)
->assertJsonPath('1.title', 'Row')
->assertJsonPath('1.items.data.0.stock_tecnico', 8)
->assertJsonCount(0, '1.items.data.0.variants');
@@ -137,15 +144,71 @@ class CatalogControllerTest extends TestCase
->assertJsonPath('data.0.nombre', 'Item 13');
}
public function test_non_paginated_group_layouts_return_all_items_as_a_plain_array(): void
{
$tenant = $this->createTenant('catalog-simple-layouts');
$layouts = [
GroupLayout::Simple,
GroupLayout::SimpleVertical,
GroupLayout::Carousel,
];
foreach ($layouts as $order => $groupLayout) {
$group = $this->createGroup(
$tenant,
ProductLayout::Row,
$groupLayout->value,
$order,
$groupLayout,
);
foreach (range(1, 13) as $number) {
$item = $this->createItem($tenant, "{$groupLayout->value} Item {$number}");
$group->featuredItems()->create([
'catalog_item_id' => $item->id,
'order' => $number,
]);
}
}
$response = $this->getJson("/api/tenants/{$tenant->codigo}/catalog")->assertOk();
$groups = $response->json();
foreach ($layouts as $index => $groupLayout) {
$this->assertSame($groupLayout->value, $groups[$index]['group_layout']);
$this->assertCount(13, $groups[$index]['items']);
$this->assertSame(
"{$groupLayout->value} Item 1",
$groups[$index]['items'][0]['nombre'],
);
$this->assertArrayNotHasKey('data', $groups[$index]['items']);
$this->assertArrayNotHasKey('meta', $groups[$index]['items']);
}
$carouselGroup = FeaturedGroup::query()
->where('tenant_code', $tenant->codigo)
->where('group_layout', GroupLayout::Carousel)
->sole();
$this->getJson(
"/api/tenants/{$tenant->codigo}/catalog/featured-groups/{$carouselGroup->id}/items?page=2"
)
->assertOk()
->assertJsonCount(13)
->assertJsonPath('0.nombre', 'carousel Item 1');
}
private function createGroup(
Tenant $tenant,
ProductLayout $layout,
string $name,
int $order = 0,
GroupLayout $groupLayout = GroupLayout::Paginated,
): FeaturedGroup {
return FeaturedGroup::query()->create([
'tenant_code' => $tenant->codigo,
'product_layout' => $layout,
'group_layout' => $groupLayout,
'group_name' => $name,
'group_order' => $order,
]);

View File

@@ -78,6 +78,7 @@ class CatalogSchemaTest extends TestCase
'id',
'tenant_code',
'product_layout',
'group_layout',
'group_name',
'group_order',
], Schema::getColumnListing('featured_groups'));

View File

@@ -5,6 +5,7 @@ namespace Tests\Feature\Seeders;
use App\Domains\Attachable\Enums\AttachmentType;
use App\Domains\Attachable\Models\Attachment;
use App\Domains\Catalog\Enums\CatalogItemType;
use App\Domains\Catalog\Enums\GroupLayout;
use App\Domains\Catalog\Models\Attribute;
use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\FeaturedGroup;
@@ -133,6 +134,10 @@ class FiestaFutbolInfantilProductSeederTest extends TestCase
->with('featuredItems.catalogItem')
->orderBy('group_order')
->get()
->each(fn (FeaturedGroup $group) => $this->assertSame(
GroupLayout::Paginated,
$group->group_layout,
))
->mapWithKeys(fn (FeaturedGroup $group): array => [
$group->group_name => $group->featuredItems->pluck('catalogItem.slug')->all(),
])

View File

@@ -4,6 +4,7 @@ namespace Tests\Feature\Seeders;
use App\Domains\Attachable\Enums\AttachmentType;
use App\Domains\Attachable\Models\Attachment;
use App\Domains\Catalog\Enums\GroupLayout;
use App\Domains\Catalog\Enums\ProductLayout;
use App\Domains\Catalog\Models\CatalogItem;
use App\Domains\Catalog\Models\FeaturedGroup;
@@ -20,7 +21,7 @@ class ProductCatalogFromImagesSeederTest extends TestCase
{
use RefreshDatabase;
public function test_it_features_every_sonder_product_in_a_single_image_column(): void
public function test_it_creates_paginated_and_random_carousel_groups_for_sonder(): void
{
Storage::fake('s3');
@@ -51,18 +52,44 @@ class ProductCatalogFromImagesSeederTest extends TestCase
ProductCatalogFromImagesSeeder::class,
]);
$group = FeaturedGroup::query()
$groups = FeaturedGroup::query()
->where('tenant_code', $tenant->codigo)
->with('featuredItems.catalogItem')
->sole();
->orderBy('group_order')
->get()
->keyBy('group_name');
$this->assertSame('Productos', $group->group_name);
$this->assertSame(ProductLayout::ColumnWithImage, $group->product_layout);
$this->assertSame(0, $group->group_order);
$paginatedGroup = $groups->get('Productos');
$carouselGroup = $groups->get('Productos destacados');
$catalogItemIds = CatalogItem::query()
->where('tenant_code', $tenant->codigo)
->orderBy('id')
->pluck('id');
$this->assertNotNull($paginatedGroup);
$this->assertSame(ProductLayout::ColumnWithImage, $paginatedGroup->product_layout);
$this->assertSame(GroupLayout::Paginated, $paginatedGroup->group_layout);
$this->assertSame(0, $paginatedGroup->group_order);
$this->assertSame(
CatalogItem::query()->where('tenant_code', $tenant->codigo)->orderBy('id')->pluck('slug')->all(),
$group->featuredItems->pluck('catalogItem.slug')->all(),
$paginatedGroup->featuredItems->pluck('catalogItem.slug')->all(),
);
$this->assertCount(10, $paginatedGroup->featuredItems);
$this->assertNotNull($carouselGroup);
$this->assertSame(ProductLayout::ColumnWithImage, $carouselGroup->product_layout);
$this->assertSame(GroupLayout::Carousel, $carouselGroup->group_layout);
$this->assertSame(1, $carouselGroup->group_order);
$this->assertCount(5, $carouselGroup->featuredItems);
$this->assertCount(
5,
$carouselGroup->featuredItems->pluck('catalog_item_id')->unique(),
);
$this->assertCount(
0,
$carouselGroup->featuredItems
->pluck('catalog_item_id')
->diff($catalogItemIds),
);
$this->assertCount(10, $group->featuredItems);
}
}

View File

@@ -4,6 +4,7 @@ namespace Tests\Unit\Catalog;
use App\Domains\Attachable\Models\Attachment;
use App\Domains\Catalog\Enums\CatalogItemType;
use App\Domains\Catalog\Enums\GroupLayout;
use App\Domains\Catalog\Enums\InventoryPolicy;
use App\Domains\Catalog\Enums\ProductLayout;
use App\Domains\Catalog\Models\Attribute;
@@ -25,6 +26,16 @@ use Tests\TestCase;
class CatalogModelsTest extends TestCase
{
public function test_group_layout_has_all_supported_values(): void
{
$this->assertSame([
'paginated',
'simple',
'simple_vertical',
'carousel',
], GroupLayout::values());
}
public function test_attribute_maps_its_values_and_relations(): void
{
$attribute = new Attribute;
@@ -83,6 +94,7 @@ class CatalogModelsTest extends TestCase
$group = new FeaturedGroup;
$group->setRawAttributes([
'product_layout' => ProductLayout::ColumnWithImage->value,
'group_layout' => GroupLayout::SimpleVertical->value,
'group_order' => '2',
]);
$featuredItem = new FeaturedItem;
@@ -95,6 +107,7 @@ class CatalogModelsTest extends TestCase
$this->assertSame('featured_groups', $group->getTable());
$this->assertFalse($group->usesTimestamps());
$this->assertSame(ProductLayout::ColumnWithImage, $group->product_layout);
$this->assertSame(GroupLayout::SimpleVertical, $group->group_layout);
$this->assertSame(2, $group->group_order);
$this->assertInstanceOf(Tenant::class, $group->tenant()->getRelated());
$this->assertInstanceOf(FeaturedItem::class, $group->featuredItems()->getRelated());