Files
shopit-back/app/Domains/Ticket/Services/ResolvedValidityGroup.php
ncoronel 573d4fe5e6 Refactor ticket validity handling and improve tests
- 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.
2026-08-14 09:00:51 -03:00

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();
}
}