367 lines
9.7 KiB
PHP
367 lines
9.7 KiB
PHP
<?php
|
|
|
|
namespace App\Domains\Ticket\Models;
|
|
|
|
use App\Domains\Auth\Models\User;
|
|
use App\Domains\Catalog\Models\CatalogItem;
|
|
use App\Domains\Catalog\Models\Variant;
|
|
use App\Domains\Logging\Models\Concerns\LogsValueChanges;
|
|
use App\Domains\Purchase\Models\PurchaseItem;
|
|
use App\Domains\Tenant\Models\Tenant;
|
|
use App\Domains\Ticket\Services\ResolvedTicketValidity;
|
|
use App\Domains\Ticket\Services\ResolvedValidityGroup;
|
|
use App\Domains\Ticket\Services\TicketPresentationResolver;
|
|
use App\Domains\Ticket\Services\TicketValidityResolver;
|
|
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\BelongsTo;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
#[Fillable([
|
|
'tenant_code',
|
|
'ticket',
|
|
'source_purchase_item_id',
|
|
'source_catalog_item_id',
|
|
'source_variant_id',
|
|
'used_at',
|
|
'disabled_at',
|
|
'cancelled_at',
|
|
'refunded_at',
|
|
'scanner_user_id',
|
|
'user_id',
|
|
])]
|
|
class Ticket extends Model
|
|
{
|
|
use HasFactory, LogsValueChanges;
|
|
|
|
private ?ResolvedTicketValidity $resolvedValidity = null;
|
|
|
|
public const STATUS_ACTIVE = 'active';
|
|
|
|
public const STATUS_EXPIRED = 'expired';
|
|
|
|
public const STATUS_USED = 'used';
|
|
|
|
public const STATUS_DISABLED = 'disabled';
|
|
|
|
public const STATUS_CANCELLED = 'cancelled';
|
|
|
|
public const STATUS_REFUNDED = 'refunded';
|
|
|
|
public $timestamps = false;
|
|
|
|
/** @var list<string> */
|
|
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<string> */
|
|
public static function statuses(): array
|
|
{
|
|
return array_keys(self::statusLabels());
|
|
}
|
|
|
|
/** @return array<string, string> */
|
|
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<array{value: string, label: string}> */
|
|
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<Tenant, $this> */
|
|
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<User, $this> */
|
|
public function user(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class);
|
|
}
|
|
|
|
/** @return BelongsTo<User, $this> */
|
|
public function scannerUser(): BelongsTo
|
|
{
|
|
return $this->belongsTo(User::class, 'scanner_user_id')->withTrashed();
|
|
}
|
|
|
|
/** @return HasMany<ScanAttempt, $this> */
|
|
public function scanAttempts(): HasMany
|
|
{
|
|
return $this->hasMany(ScanAttempt::class);
|
|
}
|
|
|
|
/** @return BelongsTo<PurchaseItem, $this> */
|
|
public function sourcePurchaseItem(): BelongsTo
|
|
{
|
|
return $this->belongsTo(PurchaseItem::class, 'source_purchase_item_id');
|
|
}
|
|
|
|
/** @return BelongsTo<CatalogItem, $this> */
|
|
public function sourceCatalogItem(): BelongsTo
|
|
{
|
|
return $this->belongsTo(CatalogItem::class, 'source_catalog_item_id')->withTrashed();
|
|
}
|
|
|
|
/** @return BelongsTo<Variant, $this> */
|
|
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<string, mixed> $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<int, ResolvedValidityGroup> */
|
|
public function resolvedValidityGroups(): Collection
|
|
{
|
|
return $this->resolvedValidity()->groups;
|
|
}
|
|
|
|
public function resolvedValidity(): ResolvedTicketValidity
|
|
{
|
|
return $this->resolvedValidity ??= app(TicketValidityResolver::class)->resolveTicket($this);
|
|
}
|
|
}
|