feat(catalog): add scope to filter available variants and update related queries; enhance seeder and tests for stock validation
This commit is contained in:
@@ -10,6 +10,7 @@ use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Ticket\Models\Ticket;
|
||||
use App\Domains\Ticket\Models\ValidityTime;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
@@ -168,6 +169,21 @@ class CatalogItem extends Model
|
||||
return ($this->availableStock() ?? 0) > 0;
|
||||
}
|
||||
|
||||
/** @param Builder<CatalogItem> $query */
|
||||
public function scopeWhereVariantsAvailable(Builder $query): Builder
|
||||
{
|
||||
return $query->where(function (Builder $query): void {
|
||||
$query
|
||||
->whereDoesntHave('variants')
|
||||
->orWhere('catalog_items.inventory_policy', InventoryPolicy::Unlimited->value)
|
||||
->orWhereHas(
|
||||
'variants.inventory',
|
||||
fn (Builder $inventoryQuery): Builder => $inventoryQuery
|
||||
->whereColumn('inventories.real_stock', '>', 'inventories.reserved_stock')
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** @return Collection<int, Variant> */
|
||||
public function visibleVariants(?int $includedVariantId = null): Collection
|
||||
{
|
||||
|
||||
@@ -207,6 +207,7 @@ class CatalogService
|
||||
|
||||
$paginator = CatalogItem::query()
|
||||
->where('tenant_code', $tenant->codigo)
|
||||
->whereVariantsAvailable()
|
||||
->where(function (Builder $query) use ($containsPattern): void {
|
||||
$query
|
||||
->whereRaw('LOWER(nombre) LIKE ?', [$containsPattern])
|
||||
@@ -257,6 +258,7 @@ class CatalogService
|
||||
return CatalogItem::query()
|
||||
->where('tenant_code', $tenant->codigo)
|
||||
->where('category_id', $category->id)
|
||||
->whereVariantsAvailable()
|
||||
->with([
|
||||
'attachments',
|
||||
'inventory',
|
||||
|
||||
@@ -37,6 +37,7 @@ class FeaturedGroupService
|
||||
{
|
||||
$query = CatalogItem::query()
|
||||
->where('catalog_items.tenant_code', $featuredGroup->tenant_code)
|
||||
->whereVariantsAvailable()
|
||||
->with([
|
||||
'inventory',
|
||||
'attachments',
|
||||
|
||||
@@ -63,8 +63,8 @@ class FiestaFutbolInfantilProductSeeder extends Seeder
|
||||
'attribute_codes' => ['color', 'talle'],
|
||||
'variants' => collect(['Verde', 'Blanco'])
|
||||
->crossJoin(['14', 'S', 'M', 'L', 'XL', 'XXL'])
|
||||
->map(fn (array $values): array => [
|
||||
'real_stock' => 0,
|
||||
->map(fn (array $values, int $index): array => [
|
||||
'real_stock' => [24, 3, 18, 0, 12, 2, 25, 0, 14, 1, 19, 8][$index],
|
||||
'values' => ['color' => $values[0], 'talle' => $values[1]],
|
||||
])->all(),
|
||||
]);
|
||||
@@ -75,8 +75,8 @@ class FiestaFutbolInfantilProductSeeder extends Seeder
|
||||
'category_id' => $categories['alojamientos']->id,
|
||||
'precio' => 35000,
|
||||
'attribute_codes' => ['tipo_alojamiento'],
|
||||
'variants' => collect(['Carpa', 'Motorhome'])->map(fn (string $type): array => [
|
||||
'real_stock' => 0,
|
||||
'variants' => collect(['Carpa', 'Motorhome'])->map(fn (string $type, int $index): array => [
|
||||
'real_stock' => [0, 3][$index],
|
||||
'values' => ['tipo_alojamiento' => $type],
|
||||
])->all(),
|
||||
]);
|
||||
@@ -89,8 +89,8 @@ class FiestaFutbolInfantilProductSeeder extends Seeder
|
||||
'attribute_codes' => ['event_date', 'horario', 'servicio'],
|
||||
'variants' => $eventDates
|
||||
->crossJoin(['Desayuno', 'Almuerzo', 'Cena'], ['Comedor', 'Vianda'])
|
||||
->map(fn (array $values): array => [
|
||||
'real_stock' => 0,
|
||||
->map(fn (array $values, int $index): array => [
|
||||
'real_stock' => [80, 3, 65, 0, 42, 2, 70, 18, 0, 5, 55, 40, 90, 1, 35, 0, 60, 8, 75, 22, 0, 4, 50, 30][$index],
|
||||
'event_date_ids' => [(int) $values[0]->id],
|
||||
'descripcion' => sprintf(
|
||||
'%s del %s - %s',
|
||||
@@ -112,7 +112,7 @@ class FiestaFutbolInfantilProductSeeder extends Seeder
|
||||
'attribute_codes' => ['event_date'],
|
||||
'multi_select_attribute_codes' => ['event_date'],
|
||||
'variants' => [[
|
||||
'real_stock' => 0,
|
||||
'real_stock' => 120,
|
||||
'event_date_ids' => $dateIds->all(),
|
||||
]],
|
||||
]);
|
||||
@@ -142,7 +142,7 @@ class FiestaFutbolInfantilProductSeeder extends Seeder
|
||||
return $this->catalogService->create([
|
||||
'tenant_code' => $tenant->codigo,
|
||||
'descripcion' => $data['nombre'],
|
||||
'inventory_policy' => InventoryPolicy::Unlimited->value,
|
||||
'inventory_policy' => InventoryPolicy::Tracked->value,
|
||||
...$data,
|
||||
'has_tickets' => true,
|
||||
]);
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
@@ -80,6 +80,39 @@ class CatalogControllerTest extends TestCase
|
||||
->assertJsonCount(0, '1.items.data.0.variants');
|
||||
}
|
||||
|
||||
public function test_it_excludes_items_when_all_of_their_variants_are_out_of_stock(): void
|
||||
{
|
||||
$tenant = $this->createTenant('catalog-available-variants');
|
||||
$group = $this->createGroup(
|
||||
$tenant,
|
||||
ProductLayout::ColumnWithCart,
|
||||
'Available variants',
|
||||
groupLayout: GroupLayout::SimpleVertical,
|
||||
);
|
||||
|
||||
$unavailableItem = $this->createItem($tenant, 'Unavailable');
|
||||
$unavailableInventory = Inventory::query()->create([
|
||||
'real_stock' => 4,
|
||||
'reserved_stock' => 4,
|
||||
]);
|
||||
$unavailableItem->variants()->create(['inventory_id' => $unavailableInventory->id]);
|
||||
$group->featuredItems()->create(['catalog_item_id' => $unavailableItem->id]);
|
||||
|
||||
$availableItem = $this->createItem($tenant, 'Available');
|
||||
$availableInventory = Inventory::query()->create([
|
||||
'real_stock' => 4,
|
||||
'reserved_stock' => 3,
|
||||
]);
|
||||
$availableItem->variants()->create(['inventory_id' => $availableInventory->id]);
|
||||
$group->featuredItems()->create(['catalog_item_id' => $availableItem->id]);
|
||||
|
||||
$this->getJson("/api/tenants/{$tenant->codigo}/catalog")
|
||||
->assertOk()
|
||||
->assertJsonCount(1, '0.items')
|
||||
->assertJsonPath('0.items.0.nombre', 'Available')
|
||||
->assertJsonMissing(['nombre' => 'Unavailable']);
|
||||
}
|
||||
|
||||
public function test_column_with_image_uses_item_image_then_variant_image_then_null(): void
|
||||
{
|
||||
Storage::fake('s3');
|
||||
|
||||
@@ -76,6 +76,16 @@ class FiestaFutbolInfantilProductSeederTest extends TestCase
|
||||
$this->assertCount($count, $item->variants);
|
||||
}
|
||||
|
||||
$variantStocks = CatalogItem::query()
|
||||
->where('tenant_code', $tenant->codigo)
|
||||
->with('variants.inventory')
|
||||
->get()
|
||||
->flatMap(fn (CatalogItem $item) => $item->variants)
|
||||
->map(fn ($variant): int => $variant->inventory->real_stock);
|
||||
$this->assertGreaterThan($variantStocks->count() / 2, $variantStocks->filter(fn (int $stock): bool => $stock > 5)->count());
|
||||
$this->assertNotEmpty($variantStocks->filter(fn (int $stock): bool => $stock === 0));
|
||||
$this->assertNotEmpty($variantStocks->filter(fn (int $stock): bool => $stock >= 1 && $stock <= 5));
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
'abono' => 'Entradas',
|
||||
|
||||
Reference in New Issue
Block a user