source_variant_id === null) { return ResolvedTicketValidity::unrestricted(); } $ticket->loadMissing(self::RELATIONS); if ($ticket->sourceVariant === null) { return ResolvedTicketValidity::unresolvable(); } return $this->resolveVariant($ticket->sourceVariant); } /** * Convierte las fechas y definiciones temporales de la variante en grupos * normalizados: AND dentro de cada grupo y OR entre grupos. */ public function resolveVariant(Variant $variant): ResolvedTicketValidity { $variant->loadMissing([ 'eventDates.validityTime', 'eventDate.validityTime', 'definitions.itemAttribute.attribute.options.validityTime', ]); $dimensions = collect(); $eventDates = $variant->selectedEventDates(); if ($eventDates->contains(fn ($eventDate): bool => $eventDate->validityTime === null)) { return ResolvedTicketValidity::unresolvable(); } if ($eventDates->isNotEmpty()) { // Todas las fechas pertenecen a una misma dimensión alternativa: // fecha 1 OR fecha 2 OR fecha 3. $dimensions->push( $eventDates->map(fn ($eventDate): Collection => collect([$eventDate->validityTime])) ); } foreach ($variant->definitions->groupBy('item_attribute_id') as $definitions) { $itemAttribute = $definitions->first()?->itemAttribute; $attribute = $itemAttribute?->attribute; if ($itemAttribute === null || $attribute === null) { return ResolvedTicketValidity::unresolvable(); } if (! $attribute->type->supportsOptions() || $attribute->type->usesDynamicOptions()) { // Texto, números y demás atributos no temporales no restringen // la vigencia. EventDate se procesó arriba mediante su relación. continue; } if (! $itemAttribute->allow_multi_select && $definitions->count() > 1) { return ResolvedTicketValidity::unresolvable(); } $alternatives = $definitions->map(function (VariantDefinition $definition) use ($attribute): ?Collection { $option = $attribute->options->firstWhere('value', $definition->value); if ($option === null) { return null; } return collect([$option->validityTime])->filter()->values(); }); if ($alternatives->contains(null)) { return ResolvedTicketValidity::unresolvable(); } if ($alternatives->contains(fn (Collection $alternative): bool => $alternative->isNotEmpty())) { // Las opciones elegidas del mismo atributo son alternativas OR. $dimensions->push($alternatives->values()); } } if ($dimensions->isEmpty()) { return ResolvedTicketValidity::unrestricted(); } $groups = collect([collect()]); foreach ($dimensions as $alternatives) { // El producto cartesiano agrega cada dimensión como una condición // AND y conserva sus opciones internas como alternativas OR. $groups = $groups->flatMap( fn (Collection $group): Collection => $alternatives->map( fn (Collection $alternative): Collection => $group ->merge($alternative) ->unique(fn (ValidityTime $time): int => $time->getKey() ?? spl_object_id($time)) ->values() ) )->values(); } return new ResolvedTicketValidity( $groups->map(fn (Collection $times): ResolvedValidityGroup => new ResolvedValidityGroup($times)) ); } }