feat(sales): scope sales actions totals history and exports by event

This commit is contained in:
2026-09-30 15:31:55 -03:00
parent 55c28f33ef
commit 655364bfec
6 changed files with 58 additions and 36 deletions

View File

@@ -8,12 +8,13 @@ use Illuminate\Database\Eloquent\Builder;
class PurchaseRefundSummaryService
{
public function totalForTenant(Tenant $tenant): string
public function totalForTenant(Tenant $tenant, ?int $eventId = null): string
{
$total = TicketRefund::query()
->whereHas(
'purchaseItem.purchase',
fn (Builder $query): Builder => $query->where('tenant_codigo', $tenant->codigo)
->when($eventId !== null, fn (Builder $purchase) => $purchase->where('event_id', $eventId))
)
->sum('amount');

View File

@@ -13,6 +13,7 @@ use App\Domains\Commerce\Sale\Resources\AdminApp\SaleTicketResource;
use App\Domains\Commerce\Sale\Services\AdminAppSaleExcelService;
use App\Domains\Commerce\Sale\Services\AdminAppSalePdfService;
use App\Domains\Commerce\Sale\Services\AdminAppSaleService;
use App\Domains\Core\Auth\Services\EventScopeService;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;
@@ -27,15 +28,20 @@ class SaleController extends Controller
protected AdminAppSaleExcelService $saleExcelService,
) {}
private function eventId(Request $request): ?int
{
return app(EventScopeService::class)->eventId($request->user());
}
public function index(AdminAppSaleIndexRequest $request): AnonymousResourceCollection
{
$tenant = $request->user()->tenant()->firstOrFail();
return SaleResource::collection(
$this->saleService->sales($tenant, $request->validated())
$this->saleService->sales($tenant, $request->validated(), $this->eventId($request))
)->additional([
'confirmed_sales_total' => $this->saleService->confirmedSalesTotal($tenant),
'refunded_total' => $this->saleService->refundedTotal($tenant),
'confirmed_sales_total' => $this->saleService->confirmedSalesTotal($tenant, $this->eventId($request)),
'refunded_total' => $this->saleService->refundedTotal($tenant, $this->eventId($request)),
]);
}
@@ -43,7 +49,7 @@ class SaleController extends Controller
{
$tenant = $request->user()->tenant()->firstOrFail();
return new SaleDetailResource($this->saleService->detail($tenant, $sale));
return new SaleDetailResource($this->saleService->detail($tenant, $sale, $this->eventId($request)));
}
public function tickets(Request $request, int $sale): AnonymousResourceCollection
@@ -51,7 +57,7 @@ class SaleController extends Controller
$tenant = $request->user()->tenant()->firstOrFail();
return SaleTicketResource::collection(
$this->saleService->tickets($tenant, $sale)
$this->saleService->tickets($tenant, $sale, $this->eventId($request))
);
}
@@ -59,14 +65,14 @@ class SaleController extends Controller
{
$tenant = $request->user()->tenant()->firstOrFail();
return new SaleResource($this->saleService->confirm($tenant, $sale));
return new SaleResource($this->saleService->confirm($tenant, $sale, $this->eventId($request)));
}
public function cancel(Request $request, int $sale): SaleResource
{
$tenant = $request->user()->tenant()->firstOrFail();
return new SaleResource($this->saleService->cancel($tenant, $sale));
return new SaleResource($this->saleService->cancel($tenant, $sale, $this->eventId($request)));
}
public function modifications(
@@ -76,6 +82,7 @@ class SaleController extends Controller
$this->saleService->modifications(
$request->user()->tenant()->firstOrFail(),
$request->validated(),
$this->eventId($request),
)
);
}
@@ -86,7 +93,7 @@ class SaleController extends Controller
return $this->salePdfService->downloadSales(
$tenant,
$this->saleService->salesForExport($tenant, $request->validated()),
$this->saleService->salesForExport($tenant, $request->validated(), $this->eventId($request)),
$request->validated('timezone'),
);
}
@@ -97,7 +104,7 @@ class SaleController extends Controller
return $this->salePdfService->downloadModifications(
$tenant,
$this->saleService->modificationsForExport($tenant, $request->validated()),
$this->saleService->modificationsForExport($tenant, $request->validated(), $this->eventId($request)),
$request->validated('timezone'),
);
}
@@ -108,7 +115,7 @@ class SaleController extends Controller
return $this->saleExcelService->downloadSales(
$tenant,
$this->saleService->salesForExport($tenant, $request->validated()),
$this->saleService->salesForExport($tenant, $request->validated(), $this->eventId($request)),
$request->validated('timezone'),
);
}
@@ -120,7 +127,7 @@ class SaleController extends Controller
return $this->saleExcelService->downloadModifications(
$tenant,
$this->saleService->modificationsForExport($tenant, $request->validated()),
$this->saleService->modificationsForExport($tenant, $request->validated(), $this->eventId($request)),
$request->validated('timezone'),
);
}

