feature/refund #7

Merged
ncoronel merged 63 commits from feature/refund into homo 2026-09-17 15:19:27 +00:00
5 changed files with 110 additions and 12 deletions
Showing only changes of commit dcf4383fbf - Show all commits

View File

@ -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<int, int> $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<string> */
private function definitionSignature(Variant $variant): array
{

View File

@ -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;

View File

@ -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()

View File

@ -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());

View File

@ -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,