Merge branch 'refactor/inventory_stock' of https://gitea.quo.ar/tbianchini/shopit-back into refactor/inventory_stock
This commit is contained in:
@@ -72,29 +72,6 @@ class CategoryDetailTest extends TestCase
|
||||
->assertJsonCount(0, 'data');
|
||||
}
|
||||
|
||||
public function test_it_includes_products_from_all_descendant_categories(): void
|
||||
{
|
||||
$tenant = $this->createTenant('category-descendants');
|
||||
$parent = $this->createCategory($tenant, 'Indumentaria');
|
||||
$child = $this->createCategory($tenant, 'Remeras', $parent);
|
||||
$grandchild = $this->createCategory($tenant, 'Manga corta', $child);
|
||||
$sibling = $this->createCategory($tenant, 'Pantalones');
|
||||
|
||||
$parentItem = $this->createCatalogItem($tenant, $parent, 'Producto padre');
|
||||
$childItem = $this->createCatalogItem($tenant, $child, 'Producto hijo');
|
||||
$grandchildItem = $this->createCatalogItem($tenant, $grandchild, 'Producto nieto');
|
||||
$this->createCatalogItem($tenant, $sibling, 'Producto ajeno');
|
||||
|
||||
$this->getJson("/api/tenants/{$tenant->codigo}/categories/{$parent->id}")
|
||||
->assertOk()
|
||||
->assertJsonPath('meta.total', 3)
|
||||
->assertJsonCount(3, 'data')
|
||||
->assertJsonPath('data.0.id', $childItem->id)
|
||||
->assertJsonPath('data.1.id', $grandchildItem->id)
|
||||
->assertJsonPath('data.2.id', $parentItem->id)
|
||||
->assertJsonMissing(['nombre' => 'Producto ajeno']);
|
||||
}
|
||||
|
||||
public function test_it_rejects_categories_from_another_tenant(): void
|
||||
{
|
||||
$tenant = $this->createTenant('category-owner');
|
||||
@@ -130,14 +107,10 @@ class CategoryDetailTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
private function createCategory(
|
||||
Tenant $tenant,
|
||||
string $name,
|
||||
?Category $parent = null,
|
||||
): Category {
|
||||
private function createCategory(Tenant $tenant, string $name): Category
|
||||
{
|
||||
return Category::query()->create([
|
||||
'tenant_code' => $tenant->codigo,
|
||||
'categoria_id' => $parent?->id,
|
||||
'nombre' => $name,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -97,8 +97,6 @@ class AccommodationControllerTest extends TestCase
|
||||
|
||||
$updated = $this->variantPayload('Casa Rodante Premium', 'Con electricidad', 15, 50000);
|
||||
$updated['id'] = $variantId;
|
||||
unset($updated['stock']);
|
||||
$updated['stock_difference'] = -5;
|
||||
|
||||
$this->postJson('/api/v1/adminapp/tenant/accommodations', [
|
||||
'variants' => [
|
||||
|
||||
@@ -194,7 +194,7 @@ class EntryControllerTest extends TestCase
|
||||
'title' => 'Abono actualizado',
|
||||
'description' => 'Ahora incluye ambas fechas',
|
||||
'event_date_ids' => [$firstDate->id, $secondDate->id],
|
||||
'stock_difference' => 10,
|
||||
'stock' => 20,
|
||||
'price' => 250,
|
||||
],
|
||||
[
|
||||
@@ -268,7 +268,7 @@ class EntryControllerTest extends TestCase
|
||||
'id' => $entryId,
|
||||
'title' => 'Abono editado',
|
||||
'event_date_ids' => [$date->id, $otherDate->id],
|
||||
'stock_difference' => 10,
|
||||
'stock' => 30,
|
||||
'price' => 200,
|
||||
]],
|
||||
])->assertOk()
|
||||
@@ -329,7 +329,7 @@ class EntryControllerTest extends TestCase
|
||||
'title' => 'Entrada inválida',
|
||||
'description' => null,
|
||||
'event_date_ids' => [$foreignDate->id],
|
||||
'stock_difference' => 0,
|
||||
'stock' => 10,
|
||||
'price' => 100,
|
||||
]],
|
||||
])
|
||||
@@ -371,7 +371,7 @@ class EntryControllerTest extends TestCase
|
||||
'title' => 'Entrada inválida',
|
||||
'description' => null,
|
||||
'event_date_ids' => [$eventDate->id],
|
||||
'stock_difference' => 0,
|
||||
'stock' => 10,
|
||||
'price' => 100,
|
||||
]],
|
||||
])
|
||||
|
||||
@@ -124,8 +124,6 @@ class FoodControllerTest extends TestCase
|
||||
|
||||
$updated = $this->variantPayload($secondDate->id, 'Cena', 'Vianda', 80, 8500);
|
||||
$updated['id'] = $variantId;
|
||||
unset($updated['stock']);
|
||||
$updated['stock_difference'] = -20;
|
||||
$updated['description'] = 'Cena para llevar';
|
||||
|
||||
$this->postJson('/api/v1/adminapp/tenant/foods', [
|
||||
@@ -242,7 +240,7 @@ class FoodControllerTest extends TestCase
|
||||
Inventory::query()->whereKey($replacementInventoryId)->delete();
|
||||
|
||||
$updated = $this->patchJson('/api/v1/adminapp/tenant/foods/history-stock', [
|
||||
'variants' => [['id' => $historicalVariantId, 'stock_difference' => -55]],
|
||||
'variants' => [['id' => $historicalVariantId, 'stock' => 45]],
|
||||
])
|
||||
->assertOk()
|
||||
->assertJsonPath('data.history.0.variants.0.id', $historicalVariantId)
|
||||
@@ -260,7 +258,7 @@ class FoodControllerTest extends TestCase
|
||||
]);
|
||||
|
||||
$this->patchJson('/api/v1/adminapp/tenant/foods/history-stock', [
|
||||
'variants' => [['id' => $activeVariantId, 'stock_difference' => -60]],
|
||||
'variants' => [['id' => $activeVariantId, 'stock' => 20]],
|
||||
])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors(['variants.0.id']);
|
||||
|
||||
@@ -112,8 +112,6 @@ class MerchandiseControllerTest extends TestCase
|
||||
|
||||
$updatedVariant = $this->variantPayload('Blanco', 'M', 80, 12500);
|
||||
$updatedVariant['id'] = $variantId;
|
||||
unset($updatedVariant['stock']);
|
||||
$updatedVariant['stock_difference'] = -20;
|
||||
$updatedItem = $this->itemPayload('Camiseta oficial', 2, [
|
||||
$updatedVariant,
|
||||
$this->variantPayload('Verde', 'L', 60, 15000),
|
||||
@@ -236,8 +234,6 @@ class MerchandiseControllerTest extends TestCase
|
||||
$secondItemVariantId = $created->json('data.1.variants.0.id');
|
||||
$foreignVariant = $this->variantPayload('Azul Marino', 'XL', 20, 30000);
|
||||
$foreignVariant['id'] = $secondItemVariantId;
|
||||
unset($foreignVariant['stock']);
|
||||
$foreignVariant['stock_difference'] = 0;
|
||||
$firstItem = $this->itemPayload('Camiseta', 3, [$foreignVariant]);
|
||||
$firstItem['id'] = $firstItemId;
|
||||
|
||||
|
||||
@@ -2,22 +2,20 @@
|
||||
|
||||
namespace Tests\Feature\Integration;
|
||||
|
||||
use App\Shared\Attachable\Enums\AttachmentType;
|
||||
use App\Shared\Attachable\Models\Attachment;
|
||||
use App\Domains\Core\Auth\Models\User;
|
||||
use App\Domains\Commerce\Cart\Models\Cart;
|
||||
use App\Domains\Commerce\Catalog\Enums\InventoryPolicy;
|
||||
use App\Domains\Commerce\Catalog\Models\CatalogItem;
|
||||
use App\Domains\Commerce\Catalog\Models\Category;
|
||||
use App\Domains\Commerce\Catalog\Models\Inventory;
|
||||
use App\Domains\Commerce\Catalog\Models\Variant;
|
||||
use App\Shared\Integration\Models\Integration;
|
||||
use App\Domains\Commerce\Purchase\Models\Purchase;
|
||||
use App\Domains\Commerce\Purchase\Models\TelepagosPayment;
|
||||
use App\Domains\Commerce\Purchase\Services\CheckoutService;
|
||||
use App\Domains\Core\Auth\Models\User;
|
||||
use App\Domains\Core\Client\Models\Client;
|
||||
use App\Domains\Core\Tenant\Models\Tenant;
|
||||
use App\Domains\Ticketing\Event\Models\Event;
|
||||
use App\Shared\Attachable\Enums\AttachmentType;
|
||||
use App\Shared\Attachable\Models\Attachment;
|
||||
use App\Shared\Integration\Models\Integration;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
@@ -229,77 +227,6 @@ class TelepagosWebhookTest extends TestCase
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_transfer_webhook_matches_the_client_configured_on_the_event(): void
|
||||
{
|
||||
$tenant = $this->createTenant('multi-event', 'Multi Event', 'multi-event.com.ar');
|
||||
$eventClient = Client::query()->create([
|
||||
'code' => 'event-client',
|
||||
'name' => 'Event Client',
|
||||
]);
|
||||
Integration::query()->create([
|
||||
'integration_code' => 'telepagos_homo',
|
||||
'name' => 'Telepagos',
|
||||
'url' => 'https://api.telepagos.com.ar',
|
||||
'integration_data_schema' => [
|
||||
'username' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
],
|
||||
]);
|
||||
$this->createClientIntegration([
|
||||
'client_id' => $eventClient->id,
|
||||
'integration_code' => 'telepagos_homo',
|
||||
'integration_data' => ['username' => 'event-user', 'password' => 'event-password'],
|
||||
]);
|
||||
$event = Event::query()->create([
|
||||
'client_id' => $eventClient->id,
|
||||
'tenant_code' => $tenant->codigo,
|
||||
'title' => 'Event with its own client',
|
||||
]);
|
||||
$tenant->update(['active_event_id' => $event->id]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$variant = $this->createVariantForTenant($tenant->codigo, 10, '50.00');
|
||||
$purchase = $this->createPendingTransferPurchase(
|
||||
$tenant,
|
||||
$user->id,
|
||||
$variant->id,
|
||||
1,
|
||||
'12345678',
|
||||
);
|
||||
|
||||
Http::fake([
|
||||
'https://api.telepagos.com.ar/v2/auth/token' => Http::response([
|
||||
'status' => 'ok',
|
||||
'token' => 'event-client-token',
|
||||
'expires_at' => now()->addHour()->toIso8601String(),
|
||||
]),
|
||||
'https://api.telepagos.com.ar/v2/payment/cashin/event-client-payment' => Http::response([
|
||||
'status' => 'ok',
|
||||
'data' => [
|
||||
'amount' => 50,
|
||||
'operation_id' => 1,
|
||||
'transaction_id' => 'tx-event-client',
|
||||
'buyer' => ['cuit' => '20123456789'],
|
||||
],
|
||||
]),
|
||||
]);
|
||||
|
||||
$this->postJson('/api/webhooks/telepagos/event-client', [
|
||||
'id' => 'event-client-payment',
|
||||
])->assertOk()->assertJsonPath('status', 'success');
|
||||
|
||||
$this->assertSame($event->id, $purchase->event_id);
|
||||
$this->assertDatabaseHas('compras', [
|
||||
'id' => $purchase->id,
|
||||
'event_id' => $event->id,
|
||||
'status' => Purchase::STATUS_PAID,
|
||||
]);
|
||||
$this->assertDatabaseHas('telepagos_payments', [
|
||||
'compra_id' => $purchase->id,
|
||||
'transaction_id' => 'tx-event-client',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_transfer_webhook_buys_unlimited_inventory_without_reducing_real_stock(): void
|
||||
{
|
||||
$tenant = $this->createTenant('unlimited', 'Unlimited', 'unlimited.com.ar');
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Migrations;
|
||||
|
||||
use App\Domains\Core\Client\Models\Client;
|
||||
use App\Domains\Ticketing\Event\Models\Event;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AddClientIdToEventsTest extends TestCase
|
||||
{
|
||||
public function test_event_client_overrides_the_tenant_client_when_configured(): void
|
||||
{
|
||||
Schema::create('clients', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('code');
|
||||
$table->string('name');
|
||||
});
|
||||
Schema::create('tenants', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('client_id');
|
||||
$table->string('codigo');
|
||||
});
|
||||
Schema::create('events', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('tenant_code');
|
||||
$table->string('title');
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
DB::table('clients')->insert([
|
||||
['id' => 10, 'code' => 'tenant-client', 'name' => 'Tenant Client'],
|
||||
['id' => 11, 'code' => 'event-client', 'name' => 'Event Client'],
|
||||
]);
|
||||
DB::table('tenants')->insert(['id' => 20, 'client_id' => 10, 'codigo' => 'tenant']);
|
||||
DB::table('events')->insert([
|
||||
'id' => 30,
|
||||
'tenant_code' => 'tenant',
|
||||
'title' => 'Event',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$migration = require database_path('migrations/2026_09_28_000000_add_client_id_to_events.php');
|
||||
$migration->up();
|
||||
|
||||
$event = Event::query()->findOrFail(30);
|
||||
$this->assertSame(10, $event->effectiveClient()->id);
|
||||
|
||||
$event->update(['client_id' => 11]);
|
||||
|
||||
$this->assertSame(11, $event->fresh()->effectiveClient()->id);
|
||||
$this->assertTrue(Client::query()->findOrFail(11)->events()->whereKey(30)->exists());
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Migrations;
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AddEventIdToPurchasesTest extends TestCase
|
||||
{
|
||||
public function test_it_backfills_the_event_from_purchase_items(): void
|
||||
{
|
||||
Schema::create('events', fn (Blueprint $table) => $table->id());
|
||||
Schema::create('compras', fn (Blueprint $table) => $table->id());
|
||||
Schema::create('catalog_items', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('event_id')->nullable();
|
||||
});
|
||||
Schema::create('compra_items', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('compra_id');
|
||||
$table->foreignId('source_catalog_item_id');
|
||||
});
|
||||
|
||||
DB::table('events')->insert([['id' => 10], ['id' => 11]]);
|
||||
DB::table('compras')->insert([['id' => 20], ['id' => 21]]);
|
||||
DB::table('catalog_items')->insert([
|
||||
['id' => 30, 'event_id' => 10],
|
||||
['id' => 31, 'event_id' => 11],
|
||||
]);
|
||||
DB::table('compra_items')->insert([
|
||||
['compra_id' => 20, 'source_catalog_item_id' => 30],
|
||||
['compra_id' => 21, 'source_catalog_item_id' => 30],
|
||||
['compra_id' => 21, 'source_catalog_item_id' => 31],
|
||||
]);
|
||||
|
||||
$migration = require database_path('migrations/2026_09_28_000100_add_event_id_to_compras.php');
|
||||
$migration->up();
|
||||
|
||||
$this->assertDatabaseHas('compras', ['id' => 20, 'event_id' => 10]);
|
||||
$this->assertDatabaseHas('compras', ['id' => 21, 'event_id' => null]);
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Migrations;
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AssignArrufoEventToPymeRuralClientTest extends TestCase
|
||||
{
|
||||
public function test_it_assigns_only_the_arrufo_event_to_pyme_rural(): void
|
||||
{
|
||||
Schema::create('clients', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('code');
|
||||
});
|
||||
Schema::create('events', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('client_id')->nullable();
|
||||
$table->string('tenant_code');
|
||||
$table->string('title');
|
||||
});
|
||||
|
||||
DB::table('clients')->insert([
|
||||
['id' => 10, 'code' => 'pyme_rural'],
|
||||
['id' => 11, 'code' => 'onticket'],
|
||||
]);
|
||||
DB::table('events')->insert([
|
||||
[
|
||||
'id' => 20,
|
||||
'client_id' => null,
|
||||
'tenant_code' => 'onticket',
|
||||
'title' => '26.º Fiesta de la Tradición y 4.º Encuentro de Agrupaciones Gauchas',
|
||||
],
|
||||
[
|
||||
'id' => 21,
|
||||
'client_id' => 11,
|
||||
'tenant_code' => 'onticket',
|
||||
'title' => 'Otro evento',
|
||||
],
|
||||
]);
|
||||
|
||||
$migration = require database_path('migrations/2026_09_28_000200_assign_arrufo_event_to_pyme_rural_client.php');
|
||||
$migration->up();
|
||||
|
||||
$this->assertDatabaseHas('events', ['id' => 20, 'client_id' => 10]);
|
||||
$this->assertDatabaseHas('events', ['id' => 21, 'client_id' => 11]);
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Migrations;
|
||||
|
||||
use App\Domains\Commerce\Catalog\Enums\InventoryMovementOperation;
|
||||
use App\Domains\Commerce\Catalog\Models\Inventory;
|
||||
use App\Domains\Commerce\Catalog\Models\InventoryMovement;
|
||||
use App\Domains\Core\Auth\Models\User;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Tests\TestCase;
|
||||
|
||||
class InventoryStockMovementMigrationTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Schema::create('users', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->softDeletes();
|
||||
});
|
||||
Schema::create('inventories', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('sold_units')->default(0);
|
||||
$table->unsignedInteger('reserved_stock')->default(0);
|
||||
$table->unsignedInteger('real_stock')->default(0);
|
||||
$table->unsignedBigInteger('refunded_units')->default(0);
|
||||
$table->unsignedInteger('entry_reserved_stock')->default(0);
|
||||
});
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Schema::disableForeignKeyConstraints();
|
||||
Schema::dropIfExists('inventory_movements');
|
||||
Schema::dropIfExists('inventories');
|
||||
Schema::dropIfExists('users');
|
||||
Schema::enableForeignKeyConstraints();
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_migration_and_rollback_preserve_the_available_balance(): void
|
||||
{
|
||||
DB::table('inventories')->insert([
|
||||
'id' => 17,
|
||||
'real_stock' => 10,
|
||||
'sold_units' => 3,
|
||||
'refunded_units' => 1,
|
||||
'reserved_stock' => 2,
|
||||
'entry_reserved_stock' => 1,
|
||||
]);
|
||||
|
||||
$stockMigration = $this->stockMigration();
|
||||
$movementMigration = $this->movementMigration();
|
||||
$stockMigration->up();
|
||||
$movementMigration->up();
|
||||
|
||||
$inventory = Inventory::findOrFail(17);
|
||||
$this->assertSame(12, $inventory->real_stock);
|
||||
$this->assertSame(7, $inventory->availableStock());
|
||||
$this->assertTrue(Schema::hasTable('inventory_movements'));
|
||||
|
||||
$inventory->adjustAvailableStock(2);
|
||||
$this->assertSame(9, $inventory->availableStock());
|
||||
|
||||
$movementMigration->down();
|
||||
$stockMigration->down();
|
||||
|
||||
$this->assertFalse(Schema::hasColumn('inventories', 'available_stock'));
|
||||
$this->assertFalse(Schema::hasTable('inventory_movements'));
|
||||
$this->assertSame(12, Inventory::findOrFail(17)->real_stock);
|
||||
$this->assertSame(9, Inventory::findOrFail(17)->availableStock());
|
||||
}
|
||||
|
||||
public function test_movements_capture_counters_actor_and_idempotency(): void
|
||||
{
|
||||
$this->stockMigration()->up();
|
||||
$this->movementMigration()->up();
|
||||
DB::table('users')->insert(['id' => 5]);
|
||||
$user = User::query()->findOrFail(5);
|
||||
|
||||
$inventory = Inventory::query()->create(['real_stock' => 10]);
|
||||
$inventory->recordInitialization($user);
|
||||
$inventory->adjustAvailableStock(3, $user, '955d2afb-1e36-46df-a9db-d9ee31ed5eb4');
|
||||
$inventory->adjustAvailableStock(3, $user, '955d2afb-1e36-46df-a9db-d9ee31ed5eb4');
|
||||
$inventory->reserve(2, true);
|
||||
$inventory->buy(2, true);
|
||||
$inventory->refundStock(1, $user);
|
||||
|
||||
$this->assertSame(12, $inventory->availableStock());
|
||||
$this->assertCount(5, $inventory->movements()->get());
|
||||
|
||||
$adjustment = InventoryMovement::query()
|
||||
->where('operation', InventoryMovementOperation::StockIncreased->value)
|
||||
->firstOrFail();
|
||||
$this->assertSame(3, $adjustment->available_stock_delta);
|
||||
$this->assertSame(10, $adjustment->available_stock_before);
|
||||
$this->assertSame(13, $adjustment->available_stock_after);
|
||||
$this->assertSame(['real_stock' => 3], $adjustment->counter_deltas);
|
||||
$this->assertSame(5, $adjustment->responsible_user_id);
|
||||
|
||||
$purchase = InventoryMovement::query()
|
||||
->where('operation', InventoryMovementOperation::PurchaseCommitted->value)
|
||||
->firstOrFail();
|
||||
$this->assertSame(0, $purchase->available_stock_delta);
|
||||
$this->assertSame(['reserved_stock' => -2, 'sold_units' => 2], $purchase->counter_deltas);
|
||||
}
|
||||
|
||||
public function test_migration_rejects_an_inventory_with_negative_available_stock(): void
|
||||
{
|
||||
DB::table('inventories')->insert([
|
||||
'id' => 23,
|
||||
'real_stock' => 1,
|
||||
'reserved_stock' => 2,
|
||||
]);
|
||||
|
||||
$this->expectException(\RuntimeException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'stock disponible actual es negativo. Inventarios detectados (máximo 20): '
|
||||
.'id=23 [real=1, vendidas=0, reintegradas=0, reservadas=2, reservas_entradas=0, resultado=-1]'
|
||||
);
|
||||
|
||||
$this->stockMigration()->up();
|
||||
}
|
||||
|
||||
private function stockMigration(): Migration
|
||||
{
|
||||
return require database_path('migrations/2026_09_25_000000_generate_available_inventory_stock.php');
|
||||
}
|
||||
|
||||
private function movementMigration(): Migration
|
||||
{
|
||||
return require database_path('migrations/2026_09_25_010000_create_inventory_movements_table.php');
|
||||
}
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Migrations;
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ProvisionMutualSmepTenantTest extends TestCase
|
||||
{
|
||||
public function test_it_provisions_the_mutual_smep_tenant_once_with_its_branding(): void
|
||||
{
|
||||
Storage::fake('s3');
|
||||
$this->createSchema();
|
||||
|
||||
DB::table('admin_website_types')->insert(['codigo' => 'shopit']);
|
||||
DB::table('storefront_website_types')->insert(['codigo' => 'shopit']);
|
||||
DB::table('menues')->insert([
|
||||
['code' => 'product.detail'],
|
||||
['code' => 'help.faq'],
|
||||
['code' => 'help.contact'],
|
||||
['code' => 'account.tickets'],
|
||||
['code' => 'event.index'],
|
||||
]);
|
||||
|
||||
$migration = require database_path('migrations/2026_09_24_000000_provision_mutual_smep_tenant.php');
|
||||
$migration->up();
|
||||
$migration->up();
|
||||
|
||||
$this->assertDatabaseCount('tenants', 1);
|
||||
$this->assertDatabaseHas('clients', [
|
||||
'code' => 'mutual_smep',
|
||||
'name' => 'Mutual SMEP',
|
||||
]);
|
||||
$this->assertDatabaseHas('tenants', [
|
||||
'codigo' => 'mutual_smep',
|
||||
'nombre' => 'Mutual SMEP',
|
||||
'dominio' => 'mutual-smep.localhost',
|
||||
'site_title' => 'Tienda Mutual SMEP',
|
||||
'primary_color' => '#4A7FF5',
|
||||
'secondary_color' => '#0051A4',
|
||||
'header_bg_color' => '#FFFFFF',
|
||||
'footer_bg_color' => '#0051A4',
|
||||
'admin_website_type_code' => 'shopit',
|
||||
'storefront_website_type_code' => 'shopit',
|
||||
'display_categories' => true,
|
||||
'display_seach_bar' => true,
|
||||
'display_cart' => true,
|
||||
'cart_editing_policy' => 'quantity_and_remove',
|
||||
'checkout_editing_policy' => 'disabled',
|
||||
]);
|
||||
|
||||
$this->assertSame([
|
||||
'mutual_smep_favicon.png',
|
||||
'mutual_smep_footer.png',
|
||||
'mutual_smep_header.png',
|
||||
], DB::table('attachments')->orderBy('filename')->pluck('filename')->all());
|
||||
|
||||
DB::table('attachments')->orderBy('id')->each(
|
||||
fn (object $attachment) => Storage::disk('s3')->assertExists($attachment->path)
|
||||
);
|
||||
|
||||
$this->assertSame(
|
||||
['instagram', 'facebook', 'whatsapp'],
|
||||
DB::table('tenant_social_media')
|
||||
->where('tenant_code', 'mutual_smep')
|
||||
->orderBy('orden')
|
||||
->pluck('social_media_code')
|
||||
->all(),
|
||||
);
|
||||
$this->assertDatabaseHas('tenants_menues', [
|
||||
'tenant_code' => 'mutual_smep',
|
||||
'menu_code' => 'product.detail',
|
||||
]);
|
||||
$this->assertDatabaseMissing('tenants_menues', [
|
||||
'tenant_code' => 'mutual_smep',
|
||||
'menu_code' => 'account.tickets',
|
||||
]);
|
||||
$this->assertDatabaseMissing('tenants_menues', [
|
||||
'tenant_code' => 'mutual_smep',
|
||||
'menu_code' => 'event.index',
|
||||
]);
|
||||
|
||||
$contact = json_decode(
|
||||
DB::table('tenants_menues')
|
||||
->where('tenant_code', 'mutual_smep')
|
||||
->where('menu_code', 'help.contact')
|
||||
->value('static_content'),
|
||||
true,
|
||||
flags: JSON_THROW_ON_ERROR,
|
||||
);
|
||||
|
||||
$this->assertSame('https://wa.me/5493412474530', $contact['whatsapp']['whatsapp_url']);
|
||||
$this->assertSame('San Lorenzo 1543, Rosario, Santa Fe', $contact['locations']['rosario']['addresses'][0]['address']);
|
||||
|
||||
$storedPaths = DB::table('attachments')->pluck('path')->all();
|
||||
$migration->down();
|
||||
|
||||
$this->assertDatabaseMissing('tenants', ['codigo' => 'mutual_smep']);
|
||||
$this->assertDatabaseMissing('clients', ['code' => 'mutual_smep']);
|
||||
$this->assertDatabaseCount('attachments', 0);
|
||||
$this->assertDatabaseCount('tenant_social_media', 0);
|
||||
$this->assertDatabaseCount('tenants_menues', 0);
|
||||
|
||||
foreach ($storedPaths as $storedPath) {
|
||||
Storage::disk('s3')->assertMissing($storedPath);
|
||||
}
|
||||
}
|
||||
|
||||
private function createSchema(): void
|
||||
{
|
||||
Schema::create('clients', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('code')->unique();
|
||||
$table->string('name');
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('attachments', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->uuid('key');
|
||||
$table->string('path');
|
||||
$table->string('filename');
|
||||
$table->string('type');
|
||||
$table->string('mime_type');
|
||||
$table->string('extension');
|
||||
$table->unsignedBigInteger('size');
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('admin_website_types', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('codigo')->unique();
|
||||
});
|
||||
Schema::create('storefront_website_types', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('codigo')->unique();
|
||||
});
|
||||
Schema::create('tenants', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('client_id');
|
||||
$table->string('codigo')->unique();
|
||||
$table->string('nombre');
|
||||
$table->string('timezone');
|
||||
$table->string('dominio');
|
||||
$table->string('base_path');
|
||||
$table->string('site_title');
|
||||
$table->string('address');
|
||||
$table->string('phone');
|
||||
$table->string('primary_color');
|
||||
$table->string('secondary_color');
|
||||
$table->string('danger_color');
|
||||
$table->string('success_color');
|
||||
$table->string('header_bg_color');
|
||||
$table->string('footer_bg_color');
|
||||
$table->foreignId('header_logo_id');
|
||||
$table->foreignId('footer_logo_id');
|
||||
$table->foreignId('favicon_id');
|
||||
$table->string('admin_website_type_code');
|
||||
$table->string('storefront_website_type_code');
|
||||
$table->boolean('display_categories');
|
||||
$table->boolean('display_seach_bar');
|
||||
$table->boolean('display_cart');
|
||||
$table->string('cart_editing_policy');
|
||||
$table->string('checkout_editing_policy');
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('social_media', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('code')->unique();
|
||||
$table->string('icon');
|
||||
$table->string('name');
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('tenant_social_media', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('tenant_code');
|
||||
$table->string('social_media_code');
|
||||
$table->string('url');
|
||||
$table->unsignedInteger('orden');
|
||||
$table->timestamps();
|
||||
$table->unique(['tenant_code', 'social_media_code']);
|
||||
});
|
||||
Schema::create('menues', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('code')->unique();
|
||||
});
|
||||
Schema::create('tenants_menues', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('tenant_code');
|
||||
$table->string('menu_code');
|
||||
$table->json('static_content')->nullable();
|
||||
$table->timestamps();
|
||||
$table->unique(['tenant_code', 'menu_code']);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Migrations;
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SeedMutualSmepCarouselTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Storage::fake('s3');
|
||||
$this->createSchema();
|
||||
|
||||
DB::table('tenants')->insert([
|
||||
'codigo' => 'mutual_smep',
|
||||
'storefront_website_type_code' => 'shopit',
|
||||
]);
|
||||
DB::table('storefront_website_type_extras')->insert([
|
||||
'storefront_website_type_code' => 'shopit',
|
||||
'codigo' => 'carousel',
|
||||
]);
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
foreach (['websites_extras', 'attachments', 'storefront_website_type_extras', 'tenants'] as $table) {
|
||||
Schema::dropIfExists($table);
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_it_loads_and_rolls_back_the_mutual_smep_carousel(): void
|
||||
{
|
||||
$migration = require database_path(
|
||||
'migrations/2026_09_24_040000_seed_mutual_smep_carousel.php'
|
||||
);
|
||||
|
||||
$migration->up();
|
||||
$migration->up();
|
||||
|
||||
$this->assertDatabaseCount('attachments', 4);
|
||||
$this->assertDatabaseCount('websites_extras', 1);
|
||||
|
||||
$attachments = DB::table('attachments')->orderBy('filename')->get();
|
||||
$this->assertSame([
|
||||
'01-tecnologia.webp',
|
||||
'02-hogar.webp',
|
||||
'03-vida-activa.webp',
|
||||
'04-descanso.webp',
|
||||
], $attachments->pluck('filename')->all());
|
||||
$this->assertSame(
|
||||
$attachments->pluck('id')->all(),
|
||||
json_decode(
|
||||
DB::table('websites_extras')->value('config'),
|
||||
true,
|
||||
flags: JSON_THROW_ON_ERROR,
|
||||
),
|
||||
);
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
$this->assertSame('image/webp', $attachment->mime_type);
|
||||
$this->assertStringStartsWith(
|
||||
'tenants/mutual_smep/extras/carousel-v1/',
|
||||
$attachment->path,
|
||||
);
|
||||
Storage::disk('s3')->assertExists($attachment->path);
|
||||
}
|
||||
|
||||
$storedPaths = $attachments->pluck('path')->all();
|
||||
$migration->down();
|
||||
|
||||
$this->assertDatabaseCount('websites_extras', 0);
|
||||
$this->assertDatabaseCount('attachments', 0);
|
||||
|
||||
foreach ($storedPaths as $storedPath) {
|
||||
Storage::disk('s3')->assertMissing($storedPath);
|
||||
}
|
||||
}
|
||||
|
||||
private function createSchema(): void
|
||||
{
|
||||
Schema::create('tenants', function (Blueprint $table): void {
|
||||
$table->string('codigo')->primary();
|
||||
$table->string('storefront_website_type_code');
|
||||
});
|
||||
Schema::create('storefront_website_type_extras', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('storefront_website_type_code');
|
||||
$table->string('codigo');
|
||||
});
|
||||
Schema::create('attachments', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->uuid('key')->unique();
|
||||
$table->string('path');
|
||||
$table->string('filename');
|
||||
$table->string('type');
|
||||
$table->string('mime_type');
|
||||
$table->string('extension')->nullable();
|
||||
$table->unsignedBigInteger('size')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('websites_extras', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('website_code');
|
||||
$table->unsignedBigInteger('website_type_extra_id');
|
||||
$table->json('config');
|
||||
$table->boolean('is_enabled')->default(true);
|
||||
$table->timestamps();
|
||||
$table->unique(['website_code', 'website_type_extra_id']);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,375 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Migrations;
|
||||
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Tests\TestCase;
|
||||
|
||||
class SeedMutualSmepCatalogTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Storage::fake('s3');
|
||||
$this->createSchema();
|
||||
DB::table('tenants')->insert(['codigo' => 'mutual_smep']);
|
||||
$this->seedCategoriesAndAttributes();
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
foreach ([
|
||||
'featured_items',
|
||||
'featured_groups',
|
||||
'catalog_items_attachments',
|
||||
'attachments',
|
||||
'variant_values',
|
||||
'item_attributes',
|
||||
'variantes',
|
||||
'catalog_items',
|
||||
'inventories',
|
||||
'brands',
|
||||
'attribute',
|
||||
'categorias',
|
||||
'tenants',
|
||||
] as $table) {
|
||||
Schema::dropIfExists($table);
|
||||
}
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_it_loads_and_rolls_back_the_complete_catalog(): void
|
||||
{
|
||||
$migration = require database_path(
|
||||
'migrations/2026_09_24_030000_seed_mutual_smep_catalog.php'
|
||||
);
|
||||
|
||||
$migration->up();
|
||||
|
||||
$this->assertSame(142, DB::table('catalog_items')->count());
|
||||
$this->assertSame(150, DB::table('inventories')->count());
|
||||
$this->assertSame(15, DB::table('variantes')->count());
|
||||
$this->assertSame(8, DB::table('item_attributes')->count());
|
||||
$this->assertSame(17, DB::table('variant_values')->count());
|
||||
$this->assertSame(511, DB::table('attachments')->count());
|
||||
$this->assertSame(511, DB::table('catalog_items_attachments')->count());
|
||||
$this->assertSame(
|
||||
511,
|
||||
count(Storage::disk('s3')->allFiles('tenants/mutual_smep/catalog-v2')),
|
||||
);
|
||||
$this->assertDatabaseCount('featured_groups', 7);
|
||||
$this->assertSame([
|
||||
'Productos destacados',
|
||||
'Dormitorio y Blanco',
|
||||
'Electrodomésticos',
|
||||
'Climatización',
|
||||
'Tecnología',
|
||||
'Bicicletas y Aire Libre',
|
||||
'Bazar',
|
||||
], DB::table('featured_groups')->orderBy('group_order')->pluck('group_name')->all());
|
||||
|
||||
DB::table('featured_groups')->orderBy('group_order')->each(function (object $group): void {
|
||||
$this->assertSame('manual', $group->source_type);
|
||||
$this->assertSame('carousel', $group->group_layout);
|
||||
$this->assertSame('column_with_image', $group->product_layout);
|
||||
if ($group->code === 'productos-destacados-smep') {
|
||||
$this->assertNull($group->category_id);
|
||||
} else {
|
||||
$this->assertNotNull($group->category_id);
|
||||
$this->assertDatabaseHas('categorias', [
|
||||
'id' => $group->category_id,
|
||||
'tenant_code' => 'mutual_smep',
|
||||
'categoria_id' => null,
|
||||
'nombre' => $group->group_name,
|
||||
]);
|
||||
}
|
||||
$this->assertGreaterThan(
|
||||
0,
|
||||
DB::table('featured_items')->where('featured_group_id', $group->id)->count(),
|
||||
);
|
||||
});
|
||||
|
||||
$highlightedGroupId = DB::table('featured_groups')
|
||||
->where('code', 'productos-destacados-smep')
|
||||
->value('id');
|
||||
$highlightedSlugs = DB::table('featured_items')
|
||||
->join('catalog_items', 'catalog_items.id', '=', 'featured_items.catalog_item_id')
|
||||
->where('featured_items.featured_group_id', $highlightedGroupId)
|
||||
->orderBy('featured_items.order')
|
||||
->pluck('catalog_items.slug')
|
||||
->all();
|
||||
$variantProductSlugs = DB::table('catalog_items')
|
||||
->whereExists(fn ($query) => $query
|
||||
->selectRaw('1')
|
||||
->from('variantes')
|
||||
->whereColumn('variantes.catalog_item_id', 'catalog_items.id'))
|
||||
->pluck('slug')
|
||||
->all();
|
||||
|
||||
$this->assertCount(13, $highlightedSlugs);
|
||||
$this->assertSame([], array_values(array_diff($variantProductSlugs, $highlightedSlugs)));
|
||||
$this->assertSame(46, DB::table('featured_items')->count());
|
||||
DB::table('featured_groups')
|
||||
->where('code', '!=', 'productos-destacados-smep')
|
||||
->orderBy('id')
|
||||
->each(function (object $group): void {
|
||||
$this->assertLessThanOrEqual(
|
||||
6,
|
||||
DB::table('featured_items')->where('featured_group_id', $group->id)->count(),
|
||||
);
|
||||
});
|
||||
$this->assertDatabaseHas('catalog_items', [
|
||||
'tenant_code' => 'mutual_smep',
|
||||
'slug' => 'motorola-g15',
|
||||
'precio' => 330000,
|
||||
]);
|
||||
$this->assertSame(10, DB::table('inventories')->min('real_stock'));
|
||||
$this->assertSame(10, DB::table('inventories')->max('real_stock'));
|
||||
|
||||
$migration->down();
|
||||
|
||||
foreach ([
|
||||
'featured_items',
|
||||
'featured_groups',
|
||||
'catalog_items_attachments',
|
||||
'attachments',
|
||||
'variant_values',
|
||||
'item_attributes',
|
||||
'variantes',
|
||||
'catalog_items',
|
||||
'inventories',
|
||||
'brands',
|
||||
] as $table) {
|
||||
$this->assertSame(0, DB::table($table)->count(), $table);
|
||||
}
|
||||
$this->assertSame([], Storage::disk('s3')->allFiles('tenants/mutual_smep/catalog-v2'));
|
||||
}
|
||||
|
||||
public function test_it_limits_existing_category_groups_and_restores_them_on_rollback(): void
|
||||
{
|
||||
$catalogMigration = require database_path(
|
||||
'migrations/2026_09_24_030000_seed_mutual_smep_catalog.php'
|
||||
);
|
||||
$limitMigration = require database_path(
|
||||
'migrations/2026_09_24_050000_limit_mutual_smep_category_featured_groups.php'
|
||||
);
|
||||
|
||||
$catalogMigration->up();
|
||||
|
||||
$limitMigration->down();
|
||||
$this->assertSame(155, DB::table('featured_items')->count());
|
||||
|
||||
$limitMigration->up();
|
||||
$this->assertSame(46, DB::table('featured_items')->count());
|
||||
|
||||
$categoryGroupCounts = DB::table('featured_groups')
|
||||
->leftJoin('featured_items', 'featured_items.featured_group_id', '=', 'featured_groups.id')
|
||||
->where('featured_groups.code', '!=', 'productos-destacados-smep')
|
||||
->groupBy('featured_groups.id', 'featured_groups.group_name', 'featured_groups.group_order')
|
||||
->orderBy('featured_groups.group_order')
|
||||
->selectRaw('featured_groups.group_name, COUNT(featured_items.id) AS item_count')
|
||||
->pluck('item_count', 'featured_groups.group_name')
|
||||
->map(fn ($count): int => (int) $count)
|
||||
->all();
|
||||
|
||||
$this->assertSame([
|
||||
'Dormitorio y Blanco' => 6,
|
||||
'Electrodomésticos' => 6,
|
||||
'Climatización' => 6,
|
||||
'Tecnología' => 6,
|
||||
'Bicicletas y Aire Libre' => 6,
|
||||
'Bazar' => 3,
|
||||
], $categoryGroupCounts);
|
||||
|
||||
$limitMigration->down();
|
||||
$this->assertSame(155, DB::table('featured_items')->count());
|
||||
|
||||
$catalogMigration->down();
|
||||
}
|
||||
|
||||
public function test_it_upgrades_legacy_catalog_images_and_restores_them_on_rollback(): void
|
||||
{
|
||||
$catalogMigration = require database_path(
|
||||
'migrations/2026_09_24_030000_seed_mutual_smep_catalog.php'
|
||||
);
|
||||
$imageMigration = require database_path(
|
||||
'migrations/2026_09_24_060000_optimize_mutual_smep_catalog_images.php'
|
||||
);
|
||||
|
||||
$catalogMigration->up();
|
||||
|
||||
DB::table('attachments')->orderBy('id')->each(function (object $attachment): void {
|
||||
$optimizedPath = (string) $attachment->path;
|
||||
$legacyPath = str_replace('catalog-v2/', 'catalog-v1/', $optimizedPath);
|
||||
$contents = Storage::disk('s3')->get($optimizedPath);
|
||||
|
||||
Storage::disk('s3')->put($legacyPath, $contents.'legacy');
|
||||
Storage::disk('s3')->delete($optimizedPath);
|
||||
DB::table('attachments')->where('id', $attachment->id)->update([
|
||||
'path' => $legacyPath,
|
||||
'size' => strlen($contents.'legacy'),
|
||||
]);
|
||||
});
|
||||
|
||||
$imageMigration->up();
|
||||
|
||||
$this->assertSame(511, count(Storage::disk('s3')->allFiles('tenants/mutual_smep/catalog-v2')));
|
||||
$this->assertSame(0, DB::table('attachments')->where('path', 'like', '%catalog-v1/%')->count());
|
||||
$this->assertSame(511, DB::table('attachments')->where('path', 'like', '%catalog-v2/%')->count());
|
||||
|
||||
$imageMigration->down();
|
||||
|
||||
$this->assertSame([], Storage::disk('s3')->allFiles('tenants/mutual_smep/catalog-v2'));
|
||||
$this->assertSame(511, DB::table('attachments')->where('path', 'like', '%catalog-v1/%')->count());
|
||||
|
||||
$catalogMigration->down();
|
||||
$this->assertSame([], Storage::disk('s3')->allFiles('tenants/mutual_smep/catalog-v1'));
|
||||
}
|
||||
|
||||
private function seedCategoriesAndAttributes(): void
|
||||
{
|
||||
$manifest = json_decode(
|
||||
(string) file_get_contents(database_path('data/mutual_smep_catalog_v1.json')),
|
||||
true,
|
||||
flags: JSON_THROW_ON_ERROR,
|
||||
);
|
||||
$parents = [];
|
||||
|
||||
foreach ($manifest['items'] as $item) {
|
||||
$parentName = $item['parent_category'];
|
||||
$parentId = $parents[$parentName] ??= DB::table('categorias')->insertGetId([
|
||||
'tenant_code' => 'mutual_smep',
|
||||
'categoria_id' => null,
|
||||
'nombre' => $parentName,
|
||||
]);
|
||||
|
||||
DB::table('categorias')->insertOrIgnore([
|
||||
'tenant_code' => 'mutual_smep',
|
||||
'categoria_id' => $parentId,
|
||||
'nombre' => $item['category'],
|
||||
]);
|
||||
}
|
||||
|
||||
foreach (['color', 'medida_cama', 'almacenamiento', 'memoria_ram', 'rodado'] as $code) {
|
||||
DB::table('attribute')->insert([
|
||||
'tenant_codigo' => 'mutual_smep',
|
||||
'codigo' => $code,
|
||||
'nombre' => $code,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private function createSchema(): void
|
||||
{
|
||||
Schema::create('tenants', fn (Blueprint $table) => $table->string('codigo')->primary());
|
||||
Schema::create('categorias', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('tenant_code');
|
||||
$table->unsignedBigInteger('categoria_id')->nullable();
|
||||
$table->string('nombre');
|
||||
$table->unique(['tenant_code', 'categoria_id', 'nombre']);
|
||||
});
|
||||
Schema::create('attribute', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('tenant_codigo');
|
||||
$table->string('codigo');
|
||||
$table->string('nombre');
|
||||
});
|
||||
Schema::create('brands', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('tenant_codigo');
|
||||
$table->string('nombre');
|
||||
$table->text('descripcion')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('inventories', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->unsignedBigInteger('sold_units')->default(0);
|
||||
$table->unsignedBigInteger('refunded_units')->default(0);
|
||||
$table->unsignedInteger('reserved_stock')->default(0);
|
||||
$table->unsignedInteger('real_stock')->default(0);
|
||||
});
|
||||
Schema::create('catalog_items', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('tenant_code');
|
||||
$table->unsignedBigInteger('category_id')->nullable();
|
||||
$table->unsignedBigInteger('brand_id')->nullable();
|
||||
$table->unsignedBigInteger('inventory_id')->nullable();
|
||||
$table->string('type');
|
||||
$table->string('slug');
|
||||
$table->string('nombre');
|
||||
$table->unsignedInteger('group_order')->default(0);
|
||||
$table->text('descripcion')->nullable();
|
||||
$table->decimal('precio', 10, 2);
|
||||
$table->string('inventory_policy');
|
||||
$table->string('inventory_subject');
|
||||
$table->boolean('has_tickets')->default(false);
|
||||
$table->unique(['tenant_code', 'slug']);
|
||||
});
|
||||
Schema::create('variantes', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('catalog_item_id')->constrained('catalog_items')->cascadeOnDelete();
|
||||
$table->unsignedBigInteger('inventory_id');
|
||||
$table->text('descripcion')->nullable();
|
||||
$table->decimal('precio', 10, 2)->nullable();
|
||||
});
|
||||
Schema::create('item_attributes', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('catalog_item_id')->constrained('catalog_items')->cascadeOnDelete();
|
||||
$table->unsignedBigInteger('attribute_id');
|
||||
$table->boolean('allow_multi_select')->default(false);
|
||||
$table->unsignedInteger('sort_order')->default(0);
|
||||
$table->boolean('show_in_selector')->default(true);
|
||||
$table->string('ticket_label')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('variant_values', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('variant_id')->constrained('variantes')->cascadeOnDelete();
|
||||
$table->foreignId('item_attribute_id')->constrained('item_attributes')->cascadeOnDelete();
|
||||
$table->text('value')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('attachments', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->uuid('key')->unique();
|
||||
$table->string('path');
|
||||
$table->string('filename');
|
||||
$table->string('type');
|
||||
$table->string('mime_type');
|
||||
$table->string('extension')->nullable();
|
||||
$table->unsignedBigInteger('size')->default(0);
|
||||
$table->timestamps();
|
||||
});
|
||||
Schema::create('catalog_items_attachments', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('variant_id')->nullable()->constrained('variantes')->cascadeOnDelete();
|
||||
$table->foreignId('catalog_item_id')->constrained('catalog_items')->cascadeOnDelete();
|
||||
$table->foreignId('attachment_id')->constrained('attachments')->cascadeOnDelete();
|
||||
$table->unsignedInteger('orden')->default(0);
|
||||
$table->boolean('is_enabled')->default(true);
|
||||
});
|
||||
Schema::create('featured_groups', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('tenant_code');
|
||||
$table->string('code');
|
||||
$table->string('source_type');
|
||||
$table->unsignedBigInteger('category_id')->nullable();
|
||||
$table->string('product_layout');
|
||||
$table->string('group_layout');
|
||||
$table->string('group_name');
|
||||
$table->unsignedInteger('group_order')->default(0);
|
||||
});
|
||||
Schema::create('featured_items', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('featured_group_id')->constrained('featured_groups')->cascadeOnDelete();
|
||||
$table->foreignId('catalog_item_id')->constrained('catalog_items')->cascadeOnDelete();
|
||||
$table->unsignedInteger('order')->default(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Seeders;
|
||||
|
||||
use Database\Seeders\MutualSmepAttributeSeeder;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MutualSmepAttributeSeederTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Schema::create('tenants', function (Blueprint $table): void {
|
||||
$table->string('codigo')->primary();
|
||||
});
|
||||
Schema::create('attribute', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('tenant_codigo');
|
||||
$table->string('codigo');
|
||||
$table->string('nombre');
|
||||
$table->boolean('is_required')->default(false);
|
||||
$table->json('metadata_schema')->nullable();
|
||||
$table->string('type');
|
||||
$table->timestamps();
|
||||
$table->unique(['tenant_codigo', 'codigo']);
|
||||
});
|
||||
Schema::create('attribute_options', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->foreignId('attribute_id')->constrained('attribute')->cascadeOnDelete();
|
||||
$table->string('value');
|
||||
$table->string('label');
|
||||
$table->unsignedInteger('sort_order')->default(0);
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Schema::dropIfExists('attribute_options');
|
||||
Schema::dropIfExists('attribute');
|
||||
Schema::dropIfExists('tenants');
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_it_creates_the_attributes_and_options_idempotently(): void
|
||||
{
|
||||
$this->createTenant('mutual_smep');
|
||||
|
||||
$this->seed(MutualSmepAttributeSeeder::class);
|
||||
$this->seed(MutualSmepAttributeSeeder::class);
|
||||
|
||||
$this->assertSame(5, DB::table('attribute')->where('tenant_codigo', 'mutual_smep')->count());
|
||||
$this->assertSame(25, DB::table('attribute_options')->count());
|
||||
$this->assertDatabaseHas('attribute', [
|
||||
'tenant_codigo' => 'mutual_smep',
|
||||
'codigo' => 'medida_cama',
|
||||
'nombre' => 'Medida',
|
||||
'type' => 'select',
|
||||
'is_required' => false,
|
||||
]);
|
||||
$this->assertDatabaseHas('attribute_options', [
|
||||
'value' => '512 GB',
|
||||
'label' => '512 GB',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_down_only_removes_mutual_smep_attributes(): void
|
||||
{
|
||||
$this->createTenant('mutual_smep');
|
||||
$this->createTenant('other');
|
||||
DB::table('attribute')->insert([
|
||||
'tenant_codigo' => 'other',
|
||||
'codigo' => 'color',
|
||||
'nombre' => 'Color',
|
||||
'type' => 'select',
|
||||
'created_at' => now(),
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
$seeder = app(MutualSmepAttributeSeeder::class);
|
||||
$seeder->run();
|
||||
$seeder->down();
|
||||
|
||||
$this->assertDatabaseMissing('attribute', ['tenant_codigo' => 'mutual_smep']);
|
||||
$this->assertDatabaseHas('attribute', [
|
||||
'tenant_codigo' => 'other',
|
||||
'codigo' => 'color',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_the_data_migration_runs_and_rolls_back_the_attributes(): void
|
||||
{
|
||||
$this->createTenant('mutual_smep');
|
||||
$migration = require database_path(
|
||||
'migrations/2026_09_24_020000_seed_mutual_smep_attributes.php'
|
||||
);
|
||||
|
||||
$migration->up();
|
||||
|
||||
$this->assertSame(5, DB::table('attribute')->where('tenant_codigo', 'mutual_smep')->count());
|
||||
$this->assertSame(25, DB::table('attribute_options')->count());
|
||||
|
||||
$migration->down();
|
||||
|
||||
$this->assertDatabaseMissing('attribute', ['tenant_codigo' => 'mutual_smep']);
|
||||
$this->assertSame(0, DB::table('attribute_options')->count());
|
||||
}
|
||||
|
||||
private function createTenant(string $code): void
|
||||
{
|
||||
DB::table('tenants')->insert(['codigo' => $code]);
|
||||
}
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Seeders;
|
||||
|
||||
use App\Domains\Commerce\Catalog\Models\Category;
|
||||
use Database\Seeders\MutualSmepCategorySeeder;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Tests\TestCase;
|
||||
|
||||
class MutualSmepCategorySeederTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
Schema::create('tenants', function (Blueprint $table): void {
|
||||
$table->string('codigo')->primary();
|
||||
});
|
||||
|
||||
Schema::create('categorias', function (Blueprint $table): void {
|
||||
$table->id();
|
||||
$table->string('tenant_code')->nullable();
|
||||
$table->unsignedBigInteger('categoria_id')->nullable();
|
||||
$table->string('nombre');
|
||||
$table->boolean('is_enabled')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
Schema::dropIfExists('categorias');
|
||||
Schema::dropIfExists('tenants');
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
public function test_it_creates_the_mutual_smep_category_hierarchy_idempotently(): void
|
||||
{
|
||||
$this->createTenant('mutual_smep');
|
||||
|
||||
$this->seed(MutualSmepCategorySeeder::class);
|
||||
$this->seed(MutualSmepCategorySeeder::class);
|
||||
|
||||
$parents = Category::query()
|
||||
->where('tenant_code', 'mutual_smep')
|
||||
->whereNull('categoria_id')
|
||||
->with('subCategories')
|
||||
->get()
|
||||
->keyBy('nombre');
|
||||
|
||||
$this->assertCount(6, $parents);
|
||||
$this->assertCount(37, Category::query()->where('tenant_code', 'mutual_smep')->get());
|
||||
$this->assertEqualsCanonicalizing([
|
||||
'Colchones',
|
||||
'Bases y Sommiers',
|
||||
'Acolchados y Edredones',
|
||||
'Frazadas y Mantas',
|
||||
'Infantil y Cuna',
|
||||
], $parents->get('Dormitorio y Blanco')->subCategories->pluck('nombre')->all());
|
||||
$this->assertEqualsCanonicalizing([
|
||||
'Celulares',
|
||||
'Notebooks',
|
||||
'Smart TV',
|
||||
'Audio',
|
||||
'Accesorios de Computación',
|
||||
'Soportes para TV',
|
||||
], $parents->get('Tecnología')->subCategories->pluck('nombre')->all());
|
||||
}
|
||||
|
||||
public function test_down_removes_only_the_seeded_mutual_smep_hierarchy(): void
|
||||
{
|
||||
$this->createTenant('mutual_smep');
|
||||
$this->createTenant('other');
|
||||
Category::query()->create([
|
||||
'tenant_code' => 'other',
|
||||
'nombre' => 'Tecnología',
|
||||
]);
|
||||
|
||||
$seeder = app(MutualSmepCategorySeeder::class);
|
||||
$seeder->run();
|
||||
$seeder->down();
|
||||
|
||||
$this->assertDatabaseMissing('categorias', [
|
||||
'tenant_code' => 'mutual_smep',
|
||||
]);
|
||||
$this->assertDatabaseHas('categorias', [
|
||||
'tenant_code' => 'other',
|
||||
'nombre' => 'Tecnología',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_the_data_migration_runs_and_rolls_back_the_seeder(): void
|
||||
{
|
||||
$this->createTenant('mutual_smep');
|
||||
$migration = require database_path(
|
||||
'migrations/2026_09_24_010000_seed_mutual_smep_categories.php'
|
||||
);
|
||||
|
||||
$migration->up();
|
||||
|
||||
$this->assertCount(37, Category::query()->where('tenant_code', 'mutual_smep')->get());
|
||||
|
||||
$migration->down();
|
||||
|
||||
$this->assertDatabaseMissing('categorias', [
|
||||
'tenant_code' => 'mutual_smep',
|
||||
]);
|
||||
}
|
||||
|
||||
private function createTenant(string $code): void
|
||||
{
|
||||
DB::table('tenants')->insert([
|
||||
'codigo' => $code,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user