feat(ticket): implement validity time management for tickets and catalog items
- Added ValidityTime model and migration to manage ticket validity periods. - Updated TicketGeneratorService to resolve and assign validity times to tickets. - Refactored ticket generation logic to remove legacy date fields and use validity time. - Introduced timezone support for tenants to handle service dates correctly. - Updated migrations to remove deprecated columns and add foreign keys for validity times. - Modified seeders and tests to accommodate new validity time structure. - Enhanced tests to validate ticket generation and validity time behavior.
This commit is contained in:
@@ -22,7 +22,7 @@ class TicketController extends Controller
|
||||
$tickets = Ticket::query()
|
||||
->where('tenant_code', $tenant->codigo)
|
||||
->where('user_id', $request->user()->getKey())
|
||||
->with('sourceVariant.eventDate', 'sourceVariant.catalogItem')
|
||||
->with('validityTime', 'tenant', 'sourceVariant.eventDate', 'sourceVariant.catalogItem')
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
|
||||
@@ -36,7 +36,7 @@ class TicketController extends Controller
|
||||
->where('tenant_code', $tenant->codigo)
|
||||
->where('user_id', $request->user()->getKey())
|
||||
->whereIn('id', $ticketIds)
|
||||
->with('sourceVariant.eventDate', 'sourceVariant.catalogItem')
|
||||
->with('validityTime', 'tenant', 'sourceVariant.eventDate', 'sourceVariant.catalogItem')
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
|
||||
|
||||
15
app/Domains/Ticket/Enums/ValidityTimeType.php
Normal file
15
app/Domains/Ticket/Enums/ValidityTimeType.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Ticket\Enums;
|
||||
|
||||
enum ValidityTimeType: string
|
||||
{
|
||||
case ServiceDateWindow = 'service_date_window';
|
||||
case FixedWindow = 'fixed_window';
|
||||
|
||||
/** @return list<string> */
|
||||
public static function values(): array
|
||||
{
|
||||
return array_column(self::cases(), 'value');
|
||||
}
|
||||
}
|
||||
@@ -24,9 +24,9 @@ class TicketGenerationException extends RuntimeException
|
||||
return new self(__('api.ticket.disabled', ['product' => $catalogItem->id]));
|
||||
}
|
||||
|
||||
public static function maximumUseDateReached(CatalogItem $catalogItem): self
|
||||
public static function ambiguousValidityTime(CatalogItem $catalogItem): self
|
||||
{
|
||||
return new self(__('api.ticket.expired', ['product' => $catalogItem->id]));
|
||||
return new self("Catalog item {$catalogItem->id} resolves more than one validity time.");
|
||||
}
|
||||
|
||||
public static function variantNotFound(
|
||||
|
||||
@@ -21,8 +21,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
'source_purchase_id',
|
||||
'source_catalog_item_id',
|
||||
'source_variant_id',
|
||||
'starts_at',
|
||||
'expires_at',
|
||||
'validity_time_id',
|
||||
'service_date',
|
||||
'used_at',
|
||||
'scanner_user_id',
|
||||
'user_id',
|
||||
@@ -45,8 +45,8 @@ class Ticket extends Model
|
||||
'source_catalog_item_id' => 'integer',
|
||||
'source_variant_id' => 'integer',
|
||||
'source_purchase_id' => 'integer',
|
||||
'starts_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
'validity_time_id' => 'integer',
|
||||
'service_date' => 'date',
|
||||
'used_at' => 'datetime',
|
||||
'scanner_user_id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
@@ -89,6 +89,12 @@ class Ticket extends Model
|
||||
return $this->belongsTo(Variant::class, 'source_variant_id');
|
||||
}
|
||||
|
||||
/** @return BelongsTo<ValidityTime, $this> */
|
||||
public function validityTime(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ValidityTime::class);
|
||||
}
|
||||
|
||||
public function isValid(): bool
|
||||
{
|
||||
$now = now();
|
||||
@@ -121,13 +127,17 @@ class Ticket extends Model
|
||||
|
||||
public function getEffectiveStartsAt(): ?CarbonInterface
|
||||
{
|
||||
return $this->sourceVariant?->getMinimumUseDate()
|
||||
?? $this->starts_at;
|
||||
return $this->validityTime?->startsAt(
|
||||
$this->service_date,
|
||||
$this->tenant?->timezone ?? config('app.timezone'),
|
||||
);
|
||||
}
|
||||
|
||||
public function getEffectiveExpiresAt(): ?CarbonInterface
|
||||
{
|
||||
return $this->sourceVariant?->getMaximumUseDate()
|
||||
?? $this->expires_at;
|
||||
return $this->validityTime?->expiresAt(
|
||||
$this->service_date,
|
||||
$this->tenant?->timezone ?? config('app.timezone'),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
87
app/Domains/Ticket/Models/ValidityTime.php
Normal file
87
app/Domains/Ticket/Models/ValidityTime.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Ticket\Models;
|
||||
|
||||
use App\Domains\Catalog\Models\AttributeOption;
|
||||
use App\Domains\Catalog\Models\CatalogItem;
|
||||
use App\Domains\Ticket\Enums\ValidityTimeType;
|
||||
use Carbon\CarbonImmutable;
|
||||
use Carbon\CarbonInterface;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
#[Fillable([
|
||||
'type',
|
||||
'start_time',
|
||||
'end_time',
|
||||
'fixed_starts_at',
|
||||
'fixed_expires_at',
|
||||
'active',
|
||||
])]
|
||||
class ValidityTime extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'type' => ValidityTimeType::class,
|
||||
'fixed_starts_at' => 'datetime',
|
||||
'fixed_expires_at' => 'datetime',
|
||||
'active' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
/** @return HasMany<CatalogItem, $this> */
|
||||
public function catalogItems(): HasMany
|
||||
{
|
||||
return $this->hasMany(CatalogItem::class);
|
||||
}
|
||||
|
||||
/** @return HasMany<AttributeOption, $this> */
|
||||
public function attributeOptions(): HasMany
|
||||
{
|
||||
return $this->hasMany(AttributeOption::class);
|
||||
}
|
||||
|
||||
/** @return HasMany<Ticket, $this> */
|
||||
public function tickets(): HasMany
|
||||
{
|
||||
return $this->hasMany(Ticket::class);
|
||||
}
|
||||
|
||||
public function startsAt(?CarbonInterface $serviceDate, string $timezone): ?CarbonInterface
|
||||
{
|
||||
if ($this->type === ValidityTimeType::FixedWindow) {
|
||||
return $this->fixed_starts_at;
|
||||
}
|
||||
|
||||
return $this->atServiceDate($serviceDate, $this->start_time, $timezone);
|
||||
}
|
||||
|
||||
public function expiresAt(?CarbonInterface $serviceDate, string $timezone): ?CarbonInterface
|
||||
{
|
||||
if ($this->type === ValidityTimeType::FixedWindow) {
|
||||
return $this->fixed_expires_at;
|
||||
}
|
||||
|
||||
return $this->atServiceDate($serviceDate, $this->end_time, $timezone);
|
||||
}
|
||||
|
||||
private function atServiceDate(
|
||||
?CarbonInterface $serviceDate,
|
||||
?string $time,
|
||||
string $timezone,
|
||||
): ?CarbonInterface {
|
||||
if ($serviceDate === null || $time === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return CarbonImmutable::parse(
|
||||
$serviceDate->format('Y-m-d').' '.$time,
|
||||
$timezone,
|
||||
)->utc();
|
||||
}
|
||||
}
|
||||
@@ -5,8 +5,10 @@ namespace App\Domains\Ticket\Services;
|
||||
use App\Domains\Auth\Models\User;
|
||||
use App\Domains\Catalog\Models\CatalogItem;
|
||||
use App\Domains\Catalog\Models\Variant;
|
||||
use App\Domains\Ticket\Enums\ValidityTimeType;
|
||||
use App\Domains\Ticket\Exceptions\TicketGenerationException;
|
||||
use App\Domains\Ticket\Models\Ticket;
|
||||
use App\Domains\Ticket\Models\ValidityTime;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
@@ -39,7 +41,8 @@ class TicketGeneratorService
|
||||
$user,
|
||||
): Ticket {
|
||||
$item = $target['catalog_item'];
|
||||
$selectedItem = $target['variant'] ?? $item;
|
||||
$variant = $target['variant'];
|
||||
$validityTime = $this->resolveValidityTime($item, $variant);
|
||||
|
||||
return Ticket::query()->create([
|
||||
'tenant_code' => $item->tenant_code,
|
||||
@@ -48,9 +51,11 @@ class TicketGeneratorService
|
||||
'description' => (string) ($item->descripcion ?? ''),
|
||||
'source_purchase_id' => $sourcePurchaseId,
|
||||
'source_catalog_item_id' => $item->getKey(),
|
||||
'source_variant_id' => $target['variant']?->getKey(),
|
||||
'starts_at' => $selectedItem->getMinimumUseDate(),
|
||||
'expires_at' => $selectedItem->getMaximumUseDate(),
|
||||
'source_variant_id' => $variant?->getKey(),
|
||||
'validity_time_id' => $validityTime?->getKey(),
|
||||
'service_date' => $validityTime?->type === ValidityTimeType::ServiceDateWindow
|
||||
? ($variant?->eventDate?->date ?? now($item->tenant->timezone)->toDateString())
|
||||
: null,
|
||||
'used_at' => null,
|
||||
'user_id' => $user->getKey(),
|
||||
]);
|
||||
@@ -134,12 +139,37 @@ class TicketGeneratorService
|
||||
throw TicketGenerationException::ticketsDisabled($catalogItem);
|
||||
}
|
||||
|
||||
// TODO: Reactivar esta validación cuando los pagos con productos vencidos
|
||||
// deban rechazarse nuevamente. Se deja deshabilitada temporalmente.
|
||||
// $selectedItem = $variant ?? $catalogItem;
|
||||
//
|
||||
// if ($selectedItem->getMaximumUseDate()?->lessThanOrEqualTo(now())) {
|
||||
// throw TicketGenerationException::maximumUseDateReached($catalogItem);
|
||||
// }
|
||||
}
|
||||
|
||||
private function resolveValidityTime(
|
||||
CatalogItem $catalogItem,
|
||||
?Variant $variant,
|
||||
): ?ValidityTime {
|
||||
if ($catalogItem->validity_time_id !== null) {
|
||||
return $catalogItem->validityTime;
|
||||
}
|
||||
|
||||
if ($variant === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$variant->loadMissing('definitions.itemAttribute.attribute.options.validityTime');
|
||||
|
||||
$validityTimes = $variant->definitions
|
||||
->map(function ($definition): ?ValidityTime {
|
||||
$option = $definition->itemAttribute?->attribute?->options
|
||||
->firstWhere('value', $definition->value);
|
||||
|
||||
return $option?->validityTime;
|
||||
})
|
||||
->filter()
|
||||
->unique(fn (ValidityTime $validityTime): int => $validityTime->getKey())
|
||||
->values();
|
||||
|
||||
if ($validityTimes->count() > 1) {
|
||||
throw TicketGenerationException::ambiguousValidityTime($catalogItem);
|
||||
}
|
||||
|
||||
return $validityTimes->first();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user