feat(admin-stock): apply idempotent availability deltas

This commit is contained in:
2026-09-25 10:15:29 -03:00
parent 863e911120
commit 717f02ce16
17 changed files with 59 additions and 72 deletions

View File

@@ -28,6 +28,7 @@ class AccommodationController extends Controller
$this->accommodationService->upsertMany( $this->accommodationService->upsertMany(
$tenant, $tenant,
$request->validated('variants'), $request->validated('variants'),
$request->validated('stock_adjustment_id'),
) )
); );
} }

View File

@@ -29,6 +29,7 @@ class EntryController extends Controller
$entries = $this->entryService->upsertMany( $entries = $this->entryService->upsertMany(
$tenant, $tenant,
$request->validated('entries'), $request->validated('entries'),
$request->validated('stock_adjustment_id'),
); );
return EntryResource::collection($entries) return EntryResource::collection($entries)

View File

@@ -29,6 +29,7 @@ class FoodController extends Controller
$this->foodService->upsertMany( $this->foodService->upsertMany(
$tenant, $tenant,
$request->validated('variants'), $request->validated('variants'),
$request->validated('stock_adjustment_id'),
) )
); );
} }
@@ -39,6 +40,7 @@ class FoodController extends Controller
$this->foodService->updateHistoricalStock( $this->foodService->updateHistoricalStock(
$request->user()->tenant()->firstOrFail(), $request->user()->tenant()->firstOrFail(),
$request->validated('variants'), $request->validated('variants'),
$request->validated('stock_adjustment_id'),
) )
); );
} }

View File

@@ -28,6 +28,7 @@ class MerchandiseController extends Controller
$items = $this->merchandiseService->upsertMany( $items = $this->merchandiseService->upsertMany(
$tenant, $tenant,
$request->validated('items'), $request->validated('items'),
$request->validated('stock_adjustment_id'),
); );
return MerchandiseResource::collection($items) return MerchandiseResource::collection($items)

View File

@@ -16,9 +16,11 @@ class UpdateHistoricalFoodStockRequest extends FormRequest
{ {
return [ return [
'variants' => ['required', 'array', 'min:1', 'max:500'], 'variants' => ['required', 'array', 'min:1', 'max:500'],
'variants.*' => ['required', 'array:id,stock'], 'stock_adjustment_id' => ['nullable', 'uuid'],
'variants.*' => ['required', 'array:id,stock,stock_difference'],
'variants.*.id' => ['required', 'integer', 'distinct'], 'variants.*.id' => ['required', 'integer', 'distinct'],
'variants.*.stock' => ['required', 'integer', 'min:0'], 'variants.*.stock' => ['required', 'integer', 'min:0'],
'variants.*.stock_difference' => ['nullable', 'integer'],
]; ];
} }
} }

View File

@@ -16,11 +16,13 @@ class UpsertAccommodationVariantsRequest extends FormRequest
{ {
return [ return [
'variants' => ['required', 'array', 'min:1', 'max:500'], 'variants' => ['required', 'array', 'min:1', 'max:500'],
'variants.*' => ['required', 'array:id,title,description,stock,price'], 'stock_adjustment_id' => ['nullable', 'uuid'],
'variants.*' => ['required', 'array:id,title,description,stock,stock_difference,price'],
'variants.*.id' => ['sometimes', 'nullable', 'integer', 'distinct'], 'variants.*.id' => ['sometimes', 'nullable', 'integer', 'distinct'],
'variants.*.title' => ['required', 'string', 'max:255'], 'variants.*.title' => ['required', 'string', 'max:255'],
'variants.*.description' => ['sometimes', 'nullable', 'string'], 'variants.*.description' => ['sometimes', 'nullable', 'string'],
'variants.*.stock' => ['required', 'integer', 'min:0'], 'variants.*.stock' => ['required', 'integer', 'min:0'],
'variants.*.stock_difference' => ['nullable', 'integer'],
'variants.*.price' => ['required', 'numeric', 'min:0', 'max:99999999.99'], 'variants.*.price' => ['required', 'numeric', 'min:0', 'max:99999999.99'],
]; ];
} }

