feat(event): replace variants when dates change
This commit is contained in:
@@ -179,11 +179,27 @@ class CatalogItem extends Model
|
||||
{
|
||||
return $query->where(function (Builder $query): void {
|
||||
$query
|
||||
->where('catalog_items.inventory_policy', InventoryPolicy::Unlimited->value)
|
||||
->where(function (Builder $unlimitedQuery): void {
|
||||
$unlimitedQuery
|
||||
->where('catalog_items.inventory_policy', InventoryPolicy::Unlimited->value)
|
||||
->where(function (Builder $selectionQuery): void {
|
||||
$selectionQuery
|
||||
->whereDoesntHave('variants')
|
||||
->orWhereHas('variants', fn (Builder $variantQuery): Builder => $variantQuery
|
||||
->whereNull('sales_disabled_at')
|
||||
->whereNull('replaced_by_variant_id'));
|
||||
});
|
||||
})
|
||||
->orWhereHas(
|
||||
'variants.inventory',
|
||||
fn (Builder $inventoryQuery): Builder => $inventoryQuery
|
||||
->whereColumn('inventories.real_stock', '>', 'inventories.reserved_stock')
|
||||
'variants',
|
||||
fn (Builder $variantQuery): Builder => $variantQuery
|
||||
->whereNull('sales_disabled_at')
|
||||
->whereNull('replaced_by_variant_id')
|
||||
->whereHas(
|
||||
'inventory',
|
||||
fn (Builder $inventoryQuery): Builder => $inventoryQuery
|
||||
->whereColumn('inventories.real_stock', '>', 'inventories.reserved_stock')
|
||||
)
|
||||
)
|
||||
->orWhere(function (Builder $directItemQuery): void {
|
||||
$directItemQuery
|
||||
@@ -206,8 +222,10 @@ class CatalogItem extends Model
|
||||
{
|
||||
return $this->variants
|
||||
->filter(fn (Variant $variant): bool => ($includedVariantId !== null && $variant->id === $includedVariantId)
|
||||
|| $this->inventory_policy === InventoryPolicy::Unlimited
|
||||
|| ($variant->inventory?->availableStock() ?? 0) > 0)
|
||||
|| ($variant->isSellable() && (
|
||||
$this->inventory_policy === InventoryPolicy::Unlimited
|
||||
|| ($variant->inventory?->availableStock() ?? 0) > 0
|
||||
)))
|
||||
->values();
|
||||
}
|
||||
|
||||
|
||||
@@ -63,9 +63,14 @@ class CatalogInventoryService
|
||||
|
||||
$selection->loadMissing('variants.inventory');
|
||||
|
||||
return $selection->variants->sum(
|
||||
fn (Variant $variant): int => $variant->inventory->availableStock(),
|
||||
);
|
||||
return $selection->variants
|
||||
->filter(fn (Variant $variant): bool => $variant->isSellable())
|
||||
->unique(fn (Variant $variant): string => $variant->inventory_id === null
|
||||
? 'object:'.spl_object_id($variant->inventory)
|
||||
: 'id:'.$variant->inventory_id)
|
||||
->sum(
|
||||
fn (Variant $variant): int => $variant->inventory->availableStock(),
|
||||
);
|
||||
}
|
||||
|
||||
$requirements = $this->inventoryRequirements($selection);
|
||||
|
||||
@@ -207,7 +207,8 @@ class CatalogService
|
||||
$visibleVariants = $catalogItem->visibleVariants();
|
||||
if ($catalogItem->type === CatalogItemType::Standard
|
||||
&& ($catalogItem->inventory_id !== null || $catalogItem->variants->isNotEmpty())
|
||||
&& ! $catalogItem->isAvailable()) {
|
||||
&& (($catalogItem->variants->isNotEmpty() && $visibleVariants->isEmpty())
|
||||
|| ! $catalogItem->isAvailable())) {
|
||||
throw new NotFoundHttpException('Catalog item is out of stock.');
|
||||
}
|
||||
|
||||
@@ -324,9 +325,13 @@ class CatalogService
|
||||
->findOrFail($variant->catalog_item_id);
|
||||
$variant->delete();
|
||||
|
||||
if (! $catalogItem->variants()->exists()) {
|
||||
$sellableVariants = $catalogItem->variants()
|
||||
->whereNull('sales_disabled_at')
|
||||
->whereNull('replaced_by_variant_id');
|
||||
|
||||
if (! (clone $sellableVariants)->exists()) {
|
||||
$this->delete($catalogItem);
|
||||
} elseif (($minimumPrice = $catalogItem->variants()->min('precio')) !== null) {
|
||||
} elseif (($minimumPrice = (clone $sellableVariants)->min('precio')) !== null) {
|
||||
$catalogItem->update(['precio' => $minimumPrice]);
|
||||
}
|
||||
|
||||
@@ -399,7 +404,11 @@ class CatalogService
|
||||
]);
|
||||
}
|
||||
|
||||
if ($variantId !== null && ! $componentItem->variants()->whereKey($variantId)->exists()) {
|
||||
if ($variantId !== null && ! $componentItem->variants()
|
||||
->whereKey($variantId)
|
||||
->whereNull('sales_disabled_at')
|
||||
->whereNull('replaced_by_variant_id')
|
||||
->exists()) {
|
||||
throw ValidationException::withMessages([
|
||||
"components.{$index}.variant_id" => [
|
||||
__('api.catalog.component_variant_invalid'),
|
||||
|
||||
137
app/Domains/Catalog/Services/VariantReplacementService.php
Normal file
137
app/Domains/Catalog/Services/VariantReplacementService.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Catalog\Services;
|
||||
|
||||
use App\Domains\Catalog\Models\BundleComponent;
|
||||
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
|
||||
{
|
||||
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()));
|
||||
})
|
||||
->update(['sales_disabled_at' => now()]);
|
||||
}
|
||||
|
||||
/** @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
|
||||
{
|
||||
$replacement = $source->replicate([
|
||||
'event_date_id',
|
||||
'replaced_by_variant_id',
|
||||
'sales_disabled_at',
|
||||
]);
|
||||
$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']);
|
||||
}
|
||||
|
||||
/** @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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user