Files
alrahma_sunday_school_api/app/Services/Users/UserManagementService.php
T
2026-06-09 00:03:03 -04:00

294 lines
9.5 KiB
PHP

<?php
namespace App\Services\Users;
use App\Models\Role;
use App\Models\User;
use App\Models\UserRole;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
class UserManagementService
{
public function __construct(private UserEventService $eventService) {}
public function create(array $payload, ?int $actorId = null): array
{
$actorId = $actorId ?? (auth()->id() ?? null);
try {
$user = DB::transaction(function () use ($payload, $actorId) {
$data = $this->normalizeForCreate($payload);
$user = User::query()->create($data);
UserRole::query()->create([
'user_id' => $user->id,
'role_id' => (int) $payload['role_id'],
'updated_by' => $actorId,
]);
return $user;
});
} catch (\Throwable $e) {
return ['ok' => false, 'message' => 'Failed to create user.'];
}
$this->eventService->handleNewAccountAdded($user->toArray());
return ['ok' => true, 'user' => $user];
}
public function update(int $userId, array $payload): array
{
$user = User::query()->find($userId);
if (! $user) {
return ['ok' => false, 'message' => 'User not found.'];
}
$data = $this->normalizeForUpdate($payload);
if (empty($data)) {
return ['ok' => false, 'message' => 'No changes requested.'];
}
try {
$user->fill($data);
$user->save();
} catch (\Throwable $e) {
return ['ok' => false, 'message' => 'Failed to update user.'];
}
return ['ok' => true, 'user' => $user];
}
public function delete(int $userId): array
{
$user = User::query()->find($userId);
if (! $user) {
return ['ok' => false, 'message' => 'User not found.'];
}
try {
DB::transaction(function () use ($userId, $user) {
UserRole::query()->where('user_id', $userId)->forceDelete();
$user->delete();
});
} catch (\Throwable $e) {
return ['ok' => false, 'message' => 'Failed to delete user.'];
}
return ['ok' => true];
}
public function assignRole(int $userId, int $roleId, ?int $actorId = null): bool
{
$actorId = $actorId ?? (auth()->id() ?? null);
if (! User::query()->whereKey($userId)->exists()) {
return false;
}
if (! Role::query()->whereKey($roleId)->exists()) {
return false;
}
return UserRole::updateOrInsertRole($userId, $roleId, $actorId);
}
public function cleanupUnverifiedUsers(int $olderThanMinutes = 15): int
{
$cutoff = now()->subMinutes(max(1, $olderThanMinutes));
$deleted = User::query()
->where('is_verified', 0)
->where('created_at', '<=', $cutoff)
->delete();
Log::info(sprintf(
'Deleted %d unverified users created before %s.',
$deleted,
$cutoff->format('Y-m-d H:i:s')
));
return $deleted;
}
public function validateRegistrationInput(array $data): array
{
$rules = [
'firstname' => ['required', 'regex:/^[a-zA-Z\\s-]+$/', 'min:2', 'max:30'],
'lastname' => ['required', 'regex:/^[a-zA-Z\\s-]+$/', 'min:2', 'max:30'],
'gender' => ['required', 'in:Male,Female'],
'email' => ['required', 'email', 'max:50'],
'confirm_email' => ['required', 'same:email'],
'cellphone' => ['required', 'regex:/^[\\d\\s\\-\\(\\)\\.]+$/', 'min:10', 'max:20'],
'address_street' => ['nullable', 'regex:/^[a-zA-Z0-9\\s\\-]+$/', 'max:150'],
'apt' => ['nullable', 'regex:/^[a-zA-Z0-9\\s-]+$/', 'max:10'],
'city' => ['required', 'regex:/^[a-zA-Z\\s]+$/', 'min:2', 'max:30'],
'state' => ['required', 'in:CT,ME,MA,NH,NY,RI,VT'],
'zip' => ['required', 'regex:/^\\d{5}$/'],
'accept_school_policy' => ['required'],
];
$isParent = ! empty($data['is_parent']);
$noSecondParent = ! empty($data['no_second_parent_info']);
if ($isParent && ! $noSecondParent) {
$rules += [
'second_firstname' => ['required', 'regex:/^[a-zA-Z\\s\\-]+$/', 'min:2', 'max:30'],
'second_lastname' => ['required', 'regex:/^[a-zA-Z\\s\\-]+$/', 'min:2', 'max:30'],
'second_gender' => ['required', 'in:Male,Female'],
'second_email' => ['required', 'email', 'max:50'],
'second_cellphone' => ['required', 'regex:/^[\\d\\s\\-\\(\\)\\.]+$/', 'min:10', 'max:20'],
];
}
$validator = Validator::make($data, $rules);
if ($validator->fails()) {
return $validator->errors()->toArray();
}
return [];
}
private function normalizeForCreate(array $payload): array
{
return [
'school_id' => $payload['school_id'] ?? null,
'firstname' => $this->titleCase($payload['firstname'] ?? ''),
'lastname' => $this->titleCase($payload['lastname'] ?? ''),
'gender' => $this->normalizeGender($payload['gender'] ?? null),
'cellphone' => (string) ($payload['cellphone'] ?? ''),
'email' => strtolower((string) ($payload['email'] ?? '')),
'address_street' => (string) ($payload['address_street'] ?? ''),
'apt' => $payload['apt'] ?? null,
'city' => $this->titleCase($payload['city'] ?? ''),
'state' => (string) ($payload['state'] ?? ''),
'zip' => (string) ($payload['zip'] ?? ''),
'accept_school_policy' => (bool) ($payload['accept_school_policy'] ?? false),
'is_verified' => isset($payload['is_verified']) ? (bool) $payload['is_verified'] : 0,
'status' => $payload['status'] ?? 'Inactive',
'is_suspended' => isset($payload['is_suspended']) ? (bool) $payload['is_suspended'] : 0,
'failed_attempts' => $payload['failed_attempts'] ?? null,
'password' => pbkdf2_hash((string) ($payload['password'] ?? '')),
'token' => $payload['token'] ?? null,
'account_id' => $payload['account_id'] ?? null,
'user_type' => $payload['user_type'] ?? 'primary',
'semester' => (string) ($payload['semester'] ?? ''),
'school_year' => $payload['school_year'] ?? null,
'rfid_tag' => $payload['rfid_tag'] ?? null,
'last_failed_at' => $this->normalizeDateTime($payload['last_failed_at'] ?? null),
];
}
private function normalizeForUpdate(array $payload): array
{
$data = [];
$map = [
'school_id',
'firstname',
'lastname',
'gender',
'cellphone',
'email',
'address_street',
'apt',
'city',
'state',
'zip',
'accept_school_policy',
'user_type',
'rfid_tag',
'failed_attempts',
'last_failed_at',
'semester',
'school_year',
'status',
'is_suspended',
'is_verified',
'token',
'account_id',
];
foreach ($map as $field) {
if (! array_key_exists($field, $payload)) {
continue;
}
$value = $payload[$field];
switch ($field) {
case 'firstname':
case 'lastname':
case 'city':
$data[$field] = $this->titleCase($value);
break;
case 'email':
$data[$field] = strtolower((string) $value);
break;
case 'gender':
$data[$field] = $this->normalizeGender($value);
break;
case 'accept_school_policy':
case 'is_suspended':
case 'is_verified':
$data[$field] = (bool) $value;
break;
case 'last_failed_at':
$data[$field] = $this->normalizeDateTime($value);
break;
case 'failed_attempts':
case 'school_id':
$data[$field] = $value === '' ? null : $value;
break;
default:
$data[$field] = $value;
break;
}
}
return $data;
}
private function normalizeGender(?string $value): ?string
{
$value = strtolower(trim((string) $value));
if ($value === '') {
return null;
}
if ($value === 'male') {
return 'Male';
}
if ($value === 'female') {
return 'Female';
}
return null;
}
private function titleCase(?string $value): string
{
$value = trim((string) $value);
if ($value === '') {
return '';
}
return ucfirst($value);
}
private function normalizeDateTime($value): ?string
{
$value = trim((string) ($value ?? ''));
if ($value === '') {
return null;
}
try {
return CarbonImmutable::parse($value)->format('Y-m-d H:i:s');
} catch (\Throwable $e) {
return null;
}
}
}