From 5a0ce08adb8012ae18198578995ecd8762339b88 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Wed, 30 Sep 2026 15:31:54 -0300 Subject: [PATCH] feat(refunds): persist and apply refund configuration per event --- app/Domains/Ticketing/Event/Models/Event.php | 17 ++++++++- .../Event/Resources/EventResource.php | 8 ++-- .../Ticketing/Event/Services/EventService.php | 2 +- .../Ticketing/Event/documentacion/README.md | 4 ++ .../Ticketing/Ticket/Models/Ticket.php | 4 +- .../Ticket/Services/AdminAppTicketService.php | 20 ++++++---- ...200_add_refund_configuration_to_events.php | 38 +++++++++++++++++++ 7 files changed, 77 insertions(+), 16 deletions(-) create mode 100644 database/migrations/2026_09_30_000200_add_refund_configuration_to_events.php diff --git a/app/Domains/Ticketing/Event/Models/Event.php b/app/Domains/Ticketing/Event/Models/Event.php index ca3e042d..2b6a47a5 100644 --- a/app/Domains/Ticketing/Event/Models/Event.php +++ b/app/Domains/Ticketing/Event/Models/Event.php @@ -15,14 +15,27 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; -#[Fillable(['client_id', 'tenant_code', 'slug', 'event_category_id', 'title', 'subtitle', 'description', 'location', 'exact_location', 'date_text', 'published_at', 'attachment_id'])] +#[Fillable(['client_id', 'tenant_code', 'slug', 'event_category_id', 'title', 'subtitle', 'description', 'location', 'exact_location', 'date_text', 'published_at', 'attachment_id', 'allow_ticket_refund', 'allow_ticket_total_refund', 'allow_ticket_partial_refund', 'ticket_partial_refund_percentage'])] class Event extends Model { use HasFactory; protected function casts(): array { - return ['client_id' => 'integer', 'published_at' => 'datetime', 'exact_location' => 'array']; + return ['allow_ticket_refund' => 'boolean', 'allow_ticket_total_refund' => 'boolean', + 'allow_ticket_partial_refund' => 'boolean', 'ticket_partial_refund_percentage' => 'decimal:2', 'client_id' => 'integer', 'published_at' => 'datetime', 'exact_location' => 'array']; + } + + public function allow_refund(): bool + { + return (bool) $this->allow_ticket_refund + && ((bool) $this->allow_ticket_total_refund || $this->allow_partial_refund()); + } + + public function allow_partial_refund(): bool + { + return (bool) $this->allow_ticket_refund && (bool) $this->allow_ticket_partial_refund + && (float) $this->ticket_partial_refund_percentage > 0; } /** @return BelongsTo */ diff --git a/app/Domains/Ticketing/Event/Resources/EventResource.php b/app/Domains/Ticketing/Event/Resources/EventResource.php index 16f802a8..377c5373 100644 --- a/app/Domains/Ticketing/Event/Resources/EventResource.php +++ b/app/Domains/Ticketing/Event/Resources/EventResource.php @@ -26,10 +26,10 @@ class EventResource extends JsonResource 'date_text' => $this->date_text, 'published_at' => $this->published_at?->toIso8601String(), 'attachment_id' => $this->attachment_id, - 'allow_ticket_refund' => $this->tenant->allow_ticket_refund, - 'allow_ticket_total_refund' => $this->tenant->allow_ticket_total_refund, - 'allow_ticket_partial_refund' => $this->tenant->allow_ticket_partial_refund, - 'ticket_partial_refund_percentage' => $this->tenant->ticket_partial_refund_percentage, + 'allow_ticket_refund' => $this->allow_ticket_refund, + 'allow_ticket_total_refund' => $this->allow_ticket_total_refund, + 'allow_ticket_partial_refund' => $this->allow_ticket_partial_refund, + 'ticket_partial_refund_percentage' => $this->ticket_partial_refund_percentage, 'dates' => EventDateResource::collection( app(EventDateGroupingService::class)->group($this->dates) ), diff --git a/app/Domains/Ticketing/Event/Services/EventService.php b/app/Domains/Ticketing/Event/Services/EventService.php index ec2b4369..212dee28 100644 --- a/app/Domains/Ticketing/Event/Services/EventService.php +++ b/app/Domains/Ticketing/Event/Services/EventService.php @@ -49,7 +49,7 @@ class EventService 'location' => $data['location'], ...array_intersect_key($data, ['attachment_id' => true, 'exact_location' => true]), ]); - $tenant->update([ + $event->update([ ...array_intersect_key($data, array_flip([ 'allow_ticket_refund', 'allow_ticket_total_refund', diff --git a/app/Domains/Ticketing/Event/documentacion/README.md b/app/Domains/Ticketing/Event/documentacion/README.md index 97ac8b42..9ce3af94 100644 --- a/app/Domains/Ticketing/Event/documentacion/README.md +++ b/app/Domains/Ticketing/Event/documentacion/README.md @@ -53,3 +53,7 @@ una lista de eventos ni existe todavía una pantalla para gestionarlos. Las fechas se vinculan con variantes de `Catalog`, que a su vez pueden generar tickets. Los avisos por suspensión y reprogramación se construyen dinámicamente después de excluir los cambios que el usuario ya vio tres veces. + +La configuración de devoluciones se persiste en `events`, se entrega en +`EventResource` y se aplica al evento de cada ticket. La migración inicial copia +los valores anteriores del tenant a sus eventos. diff --git a/app/Domains/Ticketing/Ticket/Models/Ticket.php b/app/Domains/Ticketing/Ticket/Models/Ticket.php index 12560bc3..5d98fae5 100644 --- a/app/Domains/Ticketing/Ticket/Models/Ticket.php +++ b/app/Domains/Ticketing/Ticket/Models/Ticket.php @@ -148,7 +148,9 @@ class Ticket extends Model public function allow_refund(): bool { - return $this->tenant?->allow_refund() ?? false; + return $this->event_id === null + ? ($this->tenant?->allow_refund() ?? false) + : ($this->event?->allow_refund() ?? false); } public function allowRefund(): bool diff --git a/app/Domains/Ticketing/Ticket/Services/AdminAppTicketService.php b/app/Domains/Ticketing/Ticket/Services/AdminAppTicketService.php index 81dbfa0f..eec07ee4 100644 --- a/app/Domains/Ticketing/Ticket/Services/AdminAppTicketService.php +++ b/app/Domains/Ticketing/Ticket/Services/AdminAppTicketService.php @@ -9,6 +9,7 @@ use App\Domains\Commerce\Purchase\Models\PurchaseItem; use App\Domains\Commerce\Purchase\Services\PurchaseRefundSummaryService; use App\Domains\Core\Auth\Models\User; use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Ticketing\Event\Models\Event; use App\Domains\Ticketing\Ticket\Models\Ticket; use App\Domains\Ticketing\Ticket\Models\TicketRefund; use Illuminate\Database\Eloquent\Builder; @@ -23,6 +24,7 @@ class AdminAppTicketService ...TicketValidityResolver::RELATIONS, ...TicketPresentationResolver::RELATIONS, 'tenant', + 'event', 'user', 'scannerUser', 'sourceCatalogItem.category', @@ -145,19 +147,20 @@ class AdminAppTicketService ]); } + $configuration = $ticket->event_id === null ? $tenant : $ticket->event; $unitPrice = (float) $purchaseItem->precio_unitario; $itemTotal = (float) $purchaseItem->total; $itemRefundedAmount = $this->refundedAmountForPurchaseItem($purchaseItem); $remainingItemAmount = max(0.0, round($itemTotal - $itemRefundedAmount, 2)); $total = null; - if ($tenant->allow_refund() && $tenant->allow_ticket_total_refund && $unitPrice <= $remainingItemAmount) { + if ($configuration->allow_refund() && $configuration->allow_ticket_total_refund && $unitPrice <= $remainingItemAmount) { $total = number_format($unitPrice, 2, '.', ''); } $partial = null; - if ($tenant->allow_refund() && $tenant->allow_partial_refund()) { - $partialAmount = $this->refundAmount($purchaseItem, $tenant, 'partial'); + if ($configuration->allow_refund() && $configuration->allow_partial_refund()) { + $partialAmount = $this->refundAmount($purchaseItem, $configuration, 'partial'); if ($partialAmount <= $remainingItemAmount) { $partial = number_format($partialAmount, 2, '.', ''); } @@ -175,14 +178,15 @@ class AdminAppTicketService string $refundType, ?User $createdBy = null, ): Ticket { - $this->ensureRefundIsAllowed($tenant, $refundType); - return DB::transaction(function () use ($tenant, $ticketId, $refundType, $createdBy): Ticket { $ticket = Ticket::query() ->where('tenant_code', $tenant->codigo) ->lockForUpdate() ->findOrFail($ticketId); + $configuration = $ticket->event_id === null ? $tenant : $ticket->event; + $this->ensureRefundIsAllowed($configuration, $refundType); + if (! $ticket->can_refund()) { if ($ticket->status !== Ticket::STATUS_ACTIVE) { throw ValidationException::withMessages([ @@ -205,7 +209,7 @@ class AdminAppTicketService ]); } - $refundAmount = $this->refundAmount($purchaseItem, $tenant, $refundType); + $refundAmount = $this->refundAmount($purchaseItem, $configuration, $refundType); $refundedAmount = round( $this->refundedAmountForPurchaseItem($purchaseItem) + $refundAmount, 2, @@ -286,7 +290,7 @@ class AdminAppTicketService ->sum('amount'), 2); } - private function ensureRefundIsAllowed(Tenant $tenant, string $refundType): void + private function ensureRefundIsAllowed(Tenant|Event $tenant, string $refundType): void { $isAllowed = match ($refundType) { TicketRefund::TYPE_PARTIAL => $tenant->allow_refund() && $tenant->allow_partial_refund(), @@ -300,7 +304,7 @@ class AdminAppTicketService } } - private function refundAmount(PurchaseItem $purchaseItem, Tenant $tenant, string $refundType): float + private function refundAmount(PurchaseItem $purchaseItem, Tenant|Event $tenant, string $refundType): float { $ticketAmount = (float) $purchaseItem->precio_unitario; diff --git a/database/migrations/2026_09_30_000200_add_refund_configuration_to_events.php b/database/migrations/2026_09_30_000200_add_refund_configuration_to_events.php new file mode 100644 index 00000000..33020a1f --- /dev/null +++ b/database/migrations/2026_09_30_000200_add_refund_configuration_to_events.php @@ -0,0 +1,38 @@ +boolean('allow_ticket_refund')->default(false); + $table->boolean('allow_ticket_total_refund')->default(false); + $table->boolean('allow_ticket_partial_refund')->default(false); + $table->decimal('ticket_partial_refund_percentage', 5, 2)->default(0); + }); + DB::table('tenants')->select([ + 'codigo', 'allow_ticket_refund', 'allow_ticket_total_refund', + 'allow_ticket_partial_refund', 'ticket_partial_refund_percentage', + ])->get()->each(function (object $tenant): void { + DB::table('events')->where('tenant_code', $tenant->codigo)->update([ + 'allow_ticket_refund' => $tenant->allow_ticket_refund, + 'allow_ticket_total_refund' => $tenant->allow_ticket_total_refund, + 'allow_ticket_partial_refund' => $tenant->allow_ticket_partial_refund, + 'ticket_partial_refund_percentage' => $tenant->ticket_partial_refund_percentage ?? 0, + ]); + }); + } + + public function down(): void + { + Schema::table('events', fn (Blueprint $table) => $table->dropColumn([ + 'allow_ticket_refund', 'allow_ticket_total_refund', + 'allow_ticket_partial_refund', 'ticket_partial_refund_percentage', + ])); + } +};