From dcf4383fbf973cc25ae28b533f4254a3894ffcc1 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Tue, 15 Sep 2026 15:15:18 -0300 Subject: [PATCH] feat(variants): implement inventory cloning for historical stock updates and adjust related tests --- .../Services/VariantReplacementService.php | 41 +++++++++++++++++++ .../Controllers/FoodController.php | 2 +- .../Services/FoodService.php | 33 ++++++++++++--- .../Event/AdminAppEventControllerTest.php | 24 +++++++++-- .../FoodControllerTest.php | 22 +++++++++- 5 files changed, 110 insertions(+), 12 deletions(-) diff --git a/app/Domains/Catalog/Services/VariantReplacementService.php b/app/Domains/Catalog/Services/VariantReplacementService.php index b6702ce..0c170aa 100644 --- a/app/Domains/Catalog/Services/VariantReplacementService.php +++ b/app/Domains/Catalog/Services/VariantReplacementService.php @@ -3,6 +3,9 @@ namespace App\Domains\Catalog\Services; use App\Domains\Catalog\Models\BundleComponent; +use App\Domains\Catalog\Models\Inventory; +use App\Domains\Catalog\Models\StockReservation; +use App\Domains\Catalog\Models\StockReservationLine; use App\Domains\Catalog\Models\Variant; use App\Domains\Event\Models\EventDate; use Illuminate\Support\Collection; @@ -92,11 +95,14 @@ class VariantReplacementService /** @param Collection $eventDateIds */ private function cloneWithDates(Variant $source, Collection $eventDateIds): Variant { + $replacementInventory = $this->cloneInventory($source); $replacement = $source->replicate([ 'event_date_id', + 'inventory_id', 'replaced_by_variant_id', 'sales_disabled_at', ]); + $replacement->inventory_id = $replacementInventory->getKey(); $replacement->event_date_id = $eventDateIds->count() === 1 ? $eventDateIds->first() : null; @@ -125,6 +131,41 @@ class VariantReplacementService return $replacement->load(['eventDates', 'eventDate', 'definitions', 'allAttachments']); } + private function cloneInventory(Variant $source): Inventory + { + $activeLines = StockReservationLine::query() + ->where('inventory_id', $source->inventory_id) + ->whereHas('reservation', fn ($reservation) => $reservation + ->where('status', StockReservation::STATUS_ACTIVE)) + ->orderBy('id') + ->lockForUpdate() + ->get(); + $sourceInventory = Inventory::query() + ->whereKey($source->inventory_id) + ->lockForUpdate() + ->firstOrFail(); + $reservedStock = (int) $activeLines->sum('quantity'); + + if ($sourceInventory->reserved_stock !== $reservedStock) { + throw new \LogicException('El inventario reservado de la variante es inconsistente.'); + } + + $replacementInventory = Inventory::query()->create([ + 'sold_units' => $sourceInventory->sold_units, + 'reserved_stock' => $reservedStock, + 'real_stock' => $sourceInventory->real_stock, + ]); + + if ($activeLines->isNotEmpty()) { + StockReservationLine::query() + ->whereKey($activeLines->modelKeys()) + ->update(['inventory_id' => $replacementInventory->getKey()]); + } + $sourceInventory->update(['reserved_stock' => 0]); + + return $replacementInventory; + } + /** @return list */ private function definitionSignature(Variant $variant): array { diff --git a/app/Domains/FiestaFutbolInfantil/Controllers/FoodController.php b/app/Domains/FiestaFutbolInfantil/Controllers/FoodController.php index 1923540..103bdf0 100644 --- a/app/Domains/FiestaFutbolInfantil/Controllers/FoodController.php +++ b/app/Domains/FiestaFutbolInfantil/Controllers/FoodController.php @@ -2,8 +2,8 @@ namespace App\Domains\FiestaFutbolInfantil\Controllers; -use App\Domains\FiestaFutbolInfantil\Requests\UpsertFoodVariantsRequest; use App\Domains\FiestaFutbolInfantil\Requests\UpdateHistoricalFoodStockRequest; +use App\Domains\FiestaFutbolInfantil\Requests\UpsertFoodVariantsRequest; use App\Domains\FiestaFutbolInfantil\Resources\FoodResource; use App\Domains\FiestaFutbolInfantil\Services\FoodService; use App\Http\Controllers\Controller; diff --git a/app/Domains/FiestaFutbolInfantil/Services/FoodService.php b/app/Domains/FiestaFutbolInfantil/Services/FoodService.php index 06caafb..571d1cd 100644 --- a/app/Domains/FiestaFutbolInfantil/Services/FoodService.php +++ b/app/Domains/FiestaFutbolInfantil/Services/FoodService.php @@ -126,8 +126,6 @@ class FoodService $variantIds = collect($variants)->pluck('id')->map(fn ($id): int => (int) $id); $historicalVariants = $food->variants() ->whereIn('id', $variantIds) - ->whereNull('sales_disabled_at') - ->whereNull('replaced_by_variant_id') ->with(['inventory', 'eventDate', 'eventDates']) ->lockForUpdate() ->get() @@ -145,10 +143,7 @@ class FoodService } $stock = (int) $data['stock']; - $inventory = Inventory::query() - ->whereKey($variant->inventory_id) - ->lockForUpdate() - ->firstOrFail(); + $inventory = $this->inventoryForHistoricalStockUpdate($variant); if ($stock < $inventory->reserved_stock) { throw ValidationException::withMessages([ "variants.{$index}.stock" => [ @@ -164,6 +159,32 @@ class FoodService }); } + private function inventoryForHistoricalStockUpdate(Variant $variant): Inventory + { + $inventory = Inventory::query() + ->whereKey($variant->inventory_id) + ->lockForUpdate() + ->firstOrFail(); + $variantsSharingInventory = Variant::query() + ->where('inventory_id', $inventory->getKey()) + ->orderBy('id') + ->lockForUpdate() + ->get(['id']); + + if ($variantsSharingInventory->count() === 1) { + return $inventory; + } + + $historicalInventory = Inventory::query()->create([ + 'sold_units' => $inventory->sold_units, + 'reserved_stock' => 0, + 'real_stock' => $inventory->real_stock, + ]); + $variant->update(['inventory_id' => $historicalInventory->getKey()]); + + return $historicalInventory; + } + public function delete(Tenant $tenant, int $foodId): void { $variant = Variant::query() diff --git a/tests/Feature/Event/AdminAppEventControllerTest.php b/tests/Feature/Event/AdminAppEventControllerTest.php index cd6c7c3..32cad55 100644 --- a/tests/Feature/Event/AdminAppEventControllerTest.php +++ b/tests/Feature/Event/AdminAppEventControllerTest.php @@ -8,6 +8,8 @@ use App\Domains\Auth\Models\User; use App\Domains\Authorization\Enums\RoleCode; use App\Domains\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\Inventory; +use App\Domains\Catalog\Models\StockReservation; +use App\Domains\Catalog\Models\StockReservationLine; use App\Domains\Catalog\Models\Variant; use App\Domains\Event\Events\EventDateRescheduled; use App\Domains\Event\Events\EventDateSuspended; @@ -249,7 +251,17 @@ class AdminAppEventControllerTest extends TestCase 'time_end' => '20:00', ]); $variant = $this->createVariant($tenant, $original->id); - $variant->inventory()->update(['real_stock' => 5]); + $variant->inventory()->update(['real_stock' => 5, 'reserved_stock' => 2]); + $reservation = StockReservation::query()->create([ + 'status' => StockReservation::STATUS_ACTIVE, + 'expires_at' => now()->addHour(), + ]); + $reservationLine = StockReservationLine::query()->create([ + 'stock_reservation_id' => $reservation->id, + 'inventory_id' => $variant->inventory_id, + 'quantity' => 2, + 'tracks_inventory' => true, + ]); $ticket = $this->createTicket($tenant, $admin, $variant); Sanctum::actingAs($admin); @@ -283,7 +295,12 @@ class AdminAppEventControllerTest extends TestCase $replacement = $variant->replacement()->firstOrFail(); $this->assertSame($original->id, $variant->event_date_id); $this->assertSame($destination->id, $replacement->event_date_id); - $this->assertSame($variant->inventory_id, $replacement->inventory_id); + $this->assertNotSame($variant->inventory_id, $replacement->inventory_id); + $this->assertSame(5, $variant->inventory->fresh()->real_stock); + $this->assertSame(0, $variant->inventory->fresh()->reserved_stock); + $this->assertSame(5, $replacement->inventory->real_stock); + $this->assertSame(2, $replacement->inventory->reserved_stock); + $this->assertSame($replacement->inventory_id, $reservationLine->fresh()->inventory_id); $this->assertNotNull($variant->sales_disabled_at); $this->assertSame($replacement->id, $variant->replaced_by_variant_id); $this->assertSame( @@ -350,7 +367,8 @@ class AdminAppEventControllerTest extends TestCase ]); $replacement->refresh(); $latestReplacement = $replacement->replacement()->firstOrFail(); - $this->assertSame($replacement->inventory_id, $latestReplacement->inventory_id); + $this->assertNotSame($replacement->inventory_id, $latestReplacement->inventory_id); + $this->assertSame(5, $latestReplacement->inventory->real_stock); $this->assertSame('2027-10-25', $latestReplacement->eventDate->date->format('Y-m-d')); $this->assertFalse($replacement->isSellable()); $this->assertTrue($latestReplacement->isSellable()); diff --git a/tests/Feature/FiestaFutbolInfantil/FoodControllerTest.php b/tests/Feature/FiestaFutbolInfantil/FoodControllerTest.php index d41a7bb..3bda7f1 100644 --- a/tests/Feature/FiestaFutbolInfantil/FoodControllerTest.php +++ b/tests/Feature/FiestaFutbolInfantil/FoodControllerTest.php @@ -7,6 +7,7 @@ use App\Domains\Authorization\Enums\RoleCode; use App\Domains\Catalog\Models\Attribute; use App\Domains\Catalog\Models\CatalogItem; use App\Domains\Catalog\Models\Category; +use App\Domains\Catalog\Models\Inventory; use App\Domains\Catalog\Models\Variant; use App\Domains\Event\Services\EventService; use App\Domains\Menu\Models\Menu; @@ -224,16 +225,33 @@ class FoodControllerTest extends TestCase $historicalVariantId = $created->json('data.variants.0.id'); $activeVariantId = $created->json('data.variants.1.id'); - app(EventService::class)->suspendDateForTenant($tenant, $historicalDate, $admin); + app(EventService::class)->rescheduleDateForTenant( + $tenant, + $historicalDate, + ['date' => $activeDate->date->format('Y-m-d')], + $admin, + ); + $historicalVariant = Variant::query()->findOrFail($historicalVariantId); + $replacementVariant = $historicalVariant->replacement()->firstOrFail(); + $replacementInventoryId = $replacementVariant->inventory_id; - $this->patchJson('/api/v1/adminapp/tenant/foods/history-stock', [ + // Simula variantes creadas antes de que la reprogramación separara sus inventarios. + $replacementVariant->update(['inventory_id' => $historicalVariant->inventory_id]); + Inventory::query()->whereKey($replacementInventoryId)->delete(); + + $updated = $this->patchJson('/api/v1/adminapp/tenant/foods/history-stock', [ 'variants' => [['id' => $historicalVariantId, 'stock' => 45]], ]) ->assertOk() ->assertJsonPath('data.history.0.variants.0.id', $historicalVariantId) ->assertJsonPath('data.history.0.variants.0.stock', 45); + $replacement = collect($updated->json('data.variants')) + ->firstWhere('schedule', 'Almuerzo'); + $this->assertSame(100, $replacement['stock']); $historicalInventoryId = Variant::query()->findOrFail($historicalVariantId)->inventory_id; + $currentInventoryId = $replacementVariant->fresh()->inventory_id; + $this->assertNotSame($historicalInventoryId, $currentInventoryId); $this->assertDatabaseHas('inventories', [ 'id' => $historicalInventoryId, 'real_stock' => 45,