feat(variants): enhance disableForSuspension method to manage variant replacements and inventory merging
This commit is contained in:
@@ -56,7 +56,7 @@ class VariantReplacementService
|
|||||||
|
|
||||||
public function disableForSuspension(EventDate $eventDate): void
|
public function disableForSuspension(EventDate $eventDate): void
|
||||||
{
|
{
|
||||||
Variant::query()
|
$variants = Variant::query()
|
||||||
->whereNull('sales_disabled_at')
|
->whereNull('sales_disabled_at')
|
||||||
->whereNull('replaced_by_variant_id')
|
->whereNull('replaced_by_variant_id')
|
||||||
->where(function ($query) use ($eventDate): void {
|
->where(function ($query) use ($eventDate): void {
|
||||||
@@ -64,7 +64,43 @@ class VariantReplacementService
|
|||||||
->orWhereHas('eventDates', fn ($eventDates) => $eventDates
|
->orWhereHas('eventDates', fn ($eventDates) => $eventDates
|
||||||
->where('event_dates.id', $eventDate->getKey()));
|
->where('event_dates.id', $eventDate->getKey()));
|
||||||
})
|
})
|
||||||
->update(['sales_disabled_at' => now()]);
|
->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 */
|
/** @param Collection<int, int> $eventDateIds */
|
||||||
@@ -166,6 +202,52 @@ class VariantReplacementService
|
|||||||
return $replacementInventory;
|
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> */
|
/** @return list<string> */
|
||||||
private function definitionSignature(Variant $variant): array
|
private function definitionSignature(Variant $variant): array
|
||||||
{
|
{
|
||||||
|
|||||||
130
storage/framework/lsp-1ab91c4294c8e5da.php
Normal file
130
storage/framework/lsp-1ab91c4294c8e5da.php
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<?php
|
||||||
|
error_reporting(error_reporting() & ~(E_WARNING | E_CORE_WARNING | E_COMPILE_WARNING | E_USER_WARNING | E_DEPRECATED | E_USER_DEPRECATED));
|
||||||
|
class LspHelper
|
||||||
|
{
|
||||||
|
public static function relativePath($path)
|
||||||
|
{
|
||||||
|
if (!str_contains($path, base_path())) {
|
||||||
|
return (string) $path;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ltrim(str_replace(base_path(), '', realpath($path) ?: $path), DIRECTORY_SEPARATOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function isVendor($path)
|
||||||
|
{
|
||||||
|
return str_contains($path, base_path('vendor'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function propertyDefault(ReflectionProperty $property, ?ReflectionParameter $parameter = null): array
|
||||||
|
{
|
||||||
|
if ($property->hasDefaultValue()) {
|
||||||
|
return ['default' => $property->getDefaultValue()];
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($parameter?->isDefaultValueAvailable()) {
|
||||||
|
return ['default' => $parameter->getDefaultValue()];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function formatDefaultValue(mixed $value): mixed
|
||||||
|
{
|
||||||
|
return match (true) {
|
||||||
|
is_array($value) => 'array(...)',
|
||||||
|
$value instanceof UnitEnum => get_class($value) . '::' . $value->name,
|
||||||
|
$value instanceof Closure => 'Closure',
|
||||||
|
is_object($value) => get_class($value),
|
||||||
|
is_string($value) => var_export($value, true),
|
||||||
|
is_null($value) => 'null',
|
||||||
|
is_bool($value) => $value ? 'true' : 'false',
|
||||||
|
default => $value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
use Pest\Expectation;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
|
||||||
|
$pest = new class
|
||||||
|
{
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
if ($this->isInstalled()) {
|
||||||
|
$this->boot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isInstalled(): bool
|
||||||
|
{
|
||||||
|
return class_exists(TestSuite::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function boot(): void
|
||||||
|
{
|
||||||
|
require_once base_path('vendor/pestphp/pest/overrides/Runner/TestSuiteLoader.php');
|
||||||
|
|
||||||
|
TestSuite::getInstance(base_path(), 'tests');
|
||||||
|
|
||||||
|
if (file_exists($pestFile = base_path('tests/Pest.php'))) {
|
||||||
|
require_once $pestFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function config(): ?array
|
||||||
|
{
|
||||||
|
if (!$this->isInstalled()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'uses' => $this->uses(),
|
||||||
|
'expectations' => $this->expectations(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function uses(): array
|
||||||
|
{
|
||||||
|
if (is_null($instance = TestSuite::getInstance())) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$reflection = new ReflectionProperty($instance->tests, 'uses');
|
||||||
|
$uses = $reflection->getValue($instance->tests);
|
||||||
|
|
||||||
|
return collect($uses)->map(function (array $use, string $path) {
|
||||||
|
[$classOrTraits] = $use;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'path' => LspHelper::relativePath($path),
|
||||||
|
'classes' => array_values(array_filter($classOrTraits, fn ($c) => class_exists($c))),
|
||||||
|
'traits' => array_values(array_filter($classOrTraits, fn ($c) => trait_exists($c))),
|
||||||
|
];
|
||||||
|
})->values()->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function expectations(): array
|
||||||
|
{
|
||||||
|
$reflection = new ReflectionProperty(Expectation::class, 'extends');
|
||||||
|
$extends = $reflection->getValue();
|
||||||
|
|
||||||
|
return collect($extends)->map(function (Closure $closure, string $name) {
|
||||||
|
$parameters = collect((new ReflectionFunction($closure))->getParameters())
|
||||||
|
->map(function (ReflectionParameter $param) {
|
||||||
|
$type = $param->hasType() ? $param->getType() . ' ' : '';
|
||||||
|
|
||||||
|
$default = $param->isOptional() && $param->isDefaultValueAvailable()
|
||||||
|
? ' = ' . var_export($param->getDefaultValue(), true)
|
||||||
|
: '';
|
||||||
|
|
||||||
|
return $type . '$' . $param->getName() . $default;
|
||||||
|
})
|
||||||
|
->join(', ');
|
||||||
|
|
||||||
|
return compact('name', 'parameters');
|
||||||
|
})->values()->all();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
echo json_encode($pest->config());
|
||||||
@@ -474,6 +474,13 @@ class AdminAppEventControllerTest extends TestCase
|
|||||||
$singleDateVariant = $this->createVariant($tenant, $suspendedDate->id);
|
$singleDateVariant = $this->createVariant($tenant, $suspendedDate->id);
|
||||||
$multipleDateVariant = $this->createVariant($tenant);
|
$multipleDateVariant = $this->createVariant($tenant);
|
||||||
$multipleDateVariant->eventDates()->sync([$suspendedDate->id, $otherDate->id]);
|
$multipleDateVariant->eventDates()->sync([$suspendedDate->id, $otherDate->id]);
|
||||||
|
$remainingDateVariant = Variant::query()->create([
|
||||||
|
'catalog_item_id' => $multipleDateVariant->catalog_item_id,
|
||||||
|
'inventory_id' => Inventory::query()->create()->id,
|
||||||
|
'event_date_id' => $otherDate->id,
|
||||||
|
]);
|
||||||
|
$singleDateVariant->inventory->update(['real_stock' => 5]);
|
||||||
|
$multipleDateVariant->inventory->update(['real_stock' => 5]);
|
||||||
$singleDateTicket = $this->createTicket($tenant, $admin, $singleDateVariant);
|
$singleDateTicket = $this->createTicket($tenant, $admin, $singleDateVariant);
|
||||||
$multipleDateTicket = $this->createTicket($tenant, $admin, $multipleDateVariant);
|
$multipleDateTicket = $this->createTicket($tenant, $admin, $multipleDateVariant);
|
||||||
Sanctum::actingAs($admin);
|
Sanctum::actingAs($admin);
|
||||||
@@ -508,6 +515,14 @@ class AdminAppEventControllerTest extends TestCase
|
|||||||
$this->assertNull($multipleDateTicket->fresh()->disabled_at);
|
$this->assertNull($multipleDateTicket->fresh()->disabled_at);
|
||||||
$this->assertNotNull($singleDateVariant->fresh()->sales_disabled_at);
|
$this->assertNotNull($singleDateVariant->fresh()->sales_disabled_at);
|
||||||
$this->assertNotNull($multipleDateVariant->fresh()->sales_disabled_at);
|
$this->assertNotNull($multipleDateVariant->fresh()->sales_disabled_at);
|
||||||
|
$replacement = $multipleDateVariant->fresh()->replacement;
|
||||||
|
$this->assertNotNull($replacement);
|
||||||
|
$this->assertSame($remainingDateVariant->id, $replacement->id);
|
||||||
|
$this->assertTrue($replacement->isSellable());
|
||||||
|
$this->assertSame([$otherDate->id], $replacement->selectedEventDates()->pluck('id')->all());
|
||||||
|
$this->assertSame(5, $replacement->inventory->fresh()->availableStock());
|
||||||
|
$this->assertTrue(CatalogItem::query()->whereKey($multipleDateVariant->catalog_item_id)->whereAvailable()->exists());
|
||||||
|
$this->assertFalse(CatalogItem::query()->whereKey($singleDateVariant->catalog_item_id)->whereAvailable()->exists());
|
||||||
$this->assertSame('10 de Octubre 2027', $tenant->fresh()->event_date_text);
|
$this->assertSame('10 de Octubre 2027', $tenant->fresh()->event_date_text);
|
||||||
$this->getJson('/api/tenants/bootstrap?dominio=acme.test&path=%2F')
|
$this->getJson('/api/tenants/bootstrap?dominio=acme.test&path=%2F')
|
||||||
->assertOk()
|
->assertOk()
|
||||||
@@ -525,6 +540,10 @@ class AdminAppEventControllerTest extends TestCase
|
|||||||
'2027-10-10 09:00:00',
|
'2027-10-10 09:00:00',
|
||||||
$multipleDateTicket->fresh()->resolvedValidity()->effectiveStartsAt()?->format('Y-m-d H:i:s'),
|
$multipleDateTicket->fresh()->resolvedValidity()->effectiveStartsAt()?->format('Y-m-d H:i:s'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$this->postJson("/api/v1/adminapp/tenant/event-dates/{$otherDate->id}/suspend")
|
||||||
|
->assertOk();
|
||||||
|
$this->assertFalse(CatalogItem::query()->whereKey($multipleDateVariant->catalog_item_id)->whereAvailable()->exists());
|
||||||
}
|
}
|
||||||
|
|
||||||
public function test_suspending_a_reschedule_destination_disables_tickets_from_predecessor_dates(): void
|
public function test_suspending_a_reschedule_destination_disables_tickets_from_predecessor_dates(): void
|
||||||
|
|||||||
Reference in New Issue
Block a user