Files
shopit-back/app/Domains/Ticket/Models/Ticket.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);
}
}