View File

@@ -20,7 +20,8 @@ class UpsertEntriesRequest extends FormRequest
return [ return [
'entries' => ['required', 'array', 'min:1', 'max:100'], 'entries' => ['required', 'array', 'min:1', 'max:100'],
'entries.*' => ['required', 'array:id,title,description,event_date_ids,stock,price'], 'stock_adjustment_id' => ['nullable', 'uuid'],
'entries.*' => ['required', 'array:id,title,description,event_date_ids,stock,stock_difference,price'],
'entries.*.id' => [ 'entries.*.id' => [
'sometimes', 'sometimes',
'nullable', 'nullable',
@@ -47,6 +48,7 @@ class UpsertEntriesRequest extends FormRequest
), ),
], ],
'entries.*.stock' => ['required', 'integer', 'min:0'], 'entries.*.stock' => ['required', 'integer', 'min:0'],
'entries.*.stock_difference' => ['nullable', 'integer'],
'entries.*.price' => ['required', 'numeric', 'min:0', 'max:99999999.99'], 'entries.*.price' => ['required', 'numeric', 'min:0', 'max:99999999.99'],
]; ];
} }

View File

@@ -20,7 +20,8 @@ class UpsertFoodVariantsRequest extends FormRequest
return [ return [
'variants' => ['required', 'array', 'min:1', 'max:500'], 'variants' => ['required', 'array', 'min:1', 'max:500'],
'variants.*' => ['required', 'array:id,event_date_id,schedule,service,description,stock,price'], 'stock_adjustment_id' => ['nullable', 'uuid'],
'variants.*' => ['required', 'array:id,event_date_id,schedule,service,description,stock,stock_difference,price'],
'variants.*.id' => ['sometimes', 'nullable', 'integer', 'distinct'], 'variants.*.id' => ['sometimes', 'nullable', 'integer', 'distinct'],
'variants.*.event_date_id' => [ 'variants.*.event_date_id' => [
'required', 'required',
@@ -33,6 +34,7 @@ class UpsertFoodVariantsRequest extends FormRequest
'variants.*.service' => ['required', 'string', 'max:255'], 'variants.*.service' => ['required', 'string', 'max:255'],
'variants.*.description' => ['sometimes', 'nullable', 'string'], 'variants.*.description' => ['sometimes', 'nullable', 'string'],
'variants.*.stock' => ['required', 'integer', 'min:0'], 'variants.*.stock' => ['required', 'integer', 'min:0'],
'variants.*.stock_difference' => ['nullable', 'integer'],
'variants.*.price' => ['required', 'numeric', 'min:0', 'max:99999999.99'], 'variants.*.price' => ['required', 'numeric', 'min:0', 'max:99999999.99'],
]; ];
} }

View File

@@ -39,11 +39,13 @@ class UpsertMerchandiseRequest extends FormRequest
'items.*.description' => ['sometimes', 'nullable', 'string'], 'items.*.description' => ['sometimes', 'nullable', 'string'],
'items.*.max_units_per_user' => ['required', 'integer', 'min:1'], 'items.*.max_units_per_user' => ['required', 'integer', 'min:1'],
'items.*.variants' => ['required', 'array', 'min:1', 'max:500'], 'items.*.variants' => ['required', 'array', 'min:1', 'max:500'],
'items.*.variants.*' => ['required', 'array:id,color,size,stock,price'], 'stock_adjustment_id' => ['nullable', 'uuid'],
'items.*.variants.*' => ['required', 'array:id,color,size,stock,stock_difference,price'],
'items.*.variants.*.id' => ['sometimes', 'nullable', 'integer', 'distinct'], 'items.*.variants.*.id' => ['sometimes', 'nullable', 'integer', 'distinct'],
'items.*.variants.*.color' => ['required', 'string', 'max:255'], 'items.*.variants.*.color' => ['required', 'string', 'max:255'],
'items.*.variants.*.size' => ['required', 'string', 'max:255'], 'items.*.variants.*.size' => ['required', 'string', 'max:255'],
'items.*.variants.*.stock' => ['required', 'integer', 'min:0'], 'items.*.variants.*.stock' => ['required', 'integer', 'min:0'],
'items.*.variants.*.stock_difference' => ['nullable', 'integer'],
'items.*.variants.*.price' => ['required', 'numeric', 'min:0', 'max:99999999.99'], 'items.*.variants.*.price' => ['required', 'numeric', 'min:0', 'max:99999999.99'],
]; ];
} }

