completed story 1.1 with QA tests and fixes
This commit is contained in:
parent
028ce573b9
commit
84d9c2f66a
|
|
@ -19,7 +19,7 @@ class CreateNewUser implements CreatesNewUsers
|
|||
public function create(array $input): User
|
||||
{
|
||||
Validator::make($input, [
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'full_name' => ['required', 'string', 'max:255'],
|
||||
'email' => [
|
||||
'required',
|
||||
'string',
|
||||
|
|
@ -27,12 +27,14 @@ class CreateNewUser implements CreatesNewUsers
|
|||
'max:255',
|
||||
Rule::unique(User::class),
|
||||
],
|
||||
'phone' => ['required', 'string', 'max:20'],
|
||||
'password' => $this->passwordRules(),
|
||||
])->validate();
|
||||
|
||||
return User::create([
|
||||
'name' => $input['name'],
|
||||
'full_name' => $input['full_name'],
|
||||
'email' => $input['email'],
|
||||
'phone' => $input['phone'],
|
||||
'password' => $input['password'],
|
||||
]);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum ConsultationStatus: string
|
||||
{
|
||||
case Pending = 'pending';
|
||||
case Approved = 'approved';
|
||||
case Rejected = 'rejected';
|
||||
case Completed = 'completed';
|
||||
case NoShow = 'no_show';
|
||||
case Cancelled = 'cancelled';
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum ConsultationType: string
|
||||
{
|
||||
case Free = 'free';
|
||||
case Paid = 'paid';
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum PaymentStatus: string
|
||||
{
|
||||
case Pending = 'pending';
|
||||
case Received = 'received';
|
||||
case NotApplicable = 'na';
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum PostStatus: string
|
||||
{
|
||||
case Draft = 'draft';
|
||||
case Published = 'published';
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum TimelineStatus: string
|
||||
{
|
||||
case Active = 'active';
|
||||
case Archived = 'archived';
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum UserStatus: string
|
||||
{
|
||||
case Active = 'active';
|
||||
case Deactivated = 'deactivated';
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum UserType: string
|
||||
{
|
||||
case Admin = 'admin';
|
||||
case Individual = 'individual';
|
||||
case Company = 'company';
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class AdminLog extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'admin_id',
|
||||
'action',
|
||||
'target_type',
|
||||
'target_id',
|
||||
'old_values',
|
||||
'new_values',
|
||||
'ip_address',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'old_values' => 'array',
|
||||
'new_values' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the admin that created the log.
|
||||
*/
|
||||
public function admin(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'admin_id');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BlockedTime extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'block_date',
|
||||
'start_time',
|
||||
'end_time',
|
||||
'reason',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'block_date' => 'date',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this is a full day block.
|
||||
*/
|
||||
public function isFullDay(): bool
|
||||
{
|
||||
return is_null($this->start_time) && is_null($this->end_time);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\ConsultationStatus;
|
||||
use App\Enums\ConsultationType;
|
||||
use App\Enums\PaymentStatus;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Consultation extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'booking_date',
|
||||
'booking_time',
|
||||
'problem_summary',
|
||||
'consultation_type',
|
||||
'payment_amount',
|
||||
'payment_status',
|
||||
'status',
|
||||
'admin_notes',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'booking_date' => 'date',
|
||||
'consultation_type' => ConsultationType::class,
|
||||
'payment_status' => PaymentStatus::class,
|
||||
'status' => ConsultationStatus::class,
|
||||
'payment_amount' => 'decimal:2',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user that owns the consultation.
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class Notification extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'type',
|
||||
'data',
|
||||
'read_at',
|
||||
'sent_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'data' => 'array',
|
||||
'read_at' => 'datetime',
|
||||
'sent_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user that owns the notification.
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the notification as read.
|
||||
*/
|
||||
public function markAsRead(): void
|
||||
{
|
||||
$this->update(['read_at' => now()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter unread notifications.
|
||||
*/
|
||||
public function scopeUnread($query)
|
||||
{
|
||||
return $query->whereNull('read_at');
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\PostStatus;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Post extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'title',
|
||||
'body',
|
||||
'status',
|
||||
'published_at',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'title' => 'array',
|
||||
'body' => 'array',
|
||||
'status' => PostStatus::class,
|
||||
'published_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter published posts.
|
||||
*/
|
||||
public function scopePublished($query)
|
||||
{
|
||||
return $query->where('status', PostStatus::Published);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter draft posts.
|
||||
*/
|
||||
public function scopeDraft($query)
|
||||
{
|
||||
return $query->where('status', PostStatus::Draft);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the title in the specified locale, with fallback to Arabic.
|
||||
*/
|
||||
public function getTitle(?string $locale = null): string
|
||||
{
|
||||
$locale ??= app()->getLocale();
|
||||
|
||||
return $this->title[$locale] ?? $this->title['ar'] ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the body in the specified locale, with fallback to Arabic.
|
||||
*/
|
||||
public function getBody(?string $locale = null): string
|
||||
{
|
||||
$locale ??= app()->getLocale();
|
||||
|
||||
return $this->body[$locale] ?? $this->body['ar'] ?? '';
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\TimelineStatus;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
|
||||
class Timeline extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'case_name',
|
||||
'case_reference',
|
||||
'status',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'status' => TimelineStatus::class,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user that owns the timeline.
|
||||
*/
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the updates for the timeline.
|
||||
*/
|
||||
public function updates(): HasMany
|
||||
{
|
||||
return $this->hasMany(TimelineUpdate::class);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class TimelineUpdate extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'timeline_id',
|
||||
'admin_id',
|
||||
'update_text',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the timeline that owns the update.
|
||||
*/
|
||||
public function timeline(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Timeline::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the admin that created the update.
|
||||
*/
|
||||
public function admin(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'admin_id');
|
||||
}
|
||||
}
|
||||
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use App\Enums\UserStatus;
|
||||
use App\Enums\UserType;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Support\Str;
|
||||
|
|
@ -20,9 +22,18 @@ class User extends Authenticatable
|
|||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'user_type',
|
||||
'full_name',
|
||||
'national_id',
|
||||
'company_name',
|
||||
'company_cert_number',
|
||||
'contact_person_name',
|
||||
'contact_person_id',
|
||||
'email',
|
||||
'phone',
|
||||
'password',
|
||||
'status',
|
||||
'preferred_language',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
@ -32,6 +43,7 @@ class User extends Authenticatable
|
|||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'national_id',
|
||||
'two_factor_secret',
|
||||
'two_factor_recovery_codes',
|
||||
'remember_token',
|
||||
|
|
@ -45,20 +57,119 @@ class User extends Authenticatable
|
|||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'user_type' => UserType::class,
|
||||
'status' => UserStatus::class,
|
||||
'email_verified_at' => 'datetime',
|
||||
'two_factor_confirmed_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's initials
|
||||
* Get the user's initials.
|
||||
*/
|
||||
public function initials(): string
|
||||
{
|
||||
return Str::of($this->name)
|
||||
return Str::of($this->full_name)
|
||||
->explode(' ')
|
||||
->take(2)
|
||||
->map(fn ($word) => Str::substr($word, 0, 1))
|
||||
->implode('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is an admin.
|
||||
*/
|
||||
public function isAdmin(): bool
|
||||
{
|
||||
return $this->user_type === UserType::Admin;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is an individual client.
|
||||
*/
|
||||
public function isIndividual(): bool
|
||||
{
|
||||
return $this->user_type === UserType::Individual;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is a company client.
|
||||
*/
|
||||
public function isCompany(): bool
|
||||
{
|
||||
return $this->user_type === UserType::Company;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is a client (individual or company).
|
||||
*/
|
||||
public function isClient(): bool
|
||||
{
|
||||
return $this->isIndividual() || $this->isCompany();
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter admin users.
|
||||
*/
|
||||
public function scopeAdmins($query)
|
||||
{
|
||||
return $query->where('user_type', UserType::Admin);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter client users (individual or company).
|
||||
*/
|
||||
public function scopeClients($query)
|
||||
{
|
||||
return $query->whereIn('user_type', [UserType::Individual, UserType::Company]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter active users.
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('status', UserStatus::Active);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the consultations for the user.
|
||||
*/
|
||||
public function consultations(): HasMany
|
||||
{
|
||||
return $this->hasMany(Consultation::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timelines for the user.
|
||||
*/
|
||||
public function timelines(): HasMany
|
||||
{
|
||||
return $this->hasMany(Timeline::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notifications for the user.
|
||||
*/
|
||||
public function customNotifications(): HasMany
|
||||
{
|
||||
return $this->hasMany(Notification::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the admin logs for the user.
|
||||
*/
|
||||
public function adminLogs(): HasMany
|
||||
{
|
||||
return $this->hasMany(AdminLog::class, 'admin_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the timeline updates created by this admin.
|
||||
*/
|
||||
public function timelineUpdates(): HasMany
|
||||
{
|
||||
return $this->hasMany(TimelineUpdate::class, 'admin_id');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WorkingHour extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'day_of_week',
|
||||
'start_time',
|
||||
'end_time',
|
||||
'is_active',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'day_of_week' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope to filter active working hours.
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\AdminLog>
|
||||
*/
|
||||
class AdminLogFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'admin_id' => User::factory()->admin(),
|
||||
'action' => fake()->randomElement(['create', 'update', 'delete', 'login', 'logout']),
|
||||
'target_type' => fake()->randomElement(['User', 'Consultation', 'Timeline', 'Post']),
|
||||
'target_id' => fake()->optional()->numberBetween(1, 100),
|
||||
'old_values' => fake()->optional()->randomElements(['status' => 'pending', 'name' => 'Old Name'], null),
|
||||
'new_values' => fake()->optional()->randomElements(['status' => 'approved', 'name' => 'New Name'], null),
|
||||
'ip_address' => fake()->ipv4(),
|
||||
'created_at' => fake()->dateTimeBetween('-30 days', 'now'),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\BlockedTime>
|
||||
*/
|
||||
class BlockedTimeFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'block_date' => fake()->dateTimeBetween('now', '+30 days'),
|
||||
'start_time' => null,
|
||||
'end_time' => null,
|
||||
'reason' => fake()->optional()->sentence(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a full day block.
|
||||
*/
|
||||
public function fullDay(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'start_time' => null,
|
||||
'end_time' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a partial day block with specific times.
|
||||
*/
|
||||
public function partialDay(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'start_time' => '09:00:00',
|
||||
'end_time' => '12:00:00',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\ConsultationStatus;
|
||||
use App\Enums\ConsultationType;
|
||||
use App\Enums\PaymentStatus;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Consultation>
|
||||
*/
|
||||
class ConsultationFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
$consultationType = fake()->randomElement(ConsultationType::cases());
|
||||
$paymentStatus = $consultationType === ConsultationType::Paid
|
||||
? fake()->randomElement([PaymentStatus::Pending, PaymentStatus::Received])
|
||||
: PaymentStatus::NotApplicable;
|
||||
|
||||
return [
|
||||
'user_id' => User::factory(),
|
||||
'booking_date' => fake()->dateTimeBetween('now', '+30 days'),
|
||||
'booking_time' => fake()->time('H:i:s'),
|
||||
'problem_summary' => fake()->paragraph(),
|
||||
'consultation_type' => $consultationType,
|
||||
'payment_amount' => $consultationType === ConsultationType::Paid ? fake()->randomFloat(2, 50, 500) : null,
|
||||
'payment_status' => $paymentStatus,
|
||||
'status' => fake()->randomElement(ConsultationStatus::cases()),
|
||||
'admin_notes' => fake()->optional()->paragraph(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a free consultation.
|
||||
*/
|
||||
public function free(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'consultation_type' => ConsultationType::Free,
|
||||
'payment_amount' => null,
|
||||
'payment_status' => PaymentStatus::NotApplicable,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a paid consultation.
|
||||
*/
|
||||
public function paid(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'consultation_type' => ConsultationType::Paid,
|
||||
'payment_amount' => fake()->randomFloat(2, 50, 500),
|
||||
'payment_status' => fake()->randomElement([PaymentStatus::Pending, PaymentStatus::Received]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a pending consultation.
|
||||
*/
|
||||
public function pending(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => ConsultationStatus::Pending,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an approved consultation.
|
||||
*/
|
||||
public function approved(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => ConsultationStatus::Approved,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a completed consultation.
|
||||
*/
|
||||
public function completed(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => ConsultationStatus::Completed,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Notification>
|
||||
*/
|
||||
class NotificationFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => User::factory(),
|
||||
'type' => fake()->randomElement(['consultation_approved', 'consultation_rejected', 'timeline_update', 'new_post']),
|
||||
'data' => [
|
||||
'message' => fake()->sentence(),
|
||||
'action_url' => fake()->url(),
|
||||
],
|
||||
'read_at' => null,
|
||||
'sent_at' => fake()->optional()->dateTimeBetween('-7 days', 'now'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a read notification.
|
||||
*/
|
||||
public function read(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'read_at' => fake()->dateTimeBetween('-7 days', 'now'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an unread notification.
|
||||
*/
|
||||
public function unread(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'read_at' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a sent notification.
|
||||
*/
|
||||
public function sent(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'sent_at' => fake()->dateTimeBetween('-7 days', 'now'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\PostStatus;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Post>
|
||||
*/
|
||||
class PostFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'title' => [
|
||||
'ar' => fake('ar_SA')->sentence(),
|
||||
'en' => fake()->sentence(),
|
||||
],
|
||||
'body' => [
|
||||
'ar' => fake('ar_SA')->paragraphs(3, true),
|
||||
'en' => fake()->paragraphs(3, true),
|
||||
],
|
||||
'status' => PostStatus::Draft,
|
||||
'published_at' => null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a published post.
|
||||
*/
|
||||
public function published(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => PostStatus::Published,
|
||||
'published_at' => fake()->dateTimeBetween('-30 days', 'now'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a draft post.
|
||||
*/
|
||||
public function draft(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => PostStatus::Draft,
|
||||
'published_at' => null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\TimelineStatus;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Timeline>
|
||||
*/
|
||||
class TimelineFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'user_id' => User::factory(),
|
||||
'case_name' => fake()->sentence(3),
|
||||
'case_reference' => fake()->boolean(70) ? fake()->unique()->regexify('[A-Z]{2}-[0-9]{4}-[0-9]{3}') : null,
|
||||
'status' => TimelineStatus::Active,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an active timeline.
|
||||
*/
|
||||
public function active(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => TimelineStatus::Active,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an archived timeline.
|
||||
*/
|
||||
public function archived(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => TimelineStatus::Archived,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a timeline with a case reference.
|
||||
*/
|
||||
public function withReference(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'case_reference' => fake()->unique()->regexify('[A-Z]{2}-[0-9]{4}-[0-9]{3}'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Models\Timeline;
|
||||
use App\Models\User;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\TimelineUpdate>
|
||||
*/
|
||||
class TimelineUpdateFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'timeline_id' => Timeline::factory(),
|
||||
'admin_id' => User::factory()->admin(),
|
||||
'update_text' => fake()->paragraph(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Enums\UserStatus;
|
||||
use App\Enums\UserType;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
use Illuminate\Support\Str;
|
||||
|
|
@ -24,14 +26,16 @@ class UserFactory extends Factory
|
|||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->name(),
|
||||
'user_type' => UserType::Individual,
|
||||
'full_name' => fake()->name(),
|
||||
'national_id' => fake()->numerify('#########'),
|
||||
'email' => fake()->unique()->safeEmail(),
|
||||
'email_verified_at' => now(),
|
||||
'phone' => fake()->phoneNumber(),
|
||||
'password' => static::$password ??= Hash::make('password'),
|
||||
'status' => UserStatus::Active,
|
||||
'preferred_language' => fake()->randomElement(['ar', 'en']),
|
||||
'email_verified_at' => now(),
|
||||
'remember_token' => Str::random(10),
|
||||
'two_factor_secret' => Str::random(10),
|
||||
'two_factor_recovery_codes' => Str::random(10),
|
||||
'two_factor_confirmed_at' => now(),
|
||||
];
|
||||
}
|
||||
|
||||
|
|
@ -45,6 +49,81 @@ class UserFactory extends Factory
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an admin user.
|
||||
*/
|
||||
public function admin(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'user_type' => UserType::Admin,
|
||||
'company_name' => null,
|
||||
'company_cert_number' => null,
|
||||
'contact_person_name' => null,
|
||||
'contact_person_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an individual client user.
|
||||
*/
|
||||
public function individual(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'user_type' => UserType::Individual,
|
||||
'company_name' => null,
|
||||
'company_cert_number' => null,
|
||||
'contact_person_name' => null,
|
||||
'contact_person_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a company client user.
|
||||
*/
|
||||
public function company(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'user_type' => UserType::Company,
|
||||
'national_id' => null,
|
||||
'company_name' => fake()->company(),
|
||||
'company_cert_number' => fake()->numerify('CR-######'),
|
||||
'contact_person_name' => fake()->name(),
|
||||
'contact_person_id' => fake()->numerify('#########'),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a client user (individual or company).
|
||||
*/
|
||||
public function client(): static
|
||||
{
|
||||
return fake()->boolean()
|
||||
? $this->individual()
|
||||
: $this->company();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a deactivated user.
|
||||
*/
|
||||
public function deactivated(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'status' => UserStatus::Deactivated,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model has two-factor authentication configured.
|
||||
*/
|
||||
public function withTwoFactor(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'two_factor_secret' => Str::random(10),
|
||||
'two_factor_recovery_codes' => Str::random(10),
|
||||
'two_factor_confirmed_at' => now(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the model does not have two-factor authentication configured.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\WorkingHour>
|
||||
*/
|
||||
class WorkingHourFactory extends Factory
|
||||
{
|
||||
/**
|
||||
* Define the model's default state.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'day_of_week' => fake()->numberBetween(0, 6),
|
||||
'start_time' => '09:00:00',
|
||||
'end_time' => '17:00:00',
|
||||
'is_active' => true,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create working hours for weekdays (Sunday-Thursday, 0-4).
|
||||
*/
|
||||
public function weekdays(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'day_of_week' => fake()->numberBetween(0, 4),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create inactive working hours.
|
||||
*/
|
||||
public function inactive(): static
|
||||
{
|
||||
return $this->state(fn (array $attributes) => [
|
||||
'is_active' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -13,10 +13,19 @@ return new class extends Migration
|
|||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('user_type')->default('individual');
|
||||
$table->string('full_name');
|
||||
$table->string('national_id')->nullable();
|
||||
$table->string('company_name')->nullable();
|
||||
$table->string('company_cert_number')->nullable();
|
||||
$table->string('contact_person_name')->nullable();
|
||||
$table->string('contact_person_id')->nullable();
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('phone');
|
||||
$table->string('password');
|
||||
$table->string('status')->default('active');
|
||||
$table->string('preferred_language', 2)->default('ar');
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('consultations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->date('booking_date')->index();
|
||||
$table->time('booking_time');
|
||||
$table->text('problem_summary');
|
||||
$table->string('consultation_type')->default('free');
|
||||
$table->decimal('payment_amount', 10, 2)->nullable();
|
||||
$table->string('payment_status')->default('na');
|
||||
$table->string('status')->default('pending');
|
||||
$table->text('admin_notes')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('user_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('consultations');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('timelines', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('case_name');
|
||||
$table->string('case_reference')->unique()->nullable();
|
||||
$table->string('status')->default('active');
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('user_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('timelines');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('timeline_updates', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('timeline_id')->constrained()->cascadeOnDelete();
|
||||
$table->foreignId('admin_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->text('update_text');
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('timeline_updates');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('posts', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->json('title');
|
||||
$table->json('body');
|
||||
$table->string('status')->default('draft')->index();
|
||||
$table->timestamp('published_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('posts');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('working_hours', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->tinyInteger('day_of_week');
|
||||
$table->time('start_time');
|
||||
$table->time('end_time');
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('working_hours');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('blocked_times', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->date('block_date');
|
||||
$table->time('start_time')->nullable();
|
||||
$table->time('end_time')->nullable();
|
||||
$table->string('reason')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('blocked_times');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('notifications', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
|
||||
$table->string('type');
|
||||
$table->json('data');
|
||||
$table->timestamp('read_at')->nullable();
|
||||
$table->timestamp('sent_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('notifications');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('admin_logs', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('admin_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->string('action');
|
||||
$table->string('target_type');
|
||||
$table->unsignedBigInteger('target_id')->nullable();
|
||||
$table->json('old_values')->nullable();
|
||||
$table->json('new_values')->nullable();
|
||||
$table->string('ip_address');
|
||||
$table->timestamp('created_at');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('admin_logs');
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
schema: 1
|
||||
story: "1.1"
|
||||
story_title: "Project Setup & Database Schema"
|
||||
gate: PASS
|
||||
status_reason: "All 10 acceptance criteria met. 97 tests passing. Code follows Laravel 12 conventions and architecture specifications. No security or performance concerns."
|
||||
reviewer: "Quinn (Test Architect)"
|
||||
updated: "2025-12-26T00:00:00Z"
|
||||
|
||||
waiver: { active: false }
|
||||
|
||||
top_issues: []
|
||||
|
||||
quality_score: 100
|
||||
expires: "2026-01-09T00:00:00Z"
|
||||
|
||||
evidence:
|
||||
tests_reviewed: 102
|
||||
tests_passing: 102
|
||||
risks_identified: 0
|
||||
trace:
|
||||
ac_covered: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|
||||
ac_gaps: []
|
||||
|
||||
nfr_validation:
|
||||
security:
|
||||
status: PASS
|
||||
notes: "Sensitive fields hidden from serialization. Password hashing. No injection vulnerabilities."
|
||||
performance:
|
||||
status: PASS
|
||||
notes: "Proper database indexes on frequently queried columns. No N+1 concerns at foundation level."
|
||||
reliability:
|
||||
status: PASS
|
||||
notes: "Foreign key constraints with cascade deletes. Migration rollback tested and working."
|
||||
maintainability:
|
||||
status: PASS
|
||||
notes: "Code follows Laravel conventions. Eloquent models with proper type hints and relationships."
|
||||
|
||||
risk_summary:
|
||||
totals: { critical: 0, high: 0, medium: 0, low: 0 }
|
||||
recommendations:
|
||||
must_fix: []
|
||||
monitor: []
|
||||
|
||||
recommendations:
|
||||
immediate: []
|
||||
future:
|
||||
- action: "Consider adding scopeByType($type) to User model for flexibility"
|
||||
refs: ["app/Models/User.php"]
|
||||
- action: "Add more comprehensive relationship tests when features use them"
|
||||
refs: ["tests/Unit/Models/"]
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# Story 1.1: Project Setup & Database Schema
|
||||
|
||||
## Status
|
||||
Draft
|
||||
Done
|
||||
|
||||
## Epic Reference
|
||||
**Epic 1:** Core Foundation & Infrastructure
|
||||
|
|
@ -35,62 +35,62 @@ Draft
|
|||
|
||||
## Tasks / Subtasks
|
||||
|
||||
- [ ] **Task 1: Create PHP Enums** (AC: 3)
|
||||
- [ ] Create `app/Enums/UserType.php` enum (admin, individual, company)
|
||||
- [ ] Create `app/Enums/UserStatus.php` enum (active, deactivated)
|
||||
- [ ] Create `app/Enums/ConsultationType.php` enum (free, paid)
|
||||
- [ ] Create `app/Enums/ConsultationStatus.php` enum (pending, approved, rejected, completed, no_show, cancelled)
|
||||
- [ ] Create `app/Enums/PaymentStatus.php` enum (pending, received, na)
|
||||
- [ ] Create `app/Enums/TimelineStatus.php` enum (active, archived)
|
||||
- [ ] Create `app/Enums/PostStatus.php` enum (draft, published)
|
||||
- [x] **Task 1: Create PHP Enums** (AC: 3)
|
||||
- [x] Create `app/Enums/UserType.php` enum (admin, individual, company)
|
||||
- [x] Create `app/Enums/UserStatus.php` enum (active, deactivated)
|
||||
- [x] Create `app/Enums/ConsultationType.php` enum (free, paid)
|
||||
- [x] Create `app/Enums/ConsultationStatus.php` enum (pending, approved, rejected, completed, no_show, cancelled)
|
||||
- [x] Create `app/Enums/PaymentStatus.php` enum (pending, received, na)
|
||||
- [x] Create `app/Enums/TimelineStatus.php` enum (active, archived)
|
||||
- [x] Create `app/Enums/PostStatus.php` enum (draft, published)
|
||||
|
||||
- [ ] **Task 2: Create Database Migrations** (AC: 3, 7, 9, 10)
|
||||
- [ ] Modify existing `users` migration to add: user_type, full_name, national_id, company_name, company_cert_number, contact_person_name, contact_person_id, phone, status, preferred_language, two_factor fields
|
||||
- [ ] Create `consultations` migration with: user_id (FK), booking_date, booking_time, problem_summary, consultation_type, payment_amount, payment_status, status, admin_notes
|
||||
- [ ] Create `timelines` migration with: user_id (FK), case_name, case_reference (unique nullable), status
|
||||
- [ ] Create `timeline_updates` migration with: timeline_id (FK), admin_id (FK), update_text
|
||||
- [ ] Create `posts` migration with: title (JSON), body (JSON), status, published_at
|
||||
- [ ] Create `working_hours` migration with: day_of_week, start_time, end_time, is_active
|
||||
- [ ] Create `blocked_times` migration with: block_date, start_time (nullable), end_time (nullable), reason
|
||||
- [ ] Create `notifications` migration with: user_id (FK), type, data (JSON), read_at, sent_at
|
||||
- [ ] Create `admin_logs` migration with: admin_id (FK), action, target_type, target_id, old_values (JSON), new_values (JSON), ip_address
|
||||
- [ ] Add indexes: users.email (unique), consultations.booking_date, consultations.user_id, timelines.user_id, posts.status
|
||||
- [x] **Task 2: Create Database Migrations** (AC: 3, 7, 9, 10)
|
||||
- [x] Modify existing `users` migration to add: user_type, full_name, national_id, company_name, company_cert_number, contact_person_name, contact_person_id, phone, status, preferred_language, two_factor fields
|
||||
- [x] Create `consultations` migration with: user_id (FK), booking_date, booking_time, problem_summary, consultation_type, payment_amount, payment_status, status, admin_notes
|
||||
- [x] Create `timelines` migration with: user_id (FK), case_name, case_reference (unique nullable), status
|
||||
- [x] Create `timeline_updates` migration with: timeline_id (FK), admin_id (FK), update_text
|
||||
- [x] Create `posts` migration with: title (JSON), body (JSON), status, published_at
|
||||
- [x] Create `working_hours` migration with: day_of_week, start_time, end_time, is_active
|
||||
- [x] Create `blocked_times` migration with: block_date, start_time (nullable), end_time (nullable), reason
|
||||
- [x] Create `notifications` migration with: user_id (FK), type, data (JSON), read_at, sent_at
|
||||
- [x] Create `admin_logs` migration with: admin_id (FK), action, target_type, target_id, old_values (JSON), new_values (JSON), ip_address
|
||||
- [x] Add indexes: users.email (unique), consultations.booking_date, consultations.user_id, timelines.user_id, posts.status
|
||||
|
||||
- [ ] **Task 3: Create Eloquent Models** (AC: 3, 4)
|
||||
- [ ] Update `app/Models/User.php` with relationships, casts, scopes, and helper methods
|
||||
- [ ] Create `app/Models/Consultation.php` with user relationship and enum casts
|
||||
- [ ] Create `app/Models/Timeline.php` with user relationship and updates relationship
|
||||
- [ ] Create `app/Models/TimelineUpdate.php` with timeline and admin relationships
|
||||
- [ ] Create `app/Models/Post.php` with JSON casts for bilingual fields
|
||||
- [ ] Create `app/Models/WorkingHour.php`
|
||||
- [ ] Create `app/Models/BlockedTime.php`
|
||||
- [ ] Create `app/Models/Notification.php` with user relationship and JSON data cast
|
||||
- [ ] Create `app/Models/AdminLog.php` with admin relationship and JSON casts
|
||||
- [x] **Task 3: Create Eloquent Models** (AC: 3, 4)
|
||||
- [x] Update `app/Models/User.php` with relationships, casts, scopes, and helper methods
|
||||
- [x] Create `app/Models/Consultation.php` with user relationship and enum casts
|
||||
- [x] Create `app/Models/Timeline.php` with user relationship and updates relationship
|
||||
- [x] Create `app/Models/TimelineUpdate.php` with timeline and admin relationships
|
||||
- [x] Create `app/Models/Post.php` with JSON casts for bilingual fields
|
||||
- [x] Create `app/Models/WorkingHour.php`
|
||||
- [x] Create `app/Models/BlockedTime.php`
|
||||
- [x] Create `app/Models/Notification.php` with user relationship and JSON data cast
|
||||
- [x] Create `app/Models/AdminLog.php` with admin relationship and JSON casts
|
||||
|
||||
- [ ] **Task 4: Create Model Factories** (AC: 4, 8)
|
||||
- [ ] Update `database/factories/UserFactory.php` with states: admin(), individual(), company(), client()
|
||||
- [ ] Create `database/factories/ConsultationFactory.php`
|
||||
- [ ] Create `database/factories/TimelineFactory.php`
|
||||
- [ ] Create `database/factories/TimelineUpdateFactory.php`
|
||||
- [ ] Create `database/factories/PostFactory.php` with bilingual fake data
|
||||
- [ ] Create `database/factories/WorkingHourFactory.php` with weekdays() state
|
||||
- [ ] Create `database/factories/BlockedTimeFactory.php`
|
||||
- [ ] Create `database/factories/NotificationFactory.php`
|
||||
- [ ] Create `database/factories/AdminLogFactory.php`
|
||||
- [x] **Task 4: Create Model Factories** (AC: 4, 8)
|
||||
- [x] Update `database/factories/UserFactory.php` with states: admin(), individual(), company(), client()
|
||||
- [x] Create `database/factories/ConsultationFactory.php`
|
||||
- [x] Create `database/factories/TimelineFactory.php`
|
||||
- [x] Create `database/factories/TimelineUpdateFactory.php`
|
||||
- [x] Create `database/factories/PostFactory.php` with bilingual fake data
|
||||
- [x] Create `database/factories/WorkingHourFactory.php` with weekdays() state
|
||||
- [x] Create `database/factories/BlockedTimeFactory.php`
|
||||
- [x] Create `database/factories/NotificationFactory.php`
|
||||
- [x] Create `database/factories/AdminLogFactory.php`
|
||||
|
||||
- [ ] **Task 5: Write Tests** (AC: 7, 8)
|
||||
- [ ] Create `tests/Unit/Models/UserTest.php` - test enum validation, relationships
|
||||
- [ ] Create `tests/Unit/Models/ConsultationTest.php` - test enum validation, relationships
|
||||
- [ ] Create `tests/Unit/Models/TimelineTest.php` - test relationships
|
||||
- [ ] Create `tests/Unit/Enums/` tests for all enums
|
||||
- [ ] Create `tests/Feature/Database/MigrationTest.php` - test migrate:fresh and rollback
|
||||
- [ ] Create `tests/Feature/Database/FactoryTest.php` - test all factories create valid models
|
||||
- [x] **Task 5: Write Tests** (AC: 7, 8)
|
||||
- [x] Create `tests/Unit/Models/UserTest.php` - test enum validation, relationships
|
||||
- [x] Create `tests/Unit/Models/ConsultationTest.php` - test enum validation, relationships
|
||||
- [x] Create `tests/Unit/Models/TimelineTest.php` - test relationships
|
||||
- [x] Create `tests/Unit/Enums/` tests for all enums
|
||||
- [x] Create `tests/Feature/Database/MigrationTest.php` - test migrate:fresh and rollback
|
||||
- [x] Create `tests/Feature/Database/FactoryTest.php` - test all factories create valid models
|
||||
|
||||
- [ ] **Task 6: Verify Development Environment** (AC: 5, 6)
|
||||
- [ ] Ensure `composer run dev` starts without errors
|
||||
- [ ] Verify SQLite database is created and migrations run
|
||||
- [ ] Run `php artisan migrate:fresh` successfully
|
||||
- [ ] Run `vendor/bin/pint` to format code
|
||||
- [x] **Task 6: Verify Development Environment** (AC: 5, 6)
|
||||
- [x] Ensure `composer run dev` starts without errors
|
||||
- [x] Verify SQLite database is created and migrations run
|
||||
- [x] Run `php artisan migrate:fresh` successfully
|
||||
- [x] Run `vendor/bin/pint` to format code
|
||||
|
||||
## Dev Notes
|
||||
|
||||
|
|
@ -394,9 +394,211 @@ test('user type only accepts valid values', function () {
|
|||
- **Factory errors:** Ensure factories define all required (non-nullable) fields and use valid enum values
|
||||
- **Rollback:** Use `php artisan migrate:fresh` to reset in development
|
||||
|
||||
## Dev Agent Record
|
||||
|
||||
### Agent Model Used
|
||||
Claude Opus 4.5 (claude-opus-4-5-20251101)
|
||||
|
||||
### File List
|
||||
|
||||
**New Files Created:**
|
||||
- `app/Enums/UserType.php`
|
||||
- `app/Enums/UserStatus.php`
|
||||
- `app/Enums/ConsultationType.php`
|
||||
- `app/Enums/ConsultationStatus.php`
|
||||
- `app/Enums/PaymentStatus.php`
|
||||
- `app/Enums/TimelineStatus.php`
|
||||
- `app/Enums/PostStatus.php`
|
||||
- `app/Models/Consultation.php`
|
||||
- `app/Models/Timeline.php`
|
||||
- `app/Models/TimelineUpdate.php`
|
||||
- `app/Models/Post.php`
|
||||
- `app/Models/WorkingHour.php`
|
||||
- `app/Models/BlockedTime.php`
|
||||
- `app/Models/Notification.php`
|
||||
- `app/Models/AdminLog.php`
|
||||
- `database/migrations/2025_12_26_000001_create_consultations_table.php`
|
||||
- `database/migrations/2025_12_26_000002_create_timelines_table.php`
|
||||
- `database/migrations/2025_12_26_000003_create_timeline_updates_table.php`
|
||||
- `database/migrations/2025_12_26_000004_create_posts_table.php`
|
||||
- `database/migrations/2025_12_26_000005_create_working_hours_table.php`
|
||||
- `database/migrations/2025_12_26_000006_create_blocked_times_table.php`
|
||||
- `database/migrations/2025_12_26_000007_create_notifications_table.php`
|
||||
- `database/migrations/2025_12_26_000008_create_admin_logs_table.php`
|
||||
- `database/factories/ConsultationFactory.php`
|
||||
- `database/factories/TimelineFactory.php`
|
||||
- `database/factories/TimelineUpdateFactory.php`
|
||||
- `database/factories/PostFactory.php`
|
||||
- `database/factories/WorkingHourFactory.php`
|
||||
- `database/factories/BlockedTimeFactory.php`
|
||||
- `database/factories/NotificationFactory.php`
|
||||
- `database/factories/AdminLogFactory.php`
|
||||
- `tests/Unit/Enums/UserTypeTest.php`
|
||||
- `tests/Unit/Enums/UserStatusTest.php`
|
||||
- `tests/Unit/Enums/ConsultationTypeTest.php`
|
||||
- `tests/Unit/Enums/ConsultationStatusTest.php`
|
||||
- `tests/Unit/Enums/PaymentStatusTest.php`
|
||||
- `tests/Unit/Enums/TimelineStatusTest.php`
|
||||
- `tests/Unit/Enums/PostStatusTest.php`
|
||||
- `tests/Unit/Models/UserTest.php`
|
||||
- `tests/Unit/Models/ConsultationTest.php`
|
||||
- `tests/Unit/Models/TimelineTest.php`
|
||||
- `tests/Feature/Database/MigrationTest.php`
|
||||
- `tests/Feature/Database/FactoryTest.php`
|
||||
|
||||
**Modified Files:**
|
||||
- `app/Models/User.php` - Changed from `name` to `full_name`, added new fields, enum casts, relationships, scopes, and helper methods
|
||||
- `database/migrations/0001_01_01_000000_create_users_table.php` - Updated schema with all new user fields
|
||||
- `database/factories/UserFactory.php` - Updated with new fields and states (admin, individual, company, client, withTwoFactor)
|
||||
- `tests/Pest.php` - Added RefreshDatabase for Unit/Models tests
|
||||
- `app/Actions/Fortify/CreateNewUser.php` - Changed from `name` to `full_name`, added `phone` field
|
||||
- `resources/views/livewire/settings/profile.blade.php` - Changed from `name` to `full_name`
|
||||
- `resources/views/livewire/auth/register.blade.php` - Changed from `name` to `full_name`, added `phone` field
|
||||
- `tests/Feature/Auth/RegistrationTest.php` - Updated to use `full_name` and `phone`
|
||||
- `tests/Feature/Auth/AuthenticationTest.php` - Updated to use `withTwoFactor()` factory state
|
||||
- `tests/Feature/Auth/TwoFactorChallengeTest.php` - Updated to use `withTwoFactor()` factory state
|
||||
- `tests/Feature/Settings/ProfileUpdateTest.php` - Updated to use `full_name`
|
||||
|
||||
### Debug Log References
|
||||
None - no significant debugging issues encountered.
|
||||
|
||||
### Completion Notes
|
||||
1. **Breaking Change:** User model now uses `full_name` instead of `name`. All existing code referencing `user->name` must be updated to `user->full_name`.
|
||||
2. **Registration Change:** Registration now requires `phone` field in addition to `full_name`, `email`, and `password`.
|
||||
3. **Factory Default:** UserFactory no longer includes two-factor authentication fields by default. Use `->withTwoFactor()` state to create users with 2FA enabled.
|
||||
4. All 97 tests pass successfully (including existing auth tests that were updated for compatibility).
|
||||
5. All migrations run successfully with `php artisan migrate:fresh`.
|
||||
6. Code formatted with `vendor/bin/pint`.
|
||||
|
||||
## Change Log
|
||||
|
||||
| Date | Version | Description | Author |
|
||||
|------|---------|-------------|--------|
|
||||
| Dec 21, 2025 | 1.0 | Initial story draft | SM Agent |
|
||||
| Dec 21, 2025 | 1.1 | Fixed schema alignment with architecture (booking_date/time), added Tasks/Subtasks, added Dev Notes with source tree and enums, added Change Log | Validation Task |
|
||||
| Dec 26, 2025 | 1.2 | Implementation complete: Created 7 enums, 8 migrations, 9 models, 9 factories, comprehensive tests. Updated existing auth code to use full_name instead of name. All 97 tests pass. | Dev Agent (Claude Opus 4.5) |
|
||||
|
||||
## QA Results
|
||||
|
||||
### Review Date: 2025-12-26
|
||||
|
||||
### Reviewed By: Quinn (Test Architect)
|
||||
|
||||
### Risk Assessment
|
||||
- **Review Depth:** Standard (no auto-escalation triggers)
|
||||
- **Risk Signals:** Foundation story, no auth/payment/security business logic, diff within normal range
|
||||
- **Previous Gates:** N/A (first story)
|
||||
|
||||
### Code Quality Assessment
|
||||
|
||||
**Overall: EXCELLENT**
|
||||
|
||||
The implementation is well-structured, follows Laravel 12 conventions, and adheres to the architecture document specifications. Key observations:
|
||||
|
||||
1. **Enums:** All 7 PHP 8.1+ backed enums implemented correctly with proper string values matching architecture spec
|
||||
2. **Models:** All 9 models created with appropriate:
|
||||
- Enum casts using `casts()` method (Laravel 11+ pattern)
|
||||
- Eloquent relationships with proper return type hints
|
||||
- Helper methods and scopes where appropriate
|
||||
3. **Migrations:** All 8 migrations follow Laravel conventions with:
|
||||
- Proper foreign key constraints with cascade deletes
|
||||
- Required indexes on frequently queried columns
|
||||
- Correct data types matching schema specification
|
||||
4. **Factories:** All 9 factories implemented with:
|
||||
- Useful state methods (admin, individual, company, client, withTwoFactor)
|
||||
- Proper enum usage in default values
|
||||
- Bilingual JSON data for Post factory
|
||||
|
||||
### Requirements Traceability
|
||||
|
||||
| AC# | Acceptance Criteria | Test Coverage | Status |
|
||||
|-----|---------------------|---------------|--------|
|
||||
| 1 | Laravel 12 + Livewire 3, Volt, Flux UI | Manual verification | ✓ |
|
||||
| 2 | Tailwind CSS 4 configured | Manual verification | ✓ |
|
||||
| 3 | Database migrations for all core tables | MigrationTest.php | ✓ |
|
||||
| 4 | Model factories created for testing | FactoryTest.php | ✓ |
|
||||
| 5 | Development environment working | Manual verification | ✓ |
|
||||
| 6 | SQLite configured for development | .env + test runner | ✓ |
|
||||
| 7 | All migrations run without errors | MigrationTest.php | ✓ |
|
||||
| 8 | Factories generate valid test data | FactoryTest.php | ✓ |
|
||||
| 9 | All database tables have proper indexes | Migration code review | ✓ |
|
||||
| 10 | Foreign key constraints properly defined | Migration code review | ✓ |
|
||||
|
||||
### Test Architecture Assessment
|
||||
|
||||
**Tests Reviewed:** 97 tests (all passing)
|
||||
**Test Distribution:**
|
||||
- Unit/Enums: 7 tests (enum value validation)
|
||||
- Unit/Models: 12 tests (casts, relationships, scopes, helpers)
|
||||
- Feature/Database: 6 tests (migrations, rollback, factories)
|
||||
- Feature/Auth: Pre-existing auth tests (updated for compatibility)
|
||||
|
||||
**Test Quality:**
|
||||
- Good coverage of happy paths
|
||||
- Enum tests verify correct case counts and values
|
||||
- Factory tests validate model creation and relationships
|
||||
- Migration tests verify schema completeness and rollback capability
|
||||
|
||||
**Coverage Gaps Identified:**
|
||||
- No edge case tests for model scopes with empty datasets
|
||||
- No tests for JSON field validation on Post model
|
||||
- Timeline relationship tests could be more comprehensive
|
||||
|
||||
### Refactoring Performed
|
||||
|
||||
None required. Code quality is high and follows established patterns.
|
||||
|
||||
### Compliance Check
|
||||
|
||||
- Coding Standards: ✓ Code follows Laravel conventions
|
||||
- Project Structure: ✓ Files in correct locations per architecture doc
|
||||
- Testing Strategy: ✓ Pest tests using correct patterns
|
||||
- All ACs Met: ✓ All 10 acceptance criteria satisfied
|
||||
|
||||
### Improvements Checklist
|
||||
|
||||
- [x] All enums implemented with correct values
|
||||
- [x] All migrations created with proper constraints
|
||||
- [x] All models have relationships and casts
|
||||
- [x] All factories create valid models
|
||||
- [x] Tests verify core functionality
|
||||
- [ ] Minor: Consider adding `scopeByType($type)` to User model for flexibility
|
||||
- [x] Minor: Post model could benefit from `getTitle($locale)` and `getBody($locale)` helpers - **Added by QA**
|
||||
- [ ] Future: Add more comprehensive relationship tests when features use them
|
||||
|
||||
### Security Review
|
||||
|
||||
**Status: PASS**
|
||||
|
||||
- `national_id` properly marked as hidden in User model (serialization protection)
|
||||
- `password` uses hashed cast
|
||||
- Two-factor secrets hidden from serialization
|
||||
- No SQL injection vulnerabilities (using Eloquent)
|
||||
- Cascade deletes properly configured to prevent orphaned records
|
||||
|
||||
### Performance Considerations
|
||||
|
||||
**Status: PASS**
|
||||
|
||||
- Indexes added on: `users.email`, `consultations.booking_date`, `consultations.user_id`, `timelines.user_id`, `posts.status`
|
||||
- Foreign keys properly indexed via `constrained()` method
|
||||
- No N+1 query concerns at this foundation level
|
||||
|
||||
### Files Modified During Review
|
||||
|
||||
- `app/Models/Post.php` - Added `getTitle($locale)` and `getBody($locale)` helper methods
|
||||
- `tests/Unit/Models/PostTest.php` - Created with 5 tests for locale helpers
|
||||
|
||||
### Pint Formatting Note
|
||||
|
||||
8 test files have minor formatting issues (missing trailing newlines). These are pre-existing files not created by this story. Recommend running `vendor/bin/pint` to fix, but not blocking.
|
||||
|
||||
### Gate Status
|
||||
|
||||
Gate: **PASS** → docs/qa/gates/1.1-project-setup-database-schema.yml
|
||||
|
||||
### Recommended Status
|
||||
|
||||
✓ **Ready for Done**
|
||||
|
||||
The implementation is complete, well-tested, and follows all architectural guidelines. All acceptance criteria are met with comprehensive test coverage. Minor improvement suggestions are optional and can be addressed in future stories as needed.
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@
|
|||
|
||||
<form method="POST" action="{{ route('register.store') }}" class="flex flex-col gap-6">
|
||||
@csrf
|
||||
<!-- Name -->
|
||||
<!-- Full Name -->
|
||||
<flux:input
|
||||
name="name"
|
||||
:label="__('Name')"
|
||||
:value="old('name')"
|
||||
name="full_name"
|
||||
:label="__('Full Name')"
|
||||
:value="old('full_name')"
|
||||
type="text"
|
||||
required
|
||||
autofocus
|
||||
|
|
@ -30,6 +30,17 @@
|
|||
placeholder="email@example.com"
|
||||
/>
|
||||
|
||||
<!-- Phone -->
|
||||
<flux:input
|
||||
name="phone"
|
||||
:label="__('Phone')"
|
||||
:value="old('phone')"
|
||||
type="tel"
|
||||
required
|
||||
autocomplete="tel"
|
||||
:placeholder="__('Phone number')"
|
||||
/>
|
||||
|
||||
<!-- Password -->
|
||||
<flux:input
|
||||
name="password"
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use Illuminate\Validation\Rule;
|
|||
use Livewire\Volt\Component;
|
||||
|
||||
new class extends Component {
|
||||
public string $name = '';
|
||||
public string $full_name = '';
|
||||
public string $email = '';
|
||||
|
||||
/**
|
||||
|
|
@ -15,7 +15,7 @@ new class extends Component {
|
|||
*/
|
||||
public function mount(): void
|
||||
{
|
||||
$this->name = Auth::user()->name;
|
||||
$this->full_name = Auth::user()->full_name;
|
||||
$this->email = Auth::user()->email;
|
||||
}
|
||||
|
||||
|
|
@ -27,7 +27,7 @@ new class extends Component {
|
|||
$user = Auth::user();
|
||||
|
||||
$validated = $this->validate([
|
||||
'name' => ['required', 'string', 'max:255'],
|
||||
'full_name' => ['required', 'string', 'max:255'],
|
||||
|
||||
'email' => [
|
||||
'required',
|
||||
|
|
@ -47,7 +47,7 @@ new class extends Component {
|
|||
|
||||
$user->save();
|
||||
|
||||
$this->dispatch('profile-updated', name: $user->name);
|
||||
$this->dispatch('profile-updated', name: $user->full_name);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -74,7 +74,7 @@ new class extends Component {
|
|||
|
||||
<x-settings.layout :heading="__('Profile')" :subheading="__('Update your name and email address')">
|
||||
<form wire:submit="updateProfileInformation" class="my-6 w-full space-y-6">
|
||||
<flux:input wire:model="name" :label="__('Name')" type="text" required autofocus autocomplete="name" />
|
||||
<flux:input wire:model="full_name" :label="__('Full Name')" type="text" required autofocus autocomplete="name" />
|
||||
|
||||
<div>
|
||||
<flux:input wire:model="email" :label="__('Email')" type="email" required autocomplete="email" />
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ test('login screen can be rendered', function () {
|
|||
});
|
||||
|
||||
test('users can authenticate using the login screen', function () {
|
||||
$user = User::factory()->withoutTwoFactor()->create();
|
||||
$user = User::factory()->create();
|
||||
|
||||
$response = $this->post(route('login.store'), [
|
||||
'email' => $user->email,
|
||||
|
|
@ -47,7 +47,7 @@ test('users with two factor enabled are redirected to two factor challenge', fun
|
|||
'confirmPassword' => true,
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user = User::factory()->withTwoFactor()->create();
|
||||
|
||||
$response = $this->post(route('login.store'), [
|
||||
'email' => $user->email,
|
||||
|
|
|
|||
|
|
@ -8,8 +8,9 @@ test('registration screen can be rendered', function () {
|
|||
|
||||
test('new users can register', function () {
|
||||
$response = $this->post(route('register.store'), [
|
||||
'name' => 'John Doe',
|
||||
'full_name' => 'John Doe',
|
||||
'email' => 'test@example.com',
|
||||
'phone' => '+1234567890',
|
||||
'password' => 'password',
|
||||
'password_confirmation' => 'password',
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ test('two factor challenge can be rendered', function () {
|
|||
'confirmPassword' => true,
|
||||
]);
|
||||
|
||||
$user = User::factory()->create();
|
||||
$user = User::factory()->withTwoFactor()->create();
|
||||
|
||||
$this->post(route('login.store'), [
|
||||
'email' => $user->email,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\ConsultationStatus;
|
||||
use App\Enums\PostStatus;
|
||||
use App\Enums\TimelineStatus;
|
||||
use App\Enums\UserStatus;
|
||||
use App\Enums\UserType;
|
||||
use App\Models\AdminLog;
|
||||
use App\Models\BlockedTime;
|
||||
use App\Models\Consultation;
|
||||
use App\Models\Notification;
|
||||
use App\Models\Post;
|
||||
use App\Models\Timeline;
|
||||
use App\Models\TimelineUpdate;
|
||||
use App\Models\User;
|
||||
use App\Models\WorkingHour;
|
||||
|
||||
test('user factory creates valid user', function () {
|
||||
$user = User::factory()->create();
|
||||
|
||||
expect($user)->toBeInstanceOf(User::class)
|
||||
->and($user->user_type)->toBeInstanceOf(UserType::class)
|
||||
->and($user->status)->toBeInstanceOf(UserStatus::class)
|
||||
->and($user->full_name)->toBeString()
|
||||
->and($user->email)->toBeString()
|
||||
->and($user->phone)->toBeString();
|
||||
});
|
||||
|
||||
test('user factory admin state creates admin user', function () {
|
||||
$user = User::factory()->admin()->create();
|
||||
|
||||
expect($user->user_type)->toBe(UserType::Admin);
|
||||
});
|
||||
|
||||
test('user factory individual state creates individual user', function () {
|
||||
$user = User::factory()->individual()->create();
|
||||
|
||||
expect($user->user_type)->toBe(UserType::Individual);
|
||||
});
|
||||
|
||||
test('user factory company state creates company user', function () {
|
||||
$user = User::factory()->company()->create();
|
||||
|
||||
expect($user->user_type)->toBe(UserType::Company)
|
||||
->and($user->company_name)->toBeString()
|
||||
->and($user->company_cert_number)->toBeString();
|
||||
});
|
||||
|
||||
test('consultation factory creates valid consultation', function () {
|
||||
$consultation = Consultation::factory()->create();
|
||||
|
||||
expect($consultation)->toBeInstanceOf(Consultation::class)
|
||||
->and($consultation->user)->toBeInstanceOf(User::class)
|
||||
->and($consultation->status)->toBeInstanceOf(ConsultationStatus::class)
|
||||
->and($consultation->booking_date)->toBeInstanceOf(\Illuminate\Support\Carbon::class);
|
||||
});
|
||||
|
||||
test('timeline factory creates valid timeline', function () {
|
||||
$timeline = Timeline::factory()->create();
|
||||
|
||||
expect($timeline)->toBeInstanceOf(Timeline::class)
|
||||
->and($timeline->user)->toBeInstanceOf(User::class)
|
||||
->and($timeline->status)->toBeInstanceOf(TimelineStatus::class)
|
||||
->and($timeline->case_name)->toBeString();
|
||||
});
|
||||
|
||||
test('timeline update factory creates valid timeline update', function () {
|
||||
$update = TimelineUpdate::factory()->create();
|
||||
|
||||
expect($update)->toBeInstanceOf(TimelineUpdate::class)
|
||||
->and($update->timeline)->toBeInstanceOf(Timeline::class)
|
||||
->and($update->admin)->toBeInstanceOf(User::class)
|
||||
->and($update->update_text)->toBeString();
|
||||
});
|
||||
|
||||
test('post factory creates valid post', function () {
|
||||
$post = Post::factory()->create();
|
||||
|
||||
expect($post)->toBeInstanceOf(Post::class)
|
||||
->and($post->status)->toBeInstanceOf(PostStatus::class)
|
||||
->and($post->title)->toBeArray()
|
||||
->and($post->body)->toBeArray()
|
||||
->and($post->title)->toHaveKeys(['ar', 'en'])
|
||||
->and($post->body)->toHaveKeys(['ar', 'en']);
|
||||
});
|
||||
|
||||
test('working hour factory creates valid working hour', function () {
|
||||
$workingHour = WorkingHour::factory()->create();
|
||||
|
||||
expect($workingHour)->toBeInstanceOf(WorkingHour::class)
|
||||
->and($workingHour->day_of_week)->toBeInt()
|
||||
->and($workingHour->day_of_week)->toBeBetween(0, 6)
|
||||
->and($workingHour->is_active)->toBeBool();
|
||||
});
|
||||
|
||||
test('blocked time factory creates valid blocked time', function () {
|
||||
$blockedTime = BlockedTime::factory()->create();
|
||||
|
||||
expect($blockedTime)->toBeInstanceOf(BlockedTime::class)
|
||||
->and($blockedTime->block_date)->toBeInstanceOf(\Illuminate\Support\Carbon::class);
|
||||
});
|
||||
|
||||
test('notification factory creates valid notification', function () {
|
||||
$notification = Notification::factory()->create();
|
||||
|
||||
expect($notification)->toBeInstanceOf(Notification::class)
|
||||
->and($notification->user)->toBeInstanceOf(User::class)
|
||||
->and($notification->type)->toBeString()
|
||||
->and($notification->data)->toBeArray();
|
||||
});
|
||||
|
||||
test('admin log factory creates valid admin log', function () {
|
||||
$log = AdminLog::factory()->create();
|
||||
|
||||
expect($log)->toBeInstanceOf(AdminLog::class)
|
||||
->and($log->admin)->toBeInstanceOf(User::class)
|
||||
->and($log->action)->toBeString()
|
||||
->and($log->ip_address)->toBeString();
|
||||
});
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
test('all tables exist after migration', function () {
|
||||
expect(Schema::hasTable('users'))->toBeTrue()
|
||||
->and(Schema::hasTable('consultations'))->toBeTrue()
|
||||
->and(Schema::hasTable('timelines'))->toBeTrue()
|
||||
->and(Schema::hasTable('timeline_updates'))->toBeTrue()
|
||||
->and(Schema::hasTable('posts'))->toBeTrue()
|
||||
->and(Schema::hasTable('working_hours'))->toBeTrue()
|
||||
->and(Schema::hasTable('blocked_times'))->toBeTrue()
|
||||
->and(Schema::hasTable('notifications'))->toBeTrue()
|
||||
->and(Schema::hasTable('admin_logs'))->toBeTrue();
|
||||
});
|
||||
|
||||
test('users table has all required columns', function () {
|
||||
expect(Schema::hasColumns('users', [
|
||||
'id',
|
||||
'user_type',
|
||||
'full_name',
|
||||
'national_id',
|
||||
'company_name',
|
||||
'company_cert_number',
|
||||
'contact_person_name',
|
||||
'contact_person_id',
|
||||
'email',
|
||||
'phone',
|
||||
'password',
|
||||
'status',
|
||||
'preferred_language',
|
||||
'email_verified_at',
|
||||
'remember_token',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
]))->toBeTrue();
|
||||
});
|
||||
|
||||
test('consultations table has all required columns', function () {
|
||||
expect(Schema::hasColumns('consultations', [
|
||||
'id',
|
||||
'user_id',
|
||||
'booking_date',
|
||||
'booking_time',
|
||||
'problem_summary',
|
||||
'consultation_type',
|
||||
'payment_amount',
|
||||
'payment_status',
|
||||
'status',
|
||||
'admin_notes',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
]))->toBeTrue();
|
||||
});
|
||||
|
||||
test('timelines table has all required columns', function () {
|
||||
expect(Schema::hasColumns('timelines', [
|
||||
'id',
|
||||
'user_id',
|
||||
'case_name',
|
||||
'case_reference',
|
||||
'status',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
]))->toBeTrue();
|
||||
});
|
||||
|
||||
test('posts table has all required columns', function () {
|
||||
expect(Schema::hasColumns('posts', [
|
||||
'id',
|
||||
'title',
|
||||
'body',
|
||||
'status',
|
||||
'published_at',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
]))->toBeTrue();
|
||||
});
|
||||
|
||||
test('migrate rollback works without errors', function () {
|
||||
$result = Artisan::call('migrate:rollback', ['--force' => true]);
|
||||
expect($result)->toBe(0);
|
||||
|
||||
$result = Artisan::call('migrate', ['--force' => true]);
|
||||
expect($result)->toBe(0);
|
||||
});
|
||||
|
|
@ -15,7 +15,7 @@ test('profile information can be updated', function () {
|
|||
$this->actingAs($user);
|
||||
|
||||
$response = Volt::test('settings.profile')
|
||||
->set('name', 'Test User')
|
||||
->set('full_name', 'Test User')
|
||||
->set('email', 'test@example.com')
|
||||
->call('updateProfileInformation');
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ test('profile information can be updated', function () {
|
|||
|
||||
$user->refresh();
|
||||
|
||||
expect($user->name)->toEqual('Test User');
|
||||
expect($user->full_name)->toEqual('Test User');
|
||||
expect($user->email)->toEqual('test@example.com');
|
||||
expect($user->email_verified_at)->toBeNull();
|
||||
});
|
||||
|
|
@ -34,7 +34,7 @@ test('email verification status is unchanged when email address is unchanged', f
|
|||
$this->actingAs($user);
|
||||
|
||||
$response = Volt::test('settings.profile')
|
||||
->set('name', 'Test User')
|
||||
->set('full_name', 'Test User')
|
||||
->set('email', $user->email)
|
||||
->call('updateProfileInformation');
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ pest()->extend(Tests\TestCase::class)
|
|||
->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
|
||||
->in('Feature');
|
||||
|
||||
pest()->extend(Tests\TestCase::class)
|
||||
->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
|
||||
->in('Unit/Models');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Expectations
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\ConsultationStatus;
|
||||
|
||||
test('consultation status has correct cases', function () {
|
||||
expect(ConsultationStatus::cases())->toHaveCount(6);
|
||||
});
|
||||
|
||||
test('consultation status pending has correct value', function () {
|
||||
expect(ConsultationStatus::Pending->value)->toBe('pending');
|
||||
});
|
||||
|
||||
test('consultation status approved has correct value', function () {
|
||||
expect(ConsultationStatus::Approved->value)->toBe('approved');
|
||||
});
|
||||
|
||||
test('consultation status rejected has correct value', function () {
|
||||
expect(ConsultationStatus::Rejected->value)->toBe('rejected');
|
||||
});
|
||||
|
||||
test('consultation status completed has correct value', function () {
|
||||
expect(ConsultationStatus::Completed->value)->toBe('completed');
|
||||
});
|
||||
|
||||
test('consultation status no show has correct value', function () {
|
||||
expect(ConsultationStatus::NoShow->value)->toBe('no_show');
|
||||
});
|
||||
|
||||
test('consultation status cancelled has correct value', function () {
|
||||
expect(ConsultationStatus::Cancelled->value)->toBe('cancelled');
|
||||
});
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\ConsultationType;
|
||||
|
||||
test('consultation type has correct cases', function () {
|
||||
expect(ConsultationType::cases())->toHaveCount(2);
|
||||
});
|
||||
|
||||
test('consultation type free has correct value', function () {
|
||||
expect(ConsultationType::Free->value)->toBe('free');
|
||||
});
|
||||
|
||||
test('consultation type paid has correct value', function () {
|
||||
expect(ConsultationType::Paid->value)->toBe('paid');
|
||||
});
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\PaymentStatus;
|
||||
|
||||
test('payment status has correct cases', function () {
|
||||
expect(PaymentStatus::cases())->toHaveCount(3);
|
||||
});
|
||||
|
||||
test('payment status pending has correct value', function () {
|
||||
expect(PaymentStatus::Pending->value)->toBe('pending');
|
||||
});
|
||||
|
||||
test('payment status received has correct value', function () {
|
||||
expect(PaymentStatus::Received->value)->toBe('received');
|
||||
});
|
||||
|
||||
test('payment status not applicable has correct value', function () {
|
||||
expect(PaymentStatus::NotApplicable->value)->toBe('na');
|
||||
});
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\PostStatus;
|
||||
|
||||
test('post status has correct cases', function () {
|
||||
expect(PostStatus::cases())->toHaveCount(2);
|
||||
});
|
||||
|
||||
test('post status draft has correct value', function () {
|
||||
expect(PostStatus::Draft->value)->toBe('draft');
|
||||
});
|
||||
|
||||
test('post status published has correct value', function () {
|
||||
expect(PostStatus::Published->value)->toBe('published');
|
||||
});
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\TimelineStatus;
|
||||
|
||||
test('timeline status has correct cases', function () {
|
||||
expect(TimelineStatus::cases())->toHaveCount(2);
|
||||
});
|
||||
|
||||
test('timeline status active has correct value', function () {
|
||||
expect(TimelineStatus::Active->value)->toBe('active');
|
||||
});
|
||||
|
||||
test('timeline status archived has correct value', function () {
|
||||
expect(TimelineStatus::Archived->value)->toBe('archived');
|
||||
});
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\UserStatus;
|
||||
|
||||
test('user status has correct cases', function () {
|
||||
expect(UserStatus::cases())->toHaveCount(2);
|
||||
});
|
||||
|
||||
test('user status active has correct value', function () {
|
||||
expect(UserStatus::Active->value)->toBe('active');
|
||||
});
|
||||
|
||||
test('user status deactivated has correct value', function () {
|
||||
expect(UserStatus::Deactivated->value)->toBe('deactivated');
|
||||
});
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\UserType;
|
||||
|
||||
test('user type has correct cases', function () {
|
||||
expect(UserType::cases())->toHaveCount(3);
|
||||
});
|
||||
|
||||
test('user type admin has correct value', function () {
|
||||
expect(UserType::Admin->value)->toBe('admin');
|
||||
});
|
||||
|
||||
test('user type individual has correct value', function () {
|
||||
expect(UserType::Individual->value)->toBe('individual');
|
||||
});
|
||||
|
||||
test('user type company has correct value', function () {
|
||||
expect(UserType::Company->value)->toBe('company');
|
||||
});
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\ConsultationStatus;
|
||||
use App\Enums\ConsultationType;
|
||||
use App\Enums\PaymentStatus;
|
||||
use App\Models\Consultation;
|
||||
use App\Models\User;
|
||||
|
||||
test('consultation has correct consultation type cast', function () {
|
||||
$consultation = Consultation::factory()->create(['consultation_type' => 'free']);
|
||||
|
||||
expect($consultation->consultation_type)->toBeInstanceOf(ConsultationType::class)
|
||||
->and($consultation->consultation_type)->toBe(ConsultationType::Free);
|
||||
});
|
||||
|
||||
test('consultation has correct status cast', function () {
|
||||
$consultation = Consultation::factory()->create(['status' => 'pending']);
|
||||
|
||||
expect($consultation->status)->toBeInstanceOf(ConsultationStatus::class)
|
||||
->and($consultation->status)->toBe(ConsultationStatus::Pending);
|
||||
});
|
||||
|
||||
test('consultation has correct payment status cast', function () {
|
||||
$consultation = Consultation::factory()->create(['payment_status' => 'na']);
|
||||
|
||||
expect($consultation->payment_status)->toBeInstanceOf(PaymentStatus::class)
|
||||
->and($consultation->payment_status)->toBe(PaymentStatus::NotApplicable);
|
||||
});
|
||||
|
||||
test('consultation belongs to user', function () {
|
||||
$user = User::factory()->create();
|
||||
$consultation = Consultation::factory()->create(['user_id' => $user->id]);
|
||||
|
||||
expect($consultation->user)->toBeInstanceOf(User::class)
|
||||
->and($consultation->user->id)->toBe($user->id);
|
||||
});
|
||||
|
||||
test('consultation has booking date cast as date', function () {
|
||||
$consultation = Consultation::factory()->create();
|
||||
|
||||
expect($consultation->booking_date)->toBeInstanceOf(\Illuminate\Support\Carbon::class);
|
||||
});
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
use App\Models\Post;
|
||||
|
||||
test('post getTitle returns title in specified locale', function () {
|
||||
$post = Post::factory()->create([
|
||||
'title' => ['ar' => 'عنوان عربي', 'en' => 'English Title'],
|
||||
]);
|
||||
|
||||
expect($post->getTitle('ar'))->toBe('عنوان عربي')
|
||||
->and($post->getTitle('en'))->toBe('English Title');
|
||||
});
|
||||
|
||||
test('post getTitle falls back to Arabic when locale missing', function () {
|
||||
$post = Post::factory()->create([
|
||||
'title' => ['ar' => 'عنوان عربي'],
|
||||
]);
|
||||
|
||||
expect($post->getTitle('en'))->toBe('عنوان عربي');
|
||||
});
|
||||
|
||||
test('post getTitle uses current locale by default', function () {
|
||||
$post = Post::factory()->create([
|
||||
'title' => ['ar' => 'عنوان عربي', 'en' => 'English Title'],
|
||||
]);
|
||||
|
||||
app()->setLocale('en');
|
||||
expect($post->getTitle())->toBe('English Title');
|
||||
|
||||
app()->setLocale('ar');
|
||||
expect($post->getTitle())->toBe('عنوان عربي');
|
||||
});
|
||||
|
||||
test('post getBody returns body in specified locale', function () {
|
||||
$post = Post::factory()->create([
|
||||
'body' => ['ar' => 'محتوى عربي', 'en' => 'English Content'],
|
||||
]);
|
||||
|
||||
expect($post->getBody('ar'))->toBe('محتوى عربي')
|
||||
->and($post->getBody('en'))->toBe('English Content');
|
||||
});
|
||||
|
||||
test('post getBody falls back to Arabic when locale missing', function () {
|
||||
$post = Post::factory()->create([
|
||||
'body' => ['ar' => 'محتوى عربي'],
|
||||
]);
|
||||
|
||||
expect($post->getBody('en'))->toBe('محتوى عربي');
|
||||
});
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\TimelineStatus;
|
||||
use App\Models\Timeline;
|
||||
use App\Models\TimelineUpdate;
|
||||
use App\Models\User;
|
||||
|
||||
test('timeline belongs to user', function () {
|
||||
$user = User::factory()->create();
|
||||
$timeline = Timeline::factory()->create(['user_id' => $user->id]);
|
||||
|
||||
expect($timeline->user)->toBeInstanceOf(User::class)
|
||||
->and($timeline->user->id)->toBe($user->id);
|
||||
});
|
||||
|
||||
test('timeline has correct status cast', function () {
|
||||
$timeline = Timeline::factory()->create(['status' => 'active']);
|
||||
|
||||
expect($timeline->status)->toBeInstanceOf(TimelineStatus::class)
|
||||
->and($timeline->status)->toBe(TimelineStatus::Active);
|
||||
});
|
||||
|
||||
test('timeline has many updates', function () {
|
||||
$timeline = Timeline::factory()->create();
|
||||
TimelineUpdate::factory()->count(3)->create(['timeline_id' => $timeline->id]);
|
||||
|
||||
expect($timeline->updates)->toHaveCount(3)
|
||||
->and($timeline->updates->first())->toBeInstanceOf(TimelineUpdate::class);
|
||||
});
|
||||
|
||||
test('timeline update belongs to timeline', function () {
|
||||
$timeline = Timeline::factory()->create();
|
||||
$update = TimelineUpdate::factory()->create(['timeline_id' => $timeline->id]);
|
||||
|
||||
expect($update->timeline)->toBeInstanceOf(Timeline::class)
|
||||
->and($update->timeline->id)->toBe($timeline->id);
|
||||
});
|
||||
|
||||
test('timeline update belongs to admin', function () {
|
||||
$admin = User::factory()->admin()->create();
|
||||
$update = TimelineUpdate::factory()->create(['admin_id' => $admin->id]);
|
||||
|
||||
expect($update->admin)->toBeInstanceOf(User::class)
|
||||
->and($update->admin->id)->toBe($admin->id);
|
||||
});
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<?php
|
||||
|
||||
use App\Enums\UserStatus;
|
||||
use App\Enums\UserType;
|
||||
use App\Models\User;
|
||||
|
||||
test('user has correct user type cast', function () {
|
||||
$user = User::factory()->create(['user_type' => 'admin']);
|
||||
|
||||
expect($user->user_type)->toBeInstanceOf(UserType::class)
|
||||
->and($user->user_type)->toBe(UserType::Admin);
|
||||
});
|
||||
|
||||
test('user has correct status cast', function () {
|
||||
$user = User::factory()->create(['status' => 'active']);
|
||||
|
||||
expect($user->status)->toBeInstanceOf(UserStatus::class)
|
||||
->and($user->status)->toBe(UserStatus::Active);
|
||||
});
|
||||
|
||||
test('user is admin returns true for admin users', function () {
|
||||
$user = User::factory()->admin()->create();
|
||||
|
||||
expect($user->isAdmin())->toBeTrue()
|
||||
->and($user->isClient())->toBeFalse();
|
||||
});
|
||||
|
||||
test('user is individual returns true for individual users', function () {
|
||||
$user = User::factory()->individual()->create();
|
||||
|
||||
expect($user->isIndividual())->toBeTrue()
|
||||
->and($user->isClient())->toBeTrue();
|
||||
});
|
||||
|
||||
test('user is company returns true for company users', function () {
|
||||
$user = User::factory()->company()->create();
|
||||
|
||||
expect($user->isCompany())->toBeTrue()
|
||||
->and($user->isClient())->toBeTrue();
|
||||
});
|
||||
|
||||
test('user initials are generated correctly', function () {
|
||||
$user = User::factory()->create(['full_name' => 'John Doe']);
|
||||
|
||||
expect($user->initials())->toBe('JD');
|
||||
});
|
||||
|
||||
test('user admins scope returns only admin users', function () {
|
||||
User::factory()->admin()->count(2)->create();
|
||||
User::factory()->individual()->count(3)->create();
|
||||
|
||||
expect(User::admins()->count())->toBe(2);
|
||||
});
|
||||
|
||||
test('user clients scope returns only client users', function () {
|
||||
User::factory()->admin()->count(2)->create();
|
||||
User::factory()->individual()->count(2)->create();
|
||||
User::factory()->company()->count(1)->create();
|
||||
|
||||
expect(User::clients()->count())->toBe(3);
|
||||
});
|
||||
|
||||
test('user active scope returns only active users', function () {
|
||||
User::factory()->count(2)->create();
|
||||
User::factory()->deactivated()->count(1)->create();
|
||||
|
||||
expect(User::active()->count())->toBe(2);
|
||||
});
|
||||
Loading…
Reference in New Issue