diff --git a/app/Domains/Core/Staff/Controllers/AdminAppStaffController.php b/app/Domains/Core/Staff/Controllers/AdminAppStaffController.php index 1f1d69cb..30e16b17 100644 --- a/app/Domains/Core/Staff/Controllers/AdminAppStaffController.php +++ b/app/Domains/Core/Staff/Controllers/AdminAppStaffController.php @@ -2,6 +2,7 @@ namespace App\Domains\Core\Staff\Controllers; +use App\Domains\Core\Auth\Services\EventScopeService; use App\Domains\Core\Staff\Requests\StoreStaffRequest; use App\Domains\Core\Staff\Requests\UpdateStaffRequest; use App\Domains\Core\Staff\Resources\StaffResource; @@ -21,11 +22,17 @@ class AdminAppStaffController extends Controller private readonly ScannerTicketService $scannerTicketService, ) {} + private function eventId(Request $request): ?int + { + return app(EventScopeService::class)->eventId($request->user()); + } + public function index(Request $request): AnonymousResourceCollection { return StaffResource::collection($this->staffService->list( $request->user()->tenant()->firstOrFail(), $request->string('search')->trim()->toString() ?: null, + $this->eventId($request), )); } @@ -34,6 +41,7 @@ class AdminAppStaffController extends Controller return StaffResource::make($this->staffService->create( $request->user()->tenant()->firstOrFail(), $request->validated(), + $this->eventId($request), )); } @@ -43,12 +51,13 @@ class AdminAppStaffController extends Controller $request->user()->tenant()->firstOrFail(), $staff, $request->validated(), + $this->eventId($request), )); } public function destroy(Request $request, int $staff): Response { - $this->staffService->delete($request->user()->tenant()->firstOrFail(), $staff); + $this->staffService->delete($request->user()->tenant()->firstOrFail(), $staff, $this->eventId($request)); return response()->noContent(); } @@ -60,6 +69,7 @@ class AdminAppStaffController extends Controller $scanner = $this->staffService->find( $request->user()->tenant()->firstOrFail(), $staff, + $this->eventId($request), ); return ScanAttemptResource::collection( diff --git a/app/Domains/Core/Staff/Resources/StaffResource.php b/app/Domains/Core/Staff/Resources/StaffResource.php index 5892a514..c6227117 100644 --- a/app/Domains/Core/Staff/Resources/StaffResource.php +++ b/app/Domains/Core/Staff/Resources/StaffResource.php @@ -14,6 +14,7 @@ class StaffResource extends JsonResource { return [ 'id' => $this->id, + 'event_id' => $this->event_id, 'nombre_apellido' => $this->nombre_apellido, 'dni' => $this->dni, 'email' => $this->email, diff --git a/app/Domains/Core/Staff/Services/StaffService.php b/app/Domains/Core/Staff/Services/StaffService.php index 89ca950b..02b2b8b2 100644 --- a/app/Domains/Core/Staff/Services/StaffService.php +++ b/app/Domains/Core/Staff/Services/StaffService.php @@ -2,12 +2,13 @@ namespace App\Domains\Core\Staff\Services; +use App\Domains\Commerce\Catalog\Models\Category; use App\Domains\Core\Auth\Models\ResetPasswordAttempt; use App\Domains\Core\Auth\Models\User; use App\Domains\Core\Auth\Services\ResetPasswordAttemptService; use App\Domains\Core\Authorization\Enums\RoleCode; -use App\Domains\Commerce\Catalog\Models\Category; use App\Domains\Core\Tenant\Models\Tenant; +use App\Domains\Ticketing\Event\Models\Event; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Support\Arr; @@ -22,9 +23,9 @@ class StaffService ) {} /** @return Collection */ - public function list(Tenant $tenant, ?string $search = null): Collection + public function list(Tenant $tenant, ?string $search = null, ?int $eventId = null): Collection { - return $this->staffQuery($tenant) + return $this->staffQuery($tenant, $eventId) ->with(['role', 'scanCategories' => fn ($query) => $query->orderBy('nombre')]) ->when($search, function (Builder $query, string $search): void { $query->where(function (Builder $query) use ($search): void { @@ -38,7 +39,7 @@ class StaffService } /** @return Collection */ - private function assignableCategories(Tenant $tenant): Collection + private function assignableCategories(Tenant $tenant, ?int $eventId = null): Collection { return Category::query() ->whereNull('categoria_id') @@ -47,23 +48,30 @@ class StaffService ->orWhereHas('catalogItems', fn (Builder $items) => $items ->where('tenant_code', $tenant->codigo)); }) + ->when($eventId !== null, fn (Builder $query) => $query->whereHas( + 'catalogItems', fn (Builder $items) => $items->where('tenant_code', $tenant->codigo)->where('event_id', $eventId))) ->orderBy('nombre') ->get(); } /** @param array $data */ - public function create(Tenant $tenant, array $data): User + public function create(Tenant $tenant, array $data, ?int $eventId = null): User { + $eventId ??= $tenant->active_event_id; + Event::query() + ->where('tenant_code', $tenant->codigo)->findOrFail($eventId); $categoryIds = $this->categoryIdsFor($tenant, $data); - $this->assertCategoriesBelongToTenant($tenant, $categoryIds); + $this->assertCategoriesBelongToTenant($tenant, $categoryIds, $eventId); - return DB::transaction(function () use ($tenant, $data, $categoryIds): User { + return DB::transaction(function () use ($tenant, $data, $categoryIds, $eventId): User { $staff = User::query()->create([ ...Arr::only($data, ['nombre_apellido', 'dni', 'email']), 'email' => mb_strtolower(trim((string) $data['email'])), 'password' => Str::random(64), 'rol_codigo' => RoleCode::Scanner->value, 'tenant_codigo' => $tenant->codigo, + 'event_id' => $eventId, + 'admin_scope' => $eventId === null ? 'tenant' : 'event', ]); $staff->scanCategories()->sync($categoryIds); $this->resetPasswordAttemptService->createForScannerEmail( @@ -76,11 +84,11 @@ class StaffService } /** @param array $data */ - public function update(Tenant $tenant, int $staffId, array $data): User + public function update(Tenant $tenant, int $staffId, array $data, ?int $eventId = null): User { - $staff = $this->find($tenant, $staffId); + $staff = $this->find($tenant, $staffId, $eventId); $categoryIds = $this->categoryIdsFor($tenant, $data); - $this->assertCategoriesBelongToTenant($tenant, $categoryIds); + $this->assertCategoriesBelongToTenant($tenant, $categoryIds, $eventId); return DB::transaction(function () use ($staff, $data, $categoryIds): User { $attributes = Arr::only($data, ['nombre_apellido', 'dni', 'email']); @@ -92,9 +100,9 @@ class StaffService }); } - public function delete(Tenant $tenant, int $staffId): void + public function delete(Tenant $tenant, int $staffId, ?int $eventId = null): void { - $staff = $this->find($tenant, $staffId); + $staff = $this->find($tenant, $staffId, $eventId); DB::transaction(function () use ($staff): void { $staff->tokens()->delete(); @@ -102,23 +110,24 @@ class StaffService }); } - public function find(Tenant $tenant, int $staffId): User + public function find(Tenant $tenant, int $staffId, ?int $eventId = null): User { - return $this->staffQuery($tenant)->findOrFail($staffId); + return $this->staffQuery($tenant, $eventId)->findOrFail($staffId); } - private function staffQuery(Tenant $tenant): Builder + private function staffQuery(Tenant $tenant, ?int $eventId = null): Builder { return User::query() ->where('tenant_codigo', $tenant->codigo) - ->where('rol_codigo', RoleCode::Scanner->value); + ->where('rol_codigo', RoleCode::Scanner->value) + ->when($eventId !== null, fn (Builder $query) => $query->where('event_id', $eventId)); } /** * @param array $data * @return array */ - private function categoryIdsFor(Tenant $tenant, array $data): array + private function categoryIdsFor(Tenant $tenant, array $data, ?int $eventId = null): array { if (! $tenant->requiresScannerCategoryValidation()) { return []; @@ -128,9 +137,9 @@ class StaffService } /** @param array $categoryIds */ - private function assertCategoriesBelongToTenant(Tenant $tenant, array $categoryIds): void + private function assertCategoriesBelongToTenant(Tenant $tenant, array $categoryIds, ?int $eventId = null): void { - $validIds = $this->assignableCategories($tenant) + $validIds = $this->assignableCategories($tenant, $eventId) ->whereIn('id', $categoryIds) ->pluck('id'); diff --git a/app/Domains/Core/Staff/documentacion/README.md b/app/Domains/Core/Staff/documentacion/README.md index f72d4a63..1270ed71 100644 --- a/app/Domains/Core/Staff/documentacion/README.md +++ b/app/Domains/Core/Staff/documentacion/README.md @@ -18,3 +18,13 @@ Recurso REST `/v1/adminapp/tenant/staff`, excepto detalle individual, protegido ## Dependencias y reglas Usa `Auth/User` como entidad de personal, `Authorization` para su rol, `Catalog/Category` para asignaciones y `Tenant` para aislamiento. Toda búsqueda, edición o borrado debe comprobar que el usuario pertenece al tenant autenticado. + +## Alcance por evento + +Los endpoints de Staff y su formulario aceptan `adminapp.tenant:event`. Un admin +de evento lista, edita, elimina y consulta el historial solo de scanners de su +evento. El backend asigna el evento al crear un scanner y no acepta cambios de +asignación desde el formulario. Las categorías autorizables se limitan a las +usadas por productos del evento. Los scanners aplican además su evento en +lectura de tickets, escaneo e historial. Los intentos registran `event_id` para +conservar el aislamiento aunque cambie la asignación del scanner. diff --git a/app/Domains/Core/Staff/routes/api.php b/app/Domains/Core/Staff/routes/api.php index 5ab69166..c5ca6af8 100644 --- a/app/Domains/Core/Staff/routes/api.php +++ b/app/Domains/Core/Staff/routes/api.php @@ -4,7 +4,7 @@ use App\Domains\Core\Staff\Controllers\AdminAppStaffController; 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('staff/{staff}/scan-attempts', [AdminAppStaffController::class, 'scanAttempts']); Route::apiResource('staff', AdminAppStaffController::class)->except('show'); diff --git a/app/Shared/Forms/Controllers/AdminApp/StaffFormController.php b/app/Shared/Forms/Controllers/AdminApp/StaffFormController.php index bcbc9a27..8da55229 100644 --- a/app/Shared/Forms/Controllers/AdminApp/StaffFormController.php +++ b/app/Shared/Forms/Controllers/AdminApp/StaffFormController.php @@ -2,9 +2,10 @@ namespace App\Shared\Forms\Controllers\AdminApp; +use App\Domains\Core\Auth\Services\EventScopeService; +use App\Http\Controllers\Controller; use App\Shared\Forms\Resources\StaffFormResource; use App\Shared\Forms\Services\StaffFormService; -use App\Http\Controllers\Controller; use Illuminate\Http\Request; class StaffFormController extends Controller @@ -15,7 +16,8 @@ class StaffFormController extends Controller { return StaffFormResource::make( $this->staffFormService->get( - $request->user('sanctum')->tenant()->firstOrFail() + $request->user('sanctum')->tenant()->firstOrFail(), + app(EventScopeService::class)->eventId($request->user()) ) ); } diff --git a/app/Shared/Forms/Services/StaffFormService.php b/app/Shared/Forms/Services/StaffFormService.php index 4e24c085..f809b5fc 100644 --- a/app/Shared/Forms/Services/StaffFormService.php +++ b/app/Shared/Forms/Services/StaffFormService.php @@ -10,7 +10,7 @@ use Illuminate\Database\Eloquent\Collection; class StaffFormService { /** @return array{categories: Collection} */ - public function get(Tenant $tenant): array + public function get(Tenant $tenant, ?int $eventId = null): array { return [ 'categories' => Category::query() @@ -20,6 +20,8 @@ class StaffFormService ->orWhereHas('catalogItems', fn (Builder $items) => $items ->where('tenant_code', $tenant->codigo)); }) + ->when($eventId !== null, fn (Builder $query) => $query->whereHas( + 'catalogItems', fn (Builder $items) => $items->where('tenant_code', $tenant->codigo)->where('event_id', $eventId))) ->orderBy('nombre') ->get(), ]; diff --git a/app/Shared/Forms/routes/adminapp.php b/app/Shared/Forms/routes/adminapp.php index b904f139..47fa30a0 100644 --- a/app/Shared/Forms/routes/adminapp.php +++ b/app/Shared/Forms/routes/adminapp.php @@ -21,7 +21,7 @@ Route::prefix('v1/adminapp/forms') )->middleware('adminapp.tenant')->middleware('tenant.menu:adminapp.desfile.reservas') ->name('adminapp.forms.desfile.entry-reservation'); Route::get('sale', SaleFormController::class); - Route::get('staff', StaffFormController::class)->middleware('adminapp.tenant'); + Route::get('staff', StaffFormController::class); Route::get('tickets-filter', TicketFilterFormController::class)->middleware('adminapp.tenant') ->middleware('tenant.menu:adminapp.tickets') ->name('adminapp.forms.tickets-filter');