View File

@@ -37,7 +37,7 @@ class AccommodationResource extends JsonResource
'title' => $options->get($value)?->label ?? $value, 'title' => $options->get($value)?->label ?? $value,
'value' => $value, 'value' => $value,
'description' => $variant->descripcion, 'description' => $variant->descripcion,
'stock' => $variant->inventory->real_stock, 'stock' => $variant->inventory->availableStock(),
'price' => number_format($variant->getPrice(), 2, '.', ''), 'price' => number_format($variant->getPrice(), 2, '.', ''),
]; ];
})->values(), })->values(),

View File

@@ -33,7 +33,7 @@ class EntryResource extends JsonResource
'title' => $this->nombre, 'title' => $this->nombre,
'description' => $this->descripcion, 'description' => $this->descripcion,
'event_date_ids' => $variant->selectedEventDates()->pluck('id')->values(), 'event_date_ids' => $variant->selectedEventDates()->pluck('id')->values(),
'stock' => $variant->inventory->real_stock, 'stock' => $variant->inventory->availableStock(),
'price' => $this->precio, 'price' => $this->precio,
]; ];
} }

View File

@@ -81,7 +81,7 @@ class FoodResource extends JsonResource
'schedule' => $values->get('horario'), 'schedule' => $values->get('horario'),
'service' => $values->get('servicio'), 'service' => $values->get('servicio'),
'description' => $variant->descripcion, 'description' => $variant->descripcion,
'stock' => $variant->inventory->real_stock, 'stock' => $variant->inventory->availableStock(),
'price' => number_format($variant->getPrice(), 2, '.', ''), 'price' => number_format($variant->getPrice(), 2, '.', ''),
]; ];
} }

View File

@@ -44,7 +44,7 @@ class MerchandiseResource extends JsonResource
'color_value' => $colorValue, 'color_value' => $colorValue,
'size' => $sizeOptions->get($sizeValue)?->label ?? $sizeValue, 'size' => $sizeOptions->get($sizeValue)?->label ?? $sizeValue,
'size_value' => $sizeValue, 'size_value' => $sizeValue,
'stock' => $variant->inventory->real_stock, 'stock' => $variant->inventory->availableStock(),
'price' => number_format($variant->getPrice(), 2, '.', ''), 'price' => number_format($variant->getPrice(), 2, '.', ''),
]; ];
})->values(), })->values(),

View File

