feat(ticket): implement refund functionality and expose allow_refund flag

This commit is contained in:
2026-09-10 17:05:57 -03:00
parent 210c854fee
commit 5d00dc439e
8 changed files with 273 additions and 0 deletions

View File

@@ -4,6 +4,7 @@ namespace App\Domains\Ticket\Controllers\AdminApp;
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\AdminAppTicketResource;
use App\Domains\Ticket\Services\AdminAppTicketExcelService;
@@ -38,6 +39,15 @@ class TicketController extends Controller
return new AdminAppTicketResource($this->ticketService->cancel($tenant, $ticket));
}
public function refund(AdminAppTicketRefundRequest $request, int $ticket): AdminAppTicketResource
{
$tenant = $request->user()->tenant()->firstOrFail();
return new AdminAppTicketResource(
$this->ticketService->refund($tenant, $ticket, $request->validated('refund_type'))
);
}
public function downloadPdf(AdminAppTicketExportRequest $request): Response
{
$tenant = $request->user()->tenant()->firstOrFail();

View File

@@ -135,6 +135,21 @@ class Ticket extends Model
return $this->belongsTo(Tenant::class, 'tenant_code', 'codigo');
}
public function allow_refund(): bool
{
return $this->tenant?->allow_refund() ?? false;
}
public function allowRefund(): bool
{
return $this->allow_refund();
}
public function getAllowRefundAttribute(): bool
{
return $this->allow_refund();
}
/** @return BelongsTo<User, $this> */
public function user(): BelongsTo
{

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Domains\Ticket\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class AdminAppTicketRefundRequest extends FormRequest
{
public function authorize(): bool
{
return true;
}
/** @return array<string, list<string|object>> */
public function rules(): array
{
return [
'refund_type' => ['required', 'string', Rule::in(['partial', 'total'])],
];
}
}

View File

@@ -19,6 +19,7 @@ class AdminAppTicketResource extends TicketResource
return [
...parent::toArray($request),
...$details,
'allow_refund' => $this->resource->allow_refund(),
'values' => $rowService->values($this->resource, $details),
];
}

View File

@@ -36,6 +36,7 @@ class AdminAppTicketRowService
'status' => $ticket->status,
'scanned_by' => $ticket->scannerUser?->nombre_apellido,
'variant_properties' => $this->variantProperties($ticket),
'allow_refund' => $ticket->allow_refund(),
];
}

View File

@@ -9,12 +9,15 @@ use App\Domains\Ticket\Models\Ticket;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class AdminAppTicketService
{
private const RELATIONS = [
...TicketValidityResolver::RELATIONS,
...TicketPresentationResolver::RELATIONS,
'tenant',
'user',
'scannerUser',
'sourceCatalogItem.category',
@@ -87,6 +90,70 @@ class AdminAppTicketService
return $ticket->refresh()->load(self::RELATIONS);
}
public function refund(Tenant $tenant, int $ticketId, string $refundType): Ticket
{
$this->ensureRefundIsAllowed($tenant, $refundType);
return DB::transaction(function () use ($tenant, $ticketId, $refundType): Ticket {
$ticket = Ticket::query()
->where('tenant_code', $tenant->codigo)
->lockForUpdate()
->findOrFail($ticketId);
$purchaseItem = PurchaseItem::query()
->lockForUpdate()
->find($ticket->source_purchase_item_id);
if ($purchaseItem === null) {
throw ValidationException::withMessages([
'ticket' => 'El ticket no tiene un ítem de compra asociado para reembolsar.',
]);
}
$refundAmount = $this->refundAmount($purchaseItem, $tenant, $refundType);
$refundedAmount = round((float) $purchaseItem->refunded_amount + $refundAmount, 2);
if ($refundedAmount > (float) $purchaseItem->total) {
throw ValidationException::withMessages([
'refund_type' => 'El importe reembolsado no puede superar el total del ítem de compra.',
]);
}
$ticket->markAsRefunded();
$ticket->save();
$purchaseItem->update([
'refunded_amount' => number_format($refundedAmount, 2, '.', ''),
]);
return $ticket->refresh()->load(self::RELATIONS);
});
}
private function ensureRefundIsAllowed(Tenant $tenant, string $refundType): void
{
$isAllowed = match ($refundType) {
'partial' => $tenant->allow_partial_refund(),
'total' => (bool) $tenant->allow_ticket_total_refund,
};
if (! $isAllowed) {
throw ValidationException::withMessages([
'refund_type' => 'El tipo de reembolso solicitado no está habilitado para este tenant.',
]);
}
}
private function refundAmount(PurchaseItem $purchaseItem, Tenant $tenant, string $refundType): float
{
$ticketAmount = (float) $purchaseItem->precio_unitario;
return match ($refundType) {
'partial' => round($ticketAmount * (float) $tenant->ticket_partial_refund_percentage / 100, 2),
'total' => $ticketAmount,
};
}
/**
* @param array{q?: string|null, category?: string|null, product?: string|null, type?: string|null, date?: string|null, size?: string|null, status?: string|null, page?: int, per_page?: int} $filters
* @return Builder<Ticket>

View File

@@ -13,6 +13,10 @@ Route::prefix('v1/adminapp/tenant')
->whereNumber('ticket')
->middleware('tenant.menu:adminapp.tickets')
->name('adminapp.tickets.cancel');
Route::post('tickets/{ticket}/refund', [TicketController::class, 'refund'])
->whereNumber('ticket')
->middleware('tenant.menu:adminapp.tickets')
->name('adminapp.tickets.refund');
Route::get('tickets/pdf', [TicketController::class, 'downloadPdf'])
->middleware('tenant.menu:adminapp.tickets')
->name('adminapp.tickets.pdf');