- Updated tests for Accommodation, Entry, Food, Merchandise, and Sale controllers to use soft deletes for variants and ensure proper inventory counts. - Enhanced ticket generation logic to resolve validity from soft-deleted catalog sources. - Introduced a new TicketValidityResolver service to manage ticket validity based on event dates and variant definitions. - Removed unnecessary database assertions and improved the clarity of validity checks in tests. - Added comprehensive tests for the new TicketValidityResolver service, ensuring correct handling of event dates and multi-select options. - Cleaned up unused code and assertions in existing tests for better maintainability.
98 lines
3.5 KiB
PHP
98 lines
3.5 KiB
PHP
<?php
|
|
|
|
namespace App\Domains\Ticket\Services;
|
|
|
|
use App\Domains\Ticket\Enums\ValidityTimeType;
|
|
use App\Domains\Ticket\Models\ValidityTime;
|
|
use Carbon\CarbonInterface;
|
|
use Illuminate\Support\Collection;
|
|
|
|
/**
|
|
* Representa una intersección de vigencias: todos los ValidityTime del grupo
|
|
* deben cumplirse simultáneamente (AND).
|
|
*
|
|
* Ejemplo: [fecha del evento, horario de almuerzo] significa que el ticket
|
|
* solamente es válido durante la intersección de ambas ventanas.
|
|
*/
|
|
final readonly class ResolvedValidityGroup
|
|
{
|
|
/** @param Collection<int, ValidityTime> $validityTimes */
|
|
public function __construct(public Collection $validityTimes) {}
|
|
|
|
/** Comprueba si el instante pertenece a la intersección efectiva del grupo. */
|
|
public function isValid(?CarbonInterface $at = null): bool
|
|
{
|
|
$at ??= now();
|
|
$startsAt = $this->effectiveStartsAt($at);
|
|
$expiresAt = $this->effectiveExpiresAt($at);
|
|
|
|
return $this->validityTimes->isNotEmpty()
|
|
&& ($startsAt === null || $startsAt->lessThanOrEqualTo($at))
|
|
&& ($expiresAt === null || $expiresAt->greaterThan($at));
|
|
}
|
|
|
|
/** Un grupo vence cuando termina su intersección efectiva. */
|
|
public function isExpired(?CarbonInterface $at = null): bool
|
|
{
|
|
$at ??= now();
|
|
$expiresAt = $this->effectiveExpiresAt($at);
|
|
|
|
return $expiresAt !== null && $expiresAt->lessThanOrEqualTo($at);
|
|
}
|
|
|
|
/**
|
|
* En un AND, la intersección comienza en el inicio más tardío.
|
|
* Por ejemplo, fecha 00:00 + horario 12:00 comienza a las 12:00.
|
|
*/
|
|
public function effectiveStartsAt(?CarbonInterface $at = null): ?CarbonInterface
|
|
{
|
|
$at ??= now();
|
|
$anchor = $this->dateAnchor() ?? $at;
|
|
|
|
return $this->validityTimes
|
|
->map(fn (ValidityTime $validityTime): ?CarbonInterface => $validityTime->startsAt($anchor))
|
|
->filter()
|
|
->sortByDesc(fn (CarbonInterface $startsAt): int => $startsAt->getTimestamp())
|
|
->first();
|
|
}
|
|
|
|
/**
|
|
* En un AND, la intersección termina en el vencimiento más temprano.
|
|
* Los horarios cuyo fin no supera al inicio se interpretan como nocturnos.
|
|
*/
|
|
public function effectiveExpiresAt(?CarbonInterface $at = null): ?CarbonInterface
|
|
{
|
|
$at ??= now();
|
|
$anchor = $this->dateAnchor() ?? $at;
|
|
|
|
return $this->validityTimes
|
|
->map(function (ValidityTime $validityTime) use ($anchor): ?CarbonInterface {
|
|
$startsAt = $validityTime->startsAt($anchor);
|
|
$expiresAt = $validityTime->expiresAt($anchor);
|
|
|
|
if ($startsAt !== null && $expiresAt !== null && $expiresAt->lessThanOrEqualTo($startsAt)) {
|
|
return $expiresAt->addDay();
|
|
}
|
|
|
|
return $expiresAt;
|
|
})
|
|
->filter()
|
|
->sortBy(fn (CarbonInterface $expiresAt): int => $expiresAt->getTimestamp())
|
|
->first();
|
|
}
|
|
|
|
/**
|
|
* Usa la fecha de un fixed_window como ancla para convertir ventanas que
|
|
* sólo contienen horas (time_window) en instantes concretos.
|
|
*/
|
|
private function dateAnchor(): ?CarbonInterface
|
|
{
|
|
return $this->validityTimes
|
|
->filter(fn (ValidityTime $validityTime): bool => $validityTime->type === ValidityTimeType::FixedWindow)
|
|
->map(fn (ValidityTime $validityTime): ?CarbonInterface => $validityTime->fixed_starts_at)
|
|
->filter()
|
|
->sortByDesc(fn (CarbonInterface $startsAt): int => $startsAt->getTimestamp())
|
|
->first();
|
|
}
|
|
}
|