diff --git a/app/Domains/Auth/Models/User.php b/app/Domains/Auth/Models/User.php index 3dbf641..1826676 100644 --- a/app/Domains/Auth/Models/User.php +++ b/app/Domains/Auth/Models/User.php @@ -19,7 +19,7 @@ use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; #[Fillable(['nombre_apellido', 'email', 'password', 'dni', 'telefono', 'google_id', 'rol_codigo', 'tenant_codigo'])] -#[Hidden(['password', 'remember_token'])] +#[Hidden(['password', 'remember_token', 'active_email'])] class User extends Authenticatable { /** @use HasFactory */ diff --git a/app/Domains/Auth/Requests/RegisterUserRequest.php b/app/Domains/Auth/Requests/RegisterUserRequest.php index 8590fa1..5acfb09 100644 --- a/app/Domains/Auth/Requests/RegisterUserRequest.php +++ b/app/Domains/Auth/Requests/RegisterUserRequest.php @@ -21,7 +21,13 @@ class RegisterUserRequest extends FormRequest return [ 'tenant_codigo' => ['nullable', 'string', Rule::exists('tenants', 'codigo')], 'nombre_apellido' => ['required', 'string', 'max:255'], - 'email' => ['required', 'string', 'email', 'max:255', Rule::unique('users', 'email')], + 'email' => [ + 'required', + 'string', + 'email', + 'max:255', + Rule::unique('users', 'email')->whereNull('deleted_at'), + ], 'password' => ['required', 'string', 'confirmed', Password::min(8)->mixedCase()->symbols()], 'dni' => ['nullable', 'string', 'max:255'], 'telefono' => ['nullable', 'string', 'max:255'], diff --git a/app/Domains/Auth/Requests/UpdateProfileRequest.php b/app/Domains/Auth/Requests/UpdateProfileRequest.php index dae0ed4..a4984d7 100644 --- a/app/Domains/Auth/Requests/UpdateProfileRequest.php +++ b/app/Domains/Auth/Requests/UpdateProfileRequest.php @@ -4,6 +4,7 @@ namespace App\Domains\Auth\Requests; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; +use Illuminate\Validation\Rules\Password; class UpdateProfileRequest extends FormRequest { @@ -19,11 +20,13 @@ class UpdateProfileRequest extends FormRequest 'email' => [ 'required', 'email', - Rule::unique('users', 'email')->ignore($this->user()->id), + Rule::unique('users', 'email') + ->whereNull('deleted_at') + ->ignore($this->user()->id), ], 'dni' => ['nullable', 'string', 'regex:/^[0-9]{7,8}$/'], 'telefono' => ['nullable', 'string', 'regex:/^\+?[0-9\s\-]+$/'], - 'password' => ['nullable', 'string', \Illuminate\Validation\Rules\Password::min(8)->mixedCase()->symbols()], + 'password' => ['nullable', 'string', Password::min(8)->mixedCase()->symbols()], ]; } } diff --git a/app/Domains/Staff/Requests/StoreStaffRequest.php b/app/Domains/Staff/Requests/StoreStaffRequest.php index 152f923..b592e4a 100644 --- a/app/Domains/Staff/Requests/StoreStaffRequest.php +++ b/app/Domains/Staff/Requests/StoreStaffRequest.php @@ -23,7 +23,12 @@ class StoreStaffRequest extends FormRequest return [ 'nombre_apellido' => ['required', 'string', 'max:255'], 'dni' => ['required', 'string', 'max:50'], - 'email' => ['required', 'email', 'max:255', 'unique:users,email'], + 'email' => [ + 'required', + 'email', + 'max:255', + Rule::unique('users', 'email')->whereNull('deleted_at'), + ], 'category_ids' => $categoryRules, 'category_ids.*' => ['integer', 'distinct', Rule::exists('categorias', 'id')], ]; diff --git a/app/Domains/Staff/Requests/UpdateStaffRequest.php b/app/Domains/Staff/Requests/UpdateStaffRequest.php index cc1b6c4..19e1a94 100644 --- a/app/Domains/Staff/Requests/UpdateStaffRequest.php +++ b/app/Domains/Staff/Requests/UpdateStaffRequest.php @@ -28,7 +28,9 @@ class UpdateStaffRequest extends FormRequest 'required', 'email', 'max:255', - Rule::unique('users', 'email')->ignore($staffId), + Rule::unique('users', 'email') + ->whereNull('deleted_at') + ->ignore($staffId), ], 'category_ids' => $categoryRules, 'category_ids.*' => ['integer', 'distinct', Rule::exists('categorias', 'id')], diff --git a/database/migrations/2026_09_03_000100_scope_user_email_unique_to_active_accounts.php b/database/migrations/2026_09_03_000100_scope_user_email_unique_to_active_accounts.php new file mode 100644 index 0000000..91e0acc --- /dev/null +++ b/database/migrations/2026_09_03_000100_scope_user_email_unique_to_active_accounts.php @@ -0,0 +1,28 @@ +dropUnique(['email']); + $table->string('active_email') + ->nullable() + ->storedAs('CASE WHEN `deleted_at` IS NULL THEN LOWER(`email`) ELSE NULL END'); + $table->unique('active_email'); + }); + } + + public function down(): void + { + Schema::table('users', function (Blueprint $table): void { + $table->dropUnique(['active_email']); + $table->dropColumn('active_email'); + $table->unique('email'); + }); + } +}; diff --git a/tests/Feature/Staff/StaffControllerTest.php b/tests/Feature/Staff/StaffControllerTest.php index 50ac709..194cd35 100644 --- a/tests/Feature/Staff/StaffControllerTest.php +++ b/tests/Feature/Staff/StaffControllerTest.php @@ -121,6 +121,11 @@ class StaffControllerTest extends TestCase $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, @@ -133,6 +138,24 @@ class StaffControllerTest extends TestCase ->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