Compare commits
3 Commits
feature/ti
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| eda2343380 | |||
| 0baad641d5 | |||
| ddb8c3743f |
@@ -13,16 +13,17 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
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', 'active_google_id'])]
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<UserFactory> */
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
use HasApiTokens, HasFactory, Notifiable, SoftDeletes;
|
||||
|
||||
protected $attributes = [
|
||||
'rol_codigo' => RoleCode::User->value,
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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()],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +104,10 @@ class InvitationPurchaseProvisioner
|
||||
|
||||
private function userId(DateTimeInterface $now): int
|
||||
{
|
||||
$user = DB::table('users')->where('email', self::USER_EMAIL)->first();
|
||||
$user = DB::table('users')
|
||||
->where('email', self::USER_EMAIL)
|
||||
->whereNull('deleted_at')
|
||||
->first();
|
||||
|
||||
if ($user !== null) {
|
||||
if ($user->tenant_codigo !== self::TENANT_CODE) {
|
||||
|
||||
@@ -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')],
|
||||
];
|
||||
|
||||
@@ -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')],
|
||||
|
||||
@@ -94,7 +94,12 @@ class StaffService
|
||||
|
||||
public function delete(Tenant $tenant, int $staffId): void
|
||||
{
|
||||
$this->find($tenant, $staffId)->delete();
|
||||
$staff = $this->find($tenant, $staffId);
|
||||
|
||||
DB::transaction(function () use ($staff): void {
|
||||
$staff->tokens()->delete();
|
||||
$staff->delete();
|
||||
});
|
||||
}
|
||||
|
||||
public function find(Tenant $tenant, int $staffId): User
|
||||
|
||||
@@ -78,7 +78,7 @@ class Ticket extends Model
|
||||
/** @return BelongsTo<User, $this> */
|
||||
public function scannerUser(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'scanner_user_id');
|
||||
return $this->belongsTo(User::class, 'scanner_user_id')->withTrashed();
|
||||
}
|
||||
|
||||
/** @return BelongsTo<PurchaseItem, $this> */
|
||||
|
||||
@@ -293,6 +293,7 @@ class AdminAppTicketService
|
||||
'id' => 'tickets.id',
|
||||
'amount' => $this->purchaseItemColumnQuery('precio_unitario'),
|
||||
'scanned_by' => User::query()
|
||||
->withTrashed()
|
||||
->select('nombre_apellido')
|
||||
->whereColumn('users.id', 'tickets.scanner_user_id'),
|
||||
'product' => $tenant->codigo === 'fiesta_futbol_infantil'
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table): void {
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table): void {
|
||||
$table->dropSoftDeletes();
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table): void {
|
||||
$table->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');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table): void {
|
||||
$table->dropUnique(['google_id']);
|
||||
$table->string('active_google_id')
|
||||
->nullable()
|
||||
->storedAs('CASE WHEN `deleted_at` IS NULL THEN `google_id` ELSE NULL END');
|
||||
$table->unique('active_google_id');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table): void {
|
||||
$table->dropUnique(['active_google_id']);
|
||||
$table->dropColumn('active_google_id');
|
||||
$table->unique('google_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -135,4 +135,36 @@ class RegisterControllerTest extends TestCase
|
||||
'password',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_it_can_reuse_the_email_of_a_soft_deleted_user(): void
|
||||
{
|
||||
$deletedUser = User::factory()->create([
|
||||
'email' => 'reused@example.com',
|
||||
]);
|
||||
$deletedUser->delete();
|
||||
|
||||
$response = $this->postJson('/api/register', [
|
||||
'nombre_apellido' => 'New Account',
|
||||
'email' => 'reused@example.com',
|
||||
'password' => 'Secret!123',
|
||||
'password_confirmation' => 'Secret!123',
|
||||
])->assertCreated();
|
||||
|
||||
$newUserId = $response->json('data.id');
|
||||
|
||||
$this->assertNotSame($deletedUser->id, $newUserId);
|
||||
$this->assertSame(
|
||||
2,
|
||||
User::withTrashed()->where('email', 'reused@example.com')->count(),
|
||||
);
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $deletedUser->id,
|
||||
'active_email' => null,
|
||||
]);
|
||||
$this->assertDatabaseHas('users', [
|
||||
'id' => $newUserId,
|
||||
'active_email' => 'reused@example.com',
|
||||
'deleted_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace Tests\Feature\Staff;
|
||||
|
||||
use App\Domains\Attachable\Enums\AttachmentType;
|
||||
use App\Domains\Attachable\Models\Attachment;
|
||||
use App\Domains\Auth\Models\ResetPasswordAttempt;
|
||||
use App\Domains\Auth\Models\User;
|
||||
use App\Domains\Authorization\Enums\RoleCode;
|
||||
@@ -9,9 +11,11 @@ use App\Domains\Catalog\Models\Category;
|
||||
use App\Domains\Notification\Events\PasswordResetRequested;
|
||||
use App\Domains\Tenant\Models\Tenant;
|
||||
use App\Domains\Tenant\Models\WebsiteType;
|
||||
use App\Domains\Ticket\Models\Ticket;
|
||||
use Database\Seeders\AuthorizationSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Event;
|
||||
use Illuminate\Support\Str;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
@@ -30,11 +34,21 @@ class StaffControllerTest extends TestCase
|
||||
Event::fake([PasswordResetRequested::class]);
|
||||
$this->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,
|
||||
@@ -92,8 +106,56 @@ class StaffControllerTest extends TestCase
|
||||
'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->assertDatabaseMissing('users', ['id' => $staffId]);
|
||||
$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
|
||||
@@ -104,6 +166,14 @@ class StaffControllerTest extends TestCase
|
||||
'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,
|
||||
@@ -183,6 +253,16 @@ class StaffControllerTest extends TestCase
|
||||
$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([
|
||||
|
||||
Reference in New Issue
Block a user