feat(ticket): add refund calculation functionality and corresponding tests
This commit is contained in:
@@ -6,6 +6,7 @@ use App\Domains\Ticket\Requests\AdminAppTicketExportRequest;
|
||||
use App\Domains\Ticket\Requests\AdminAppTicketIndexRequest;
|
||||
use App\Domains\Ticket\Requests\AdminAppTicketRefundRequest;
|
||||
use App\Domains\Ticket\Resources\AdminApp\AdminAppTicketCollection;
|
||||
use App\Domains\Ticket\Resources\AdminApp\AdminAppTicketRefundCalculationResource;
|
||||
use App\Domains\Ticket\Resources\AdminApp\AdminAppTicketResource;
|
||||
use App\Domains\Ticket\Services\AdminAppTicketExcelService;
|
||||
use App\Domains\Ticket\Services\AdminAppTicketPdfService;
|
||||
@@ -39,6 +40,15 @@ class TicketController extends Controller
|
||||
return new AdminAppTicketResource($this->ticketService->cancel($tenant, $ticket));
|
||||
}
|
||||
|
||||
public function calculateRefund(Request $request, int $ticket): AdminAppTicketRefundCalculationResource
|
||||
{
|
||||
$tenant = $request->user()->tenant()->firstOrFail();
|
||||
|
||||
return new AdminAppTicketRefundCalculationResource(
|
||||
$this->ticketService->calculateRefund($tenant, $ticket)
|
||||
);
|
||||
}
|
||||
|
||||
public function refund(AdminAppTicketRefundRequest $request, int $ticket): AdminAppTicketResource
|
||||
{
|
||||
$tenant = $request->user()->tenant()->firstOrFail();
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Domains\Ticket\Resources\AdminApp;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
/**
|
||||
* @property-read array{
|
||||
* total: string|null,
|
||||
* partial: string|null,
|
||||
* } $resource
|
||||
*/
|
||||
class AdminAppTicketRefundCalculationResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* @return array{total: string|null, partial: string|null}
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'total' => $this->resource['total'],
|
||||
'partial' => $this->resource['partial'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -13,6 +13,10 @@ Route::prefix('v1/adminapp/tenant')
|
||||
->whereNumber('ticket')
|
||||
->middleware('tenant.menu:adminapp.tickets')
|
||||
->name('adminapp.tickets.cancel');
|
||||
Route::get('tickets/{ticket}/refund', [TicketController::class, 'calculateRefund'])
|
||||
->whereNumber('ticket')
|
||||
->middleware('tenant.menu:adminapp.tickets')
|
||||
->name('adminapp.tickets.calculate-refund');
|
||||
Route::post('tickets/{ticket}/refund', [TicketController::class, 'refund'])
|
||||
->whereNumber('ticket')
|
||||
->middleware('tenant.menu:adminapp.tickets')
|
||||
|
||||
Reference in New Issue
Block a user