View File

@@ -2,7 +2,6 @@
namespace App\Domains\Commerce\Sale\Services;
use App\Shared\Logging\Models\ValueChange;
use App\Domains\Commerce\Purchase\Models\Purchase;
use App\Domains\Commerce\Purchase\Services\CheckoutService;
use App\Domains\Commerce\Purchase\Services\PurchaseRefundSummaryService;
@@ -10,6 +9,7 @@ use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Ticket\Models\Ticket;
use App\Domains\Ticketing\Ticket\Services\TicketPresentationResolver;
use App\Domains\Ticketing\Ticket\Services\TicketValidityResolver;
use App\Shared\Logging\Models\ValueChange;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
@@ -21,19 +21,20 @@ class AdminAppSaleService
protected PurchaseRefundSummaryService $refundSummaryService,
) {}
public function confirmedSalesTotal(Tenant $tenant): string
public function confirmedSalesTotal(Tenant $tenant, ?int $eventId = null): string
{
$total = Purchase::query()
->where('tenant_codigo', $tenant->codigo)
->when($eventId !== null, fn (Builder $query) => $query->where('event_id', $eventId))
->where('status', Purchase::STATUS_PAID)
->sum('total');
return number_format((float) $total, 2, '.', '');
}
public function refundedTotal(Tenant $tenant): string
public function refundedTotal(Tenant $tenant, ?int $eventId = null): string
{
return $this->refundSummaryService->totalForTenant($tenant);
return $this->refundSummaryService->totalForTenant($tenant, $eventId);
}
/**
@@ -47,43 +48,44 @@ class AdminAppSaleService
* } $filters
* @return LengthAwarePaginator<Purchase>
*/
public function sales(Tenant $tenant, array $filters = []): LengthAwarePaginator
public function sales(Tenant $tenant, array $filters = [], ?int $eventId = null): LengthAwarePaginator
{
return $this->salesQuery($tenant, $filters)
return $this->salesQuery($tenant, $filters, $eventId)
->paginateFromRequest()
->withQueryString();
}
public function detail(Tenant $tenant, int $saleId): Purchase
public function detail(Tenant $tenant, int $saleId, ?int $eventId = null): Purchase
{
return Purchase::query()
->where('tenant_codigo', $tenant->codigo)
->when($eventId !== null, fn (Builder $query) => $query->where('event_id', $eventId))
->with('items')
->findOrFail($saleId);
}
/** @return Collection<int, Ticket> */
public function tickets(Tenant $tenant, int $saleId): Collection
public function tickets(Tenant $tenant, int $saleId, ?int $eventId = null): Collection
{
return $this->findForTenant($tenant, $saleId)
return $this->findForTenant($tenant, $saleId, $eventId)
->tickets()
->with([...TicketValidityResolver::RELATIONS, ...TicketPresentationResolver::RELATIONS, 'refund'])
->orderBy('id')
->get();
}
public function confirm(Tenant $tenant, int $saleId): Purchase
public function confirm(Tenant $tenant, int $saleId, ?int $eventId = null): Purchase
{
$sale = $this->findForTenant($tenant, $saleId);
$sale = $this->findForTenant($tenant, $saleId, $eventId);
return $this->saleForResponse(
$this->checkoutService->confirmPaidPurchase($sale)
);
}
public function cancel(Tenant $tenant, int $saleId): Purchase
public function cancel(Tenant $tenant, int $saleId, ?int $eventId = null): Purchase
{
$sale = $this->findForTenant($tenant, $saleId);
$sale = $this->findForTenant($tenant, $saleId, $eventId);
return $this->saleForResponse(
$this->checkoutService->cancelPurchaseFromAdmin($sale)
@@ -94,18 +96,18 @@ class AdminAppSaleService
* @param array<string, mixed> $filters
* @return Collection<int, Purchase>
*/
public function salesForExport(Tenant $tenant, array $filters = []): Collection
public function salesForExport(Tenant $tenant, array $filters = [], ?int $eventId = null): Collection
{
return $this->salesQuery($tenant, $filters)->get();
return $this->salesQuery($tenant, $filters, $eventId)->get();
}
/**
* @param array<string, mixed> $filters
* @return LengthAwarePaginator<ValueChange>
*/
public function modifications(Tenant $tenant, array $filters = []): LengthAwarePaginator
public function modifications(Tenant $tenant, array $filters = [], ?int $eventId = null): LengthAwarePaginator
{
return $this->modificationsQuery($tenant, $filters)
return $this->modificationsQuery($tenant, $filters, $eventId)
->paginateFromRequest()
->withQueryString();
}
@@ -114,13 +116,13 @@ class AdminAppSaleService
* @param array<string, mixed> $filters
* @return Collection<int, ValueChange>
*/
public function modificationsForExport(Tenant $tenant, array $filters = []): Collection
public function modificationsForExport(Tenant $tenant, array $filters = [], ?int $eventId = null): Collection
{
return $this->modificationsQuery($tenant, $filters)->get();
return $this->modificationsQuery($tenant, $filters, $eventId)->get();
}
/** @param array<string, mixed> $filters */
protected function salesQuery(Tenant $tenant, array $filters): Builder
protected function salesQuery(Tenant $tenant, array $filters, ?int $eventId = null): Builder
{
$sortColumns = [
'id' => 'id',
@@ -139,6 +141,7 @@ class AdminAppSaleService
return Purchase::query()
->where('tenant_codigo', $tenant->codigo)
->when($eventId !== null, fn (Builder $query) => $query->where('event_id', $eventId))
->when($filters['q'] ?? null, function (Builder $query, string $search): void {
$term = trim($search);
@@ -176,11 +179,14 @@ class AdminAppSaleService
* @param array<string, mixed> $filters
* @return Builder<ValueChange>
*/
protected function modificationsQuery(Tenant $tenant, array $filters): Builder
protected function modificationsQuery(Tenant $tenant, array $filters, ?int $eventId = null): Builder
{
return ValueChange::query()
->where('tenant_code', $tenant->codigo)
->where('trackable_type', (new Purchase)->getMorphClass())
->when($eventId !== null, fn (Builder $query) => $query->whereHasMorph(
'trackable', [Purchase::class],
fn (Builder $sales) => $sales->where('event_id', $eventId)))
->when($filters['q'] ?? null, function (Builder $query, string $search): void {
$term = trim($search);
@@ -221,10 +227,11 @@ class AdminAppSaleService
->orderByDesc('id');
}
protected function findForTenant(Tenant $tenant, int $saleId): Purchase
protected function findForTenant(Tenant $tenant, int $saleId, ?int $eventId = null): Purchase
{
return Purchase::query()
->where('tenant_codigo', $tenant->codigo)
->when($eventId !== null, fn (Builder $query) => $query->where('event_id', $eventId))
->findOrFail($saleId);
}

View File

@@ -30,3 +30,10 @@ Consume compras de `Purchase`, datos del tenant y entradas de `Logging`. No es d
La consulta paginada y la colección de exportación deben aplicar los mismos filtros para evitar diferencias entre pantalla, PDF y Excel.
El historial comparte con ventas los filtros de búsqueda, ID, fecha de venta y estado. En el historial, el estado se evalúa sobre `ValueChange.new_value`: representa el resultado de esa modificación y no el estado actual de la venta.
## Alcance por evento
Las rutas usan `adminapp.tenant:event`. Para admins de evento, listados, totales,
detalles, tickets de compras, confirmación, cancelación, historial y exportaciones
se filtran por `user.event_id` además del tenant. Las compras pertenecen a un
único evento. Un admin de tenant conserva acceso a sus compras de todos los eventos.

View File

@@ -4,7 +4,7 @@ use App\Domains\Commerce\Sale\Controllers\AdminApp\SaleController;
use Illuminate\Support\Facades\Route;
Route::prefix('v1/adminapp/tenant')
->middleware(['auth:sanctum', 'adminapp.tenant'])
->middleware(['auth:sanctum', 'adminapp.tenant:event'])
->group(function (): void {
Route::get('sales', [SaleController::class, 'index']);
Route::get('sales/pdf', [SaleController::class, 'downloadPdf']);

View File

@@ -20,7 +20,7 @@ Route::prefix('v1/adminapp/forms')
DesfileEntryReservationFormController::class
)->middleware('adminapp.tenant')->middleware('tenant.menu:adminapp.desfile.reservas')
->name('adminapp.forms.desfile.entry-reservation');
Route::get('sale', SaleFormController::class)->middleware('adminapp.tenant');
Route::get('sale', SaleFormController::class);
Route::get('staff', StaffFormController::class)->middleware('adminapp.tenant');
Route::get('tickets-filter', TicketFilterFormController::class)->middleware('adminapp.tenant')
->middleware('tenant.menu:adminapp.tickets')