diff --git a/app/Domains/Commerce/Purchase/Services/PurchaseRefundSummaryService.php b/app/Domains/Commerce/Purchase/Services/PurchaseRefundSummaryService.php index 9c113035..ee0649ed 100644 --- a/app/Domains/Commerce/Purchase/Services/PurchaseRefundSummaryService.php +++ b/app/Domains/Commerce/Purchase/Services/PurchaseRefundSummaryService.php @@ -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'); diff --git a/app/Domains/Commerce/Sale/Controllers/AdminApp/SaleController.php b/app/Domains/Commerce/Sale/Controllers/AdminApp/SaleController.php index bb94f0dc..bbca4cff 100644 --- a/app/Domains/Commerce/Sale/Controllers/AdminApp/SaleController.php +++ b/app/Domains/Commerce/Sale/Controllers/AdminApp/SaleController.php @@ -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'), ); } diff --git a/app/Domains/Commerce/Sale/Services/AdminAppSaleService.php b/app/Domains/Commerce/Sale/Services/AdminAppSaleService.php index 79a663e5..4d0c382c 100644 --- a/app/Domains/Commerce/Sale/Services/AdminAppSaleService.php +++ b/app/Domains/Commerce/Sale/Services/AdminAppSaleService.php @@ -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 */ - 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 */ - 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 $filters * @return Collection */ - 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 $filters * @return LengthAwarePaginator */ - 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 $filters * @return Collection */ - 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 $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 $filters * @return Builder */ - 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); } diff --git a/app/Domains/Commerce/Sale/documentacion/README.md b/app/Domains/Commerce/Sale/documentacion/README.md index 2c1b5fe8..dd2ee993 100644 --- a/app/Domains/Commerce/Sale/documentacion/README.md +++ b/app/Domains/Commerce/Sale/documentacion/README.md @@ -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. diff --git a/app/Domains/Commerce/Sale/routes/adminapp.php b/app/Domains/Commerce/Sale/routes/adminapp.php index 028eed35..559cba17 100644 --- a/app/Domains/Commerce/Sale/routes/adminapp.php +++ b/app/Domains/Commerce/Sale/routes/adminapp.php @@ -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']); diff --git a/app/Shared/Forms/routes/adminapp.php b/app/Shared/Forms/routes/adminapp.php index 6e46f3a6..b904f139 100644 --- a/app/Shared/Forms/routes/adminapp.php +++ b/app/Shared/Forms/routes/adminapp.php @@ -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')