feat(catalog): add max_units_per_user field to catalog items and implement purchase limit validation
This commit is contained in:
@@ -32,6 +32,7 @@ class CatalogItemControllerTest extends TestCase
|
||||
'slug' => 'shirt',
|
||||
'nombre' => 'Shirt',
|
||||
'precio' => 100,
|
||||
'max_units_per_user' => 4,
|
||||
'attribute_codes' => [$attribute->codigo],
|
||||
'images' => [$image, $image],
|
||||
'variants' => [
|
||||
@@ -46,6 +47,7 @@ class CatalogItemControllerTest extends TestCase
|
||||
$response
|
||||
->assertCreated()
|
||||
->assertJsonPath('data.nombre', 'Shirt')
|
||||
->assertJsonPath('data.max_units_per_user', 4)
|
||||
->assertJsonCount(2, 'data.images')
|
||||
->assertJsonCount(1, 'data.variants')
|
||||
->assertJsonCount(1, 'data.variants.0.images');
|
||||
@@ -54,6 +56,7 @@ class CatalogItemControllerTest extends TestCase
|
||||
$variant = $item->variants()->firstOrFail();
|
||||
|
||||
$this->assertSame([0, 1], $item->attachments()->get()->pluck('pivot.orden')->all());
|
||||
$this->assertSame(4, $item->max_units_per_user);
|
||||
$this->assertSame([0], $variant->attachments()->get()->pluck('pivot.orden')->all());
|
||||
$this->assertDatabaseHas('catalog_items_attachments', [
|
||||
'catalog_item_id' => $item->id,
|
||||
@@ -77,6 +80,21 @@ class CatalogItemControllerTest extends TestCase
|
||||
$this->assertDatabaseMissing('catalog_items', ['slug' => 'invalid-image']);
|
||||
}
|
||||
|
||||
public function test_it_rejects_a_non_positive_user_purchase_limit(): void
|
||||
{
|
||||
$tenant = $this->createTenant('purchase-limit-validation');
|
||||
|
||||
$this->postJson("/api/tenants/{$tenant->codigo}/catalog-items", [
|
||||
'slug' => 'invalid-purchase-limit',
|
||||
'nombre' => 'Invalid purchase limit',
|
||||
'precio' => 100,
|
||||
'real_stock' => 10,
|
||||
'max_units_per_user' => 0,
|
||||
])->assertUnprocessable()->assertJsonValidationErrors('max_units_per_user');
|
||||
|
||||
$this->assertDatabaseMissing('catalog_items', ['slug' => 'invalid-purchase-limit']);
|
||||
}
|
||||
|
||||
private function createTenant(string $code = 'catalog-controller'): Tenant
|
||||
{
|
||||
$headerLogo = $this->createAttachment("{$code}-header");
|
||||
|
||||
@@ -43,6 +43,7 @@ class CatalogSchemaTest extends TestCase
|
||||
'descripcion',
|
||||
'precio',
|
||||
'inventory_policy',
|
||||
'max_units_per_user',
|
||||
'has_tickets',
|
||||
'validity_time_id',
|
||||
], Schema::getColumnListing('catalog_items'));
|
||||
|
||||
@@ -190,6 +190,98 @@ class StorePurchaseTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_it_enforces_the_user_purchase_limit_and_releases_it_after_cancellation(): void
|
||||
{
|
||||
$this->createTenant('sonder', 'Sonder', 'sonder.com.ar');
|
||||
$user = User::factory()->create();
|
||||
$otherUser = User::factory()->create();
|
||||
$variant = $this->createVariantForTenant('sonder', 20, '50.00');
|
||||
$variant->catalogItem->update(['max_units_per_user' => 3]);
|
||||
|
||||
$firstPurchaseId = $this->actingAs($user, 'sanctum')
|
||||
->postJson('/api/tenants/sonder/compras/start-checkout', [
|
||||
'direct_item' => [
|
||||
'catalog_item_id' => $variant->catalog_item_id,
|
||||
'variant_id' => $variant->id,
|
||||
'cantidad' => 2,
|
||||
],
|
||||
])
|
||||
->assertCreated()
|
||||
->json('data.id');
|
||||
|
||||
$this->actingAs($user, 'sanctum')
|
||||
->postJson('/api/tenants/sonder/compras/start-checkout', [
|
||||
'direct_item' => [
|
||||
'catalog_item_id' => $variant->catalog_item_id,
|
||||
'variant_id' => $variant->id,
|
||||
'cantidad' => 2,
|
||||
],
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('direct_item.cantidad');
|
||||
|
||||
$this->actingAs($otherUser, 'sanctum')
|
||||
->postJson('/api/tenants/sonder/compras/start-checkout', [
|
||||
'direct_item' => [
|
||||
'catalog_item_id' => $variant->catalog_item_id,
|
||||
'variant_id' => $variant->id,
|
||||
'cantidad' => 3,
|
||||
],
|
||||
])
|
||||
->assertCreated();
|
||||
|
||||
$this->actingAs($user, 'sanctum')
|
||||
->postJson("/api/tenants/sonder/compras/{$firstPurchaseId}/cancel")
|
||||
->assertOk();
|
||||
|
||||
$this->actingAs($user, 'sanctum')
|
||||
->postJson('/api/tenants/sonder/compras/start-checkout', [
|
||||
'direct_item' => [
|
||||
'catalog_item_id' => $variant->catalog_item_id,
|
||||
'variant_id' => $variant->id,
|
||||
'cantidad' => 3,
|
||||
],
|
||||
])
|
||||
->assertCreated();
|
||||
}
|
||||
|
||||
public function test_the_user_purchase_limit_is_shared_by_all_item_variants(): void
|
||||
{
|
||||
$tenant = $this->createTenant('sonder', 'Sonder', 'sonder.com.ar');
|
||||
$user = User::factory()->create();
|
||||
$firstVariant = $this->createVariantForTenant('sonder', 20, '50.00');
|
||||
$firstVariant->catalogItem->update(['max_units_per_user' => 3]);
|
||||
$secondInventory = Inventory::query()->create(['real_stock' => 20]);
|
||||
$secondVariant = Variant::query()->create([
|
||||
'catalog_item_id' => $firstVariant->catalog_item_id,
|
||||
'inventory_id' => $secondInventory->id,
|
||||
]);
|
||||
$cart = Cart::query()->create([
|
||||
'tenant_codigo' => $tenant->codigo,
|
||||
'user_id' => $user->id,
|
||||
'status' => 'active',
|
||||
]);
|
||||
$cart->addItem($firstVariant->catalog_item_id, $firstVariant->id, 2);
|
||||
$cart->addItem($secondVariant->catalog_item_id, $secondVariant->id, 2);
|
||||
|
||||
$this->actingAs($user, 'sanctum')
|
||||
->postJson('/api/tenants/sonder/compras/start-checkout', [
|
||||
'cart_id' => $cart->id,
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('cart_id');
|
||||
|
||||
$this->assertDatabaseCount('compras', 0);
|
||||
$this->assertDatabaseHas('inventories', [
|
||||
'id' => $firstVariant->inventory_id,
|
||||
'reserved_stock' => 2,
|
||||
]);
|
||||
$this->assertDatabaseHas('inventories', [
|
||||
'id' => $secondInventory->id,
|
||||
'reserved_stock' => 2,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_it_restores_the_source_cart_when_checkout_is_cancelled(): void
|
||||
{
|
||||
$this->createTenant('sonder', 'Sonder', 'sonder.com.ar');
|
||||
@@ -382,6 +474,32 @@ class StorePurchaseTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_it_rejects_a_quantity_update_above_the_user_purchase_limit(): void
|
||||
{
|
||||
$this->createTenant('sonder', 'Sonder', 'sonder.com.ar');
|
||||
$user = User::factory()->create();
|
||||
$variant = $this->createVariantForTenant('sonder', 10, '50.00');
|
||||
$variant->catalogItem->update(['max_units_per_user' => 3]);
|
||||
$purchase = $this->createCheckoutPurchase($user, 'sonder', $variant, 2);
|
||||
$itemId = $purchase->items->firstOrFail()->id;
|
||||
|
||||
$this->actingAs($user, 'sanctum')
|
||||
->patchJson("/api/tenants/sonder/compras/{$purchase->id}/items/{$itemId}", [
|
||||
'quantity' => 4,
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('quantity');
|
||||
|
||||
$this->assertDatabaseHas('compra_items', [
|
||||
'id' => $itemId,
|
||||
'cantidad' => 2,
|
||||
]);
|
||||
$this->assertDatabaseHas('inventories', [
|
||||
'id' => $variant->inventory_id,
|
||||
'reserved_stock' => 2,
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_it_reopens_a_pending_purchase_before_editing_items(): void
|
||||
{
|
||||
$this->createTenant('sonder', 'Sonder', 'sonder.com.ar');
|
||||
|
||||
@@ -17,6 +17,7 @@ use App\Domains\Catalog\Models\Inventory;
|
||||
use App\Domains\Event\Models\Event;
|
||||
use App\Domains\Event\Models\EventDate;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Ticket\Enums\ValidityTimeType;
|
||||
use Database\Seeders\AttributeSeeder;
|
||||
use Database\Seeders\FiestaFutbolInfantilProductSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
@@ -64,9 +65,36 @@ class FiestaFutbolInfantilProductSeederTest extends TestCase
|
||||
FiestaFutbolInfantilProductSeeder::class,
|
||||
]);
|
||||
|
||||
$this->assertFalse(Attribute::query()
|
||||
$attributes = Attribute::query()
|
||||
->where('tenant_codigo', $tenant->codigo)
|
||||
->exists());
|
||||
->with('options.validityTime')
|
||||
->orderBy('id')
|
||||
->get();
|
||||
|
||||
$this->assertSame(
|
||||
['servicio', 'color', 'horario', 'talle'],
|
||||
$attributes->pluck('codigo')->all(),
|
||||
);
|
||||
$this->assertSame(['Comedor', 'Vianda'], $attributes[0]->options->pluck('value')->all());
|
||||
$this->assertSame(['Verde', 'Blanco'], $attributes[1]->options->pluck('value')->all());
|
||||
$this->assertSame(['Desayuno', 'Almuerzo', 'Cena'], $attributes[2]->options->pluck('value')->all());
|
||||
$this->assertSame(['14', 'S', 'M', 'L', 'XL', 'XXL'], $attributes[3]->options->pluck('value')->all());
|
||||
|
||||
$this->assertSame(
|
||||
[
|
||||
['Desayuno', ValidityTimeType::TimeWindow, '07:00:00', '12:00:00'],
|
||||
['Almuerzo', ValidityTimeType::TimeWindow, '12:00:00', '15:00:00'],
|
||||
['Cena', ValidityTimeType::TimeWindow, '20:00:00', '24:00:00'],
|
||||
],
|
||||
$attributes[2]->options
|
||||
->map(fn ($option): array => [
|
||||
$option->value,
|
||||
$option->validityTime->type,
|
||||
$option->validityTime->start_time,
|
||||
$option->validityTime->end_time,
|
||||
])
|
||||
->all(),
|
||||
);
|
||||
|
||||
$event = Event::query()
|
||||
->where('tenant_code', $tenant->codigo)
|
||||
|
||||
Reference in New Issue
Block a user