feat(refunds): persist and apply refund configuration per event

This commit is contained in:
2026-09-30 15:31:54 -03:00
parent a770d435eb
commit 5a0ce08adb
7 changed files with 77 additions and 16 deletions

View File

@@ -15,14 +15,27 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany; 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 class Event extends Model
{ {
use HasFactory; use HasFactory;
protected function casts(): array 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<Tenant, $this> */ /** @return BelongsTo<Tenant, $this> */

View File

@@ -26,10 +26,10 @@ class EventResource extends JsonResource
'date_text' => $this->date_text, 'date_text' => $this->date_text,
'published_at' => $this->published_at?->toIso8601String(), 'published_at' => $this->published_at?->toIso8601String(),
'attachment_id' => $this->attachment_id, 'attachment_id' => $this->attachment_id,
'allow_ticket_refund' => $this->tenant->allow_ticket_refund, 'allow_ticket_refund' => $this->allow_ticket_refund,
'allow_ticket_total_refund' => $this->tenant->allow_ticket_total_refund, 'allow_ticket_total_refund' => $this->allow_ticket_total_refund,
'allow_ticket_partial_refund' => $this->tenant->allow_ticket_partial_refund, 'allow_ticket_partial_refund' => $this->allow_ticket_partial_refund,
'ticket_partial_refund_percentage' => $this->tenant->ticket_partial_refund_percentage, 'ticket_partial_refund_percentage' => $this->ticket_partial_refund_percentage,
'dates' => EventDateResource::collection( 'dates' => EventDateResource::collection(
app(EventDateGroupingService::class)->group($this->dates) app(EventDateGroupingService::class)->group($this->dates)
), ),

View File

@@ -49,7 +49,7 @@ class EventService
'location' => $data['location'], 'location' => $data['location'],
...array_intersect_key($data, ['attachment_id' => true, 'exact_location' => true]), ...array_intersect_key($data, ['attachment_id' => true, 'exact_location' => true]),
]); ]);
$tenant->update([ $event->update([
...array_intersect_key($data, array_flip([ ...array_intersect_key($data, array_flip([
'allow_ticket_refund', 'allow_ticket_refund',
'allow_ticket_total_refund', 'allow_ticket_total_refund',

View File

@@ -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 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 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. 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.

View File

@@ -148,7 +148,9 @@ class Ticket extends Model
public function allow_refund(): bool 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 public function allowRefund(): bool

View File

@@ -9,6 +9,7 @@ use App\Domains\Commerce\Purchase\Models\PurchaseItem;
use App\Domains\Commerce\Purchase\Services\PurchaseRefundSummaryService; use App\Domains\Commerce\Purchase\Services\PurchaseRefundSummaryService;
use App\Domains\Core\Auth\Models\User; use App\Domains\Core\Auth\Models\User;
use App\Domains\Core\Tenant\Models\Tenant; 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\Ticket;
use App\Domains\Ticketing\Ticket\Models\TicketRefund; use App\Domains\Ticketing\Ticket\Models\TicketRefund;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
@@ -23,6 +24,7 @@ class AdminAppTicketService
...TicketValidityResolver::RELATIONS, ...TicketValidityResolver::RELATIONS,
...TicketPresentationResolver::RELATIONS, ...TicketPresentationResolver::RELATIONS,
'tenant', 'tenant',
'event',
'user', 'user',
'scannerUser', 'scannerUser',
'sourceCatalogItem.category', 'sourceCatalogItem.category',
@@ -145,19 +147,20 @@ class AdminAppTicketService
]); ]);
} }
$configuration = $ticket->event_id === null ? $tenant : $ticket->event;
$unitPrice = (float) $purchaseItem->precio_unitario; $unitPrice = (float) $purchaseItem->precio_unitario;
$itemTotal = (float) $purchaseItem->total; $itemTotal = (float) $purchaseItem->total;
$itemRefundedAmount = $this->refundedAmountForPurchaseItem($purchaseItem); $itemRefundedAmount = $this->refundedAmountForPurchaseItem($purchaseItem);
$remainingItemAmount = max(0.0, round($itemTotal - $itemRefundedAmount, 2)); $remainingItemAmount = max(0.0, round($itemTotal - $itemRefundedAmount, 2));
$total = null; $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, '.', ''); $total = number_format($unitPrice, 2, '.', '');
} }
$partial = null; $partial = null;
if ($tenant->allow_refund() && $tenant->allow_partial_refund()) { if ($configuration->allow_refund() && $configuration->allow_partial_refund()) {
$partialAmount = $this->refundAmount($purchaseItem, $tenant, 'partial'); $partialAmount = $this->refundAmount($purchaseItem, $configuration, 'partial');
if ($partialAmount <= $remainingItemAmount) { if ($partialAmount <= $remainingItemAmount) {
$partial = number_format($partialAmount, 2, '.', ''); $partial = number_format($partialAmount, 2, '.', '');
} }
@@ -175,14 +178,15 @@ class AdminAppTicketService
string $refundType, string $refundType,
?User $createdBy = null, ?User $createdBy = null,
): Ticket { ): Ticket {
$this->ensureRefundIsAllowed($tenant, $refundType);
return DB::transaction(function () use ($tenant, $ticketId, $refundType, $createdBy): Ticket { return DB::transaction(function () use ($tenant, $ticketId, $refundType, $createdBy): Ticket {
$ticket = Ticket::query() $ticket = Ticket::query()
->where('tenant_code', $tenant->codigo) ->where('tenant_code', $tenant->codigo)
->lockForUpdate() ->lockForUpdate()
->findOrFail($ticketId); ->findOrFail($ticketId);
$configuration = $ticket->event_id === null ? $tenant : $ticket->event;
$this->ensureRefundIsAllowed($configuration, $refundType);
if (! $ticket->can_refund()) { if (! $ticket->can_refund()) {
if ($ticket->status !== Ticket::STATUS_ACTIVE) { if ($ticket->status !== Ticket::STATUS_ACTIVE) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([
@@ -205,7 +209,7 @@ class AdminAppTicketService
]); ]);
} }
$refundAmount = $this->refundAmount($purchaseItem, $tenant, $refundType); $refundAmount = $this->refundAmount($purchaseItem, $configuration, $refundType);
$refundedAmount = round( $refundedAmount = round(
$this->refundedAmountForPurchaseItem($purchaseItem) + $refundAmount, $this->refundedAmountForPurchaseItem($purchaseItem) + $refundAmount,
2, 2,
@@ -286,7 +290,7 @@ class AdminAppTicketService
->sum('amount'), 2); ->sum('amount'), 2);
} }
private function ensureRefundIsAllowed(Tenant $tenant, string $refundType): void private function ensureRefundIsAllowed(Tenant|Event $tenant, string $refundType): void
{ {
$isAllowed = match ($refundType) { $isAllowed = match ($refundType) {
TicketRefund::TYPE_PARTIAL => $tenant->allow_refund() && $tenant->allow_partial_refund(), 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; $ticketAmount = (float) $purchaseItem->precio_unitario;

View File

@@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('events', function (Blueprint $table): void {
$table->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',
]));
}
};