seed(AuthorizationSeeder::class); WebsiteType::query()->create(['codigo' => 'onticket', 'nombre' => 'OnTicket']); $headerLogo = $this->createAttachment('header.png'); $footerLogo = $this->createAttachment('footer.png'); $this->tenant = Tenant::query()->create([ 'codigo' => 'acme', 'nombre' => 'Acme', 'dominio' => 'acme.test', 'website_type_code' => 'onticket', 'primary_color' => '#111111', 'secondary_color' => '#222222', 'danger_color' => '#cc0000', 'success_color' => '#008800', 'header_bg_color' => '#ffffff', 'footer_bg_color' => '#ffffff', 'header_logo_id' => $headerLogo->id, 'footer_logo_id' => $footerLogo->id, ]); $this->admin = User::factory()->create([ 'rol_codigo' => RoleCode::AdminApp->value, 'tenant_codigo' => $this->tenant->codigo, ]); } public function test_adminapp_can_create_update_list_and_delete_staff_with_categories(): void { Sanctum::actingAs($this->admin); $firstCategory = $this->createCategory('Bebidas'); $secondCategory = $this->createCategory('Comidas'); $response = $this->postJson('/api/v1/adminapp/tenant/staff', [ 'nombre_apellido' => 'Ada Lovelace', 'dni' => '12345678', 'email' => 'ADA@example.test', 'category_ids' => [$firstCategory->id], ])->assertSuccessful() ->assertJsonPath('data.email', 'ada@example.test') ->assertJsonPath('data.role.codigo', RoleCode::Scanner->value) ->assertJsonPath('data.categories.0.id', $firstCategory->id); $staffId = $response->json('data.id'); $this->assertDatabaseHas('category_scanners', [ 'user_id' => $staffId, 'categoria_id' => $firstCategory->id, ]); $this->assertDatabaseHas('reset_password_attempts', [ 'user_id' => $staffId, 'reason' => ResetPasswordAttempt::REASON_STAFF_CREATED, 'status' => ResetPasswordAttempt::STATUS_PENDING, ]); Event::assertDispatched( PasswordResetRequested::class, fn (PasswordResetRequested $event): bool => $event->tenantCode === $this->tenant->codigo && $event->channel === PasswordResetRequested::CHANNEL_SCANNER, ); $this->getJson('/api/v1/adminapp/tenant/staff?search=ada') ->assertOk() ->assertJsonCount(1, 'data'); $this->putJson("/api/v1/adminapp/tenant/staff/{$staffId}", [ 'nombre_apellido' => 'Ada Byron', 'dni' => '12345678', 'email' => 'ada@example.test', 'category_ids' => [$secondCategory->id], ])->assertOk() ->assertJsonPath('data.nombre_apellido', 'Ada Byron') ->assertJsonPath('data.categories.0.id', $secondCategory->id); $this->assertDatabaseMissing('category_scanners', [ 'user_id' => $staffId, 'categoria_id' => $firstCategory->id, ]); $ticket = Ticket::query()->create([ 'tenant_code' => $this->tenant->codigo, 'ticket' => (string) Str::uuid(), 'user_id' => $this->admin->id, 'used_at' => now(), 'scanner_user_id' => $staffId, ]); $accessTokenId = User::query() ->findOrFail($staffId) ->createToken('scanner', ['scanner']) ->accessToken ->getKey(); $this->deleteJson("/api/v1/adminapp/tenant/staff/{$staffId}")->assertNoContent(); $this->assertSoftDeleted('users', ['id' => $staffId]); $this->assertDatabaseHas('users', [ 'id' => $staffId, 'email' => 'ada@example.test', 'active_email' => null, ]); $this->assertDatabaseMissing('personal_access_tokens', ['id' => $accessTokenId]); $this->assertDatabaseHas('category_scanners', [ 'user_id' => $staffId, 'categoria_id' => $secondCategory->id, ]); $this->assertSame($staffId, $ticket->fresh()->scanner_user_id); $this->assertSame('Ada Byron', $ticket->fresh()->scannerUser?->nombre_apellido); $this->getJson('/api/v1/adminapp/tenant/staff') ->assertOk() ->assertJsonCount(0, 'data'); $replacementResponse = $this->postJson('/api/v1/adminapp/tenant/staff', [ 'nombre_apellido' => 'Nueva Ada', 'dni' => '11223344', 'email' => 'ADA@example.test', 'category_ids' => [$firstCategory->id], ])->assertSuccessful() ->assertJsonPath('data.email', 'ada@example.test'); $replacementStaffId = $replacementResponse->json('data.id'); $this->assertNotSame($staffId, $replacementStaffId); $this->assertSame('Ada Byron', $ticket->fresh()->scannerUser?->nombre_apellido); $this->assertDatabaseHas('users', [ 'id' => $replacementStaffId, 'email' => 'ada@example.test', 'active_email' => 'ada@example.test', 'deleted_at' => null, ]); } public function test_admin_cannot_assign_another_tenants_category(): void { Sanctum::actingAs($this->admin); $otherTenant = Tenant::query()->create([ 'codigo' => 'other', 'nombre' => 'Other', 'dominio' => 'other.test', 'website_type_code' => 'onticket', 'primary_color' => '#111111', 'secondary_color' => '#222222', 'danger_color' => '#cc0000', 'success_color' => '#008800', 'header_bg_color' => '#ffffff', 'footer_bg_color' => '#ffffff', 'header_logo_id' => $this->tenant->header_logo_id, 'footer_logo_id' => $this->tenant->footer_logo_id, ]); $foreignCategory = Category::query()->create([ 'tenant_code' => $otherTenant->codigo, 'nombre' => 'Privada', ]); $payload = [ 'nombre_apellido' => 'Grace Hopper', 'dni' => '87654321', 'email' => 'grace@example.test', 'category_ids' => [$foreignCategory->id], ]; $this->postJson('/api/v1/adminapp/tenant/staff', $payload) ->assertUnprocessable() ->assertJsonValidationErrors('category_ids'); $parent = $this->createCategory('Local'); $child = Category::query()->create([ 'tenant_code' => $this->tenant->codigo, 'categoria_id' => $parent->id, 'nombre' => 'SubcategorĂ­a', ]); $payload['category_ids'] = [$child->id]; $this->postJson('/api/v1/adminapp/tenant/staff', $payload) ->assertUnprocessable() ->assertJsonValidationErrors('category_ids'); } public function test_categories_are_required_by_default_for_scanner_staff(): void { Sanctum::actingAs($this->admin); $this->postJson('/api/v1/adminapp/tenant/staff', [ 'nombre_apellido' => 'Grace Hopper', 'dni' => '87654321', 'email' => 'grace@example.test', 'category_ids' => [], ])->assertUnprocessable() ->assertJsonValidationErrors('category_ids'); } public function test_admin_can_manage_staff_without_categories_when_tenant_disables_validation(): void { $this->tenant->update(['scanner_category_validation_enabled' => false]); Sanctum::actingAs($this->admin); $response = $this->postJson('/api/v1/adminapp/tenant/staff', [ 'nombre_apellido' => 'Grace Hopper', 'dni' => '87654321', 'email' => 'grace@example.test', ])->assertCreated() ->assertJsonCount(0, 'data.categories'); $staffId = $response->json('data.id'); $category = $this->createCategory('Entradas'); $this->putJson("/api/v1/adminapp/tenant/staff/{$staffId}", [ 'nombre_apellido' => 'Grace Murray Hopper', 'dni' => '87654321', 'email' => 'grace@example.test', 'category_ids' => [$category->id], ])->assertOk() ->assertJsonCount(0, 'data.categories'); $this->assertDatabaseMissing('category_scanners', [ 'user_id' => $staffId, ]); } public function test_customer_cannot_manage_staff(): void { Sanctum::actingAs(User::factory()->create([ 'rol_codigo' => RoleCode::User->value, ])); $this->getJson('/api/v1/adminapp/tenant/staff')->assertForbidden(); } private function createAttachment(string $filename): Attachment { return Attachment::query()->create([ 'path' => "test/{$filename}", 'filename' => $filename, 'type' => AttachmentType::Image, 'mime_type' => 'image/png', ]); } private function createCategory(string $name): Category { return Category::query()->create([ 'tenant_code' => $this->tenant->codigo, 'nombre' => $name, ]); } }