feat(ticket): implement terminal status management and validation
This commit is contained in:
@@ -19,6 +19,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
'tenant_code',
|
'tenant_code',
|
||||||
@@ -121,6 +122,13 @@ class Ticket extends Model
|
|||||||
return self::statusLabels()[$status] ?? $status;
|
return self::statusLabels()[$status] ?? $status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected static function booted(): void
|
||||||
|
{
|
||||||
|
static::saving(function (self $ticket): void {
|
||||||
|
$ticket->ensureTerminalStatusTransitionIsAllowed();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** @return BelongsTo<Tenant, $this> */
|
/** @return BelongsTo<Tenant, $this> */
|
||||||
public function tenant(): BelongsTo
|
public function tenant(): BelongsTo
|
||||||
{
|
{
|
||||||
@@ -219,6 +227,21 @@ class Ticket extends Model
|
|||||||
return self::statusLabel($this->status);
|
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
|
protected function valueChangeTenantCode(): string
|
||||||
{
|
{
|
||||||
return $this->tenant_code;
|
return $this->tenant_code;
|
||||||
@@ -226,9 +249,73 @@ class Ticket extends Model
|
|||||||
|
|
||||||
private function hasTerminalStatus(): bool
|
private function hasTerminalStatus(): bool
|
||||||
{
|
{
|
||||||
return $this->disabled_at !== null
|
return $this->terminalStatus() !== null;
|
||||||
|| $this->cancelled_at !== null
|
}
|
||||||
|| $this->refunded_at !== 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
|
public function getNameAttribute(): string
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
use Illuminate\Database\Schema\Blueprint;
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
use Illuminate\Support\Facades\Auth;
|
use Illuminate\Support\Facades\Auth;
|
||||||
use Illuminate\Support\Facades\Schema;
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class LogsValueChangesTest extends TestCase
|
class LogsValueChangesTest extends TestCase
|
||||||
@@ -175,6 +176,19 @@ class LogsValueChangesTest extends TestCase
|
|||||||
'user_id' => null,
|
'user_id' => null,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_ticket_cannot_transition_between_terminal_statuses(): void
|
||||||
|
{
|
||||||
|
$ticket = Ticket::query()->create([
|
||||||
|
'tenant_code' => 'test',
|
||||||
|
'ticket' => '794606d5-5f69-458d-9de7-03494757d626',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$ticket->update(['disabled_at' => now()]);
|
||||||
|
|
||||||
|
$this->expectException(ValidationException::class);
|
||||||
|
$ticket->update(['cancelled_at' => now()]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Fillable(['name', 'price', 'description'])]
|
#[Fillable(['name', 'price', 'description'])]
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use App\Domains\Ticket\Services\ResolvedTicketValidity;
|
|||||||
use App\Domains\Ticket\Services\ResolvedValidityGroup;
|
use App\Domains\Ticket\Services\ResolvedValidityGroup;
|
||||||
use App\Domains\Ticket\Services\TicketValidityResolver;
|
use App\Domains\Ticket\Services\TicketValidityResolver;
|
||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
use Tests\TestCase;
|
use Tests\TestCase;
|
||||||
|
|
||||||
class TicketTest extends TestCase
|
class TicketTest extends TestCase
|
||||||
@@ -202,6 +203,36 @@ class TicketTest extends TestCase
|
|||||||
$this->assertSame(Ticket::STATUS_REFUNDED, $ticket->status);
|
$this->assertSame(Ticket::STATUS_REFUNDED, $ticket->status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_it_marks_tickets_with_terminal_statuses(): void
|
||||||
|
{
|
||||||
|
Carbon::setTestNow('2026-09-10 12:00:00');
|
||||||
|
|
||||||
|
$disabled = new Ticket;
|
||||||
|
$disabled->markAsDisabled();
|
||||||
|
|
||||||
|
$cancelled = new Ticket;
|
||||||
|
$cancelled->markAsCancelled();
|
||||||
|
|
||||||
|
$refunded = new Ticket;
|
||||||
|
$refunded->markAsRefunded();
|
||||||
|
|
||||||
|
$this->assertSame(Ticket::STATUS_DISABLED, $disabled->status);
|
||||||
|
$this->assertSame('2026-09-10 12:00:00', $disabled->disabled_at->format('Y-m-d H:i:s'));
|
||||||
|
$this->assertSame(Ticket::STATUS_CANCELLED, $cancelled->status);
|
||||||
|
$this->assertSame('2026-09-10 12:00:00', $cancelled->cancelled_at->format('Y-m-d H:i:s'));
|
||||||
|
$this->assertSame(Ticket::STATUS_REFUNDED, $refunded->status);
|
||||||
|
$this->assertSame('2026-09-10 12:00:00', $refunded->refunded_at->format('Y-m-d H:i:s'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_does_not_allow_a_transition_between_terminal_statuses(): void
|
||||||
|
{
|
||||||
|
$ticket = new Ticket;
|
||||||
|
$ticket->markAsDisabled();
|
||||||
|
|
||||||
|
$this->expectException(ValidationException::class);
|
||||||
|
$ticket->markAsRefunded();
|
||||||
|
}
|
||||||
|
|
||||||
public function test_all_validity_times_in_the_same_group_must_be_active(): void
|
public function test_all_validity_times_in_the_same_group_must_be_active(): void
|
||||||
{
|
{
|
||||||
Carbon::setTestNow('2026-08-20 13:00:00');
|
Carbon::setTestNow('2026-08-20 13:00:00');
|
||||||
|
|||||||
Reference in New Issue
Block a user