diff --git a/app/Domains/Forms/Services/TicketFilterFormService.php b/app/Domains/Forms/Services/TicketFilterFormService.php index 72ccc65..7574a92 100644 --- a/app/Domains/Forms/Services/TicketFilterFormService.php +++ b/app/Domains/Forms/Services/TicketFilterFormService.php @@ -124,11 +124,7 @@ class TicketFilterFormService 'required' => false, 'default' => null, 'placeholder' => 'Estado', - 'options' => [ - ['value' => Ticket::STATUS_ACTIVE, 'label' => 'Activo'], - ['value' => Ticket::STATUS_USED, 'label' => 'Usado'], - ['value' => Ticket::STATUS_EXPIRED, 'label' => 'Vencido'], - ], + 'options' => Ticket::statusOptions(), ], ]; } diff --git a/app/Domains/Forms/Services/TicketFormService.php b/app/Domains/Forms/Services/TicketFormService.php index b9436d2..ff50bb2 100644 --- a/app/Domains/Forms/Services/TicketFormService.php +++ b/app/Domains/Forms/Services/TicketFormService.php @@ -236,11 +236,7 @@ class TicketFormService ?: $left['label'] <=> $right['label']); return [ - 'statuses' => [ - ['value' => Ticket::STATUS_ACTIVE, 'label' => 'Activo'], - ['value' => Ticket::STATUS_USED, 'label' => 'Usado'], - ['value' => Ticket::STATUS_EXPIRED, 'label' => 'Vencido'], - ], + 'statuses' => Ticket::statusOptions(), 'categories' => array_values(array_map( fn (array $category): array => [ 'value' => $category['value'], diff --git a/app/Domains/Purchase/Resources/PurchaseItemResource.php b/app/Domains/Purchase/Resources/PurchaseItemResource.php index 1bd67d5..f83ecf6 100644 --- a/app/Domains/Purchase/Resources/PurchaseItemResource.php +++ b/app/Domains/Purchase/Resources/PurchaseItemResource.php @@ -25,6 +25,7 @@ class PurchaseItemResource extends JsonResource 'quantity' => (int) $this->cantidad, 'unit_price' => $this->formatMoney($this->precio_unitario), 'line_total' => $this->formatMoney($this->total), + 'refunded_amount' => $this->formatMoney($this->refunded_amount), 'source_catalog_item_id' => $this->source_catalog_item_id, 'source_variant_id' => $this->source_variant_id, 'item_details' => [ diff --git a/app/Domains/Sale/Resources/AdminApp/SaleDetailResource.php b/app/Domains/Sale/Resources/AdminApp/SaleDetailResource.php index d389d1a..08f1736 100644 --- a/app/Domains/Sale/Resources/AdminApp/SaleDetailResource.php +++ b/app/Domains/Sale/Resources/AdminApp/SaleDetailResource.php @@ -22,6 +22,7 @@ class SaleDetailResource extends JsonResource 'quantity' => (int) $item->cantidad, 'unit_price' => $this->formatMoney($item->precio_unitario), 'total' => $this->formatMoney($item->total), + 'refunded_amount' => $this->formatMoney($item->refunded_amount), ])->values(), 'total' => $this->formatMoney($this->total), ]; diff --git a/app/Domains/Sale/Resources/AdminApp/SaleTicketResource.php b/app/Domains/Sale/Resources/AdminApp/SaleTicketResource.php index 5070b1a..33ba09b 100644 --- a/app/Domains/Sale/Resources/AdminApp/SaleTicketResource.php +++ b/app/Domains/Sale/Resources/AdminApp/SaleTicketResource.php @@ -17,6 +17,7 @@ class SaleTicketResource extends JsonResource 'id' => $this->id, 'expires_at' => $this->getEffectiveExpiresAt(), 'status' => $this->status, + 'status_label' => $this->status_label, ]; } } diff --git a/app/Domains/Ticket/Requests/AdminAppTicketIndexRequest.php b/app/Domains/Ticket/Requests/AdminAppTicketIndexRequest.php index 5a80395..90d8e35 100644 --- a/app/Domains/Ticket/Requests/AdminAppTicketIndexRequest.php +++ b/app/Domains/Ticket/Requests/AdminAppTicketIndexRequest.php @@ -32,11 +32,7 @@ class AdminAppTicketIndexRequest extends FormRequest 'status' => [ 'sometimes', 'nullable', - Rule::in([ - Ticket::STATUS_ACTIVE, - Ticket::STATUS_USED, - Ticket::STATUS_EXPIRED, - ]), + Rule::in(Ticket::statuses()), ], 'page' => ['sometimes', 'integer', 'min:1'], 'per_page' => ['sometimes', 'integer', 'min:1', 'max:100'], diff --git a/app/Domains/Ticket/Resources/TicketResource.php b/app/Domains/Ticket/Resources/TicketResource.php index 8e4c0e8..d45a3eb 100644 --- a/app/Domains/Ticket/Resources/TicketResource.php +++ b/app/Domains/Ticket/Resources/TicketResource.php @@ -16,6 +16,8 @@ class TicketResource extends JsonResource 'id' => $this->id, 'tenant_code' => $this->tenant_code, 'ticket' => $this->ticket, + 'status' => $this->status, + 'status_label' => $this->status_label, 'name' => $this->name, 'description' => $this->description, 'client' => $this->user?->nombre_apellido, diff --git a/app/Domains/Ticket/Services/AdminAppTicketRowService.php b/app/Domains/Ticket/Services/AdminAppTicketRowService.php index e29b879..31b5a7c 100644 --- a/app/Domains/Ticket/Services/AdminAppTicketRowService.php +++ b/app/Domains/Ticket/Services/AdminAppTicketRowService.php @@ -31,6 +31,7 @@ class AdminAppTicketRowService ?? $ticket->sourceCatalogItem?->nombre ?? $ticket->name, 'amount' => $purchaseItem?->precio_unitario, + 'refunded_amount' => $purchaseItem?->refunded_amount, 'client' => $purchaseItem?->purchase?->nombre_apellido ?? $ticket->user?->nombre_apellido, 'status' => $ticket->status, 'scanned_by' => $ticket->scannerUser?->nombre_apellido, @@ -95,11 +96,7 @@ class AdminAppTicketRowService return match ($type) { 'order_number' => '#'.$value, 'currency' => '$'.number_format((float) $value, 2, ',', '.'), - 'status' => match ((string) $value) { - Ticket::STATUS_USED => 'Usado', - Ticket::STATUS_EXPIRED => 'Vencido', - default => 'Activo', - }, + 'status' => Ticket::statusLabel((string) $value), default => (string) $value, }; } diff --git a/app/Domains/Ticket/Services/AdminAppTicketService.php b/app/Domains/Ticket/Services/AdminAppTicketService.php index cc41319..fe5c020 100644 --- a/app/Domains/Ticket/Services/AdminAppTicketService.php +++ b/app/Domains/Ticket/Services/AdminAppTicketService.php @@ -255,13 +255,41 @@ class AdminAppTicketService } if ($status === Ticket::STATUS_USED) { - $query->whereNotNull('used_at'); + $query + ->whereNotNull('used_at') + ->whereNull('disabled_at') + ->whereNull('cancelled_at') + ->whereNull('refunded_at'); + + return; + } + + $timestampColumn = match ($status) { + Ticket::STATUS_DISABLED => 'disabled_at', + Ticket::STATUS_CANCELLED => 'cancelled_at', + Ticket::STATUS_REFUNDED => 'refunded_at', + default => null, + }; + + if ($timestampColumn !== null) { + $query->whereNotNull($timestampColumn); + + if ($status === Ticket::STATUS_DISABLED) { + $query->whereNull('cancelled_at')->whereNull('refunded_at'); + } + + if ($status === Ticket::STATUS_CANCELLED) { + $query->whereNull('refunded_at'); + } return; } $matchingIds = (clone $query) ->whereNull('used_at') + ->whereNull('disabled_at') + ->whereNull('cancelled_at') + ->whereNull('refunded_at') ->with(TicketValidityResolver::RELATIONS) ->get() ->filter(fn (Ticket $ticket): bool => $ticket->status === $status) diff --git a/tests/Feature/Forms/AdminAppTicketFilterFormControllerTest.php b/tests/Feature/Forms/AdminAppTicketFilterFormControllerTest.php index 43d19da..6e0659c 100644 --- a/tests/Feature/Forms/AdminAppTicketFilterFormControllerTest.php +++ b/tests/Feature/Forms/AdminAppTicketFilterFormControllerTest.php @@ -78,6 +78,9 @@ class AdminAppTicketFilterFormControllerTest extends TestCase ['value' => 'active', 'label' => 'Activo'], ['value' => 'used', 'label' => 'Usado'], ['value' => 'expired', 'label' => 'Vencido'], + ['value' => 'disabled', 'label' => 'Inhabilitado'], + ['value' => 'cancelled', 'label' => 'Cancelado'], + ['value' => 'refunded', 'label' => 'Reembolsado'], ], ], ], diff --git a/tests/Feature/Forms/AdminAppTicketFormControllerTest.php b/tests/Feature/Forms/AdminAppTicketFormControllerTest.php index 5bb4ef3..78c97d7 100644 --- a/tests/Feature/Forms/AdminAppTicketFormControllerTest.php +++ b/tests/Feature/Forms/AdminAppTicketFormControllerTest.php @@ -63,6 +63,9 @@ class AdminAppTicketFormControllerTest extends TestCase ['value' => 'active', 'label' => 'Activo'], ['value' => 'used', 'label' => 'Usado'], ['value' => 'expired', 'label' => 'Vencido'], + ['value' => 'disabled', 'label' => 'Inhabilitado'], + ['value' => 'cancelled', 'label' => 'Cancelado'], + ['value' => 'refunded', 'label' => 'Reembolsado'], ], 'categories' => [ [ diff --git a/tests/Feature/Sale/AdminAppSaleControllerTest.php b/tests/Feature/Sale/AdminAppSaleControllerTest.php index 1cc0d7f..cb74909 100644 --- a/tests/Feature/Sale/AdminAppSaleControllerTest.php +++ b/tests/Feature/Sale/AdminAppSaleControllerTest.php @@ -507,12 +507,14 @@ class AdminAppSaleControllerTest extends TestCase 'id' => $firstTicket->id, 'expires_at' => null, 'status' => Ticket::STATUS_ACTIVE, + 'status_label' => 'Activo', ], [ 'product' => 'Abono general', 'id' => $usedTicket->id, 'expires_at' => null, 'status' => Ticket::STATUS_USED, + 'status_label' => 'Usado', ], ], ]); diff --git a/tests/Feature/Ticket/AdminAppTicketControllerTest.php b/tests/Feature/Ticket/AdminAppTicketControllerTest.php index 0ac1a67..f894627 100644 --- a/tests/Feature/Ticket/AdminAppTicketControllerTest.php +++ b/tests/Feature/Ticket/AdminAppTicketControllerTest.php @@ -72,6 +72,7 @@ class AdminAppTicketControllerTest extends TestCase ->assertJsonPath('data.0.tenant_code', $tenant->codigo) ->assertJsonPath('data.0.values.id', $ticket->id) ->assertJsonPath('data.0.values.status', Ticket::STATUS_ACTIVE) + ->assertJsonPath('data.0.status_label', 'Activo') ->assertJsonMissingPath('data.0.values.ticket') ->assertJsonPath('data.0.values.date', '-') ->assertJsonPath('data.0.values.size', '-') @@ -335,6 +336,7 @@ class AdminAppTicketControllerTest extends TestCase ->assertJsonPath('data.0.order_number', $purchase->id) ->assertJsonPath('data.0.product', 'Remera') ->assertJsonPath('data.0.amount', '8000.00') + ->assertJsonPath('data.0.refunded_amount', '0.00') ->assertJsonPath('data.0.status', Ticket::STATUS_USED) ->assertJsonPath('data.0.scanned_by', $admin->nombre_apellido) ->assertJsonPath('data.0.variant_properties.0.code', 'size') @@ -498,6 +500,28 @@ class AdminAppTicketControllerTest extends TestCase ->assertJsonPath('data.0.id', $active->id); } + public function test_it_filters_persisted_ticket_statuses(): void + { + $tenant = $this->createTenant('ticket-statuses'); + $admin = $this->createAdminAppUser($tenant); + $this->grantTicketsMenu($tenant); + Sanctum::actingAs($admin); + + foreach ([ + Ticket::STATUS_DISABLED => 'disabled_at', + Ticket::STATUS_CANCELLED => 'cancelled_at', + Ticket::STATUS_REFUNDED => 'refunded_at', + ] as $status => $timestamp) { + $ticket = $this->createTicket($tenant, $admin, [$timestamp => now()]); + + $this->getJson('/api/v1/adminapp/tenant/tickets?status='.$status) + ->assertOk() + ->assertJsonCount(1, 'data') + ->assertJsonPath('data.0.id', $ticket->id) + ->assertJsonPath('data.0.status', $status); + } + } + public function test_it_downloads_filtered_ticket_reports(): void { $tenant = $this->createTenant('fiesta_futbol_infantil'); diff --git a/tests/Feature/Ticket/ScannerTicketControllerTest.php b/tests/Feature/Ticket/ScannerTicketControllerTest.php index aa6fd8a..71b7a22 100644 --- a/tests/Feature/Ticket/ScannerTicketControllerTest.php +++ b/tests/Feature/Ticket/ScannerTicketControllerTest.php @@ -272,6 +272,11 @@ class ScannerTicketControllerTest extends TestCase ->assertJsonPath('data.ticket.scanner_user_id', $this->scanner->id) ->assertJsonPath('data.ticket.is_valid', false) ->assertJsonPath('data.ticket.is_used', true) + ->assertJsonPath('data.ticket.status', Ticket::STATUS_USED) + ->assertJsonPath('data.ticket.status_label', 'Usado') + ->assertJsonMissingPath('data.ticket.disabled_at') + ->assertJsonMissingPath('data.ticket.cancelled_at') + ->assertJsonMissingPath('data.ticket.refunded_at') ->assertJsonPath('data.ticket.client', $this->ticketOwner->nombre_apellido) ->assertJsonPath('data.client.id', $this->ticketOwner->id) ->assertJsonPath('data.client.nombre_apellido', $this->ticketOwner->nombre_apellido); @@ -280,6 +285,14 @@ class ScannerTicketControllerTest extends TestCase 'id' => $ticket->id, 'scanner_user_id' => $this->scanner->id, ]); + $this->assertDatabaseHas('value_changes', [ + 'tenant_code' => $this->tenant->codigo, + 'trackable_type' => $ticket->getMorphClass(), + 'trackable_id' => $ticket->id, + 'attribute' => 'used_at', + 'old_value' => null, + 'user_id' => $this->scanner->id, + ]); $this->assertNotNull($ticket->fresh()->used_at); $scanAttempt = ScanAttempt::query()->sole();