@@ -39,9 +39,9 @@ class AccommodationService
/** /**
* @param array<int, array<string, mixed>> $variants * @param array<int, array<string, mixed>> $variants
*/ */
public function upsertMany(Tenant $tenant, array $variants): CatalogItem public function upsertMany(Tenant $tenant, array $variants, ?string $stockAdjustmentId = null): CatalogItem
{ {
return DB::transaction(function () use ($tenant, $variants): CatalogItem { return DB::transaction(function () use ($tenant, $variants, $stockAdjustmentId): CatalogItem {
$attribute = $this->attribute($tenant); $attribute = $this->attribute($tenant);
$accommodation = $this->accommodation($tenant, $variants); $accommodation = $this->accommodation($tenant, $variants);
$itemAttribute = $accommodation->itemAttributes()->firstOrCreate( $itemAttribute = $accommodation->itemAttributes()->firstOrCreate(
@@ -70,7 +70,7 @@ class AccommodationService
if ($variant === null) { if ($variant === null) {
$this->createVariant($attribute, $accommodation, $itemAttribute, $data); $this->createVariant($attribute, $accommodation, $itemAttribute, $data);
} else { } else {
$this->updateVariant($attribute, $variant, $itemAttribute, $data, $index); $this->updateVariant($attribute, $variant, $itemAttribute, $data, $index, $stockAdjustmentId);
} }
} }
@@ -212,6 +212,7 @@ class AccommodationService
$this->createOption($attribute, $data['value'], $data['title']); $this->createOption($attribute, $data['value'], $data['title']);
$inventory = Inventory::query()->create(['real_stock' => $data['stock']]); $inventory = Inventory::query()->create(['real_stock' => $data['stock']]);
$inventory->recordInitialization();
$variant = $accommodation->variants()->create([ $variant = $accommodation->variants()->create([
'inventory_id' => $inventory->id, 'inventory_id' => $inventory->id,
'descripcion' => $data['description'], 'descripcion' => $data['description'],
@@ -230,20 +231,13 @@ class AccommodationService
ItemAttribute $itemAttribute, ItemAttribute $itemAttribute,
array $data, array $data,
int $index, int $index,
?string $stockAdjustmentId,
): void { ): void {
$inventory = Inventory::query() $inventory = Inventory::query()
->whereKey($variant->inventory_id) ->whereKey($variant->inventory_id)
->lockForUpdate() ->lockForUpdate()
->firstOrFail(); ->firstOrFail();
if ($data['stock'] < $inventory->reserved_stock) {
throw ValidationException::withMessages([
"variants.{$index}.stock" => [
'El stock no puede ser menor que la cantidad actualmente reservada.',
],
]);
}
$definition = $variant->definitions $definition = $variant->definitions
->firstWhere('item_attribute_id', $itemAttribute->id); ->firstWhere('item_attribute_id', $itemAttribute->id);
$option = $definition === null $option = $definition === null
@@ -263,7 +257,8 @@ class AccommodationService
'descripcion' => $data['description'], 'descripcion' => $data['description'],
'precio' => $data['price'], 'precio' => $data['price'],
]); ]);
$inventory->update(['real_stock' => $data['stock']]); $stockDifference = (int) ($data['stock_difference'] ?? ($data['stock'] - $inventory->availableStock()));
$inventory->adjustAvailableStock($stockDifference, idempotencyKey: $stockAdjustmentId);
$variant->definitions()->updateOrCreate( $variant->definitions()->updateOrCreate(
['item_attribute_id' => $itemAttribute->id], ['item_attribute_id' => $itemAttribute->id],
['value' => $data['value']], ['value' => $data['value']],

View File

@@ -43,18 +43,18 @@ class EntryService
* @param array<int, array<string, mixed>> $entries * @param array<int, array<string, mixed>> $entries
* @return Collection<int, CatalogItem> * @return Collection<int, CatalogItem>
*/ */
public function upsertMany(Tenant $tenant, array $entries): Collection public function upsertMany(Tenant $tenant, array $entries, ?string $stockAdjustmentId = null): Collection
{ {
return DB::transaction(function () use ($tenant, $entries): Collection { return DB::transaction(function () use ($tenant, $entries, $stockAdjustmentId): Collection {
$reservedSlugs = []; $reservedSlugs = [];
$category = Category::query()->firstOrCreate([ $category = Category::query()->firstOrCreate([
'tenant_code' => $tenant->codigo, 'tenant_code' => $tenant->codigo,
'nombre' => 'Entradas', 'nombre' => 'Entradas',
]); ]);
return collect($entries)->map(function (array $entry, int $index) use ($tenant, $category, &$reservedSlugs): CatalogItem { return collect($entries)->map(function (array $entry, int $index) use ($tenant, $category, $stockAdjustmentId, &$reservedSlugs): CatalogItem {
if (isset($entry['id'])) { if (isset($entry['id'])) {
return $this->update($tenant, $category, $entry, $index); return $this->update($tenant, $category, $entry, $index, $stockAdjustmentId);
} }
$slug = $this->uniqueSlug($tenant, $entry['title'], $reservedSlugs); $slug = $this->uniqueSlug($tenant, $entry['title'], $reservedSlugs);
@@ -92,7 +92,7 @@ class EntryService
} }
/** @param array<string, mixed> $entry */ /** @param array<string, mixed> $entry */
private function update(Tenant $tenant, Category $category, array $entry, int $index): CatalogItem private function update(Tenant $tenant, Category $category, array $entry, int $index, ?string $stockAdjustmentId): CatalogItem
{ {
$catalogItem = CatalogItem::query() $catalogItem = CatalogItem::query()
->whereKey($entry['id']) ->whereKey($entry['id'])
@@ -121,14 +121,6 @@ class EntryService
->lockForUpdate() ->lockForUpdate()
->firstOrFail(); ->firstOrFail();
if ((int) $entry['stock'] < $inventory->reserved_stock) {
throw ValidationException::withMessages([
"entries.{$index}.stock" => [
'El stock no puede ser menor que la cantidad actualmente reservada.',
],
]);
}
$eventDateIds = collect($entry['event_date_ids']) $eventDateIds = collect($entry['event_date_ids'])
->map(fn ($id): int => (int) $id) ->map(fn ($id): int => (int) $id)
->unique() ->unique()
@@ -149,7 +141,8 @@ class EntryService
$catalogItem->itemAttributes() $catalogItem->itemAttributes()
->whereHas('attribute', fn ($query) => $query->where('codigo', 'event_date')) ->whereHas('attribute', fn ($query) => $query->where('codigo', 'event_date'))
->update(['allow_multi_select' => true]); ->update(['allow_multi_select' => true]);
$inventory->update(['real_stock' => $entry['stock']]); $stockDifference = (int) ($entry['stock_difference'] ?? ((int) $entry['stock'] - $inventory->availableStock()));
$inventory->adjustAvailableStock($stockDifference, idempotencyKey: $stockAdjustmentId);
return $catalogItem->load([ return $catalogItem->load([
'variants.inventory', 'variants.inventory',

View File

@@ -11,9 +11,9 @@ use App\Domains\Commerce\Catalog\Models\Inventory;
use App\Domains\Commerce\Catalog\Models\ItemAttribute; use App\Domains\Commerce\Catalog\Models\ItemAttribute;
use App\Domains\Commerce\Catalog\Models\Variant; use App\Domains\Commerce\Catalog\Models\Variant;
use App\Domains\Commerce\Catalog\Services\CatalogService; use App\Domains\Commerce\Catalog\Services\CatalogService;
use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Event\Enums\EventDateStatus; use App\Domains\Ticketing\Event\Enums\EventDateStatus;
use App\Domains\Ticketing\Event\Models\EventDate; use App\Domains\Ticketing\Event\Models\EventDate;
use App\Domains\Core\Tenant\Models\Tenant;
use Illuminate\Support\Collection; use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
@@ -50,9 +50,9 @@ class FoodService
/** /**
* @param array<int, array<string, mixed>> $variants * @param array<int, array<string, mixed>> $variants
*/ */
public function upsertMany(Tenant $tenant, array $variants): CatalogItem public function upsertMany(Tenant $tenant, array $variants, ?string $stockAdjustmentId = null): CatalogItem
{ {
return DB::transaction(function () use ($tenant, $variants): CatalogItem { return DB::transaction(function () use ($tenant, $variants, $stockAdjustmentId): CatalogItem {
$attributes = $this->attributes($tenant); $attributes = $this->attributes($tenant);
$food = $this->food($tenant, $variants); $food = $this->food($tenant, $variants);
$itemAttributes = $this->itemAttributes($food, $attributes); $itemAttributes = $this->itemAttributes($food, $attributes);
@@ -85,7 +85,7 @@ class FoodService
if ($variant === null) { if ($variant === null) {
$this->createVariant($food, $itemAttributes, $data); $this->createVariant($food, $itemAttributes, $data);
} else { } else {
$this->updateVariant($variant, $itemAttributes, $data, $index); $this->updateVariant($variant, $itemAttributes, $data, $index, $stockAdjustmentId);
} }
} }
@@ -115,9 +115,9 @@ class FoodService
/** /**
* @param array<int, array{id: int, stock: int}> $variants * @param array<int, array{id: int, stock: int}> $variants
*/ */
public function updateHistoricalStock(Tenant $tenant, array $variants): CatalogItem public function updateHistoricalStock(Tenant $tenant, array $variants, ?string $stockAdjustmentId = null): CatalogItem
{ {
return DB::transaction(function () use ($tenant, $variants): CatalogItem { return DB::transaction(function () use ($tenant, $variants, $stockAdjustmentId): CatalogItem {
$food = CatalogItem::query() $food = CatalogItem::query()
->forTenantCatalog($tenant) ->forTenantCatalog($tenant)
->where('slug', 'comida') ->where('slug', 'comida')
@@ -142,17 +142,9 @@ class FoodService
]); ]);
} }
$stock = (int) $data['stock'];
$inventory = $this->inventoryForHistoricalStockUpdate($variant); $inventory = $this->inventoryForHistoricalStockUpdate($variant);
if ($stock < $inventory->reserved_stock) { $stockDifference = (int) ($data['stock_difference'] ?? ((int) $data['stock'] - $inventory->availableStock()));
throw ValidationException::withMessages([ $inventory->adjustAvailableStock($stockDifference, idempotencyKey: $stockAdjustmentId);
"variants.{$index}.stock" => [
'El stock no puede ser menor que la cantidad actualmente reservada.',
],
]);
}
$inventory->update(['real_stock' => $stock]);
} }
return $this->current($tenant) ?? $food; return $this->current($tenant) ?? $food;
@@ -181,6 +173,7 @@ class FoodService
'reserved_stock' => 0, 'reserved_stock' => 0,
'real_stock' => $inventory->real_stock, 'real_stock' => $inventory->real_stock,
]); ]);
$historicalInventory->recordTransferInitialization();
$variant->update(['inventory_id' => $historicalInventory->getKey()]); $variant->update(['inventory_id' => $historicalInventory->getKey()]);
return $historicalInventory; return $historicalInventory;
@@ -384,6 +377,7 @@ class FoodService
private function createVariant(CatalogItem $food, Collection $itemAttributes, array $data): void private function createVariant(CatalogItem $food, Collection $itemAttributes, array $data): void
{ {
$inventory = Inventory::query()->create(['real_stock' => $data['stock']]); $inventory = Inventory::query()->create(['real_stock' => $data['stock']]);
$inventory->recordInitialization();
$variant = $food->variants()->create([ $variant = $food->variants()->create([
'event_date_id' => $data['event_date_id'], 'event_date_id' => $data['event_date_id'],
'inventory_id' => $inventory->id, 'inventory_id' => $inventory->id,
@@ -400,27 +394,21 @@ class FoodService
Collection $itemAttributes, Collection $itemAttributes,
array $data, array $data,
int $index, int $index,
?string $stockAdjustmentId,
): void { ): void {
$inventory = Inventory::query() $inventory = Inventory::query()
->whereKey($variant->inventory_id) ->whereKey($variant->inventory_id)
->lockForUpdate() ->lockForUpdate()
->firstOrFail(); ->firstOrFail();
if ($data['stock'] < $inventory->reserved_stock) {
throw ValidationException::withMessages([
"variants.{$index}.stock" => [
'El stock no puede ser menor que la cantidad actualmente reservada.',
],
]);
}
$variant->update([ $variant->update([
'event_date_id' => $data['event_date_id'], 'event_date_id' => $data['event_date_id'],
'descripcion' => $data['description'], 'descripcion' => $data['description'],
'precio' => $data['price'], 'precio' => $data['price'],
]); ]);
$variant->eventDates()->sync([$data['event_date_id']]); $variant->eventDates()->sync([$data['event_date_id']]);
$inventory->update(['real_stock' => $data['stock']]); $stockDifference = (int) ($data['stock_difference'] ?? ($data['stock'] - $inventory->availableStock()));
$inventory->adjustAvailableStock($stockDifference, idempotencyKey: $stockAdjustmentId);
$this->syncDefinitions($variant, $itemAttributes, $data); $this->syncDefinitions($variant, $itemAttributes, $data);
} }

View File

@@ -43,9 +43,9 @@ class MerchandiseService
* @param array<int, array<string, mixed>> $items * @param array<int, array<string, mixed>> $items
* @return Collection<int, CatalogItem> * @return Collection<int, CatalogItem>
*/ */
public function upsertMany(Tenant $tenant, array $items): Collection public function upsertMany(Tenant $tenant, array $items, ?string $stockAdjustmentId = null): Collection
{ {
return DB::transaction(function () use ($tenant, $items): Collection { return DB::transaction(function () use ($tenant, $items, $stockAdjustmentId): Collection {
$attributes = $this->attributes($tenant); $attributes = $this->attributes($tenant);
$category = Category::query()->firstOrCreate([ $category = Category::query()->firstOrCreate([
'tenant_code' => $tenant->codigo, 'tenant_code' => $tenant->codigo,
@@ -57,6 +57,7 @@ class MerchandiseService
$tenant, $tenant,
$attributes, $attributes,
$category, $category,
$stockAdjustmentId,
&$reservedSlugs, &$reservedSlugs,
): CatalogItem { ): CatalogItem {
$item = isset($data['id']) $item = isset($data['id'])
@@ -101,7 +102,7 @@ class MerchandiseService
if ($variant === null) { if ($variant === null) {
$this->createVariant($item, $itemAttributes, $variantData); $this->createVariant($item, $itemAttributes, $variantData);
} else { } else {
$this->updateVariant($variant, $itemAttributes, $variantData, $index, $variantIndex); $this->updateVariant($variant, $itemAttributes, $variantData, $index, $variantIndex, $stockAdjustmentId);
} }
} }
@@ -342,6 +343,7 @@ class MerchandiseService
array $data, array $data,
): void { ): void {
$inventory = Inventory::query()->create(['real_stock' => $data['stock']]); $inventory = Inventory::query()->create(['real_stock' => $data['stock']]);
$inventory->recordInitialization();
$variant = $item->variants()->create([ $variant = $item->variants()->create([
'inventory_id' => $inventory->id, 'inventory_id' => $inventory->id,
'precio' => $data['price'], 'precio' => $data['price'],
@@ -359,22 +361,16 @@ class MerchandiseService
array $data, array $data,
int $itemIndex, int $itemIndex,
int $variantIndex, int $variantIndex,
?string $stockAdjustmentId,
): void { ): void {
$inventory = Inventory::query() $inventory = Inventory::query()
->whereKey($variant->inventory_id) ->whereKey($variant->inventory_id)
->lockForUpdate() ->lockForUpdate()
->firstOrFail(); ->firstOrFail();
if ($data['stock'] < $inventory->reserved_stock) {
throw ValidationException::withMessages([
"items.{$itemIndex}.variants.{$variantIndex}.stock" => [
'El stock no puede ser menor que la cantidad actualmente reservada.',
],
]);
}
$variant->update(['precio' => $data['price']]); $variant->update(['precio' => $data['price']]);
$inventory->update(['real_stock' => $data['stock']]); $stockDifference = (int) ($data['stock_difference'] ?? ($data['stock'] - $inventory->availableStock()));
$inventory->adjustAvailableStock($stockDifference, idempotencyKey: $stockAdjustmentId);
$this->syncDefinitions($variant, $itemAttributes, $data); $this->syncDefinitions($variant, $itemAttributes, $data);
} }