*/ protected array $loggedAttributes = [ 'used_at', 'disabled_at', 'cancelled_at', 'refunded_at', ]; protected $appends = [ 'name', 'description', 'is_valid', 'is_expired', 'is_used', 'status', ]; protected function casts(): array { return [ 'source_catalog_item_id' => 'integer', 'source_variant_id' => 'integer', 'source_purchase_item_id' => 'integer', 'used_at' => 'datetime', 'disabled_at' => 'datetime', 'cancelled_at' => 'datetime', 'refunded_at' => 'datetime', 'scanner_user_id' => 'integer', 'user_id' => 'integer', ]; } /** @return list */ public static function statuses(): array { return array_keys(self::statusLabels()); } /** @return array */ public static function statusLabels(): array { return [ self::STATUS_ACTIVE => 'Activo', self::STATUS_USED => 'Usado', self::STATUS_EXPIRED => 'Vencido', self::STATUS_DISABLED => 'Inhabilitado', self::STATUS_CANCELLED => 'Cancelado', self::STATUS_REFUNDED => 'Reembolsado', ]; } /** @return list */ public static function statusOptions(): array { return collect(self::statusLabels()) ->map(fn (string $label, string $status): array => [ 'value' => $status, 'label' => $label, ]) ->values() ->all(); } public static function statusLabel(string $status): string { return self::statusLabels()[$status] ?? $status; } protected static function booted(): void { static::saving(function (self $ticket): void { $ticket->ensureTerminalStatusTransitionIsAllowed(); }); } /** @return BelongsTo */ public function tenant(): BelongsTo { return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo'); } public function allow_refund(): bool { return $this->tenant?->allow_refund() ?? false; } public function allowRefund(): bool { return $this->allow_refund(); } public function getAllowRefundAttribute(): bool { return $this->allow_refund(); } /** @return BelongsTo */ public function user(): BelongsTo { return $this->belongsTo(User::class); } /** @return BelongsTo */ public function scannerUser(): BelongsTo { return $this->belongsTo(User::class, 'scanner_user_id')->withTrashed(); } /** @return HasMany */ public function scanAttempts(): HasMany { return $this->hasMany(ScanAttempt::class); } /** @return BelongsTo */ public function sourcePurchaseItem(): BelongsTo { return $this->belongsTo(PurchaseItem::class, 'source_purchase_item_id'); } /** @return BelongsTo */ public function sourceCatalogItem(): BelongsTo { return $this->belongsTo(CatalogItem::class, 'source_catalog_item_id')->withTrashed(); } /** @return BelongsTo */ public function sourceVariant(): BelongsTo { return $this->belongsTo(Variant::class, 'source_variant_id')->withTrashed(); } public function isValid(): bool { if ($this->hasTerminalStatus() || $this->used_at !== null) { return false; } return $this->resolvedValidity()->isValid(); } public function getIsValidAttribute(): bool { return $this->isValid(); } public function getIsExpiredAttribute(): bool { return ! $this->hasTerminalStatus() && $this->used_at === null && $this->resolvedValidity()->isExpired(); } public function getIsUsedAttribute(): bool { return $this->used_at !== null; } public function getStatusAttribute(): string { if ($this->refunded_at !== null) { return self::STATUS_REFUNDED; } if ($this->cancelled_at !== null) { return self::STATUS_CANCELLED; } if ($this->disabled_at !== null) { return self::STATUS_DISABLED; } if ($this->is_used) { return self::STATUS_USED; } if ($this->is_expired) { return self::STATUS_EXPIRED; } return self::STATUS_ACTIVE; } public function getStatusLabelAttribute(): string { return self::statusLabel($this->status); } public function markAsDisabled(): void { $this->markAsTerminalStatus(self::STATUS_DISABLED); } public function markAsCancelled(): void { $this->markAsTerminalStatus(self::STATUS_CANCELLED); } public function markAsRefunded(): void { $this->markAsTerminalStatus(self::STATUS_REFUNDED); } protected function valueChangeTenantCode(): string { return $this->tenant_code; } private function hasTerminalStatus(): bool { return $this->terminalStatus() !== null; } private function markAsTerminalStatus(string $status): void { $currentStatus = $this->terminalStatus(); if ($currentStatus === $status) { return; } if ($currentStatus !== null) { $this->throwTerminalStatusTransitionException(); } $this->ensureTerminalStatusTransitionIsAllowed($status); $this->{self::terminalStatusTimestampColumn($status)} = now(); } private function ensureTerminalStatusTransitionIsAllowed(?string $targetStatus = null): void { $currentStatus = $this->terminalStatusFromAttributes($this->getRawOriginal()); $nextStatus = $targetStatus ?? $this->terminalStatus(); if ($currentStatus === null || $nextStatus === null || $currentStatus === $nextStatus) { return; } $this->throwTerminalStatusTransitionException(); } private function throwTerminalStatusTransitionException(): never { throw ValidationException::withMessages([ 'status' => 'No se puede cambiar un ticket con estado terminal a otro estado terminal.', ]); } private function terminalStatus(): ?string { return $this->terminalStatusFromAttributes($this->getAttributes()); } /** @param array $attributes */ private function terminalStatusFromAttributes(array $attributes): ?string { foreach ([ self::STATUS_REFUNDED, self::STATUS_CANCELLED, self::STATUS_DISABLED, ] as $status) { if (($attributes[self::terminalStatusTimestampColumn($status)] ?? null) !== null) { return $status; } } return null; } private static function terminalStatusTimestampColumn(string $status): string { return match ($status) { self::STATUS_DISABLED => 'disabled_at', self::STATUS_CANCELLED => 'cancelled_at', self::STATUS_REFUNDED => 'refunded_at', }; } public function getNameAttribute(): string { return app(TicketPresentationResolver::class)->name($this); } public function getDescriptionAttribute(): string { return app(TicketPresentationResolver::class)->description($this); } public function getEffectiveStartsAt(): ?CarbonInterface { return $this->resolvedValidity()->effectiveStartsAt(); } public function getEffectiveExpiresAt(): ?CarbonInterface { return $this->resolvedValidity()->effectiveExpiresAt(); } /** @return Collection */ public function resolvedValidityGroups(): Collection { return $this->resolvedValidity()->groups; } public function resolvedValidity(): ResolvedTicketValidity { return $this->resolvedValidity ??= app(TicketValidityResolver::class)->resolveTicket($this); } }