261 lines
10 KiB
PHP
261 lines
10 KiB
PHP
<?php
|
|
|
|
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;
|
|
|
|
class VariantReplacementService
|
|
{
|
|
/** @return Collection<int, Variant> */
|
|
public function replaceEventDate(EventDate $source, EventDate $destination): Collection
|
|
{
|
|
$variants = Variant::query()
|
|
->whereNull('sales_disabled_at')
|
|
->whereNull('replaced_by_variant_id')
|
|
->where(function ($query) use ($source): void {
|
|
$query->where('event_date_id', $source->getKey())
|
|
->orWhereHas('eventDates', fn ($eventDates) => $eventDates
|
|
->where('event_dates.id', $source->getKey()));
|
|
})
|
|
->with(['eventDates', 'eventDate', 'definitions', 'allAttachments'])
|
|
->orderBy('id')
|
|
->lockForUpdate()
|
|
->get();
|
|
|
|
return $variants->map(function (Variant $variant) use ($source, $destination): Variant {
|
|
$destinationDateIds = $variant->selectedEventDates()
|
|
->pluck('id')
|
|
->map(fn ($id): int => (int) $id === (int) $source->getKey()
|
|
? (int) $destination->getKey()
|
|
: (int) $id)
|
|
->unique()
|
|
->sort()
|
|
->values();
|
|
|
|
$replacement = $this->findEquivalent($variant, $destinationDateIds)
|
|
?? $this->cloneWithDates($variant, $destinationDateIds);
|
|
|
|
$variant->update([
|
|
'replaced_by_variant_id' => $replacement->getKey(),
|
|
'sales_disabled_at' => now(),
|
|
]);
|
|
|
|
BundleComponent::query()
|
|
->where('component_variant_id', $variant->getKey())
|
|
->update(['component_variant_id' => $replacement->getKey()]);
|
|
|
|
return $replacement;
|
|
})->values();
|
|
}
|
|
|
|
public function disableForSuspension(EventDate $eventDate): void
|
|
{
|
|
$variants = Variant::query()
|
|
->whereNull('sales_disabled_at')
|
|
->whereNull('replaced_by_variant_id')
|
|
->where(function ($query) use ($eventDate): void {
|
|
$query->where('event_date_id', $eventDate->getKey())
|
|
->orWhereHas('eventDates', fn ($eventDates) => $eventDates
|
|
->where('event_dates.id', $eventDate->getKey()));
|
|
})
|
|
->with(['eventDates', 'eventDate', 'definitions', 'allAttachments'])
|
|
->orderBy('id')
|
|
->lockForUpdate()
|
|
->get();
|
|
|
|
foreach ($variants as $variant) {
|
|
$remainingDateIds = $variant->selectedEventDates()
|
|
->filter(fn (EventDate $date): bool => $date->suspended_at === null
|
|
&& $date->rescheduled_to_event_date_id === null)
|
|
->pluck('id')
|
|
->map(fn ($id): int => (int) $id)
|
|
->unique()
|
|
->sort()
|
|
->values();
|
|
|
|
if ($remainingDateIds->isEmpty()) {
|
|
$variant->update(['sales_disabled_at' => now()]);
|
|
|
|
continue;
|
|
}
|
|
|
|
$replacement = $this->findEquivalent($variant, $remainingDateIds);
|
|
if ($replacement === null) {
|
|
$replacement = $this->cloneWithDates($variant, $remainingDateIds);
|
|
} else {
|
|
$this->mergeInventoryInto($variant, $replacement);
|
|
}
|
|
|
|
$variant->update([
|
|
'replaced_by_variant_id' => $replacement->getKey(),
|
|
'sales_disabled_at' => now(),
|
|
]);
|
|
|
|
BundleComponent::query()
|
|
->where('component_variant_id', $variant->getKey())
|
|
->update(['component_variant_id' => $replacement->getKey()]);
|
|
}
|
|
}
|
|
|
|
/** @param Collection<int, int> $eventDateIds */
|
|
private function findEquivalent(Variant $source, Collection $eventDateIds): ?Variant
|
|
{
|
|
$definitionSignature = $this->definitionSignature($source);
|
|
$dateSignature = $eventDateIds->map(fn ($id): int => (int) $id)->sort()->values()->all();
|
|
|
|
return Variant::query()
|
|
->where('catalog_item_id', $source->catalog_item_id)
|
|
->whereKeyNot($source->getKey())
|
|
->whereNull('sales_disabled_at')
|
|
->whereNull('replaced_by_variant_id')
|
|
->with(['eventDates', 'eventDate', 'definitions'])
|
|
->orderBy('id')
|
|
->lockForUpdate()
|
|
->get()
|
|
->first(fn (Variant $candidate): bool => $this->definitionSignature($candidate) === $definitionSignature
|
|
&& $candidate->selectedEventDates()
|
|
->pluck('id')
|
|
->map(fn ($id): int => (int) $id)
|
|
->sort()
|
|
->values()
|
|
->all() === $dateSignature
|
|
);
|
|
}
|
|
|
|
/** @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;
|
|
$replacement->save();
|
|
$replacement->eventDates()->sync($eventDateIds->all());
|
|
|
|
$replacement->definitions()->createMany(
|
|
$source->definitions
|
|
->map(fn ($definition): array => [
|
|
'item_attribute_id' => $definition->item_attribute_id,
|
|
'value' => $definition->value,
|
|
])
|
|
->all(),
|
|
);
|
|
|
|
$attachments = $source->allAttachments
|
|
->mapWithKeys(fn ($attachment): array => [
|
|
$attachment->getKey() => [
|
|
'orden' => $attachment->pivot->orden,
|
|
'is_enabled' => $attachment->pivot->is_enabled,
|
|
],
|
|
])
|
|
->all();
|
|
$replacement->allAttachments()->sync($attachments);
|
|
|
|
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;
|
|
}
|
|
|
|
private function mergeInventoryInto(Variant $source, Variant $destination): void
|
|
{
|
|
if ($source->inventory_id === $destination->inventory_id) {
|
|
return;
|
|
}
|
|
|
|
$inventories = Inventory::query()
|
|
->whereKey([$source->inventory_id, $destination->inventory_id])
|
|
->orderBy('id')
|
|
->lockForUpdate()
|
|
->get()
|
|
->keyBy('id');
|
|
$sourceInventory = $inventories->get($source->inventory_id);
|
|
$destinationInventory = $inventories->get($destination->inventory_id);
|
|
if ($sourceInventory === null || $destinationInventory === null) {
|
|
throw new \LogicException('No se encontró el inventario de una variante.');
|
|
}
|
|
|
|
$activeLines = StockReservationLine::query()
|
|
->where('inventory_id', $source->inventory_id)
|
|
->whereHas('reservation', fn ($reservation) => $reservation
|
|
->where('status', StockReservation::STATUS_ACTIVE))
|
|
->orderBy('id')
|
|
->lockForUpdate()
|
|
->get();
|
|
if ($sourceInventory->reserved_stock !== (int) $activeLines->sum('quantity')) {
|
|
throw new \LogicException('El inventario reservado de la variante es inconsistente.');
|
|
}
|
|
|
|
$destinationInventory->update([
|
|
'real_stock' => $destinationInventory->real_stock + $sourceInventory->real_stock,
|
|
'reserved_stock' => $destinationInventory->reserved_stock + $sourceInventory->reserved_stock,
|
|
'sold_units' => $destinationInventory->sold_units + $sourceInventory->sold_units,
|
|
]);
|
|
if ($activeLines->isNotEmpty()) {
|
|
StockReservationLine::query()
|
|
->whereKey($activeLines->modelKeys())
|
|
->update(['inventory_id' => $destinationInventory->getKey()]);
|
|
}
|
|
$sourceInventory->update([
|
|
'real_stock' => 0,
|
|
'reserved_stock' => 0,
|
|
'sold_units' => 0,
|
|
]);
|
|
}
|
|
|
|
/** @return list<string> */
|
|
private function definitionSignature(Variant $variant): array
|
|
{
|
|
return $variant->definitions
|
|
->map(fn ($definition): string => $definition->item_attribute_id.'\0'.$definition->value)
|
|
->sort()
|
|
->values()
|
|
->all();
|
|
}
|
|
}
|