*/ 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 $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 $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 */ private function definitionSignature(Variant $variant): array { return $variant->definitions ->map(fn ($definition): string => $definition->item_attribute_id.'\0'.$definition->value) ->sort() ->values() ->all(); } }