feat: Implement inventory policy for product variants

- Introduced InventoryPolicy enum to manage tracked and unlimited inventory types.
- Updated ProductVariant model to include inventory_policy and cantidad_vendida attributes.
- Modified addItem and updateItem methods in Cart to check available quantity based on inventory policy.
- Added migration to include inventory_policy and cantidad_vendida in productos_variantes table.
- Enhanced tests to validate inventory behavior for both tracked and unlimited policies.
- Updated various request and resource classes to handle new inventory fields.
This commit is contained in:
2026-07-16 08:36:59 -03:00
parent 716d8e447c
commit 10157d7c63
16 changed files with 547 additions and 73 deletions

View File

@@ -0,0 +1,153 @@
<?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',
]);
}
}