refactor(catalog): Complete catalog refactor to simplify its data model and its querying.
source commits: refactor/catalog
This commit is contained in:
189
tests/Unit/Catalog/CatalogModelsTest.php
Normal file
189
tests/Unit/Catalog/CatalogModelsTest.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Catalog;
|
||||
|
||||
use App\Domains\Attachable\Models\Attachment;
|
||||
use App\Domains\Catalog\Enums\CatalogItemType;
|
||||
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||
use App\Domains\Catalog\Enums\ProductLayout;
|
||||
use App\Domains\Catalog\Models\Attribute;
|
||||
use App\Domains\Catalog\Models\AttributeOption;
|
||||
use App\Domains\Catalog\Models\Brand;
|
||||
use App\Domains\Catalog\Models\BundleComponent;
|
||||
use App\Domains\Catalog\Models\CatalogItem;
|
||||
use App\Domains\Catalog\Models\Category;
|
||||
use App\Domains\Catalog\Models\FeaturedGroup;
|
||||
use App\Domains\Catalog\Models\FeaturedItem;
|
||||
use App\Domains\Catalog\Models\Inventory;
|
||||
use App\Domains\Catalog\Models\ItemAttribute;
|
||||
use App\Domains\Catalog\Models\Variant;
|
||||
use App\Domains\Catalog\Models\VariantDefinition;
|
||||
use App\Domains\Shared\Enums\FieldType;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||
use Tests\TestCase;
|
||||
|
||||
class CatalogModelsTest extends TestCase
|
||||
{
|
||||
public function test_attribute_maps_its_values_and_relations(): void
|
||||
{
|
||||
$attribute = new Attribute;
|
||||
$attribute->setRawAttributes([
|
||||
'is_required' => 1,
|
||||
'metadata_schema' => '{"swatch":true}',
|
||||
'type' => FieldType::Select->value,
|
||||
]);
|
||||
|
||||
$this->assertSame('attribute', $attribute->getTable());
|
||||
$this->assertTrue($attribute->is_required);
|
||||
$this->assertSame(['swatch' => true], $attribute->metadata_schema);
|
||||
$this->assertSame(FieldType::Select, $attribute->type);
|
||||
$this->assertInstanceOf(Tenant::class, $attribute->tenant()->getRelated());
|
||||
$this->assertInstanceOf(AttributeOption::class, $attribute->options()->getRelated());
|
||||
}
|
||||
|
||||
public function test_catalog_item_is_the_catalog_root(): void
|
||||
{
|
||||
$item = new CatalogItem;
|
||||
$item->setRawAttributes([
|
||||
'category_id' => '10',
|
||||
'brand_id' => '20',
|
||||
'inventory_id' => '30',
|
||||
'type' => CatalogItemType::Standard->value,
|
||||
'precio' => '12.50',
|
||||
'inventory_policy' => InventoryPolicy::Tracked->value,
|
||||
'has_tickets' => 1,
|
||||
]);
|
||||
|
||||
$this->assertSame('catalog_items', $item->getTable());
|
||||
$this->assertFalse($item->usesTimestamps());
|
||||
$this->assertSame(10, $item->category_id);
|
||||
$this->assertSame(20, $item->brand_id);
|
||||
$this->assertSame(30, $item->inventory_id);
|
||||
$this->assertSame(CatalogItemType::Standard, $item->type);
|
||||
$this->assertSame('12.50', $item->precio);
|
||||
$this->assertSame(InventoryPolicy::Tracked, $item->inventory_policy);
|
||||
$this->assertTrue($item->has_tickets);
|
||||
$this->assertInstanceOf(Tenant::class, $item->tenant()->getRelated());
|
||||
$this->assertInstanceOf(Category::class, $item->category()->getRelated());
|
||||
$this->assertInstanceOf(Brand::class, $item->brand()->getRelated());
|
||||
$this->assertInstanceOf(Inventory::class, $item->inventory()->getRelated());
|
||||
$this->assertInstanceOf(BundleComponent::class, $item->bundleComponents()->getRelated());
|
||||
$this->assertInstanceOf(BundleComponent::class, $item->bundleComponentUsages()->getRelated());
|
||||
$this->assertInstanceOf(Variant::class, $item->variants()->getRelated());
|
||||
$this->assertInstanceOf(Attribute::class, $item->attributes()->getRelated());
|
||||
$this->assertInstanceOf(ItemAttribute::class, $item->itemAttributes()->getRelated());
|
||||
$this->assertInstanceOf(FeaturedItem::class, $item->featuredItems()->getRelated());
|
||||
$this->assertInstanceOf(Attachment::class, $item->attachments()->getRelated());
|
||||
$this->assertSame('catalog_items_attachments', $item->attachments()->getTable());
|
||||
}
|
||||
|
||||
public function test_featured_models_map_catalog_relations_and_layout(): void
|
||||
{
|
||||
$group = new FeaturedGroup;
|
||||
$group->setRawAttributes([
|
||||
'product_layout' => ProductLayout::ColumnWithImage->value,
|
||||
'group_order' => '2',
|
||||
]);
|
||||
$featuredItem = new FeaturedItem;
|
||||
$featuredItem->setRawAttributes([
|
||||
'featured_group_id' => '10',
|
||||
'catalog_item_id' => '20',
|
||||
'order' => '3',
|
||||
]);
|
||||
|
||||
$this->assertSame('featured_groups', $group->getTable());
|
||||
$this->assertFalse($group->usesTimestamps());
|
||||
$this->assertSame(ProductLayout::ColumnWithImage, $group->product_layout);
|
||||
$this->assertSame(2, $group->group_order);
|
||||
$this->assertInstanceOf(Tenant::class, $group->tenant()->getRelated());
|
||||
$this->assertInstanceOf(FeaturedItem::class, $group->featuredItems()->getRelated());
|
||||
|
||||
$this->assertSame('featured_items', $featuredItem->getTable());
|
||||
$this->assertFalse($featuredItem->usesTimestamps());
|
||||
$this->assertSame(10, $featuredItem->featured_group_id);
|
||||
$this->assertSame(20, $featuredItem->catalog_item_id);
|
||||
$this->assertSame(3, $featuredItem->order);
|
||||
$this->assertInstanceOf(FeaturedGroup::class, $featuredItem->featuredGroup()->getRelated());
|
||||
$this->assertInstanceOf(CatalogItem::class, $featuredItem->catalogItem()->getRelated());
|
||||
}
|
||||
|
||||
public function test_variant_has_direct_catalog_and_inventory_relations(): void
|
||||
{
|
||||
$variant = new Variant;
|
||||
$variant->setRawAttributes(['catalog_item_id' => '10', 'inventory_id' => '20']);
|
||||
|
||||
$this->assertSame('variantes', $variant->getTable());
|
||||
$this->assertSame(10, $variant->catalog_item_id);
|
||||
$this->assertSame(20, $variant->inventory_id);
|
||||
$this->assertInstanceOf(CatalogItem::class, $variant->catalogItem()->getRelated());
|
||||
$this->assertInstanceOf(Inventory::class, $variant->inventory()->getRelated());
|
||||
$this->assertInstanceOf(VariantDefinition::class, $variant->definitions()->getRelated());
|
||||
$this->assertInstanceOf(Attachment::class, $variant->attachments()->getRelated());
|
||||
$this->assertSame('catalog_items_attachments', $variant->attachments()->getTable());
|
||||
}
|
||||
|
||||
public function test_inventory_maps_stock_without_a_polymorphic_owner(): void
|
||||
{
|
||||
$inventory = $this->trackedInventory(realStock: 10, reservedStock: 3);
|
||||
$inventory->sold_units = '2';
|
||||
|
||||
$this->assertSame('inventories', $inventory->getTable());
|
||||
$this->assertFalse($inventory->usesTimestamps());
|
||||
$this->assertSame(2, $inventory->sold_units);
|
||||
$this->assertSame(7, $inventory->availableStock());
|
||||
$this->assertInstanceOf(CatalogItem::class, $inventory->catalogItem()->getRelated());
|
||||
$this->assertInstanceOf(Variant::class, $inventory->variant()->getRelated());
|
||||
}
|
||||
|
||||
public function test_catalog_item_aggregates_variant_inventory(): void
|
||||
{
|
||||
$first = (new Variant)->setRelation('inventory', $this->trackedInventory(10, 3));
|
||||
$second = (new Variant)->setRelation('inventory', $this->trackedInventory(5, 1));
|
||||
$item = new CatalogItem;
|
||||
$item->inventory_policy = InventoryPolicy::Tracked;
|
||||
$item->setRelation('variants', new EloquentCollection([$first, $second]));
|
||||
$item->setRelation('inventory', null);
|
||||
|
||||
$this->assertSame(11, $item->availableStock());
|
||||
$this->assertTrue($item->isAvailable());
|
||||
}
|
||||
|
||||
public function test_catalog_item_prioritizes_its_inventory_over_variants(): void
|
||||
{
|
||||
$item = new CatalogItem;
|
||||
$item->inventory_policy = InventoryPolicy::Tracked;
|
||||
$item->setRelation('variants', new EloquentCollection([
|
||||
(new Variant)->setRelation('inventory', $this->trackedInventory(100, 0)),
|
||||
]));
|
||||
$item->setRelation('inventory', $this->trackedInventory(8, 2));
|
||||
|
||||
$this->assertSame(6, $item->availableStock());
|
||||
$this->assertTrue($item->isAvailable());
|
||||
}
|
||||
|
||||
public function test_item_attribute_and_variant_definition_use_catalog_keys(): void
|
||||
{
|
||||
$itemAttribute = new ItemAttribute;
|
||||
$definition = new VariantDefinition;
|
||||
|
||||
$this->assertSame('item_attributes', $itemAttribute->getTable());
|
||||
$this->assertInstanceOf(CatalogItem::class, $itemAttribute->catalogItem()->getRelated());
|
||||
$this->assertInstanceOf(Attribute::class, $itemAttribute->attribute()->getRelated());
|
||||
$this->assertInstanceOf(VariantDefinition::class, $itemAttribute->variantDefinitions()->getRelated());
|
||||
$this->assertSame('variant_values', $definition->getTable());
|
||||
$this->assertInstanceOf(Variant::class, $definition->variant()->getRelated());
|
||||
$this->assertInstanceOf(ItemAttribute::class, $definition->itemAttribute()->getRelated());
|
||||
}
|
||||
|
||||
private function trackedInventory(int $realStock, int $reservedStock): Inventory
|
||||
{
|
||||
$inventory = new Inventory;
|
||||
$inventory->setRawAttributes([
|
||||
'real_stock' => $realStock,
|
||||
'reserved_stock' => $reservedStock,
|
||||
]);
|
||||
|
||||
return $inventory;
|
||||
}
|
||||
}
|
||||
@@ -1,153 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Catalog;
|
||||
|
||||
use App\Domains\Attachable\Enums\AttachmentType;
|
||||
use App\Domains\Attachable\Models\Attachment;
|
||||
use App\Domains\Catalog\Enums\InventoryPolicy;
|
||||
use App\Domains\Catalog\Models\Category;
|
||||
use App\Domains\Catalog\Models\Product;
|
||||
use App\Domains\Catalog\Models\ProductVariant;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Str;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProductVariantInventoryTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private Product $product;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$headerAttachment = $this->createAttachment('header.png');
|
||||
$footerAttachment = $this->createAttachment('footer.png');
|
||||
|
||||
$tenant = Tenant::query()->create([
|
||||
'codigo' => 'inventory-test',
|
||||
'nombre' => 'Inventory Test',
|
||||
'dominio' => 'inventory.test',
|
||||
'primary_color' => '#111111',
|
||||
'secondary_color' => '#222222',
|
||||
'danger_color' => '#333333',
|
||||
'success_color' => '#28a745',
|
||||
'header_bg_color' => '#444444',
|
||||
'footer_bg_color' => '#444444',
|
||||
'header_logo_id' => $headerAttachment->id,
|
||||
'footer_logo_id' => $footerAttachment->id,
|
||||
]);
|
||||
|
||||
$category = Category::query()->create([
|
||||
'tenant_code' => $tenant->codigo,
|
||||
'nombre' => 'Inventory',
|
||||
]);
|
||||
|
||||
$this->product = Product::query()->create([
|
||||
'tenant_codigo' => $tenant->codigo,
|
||||
'categoria_id' => $category->id,
|
||||
'slug' => 'inventory-product',
|
||||
'nombre' => 'Inventory Product',
|
||||
'precio' => 100,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_it_defaults_to_tracked_inventory_with_no_sales(): void
|
||||
{
|
||||
$variant = $this->createVariant(5);
|
||||
|
||||
$this->assertSame(InventoryPolicy::Tracked, $variant->inventory_policy);
|
||||
$this->assertSame(5, $variant->availableQuantity());
|
||||
$this->assertSame(0, $variant->cantidad_vendida);
|
||||
$this->assertTrue($variant->isAvailableForSale());
|
||||
}
|
||||
|
||||
public function test_tracked_inventory_cannot_reserve_more_than_available_stock(): void
|
||||
{
|
||||
$variant = $this->createVariant(5);
|
||||
$variant->reserveStock(3);
|
||||
|
||||
$this->assertSame(2, $variant->fresh()->availableQuantity());
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$variant->reserveStock(3);
|
||||
}
|
||||
|
||||
public function test_unlimited_inventory_can_reserve_more_than_real_stock(): void
|
||||
{
|
||||
$variant = $this->createVariant(0, InventoryPolicy::Unlimited);
|
||||
$variant->reserveStock(50);
|
||||
|
||||
$variant->refresh();
|
||||
$this->assertNull($variant->availableQuantity());
|
||||
$this->assertSame(50, $variant->stock_reservado);
|
||||
$this->assertTrue($variant->isAvailableForSale());
|
||||
}
|
||||
|
||||
public function test_buying_tracked_inventory_consumes_stock_and_records_the_sale(): void
|
||||
{
|
||||
$variant = $this->createVariant(10);
|
||||
$variant->reserveStock(4);
|
||||
$variant->buy(3);
|
||||
|
||||
$variant->refresh();
|
||||
$this->assertSame(7, $variant->stock_real);
|
||||
$this->assertSame(1, $variant->stock_reservado);
|
||||
$this->assertSame(3, $variant->cantidad_vendida);
|
||||
}
|
||||
|
||||
public function test_buying_unlimited_inventory_preserves_real_stock_and_records_the_sale(): void
|
||||
{
|
||||
$variant = $this->createVariant(0, InventoryPolicy::Unlimited);
|
||||
$variant->reserveStock(4);
|
||||
$variant->buy(3);
|
||||
|
||||
$variant->refresh();
|
||||
$this->assertSame(0, $variant->stock_real);
|
||||
$this->assertSame(1, $variant->stock_reservado);
|
||||
$this->assertSame(3, $variant->cantidad_vendida);
|
||||
}
|
||||
|
||||
public function test_buy_requires_enough_reserved_stock(): void
|
||||
{
|
||||
$variant = $this->createVariant(10);
|
||||
$variant->reserveStock(1);
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$variant->buy(2);
|
||||
}
|
||||
|
||||
public function test_inventory_policy_cannot_change_after_creation(): void
|
||||
{
|
||||
$variant = $this->createVariant(10);
|
||||
$variant->inventory_policy = InventoryPolicy::Unlimited;
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('La politica de inventario no puede modificarse.');
|
||||
$variant->save();
|
||||
}
|
||||
|
||||
private function createVariant(
|
||||
int $stock,
|
||||
InventoryPolicy $inventoryPolicy = InventoryPolicy::Tracked,
|
||||
): ProductVariant {
|
||||
return ProductVariant::query()->create([
|
||||
'producto_id' => $this->product->id,
|
||||
'stock' => $stock,
|
||||
'inventory_policy' => $inventoryPolicy->value,
|
||||
]);
|
||||
}
|
||||
|
||||
private function createAttachment(string $filename): Attachment
|
||||
{
|
||||
return Attachment::query()->create([
|
||||
'key' => (string) Str::uuid(),
|
||||
'path' => 'tests/'.$filename,
|
||||
'filename' => $filename,
|
||||
'type' => AttachmentType::Image,
|
||||
'mime_type' => 'image/png',
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user