feat(staff): scope scanner management and category assignments by event
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<int, User> */
|
||||
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<int, Category> */
|
||||
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<string, mixed> $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<string, mixed> $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<string, mixed> $data
|
||||
* @return array<int, int>
|
||||
*/
|
||||
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<int, int> $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');
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ use Illuminate\Database\Eloquent\Collection;
|
||||
class StaffFormService
|
||||
{
|
||||
/** @return array{categories: Collection<int, Category>} */
|
||||
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(),
|
||||
];
|
||||
|
||||
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user