feat(ticket): add refund calculation functionality and corresponding tests

This commit is contained in:
2026-09-11 10:43:03 -03:00
parent 6384c0046d
commit 4abb6c67fd
5 changed files with 231 additions and 3 deletions

View File

@@ -45,20 +45,29 @@ class AdminAppTicketService
->get();
$matchingTickets = $this->sortTickets($matchingTickets, $tenant, $filters);
$tickets = $this->paginate($matchingTickets, $filters);
$scannedTickets = $matchingTickets->whereNotNull('used_at')->count();
$scannedTickets = $matchingTickets
->filter(fn (Ticket $ticket): bool => $ticket->status === Ticket::STATUS_USED)
->count();
$activeTickets = $matchingTickets
->filter(fn (Ticket $ticket): bool => $ticket->is_active())
->count();
$totalTickets = $activeTickets + $scannedTickets;
} else {
$tickets = (clone $query)
->with(self::RELATIONS)
->when(! $databaseSorted, fn (Builder $query): Builder => $query->orderByDesc('id'))
->paginateFromRequest()
->withQueryString();
$scannedTickets = $countQuery->whereNotNull('used_at')->count();
$counts = $this->calculateTicketCounts($countQuery);
$scannedTickets = $counts['scanned'];
$totalTickets = $counts['total'];
}
return new AdminAppTicketResult(
tickets: $tickets,
scannedTickets: $scannedTickets,
totalTickets: $tickets->total(),
totalTickets: $totalTickets,
);
}
@@ -99,6 +108,57 @@ class AdminAppTicketService
});
}
/**
* @return array{
* total: string|null,
* partial: string|null,
* }
*/
public function calculateRefund(Tenant $tenant, int $ticketId): array
{
$ticket = Ticket::query()
->where('tenant_code', $tenant->codigo)
->findOrFail($ticketId);
if (! $ticket->can_refund()) {
throw ValidationException::withMessages([
'refund' => 'El reembolso no está disponible para este ticket.',
]);
}
$purchaseItem = PurchaseItem::query()
->find($ticket->source_purchase_item_id);
if ($purchaseItem === null) {
throw ValidationException::withMessages([
'ticket' => 'El ticket no tiene un ítem de compra asociado para reembolsar.',
]);
}
$unitPrice = (float) $purchaseItem->precio_unitario;
$itemTotal = (float) $purchaseItem->total;
$itemRefundedAmount = (float) ($purchaseItem->refunded_amount ?? 0);
$remainingItemAmount = max(0.0, round($itemTotal - $itemRefundedAmount, 2));
$total = null;
if ($tenant->allow_ticket_total_refund && $unitPrice <= $remainingItemAmount) {
$total = number_format($unitPrice, 2, '.', '');
}
$partial = null;
if ($tenant->allow_partial_refund()) {
$partialAmount = $this->refundAmount($purchaseItem, $tenant, 'partial');
if ($partialAmount <= $remainingItemAmount) {
$partial = number_format($partialAmount, 2, '.', '');
}
}
return [
'total' => $total,
'partial' => $partial,
];
}
public function refund(Tenant $tenant, int $ticketId, string $refundType): Ticket
{
$this->ensureRefundIsAllowed($tenant, $refundType);
@@ -398,6 +458,35 @@ class AdminAppTicketService
$query->whereIn('tickets.id', $matchingIds);
}
/**
* @param Builder<Ticket> $countQuery
* @return array{scanned: int, total: int}
*/
private function calculateTicketCounts(Builder $countQuery): array
{
$scannedTickets = (clone $countQuery)
->whereNotNull('used_at')
->whereNull('disabled_at')
->whereNull('cancelled_at')
->whereNull('refunded_at')
->count();
$activeTickets = (clone $countQuery)
->whereNull('used_at')
->whereNull('disabled_at')
->whereNull('cancelled_at')
->whereNull('refunded_at')
->with(TicketValidityResolver::RELATIONS)
->get()
->filter(fn (Ticket $ticket): bool => $ticket->is_active())
->count();
return [
'scanned' => $scannedTickets,
'total' => $activeTickets + $scannedTickets,
];
}
private function normalizedCategory(string $category): string
{
return mb_strtolower(trim($category));