diff --git a/app/Enums/Concerns/HasProgress.php b/app/Enums/Concerns/HasProgress.php new file mode 100644 index 0000000..1827024 --- /dev/null +++ b/app/Enums/Concerns/HasProgress.php @@ -0,0 +1,44 @@ + + */ + abstract public static function pendingCases(): array; + + /** + * Get list of complete enum cases. + * + * @return list + */ + abstract public static function completeCases(): array; + + /** + * Determine if the enum case is pending. + */ + public function isPending(): bool + { + return in_array($this, static::pendingCases()); + } + + /** + * Determine if the enum case is void (excluded from progress calculations). + */ + public function isVoid(): bool + { + return ! $this->isPending() && ! $this->isComplete(); + } + + /** + * Determine if the enum case is complete. + */ + public function isComplete(): bool + { + return in_array($this, static::completeCases()); + } +} diff --git a/app/Enums/DeviceStatus.php b/app/Enums/DeviceStatus.php index b0295c7..8941b0f 100644 --- a/app/Enums/DeviceStatus.php +++ b/app/Enums/DeviceStatus.php @@ -2,11 +2,12 @@ namespace App\Enums; +use App\Enums\Concerns\HasProgress; use App\Enums\Concerns\HasValues; enum DeviceStatus: string { - use HasValues; + use HasProgress, HasValues; /** * Device has been received and is awaiting repair. @@ -32,4 +33,23 @@ enum DeviceStatus: string * Device has been delivered to the customer. */ case Delivered = 'delivered'; + + // METHODS ///////////////////////////////////////////////////////////////////////////////////// + + public static function pendingCases(): array + { + return [ + self::Received, + self::OnHold, + self::UnderRepair, + ]; + } + + public static function completeCases(): array + { + return [ + self::Ready, + self::Delivered, + ]; + } } diff --git a/app/Enums/InvoiceStatus.php b/app/Enums/InvoiceStatus.php index ceeb6e2..171f3e6 100644 --- a/app/Enums/InvoiceStatus.php +++ b/app/Enums/InvoiceStatus.php @@ -2,11 +2,12 @@ namespace App\Enums; +use App\Enums\Concerns\HasProgress; use App\Enums\Concerns\HasValues; enum InvoiceStatus: string { - use HasValues; + use HasProgress, HasValues; /** * Invoice has been created and is awaiting approval. @@ -37,4 +38,23 @@ enum InvoiceStatus: string * Invoice has been cancelled. */ case Cancelled = 'cancelled'; + + // METHODS ///////////////////////////////////////////////////////////////////////////////////// + + public static function pendingCases(): array + { + return [ + self::Draft, + self::Issued, + self::Sent, + ]; + } + + public static function completeCases(): array + { + return [ + self::Paid, + self::Refunded, + ]; + } } diff --git a/app/Enums/OrderStatus.php b/app/Enums/OrderStatus.php index 53ee652..a617a6b 100644 --- a/app/Enums/OrderStatus.php +++ b/app/Enums/OrderStatus.php @@ -2,11 +2,12 @@ namespace App\Enums; +use App\Enums\Concerns\HasProgress; use App\Enums\Concerns\HasValues; enum OrderStatus: string { - use HasValues; + use HasProgress, HasValues; /** * Represents an order that has been created and is awaiting processing. @@ -29,4 +30,21 @@ enum OrderStatus: string * Represents an order that has been cancelled by the customer or by the system. */ case Cancelled = 'cancelled'; + + // METHODS ///////////////////////////////////////////////////////////////////////////////////// + + public static function pendingCases(): array + { + return [ + self::New, + self::Shipped, + ]; + } + + public static function completeCases(): array + { + return [ + self::Received, + ]; + } } diff --git a/app/Enums/Priority.php b/app/Enums/Priority.php new file mode 100644 index 0000000..3ff1f35 --- /dev/null +++ b/app/Enums/Priority.php @@ -0,0 +1,36 @@ +{static::ASSIGNEE_ID} !== null; + } + + /** + * Assign the model to a user. + */ + public function assignTo(User $user): void + { + $this->assignee()->associate($user)->save(); + } + + /** + * Unassign the model from a user. + */ + public function unassign(): void + { + $this->assignee()->dissociate()->save(); + } + + // RELATIONS /////////////////////////////////////////////////////////////////////////////////// + + /** + * Get the assignee (user) for the model. + */ + public function assignee(): BelongsTo + { + return $this->belongsTo(User::class, static::ASSIGNEE_ID); + } + + // SCOPES ////////////////////////////////////////////////////////////////////////////////////// + + /** + * Scope a query to only include models that are already assigned. + */ + public function scopeAssigned(Builder $query): void + { + $query->whereNotNull(static::ASSIGNEE_ID); + } + + /** + * Scope a query to only include models that are unassigned. + */ + public function scopeUnassigned(Builder $query): void + { + $query->whereNull(static::ASSIGNEE_ID); + } +} diff --git a/app/Models/Concerns/Billable.php b/app/Models/Concerns/Billable.php new file mode 100644 index 0000000..7cf016f --- /dev/null +++ b/app/Models/Concerns/Billable.php @@ -0,0 +1,46 @@ +casts[static::IS_BILLABLE] = 'boolean'; + + $this->attributes[static::IS_BILLABLE] = true; + } + + /** + * Determine if the model is billable. + */ + public function isBillable(): bool + { + return (bool) $this->{static::IS_BILLABLE}; + } + + // SCOPES ////////////////////////////////////////////////////////////////////////////////////// + + /** + * Scope a query to only include billable models. + */ + public function scopeBillable(Builder $query): void + { + $query->where(static::IS_BILLABLE, true); + } + + /** + * Scope a query to only include models that are not billable. + */ + public function scopeNotBillable(Builder $query): void + { + $query->where(static::IS_BILLABLE, false); + } +} diff --git a/app/Models/Concerns/Contactable.php b/app/Models/Concerns/Contactable.php new file mode 100644 index 0000000..28edfb2 --- /dev/null +++ b/app/Models/Concerns/Contactable.php @@ -0,0 +1,65 @@ +{static::EMAIL}; + } + + /** + * Determine if the model has a phone number. + */ + public function isCallable(): bool + { + return (bool) $this->{static::PHONE}; + } + + /** + * Determine if the model can be contacted via email or phone. + */ + public function isContactable(): bool + { + return $this->isMailable() || $this->isCallable(); + } + + // SCOPES ////////////////////////////////////////////////////////////////////////////////////// + + /** + * Scope a query to only include models that have an email address. + */ + public function scopeMailable(Builder $query): void + { + $query->whereNotNull(static::EMAIL); + } + + /** + * Scope a query to only include models that have a phone number. + */ + public function scopeCallable(Builder $query): void + { + $query->whereNotNull(static::PHONE); + } + + /** + * Scope a query to only include models that are contactable. + */ + public function scopeContactable(Builder $query): void + { + $query->where(function (Builder $query) { + $query->whereNotNull(static::EMAIL) + ->orWhereNotNull(static::PHONE); + }); + } +} diff --git a/app/Models/Concerns/HasApproval.php b/app/Models/Concerns/HasApproval.php new file mode 100644 index 0000000..75ac06c --- /dev/null +++ b/app/Models/Concerns/HasApproval.php @@ -0,0 +1,44 @@ +casts[static::APPROVED_AT] = 'datetime'; + } + + /** + * Determine if the model is approved. + */ + public function isApproved(): bool + { + return $this->{static::APPROVED_AT} !== null; + } + + // SCOPES ////////////////////////////////////////////////////////////////////////////////////// + + /** + * Scope a query to only include approved models. + */ + public function scopeApproved(Builder $query): void + { + $query->whereNotNull(static::APPROVED_AT); + } + + /** + * Scope a query to only include unapproved models. + */ + public function scopeUnapproved(Builder $query): void + { + $query->whereNull(static::APPROVED_AT); + } +} diff --git a/app/Models/Concerns/HasDueDate.php b/app/Models/Concerns/HasDueDate.php new file mode 100644 index 0000000..ecb86c2 --- /dev/null +++ b/app/Models/Concerns/HasDueDate.php @@ -0,0 +1,49 @@ +casts[static::DUE_DATE] = 'date'; + } + + /** + * Determine if the model is overdue. + */ + public function isOverdue(): bool + { + return $this->{static::DUE_DATE}?->startOfDay()->isPast() + && $this->isPending(); + } + + // SCOPES ////////////////////////////////////////////////////////////////////////////////////// + + /** + * Scope a query to only include overdue models. + */ + public function scopeOverdue(Builder $query): void + { + $query + ->where(static::DUE_DATE, '<', now()->startOfDay()) + ->pending(); + } + + /** + * Scope a query to only include models that are not overdue. + */ + public function scopeNotOverdue(Builder $query): void + { + $query + ->whereNotNull(static::DUE_DATE) + ->where(static::DUE_DATE, '>=', now()->startOfDay()); + } +} diff --git a/app/Models/Concerns/HasPriority.php b/app/Models/Concerns/HasPriority.php new file mode 100644 index 0000000..615aaab --- /dev/null +++ b/app/Models/Concerns/HasPriority.php @@ -0,0 +1,60 @@ +casts[static::PRIORITY] = Priority::class; + + $this->attributes[static::PRIORITY] = Priority::Medium; + } + + /** + * Determine if the model has urgent priority. + */ + public function isUrgent(): bool + { + return $this->{static::PRIORITY} === Priority::Urgent; + } + + // SCOPES ////////////////////////////////////////////////////////////////////////////////////// + + /** + * Scope a query to only include models with the given priority. + */ + public function scopeOfPriority(Builder $query, Priority $priority): void + { + $query->where(static::PRIORITY, $priority->value); + } + + /** + * Scope a query to only include urgent priority models. + */ + public function scopeUrgent(Builder $query): void + { + $query->ofPriority(Priority::Urgent); + } + + /** + * Scope a query to order models by priority from high to low. + */ + public function scopePrioritized(Builder $query): void + { + $query->orderByRaw('CASE '.static::PRIORITY.' + WHEN ? THEN 1 + WHEN ? THEN 2 + WHEN ? THEN 3 + WHEN ? THEN 4 + END DESC', Priority::values()); + } +} diff --git a/app/Models/Concerns/HasProgress.php b/app/Models/Concerns/HasProgress.php new file mode 100644 index 0000000..4a49c08 --- /dev/null +++ b/app/Models/Concerns/HasProgress.php @@ -0,0 +1,63 @@ +{static::STATUS}->isPending(); + } + + /** + * Determine if the model status is void (excluded from progress calculations). + */ + public function isVoid(): bool + { + return $this->{static::STATUS}->isVoid(); + } + + /** + * Determine if the model status is complete. + */ + public function isComplete(): bool + { + return $this->{static::STATUS}->isComplete(); + } + + // SCOPES ////////////////////////////////////////////////////////////////////////////////////// + + /** + * Scope a query to only include pending models. + */ + public function scopePending(Builder $query): void + { + $query->whereIn(static::STATUS, $this->{static::STATUS}::pendingCases()); + } + + /** + * Scope a query to only include void models. + */ + public function scopeVoid(Builder $query): void + { + $query->whereNotIn(static::STATUS, [ + ...$this->{static::STATUS}::pendingCases(), + ...$this->{static::STATUS}::completeCases(), + ]); + } + + /** + * Scope a query to only include complete models. + */ + public function scopeComplete(Builder $query): void + { + $query->whereIn(static::STATUS, $this->{static::STATUS}::completeCases()); + } +} diff --git a/app/Models/Concerns/HasStatus.php b/app/Models/Concerns/HasStatus.php new file mode 100644 index 0000000..50d79f6 --- /dev/null +++ b/app/Models/Concerns/HasStatus.php @@ -0,0 +1,38 @@ +attributes[static::STATUS] ?? null; + + // Use the class of the default status + $this->casts[static::STATUS] = $defaultStatus::class; + } + + // SCOPES ////////////////////////////////////////////////////////////////////////////////////// + + /** + * Scope a query to only include users with the specified status. + * + * @param TStatus $status + */ + public function scopeOfStatus(Builder $query, BackedEnum $status): void + { + $query->where(self::STATUS, $status->value); + } +} diff --git a/app/Models/Concerns/HasType.php b/app/Models/Concerns/HasType.php new file mode 100644 index 0000000..10b691d --- /dev/null +++ b/app/Models/Concerns/HasType.php @@ -0,0 +1,38 @@ +attributes[static::TYPE]; + + // Use the class of the default type + $this->casts[static::TYPE] = $defaultType::class; + } + + // SCOPES ////////////////////////////////////////////////////////////////////////////////////// + + /** + * Scope a query to only include records with the specified type. + * + * @param TType $type + */ + public function scopeOfType(Builder $query, BackedEnum $type): void + { + $query->where(self::TYPE, $type->value); + } +} diff --git a/app/Models/Concerns/HasWarranty.php b/app/Models/Concerns/HasWarranty.php new file mode 100644 index 0000000..da9a48f --- /dev/null +++ b/app/Models/Concerns/HasWarranty.php @@ -0,0 +1,36 @@ +casts[static::WARRANTY_EXPIRE_DATE] = 'date'; + } + + /** + * Determine if the model has valid warranty. + */ + public function hasWarranty(): bool + { + return $this->warranty_expire_date > now(); + } + + // SCOPES ////////////////////////////////////////////////////////////////////////////////////// + + /** + * Scope a query to only include models with warranty. + */ + public function scopeWithWarranty(Builder $query): void + { + $query->where(static::WARRANTY_EXPIRE_DATE, '>', now()); + } +} diff --git a/app/Models/Customer.php b/app/Models/Customer.php index 5b3fba3..1c6f83c 100644 --- a/app/Models/Customer.php +++ b/app/Models/Customer.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Models\Concerns\Contactable; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -10,7 +11,7 @@ class Customer extends Model { /** @use HasFactory<\Database\Factories\CustomerFactory> */ - use HasFactory; + use Contactable, HasFactory; /** * The attributes that are mass assignable. diff --git a/app/Models/Device.php b/app/Models/Device.php index 965404d..a1f3b0c 100644 --- a/app/Models/Device.php +++ b/app/Models/Device.php @@ -4,7 +4,10 @@ use App\Enums\DeviceStatus; use App\Enums\DeviceType; -use Illuminate\Database\Eloquent\Builder; +use App\Models\Concerns\HasProgress; +use App\Models\Concerns\HasStatus; +use App\Models\Concerns\HasType; +use App\Models\Concerns\HasWarranty; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -12,8 +15,12 @@ class Device extends Model { - /** @use HasFactory<\Database\Factories\DeviceFactory> */ - use HasFactory; + /** + * @use HasFactory<\Database\Factories\DeviceFactory> + * @use HasStatus<\App\Enums\DeviceStatus> + * @use HasType<\App\Enums\DeviceType> + */ + use HasFactory, HasProgress, HasStatus, HasType, HasWarranty; /** * The model's default values for attributes. @@ -47,9 +54,6 @@ class Device extends Model */ protected $casts = [ 'purchase_date' => 'date', - 'warranty_expire_date' => 'date', - 'type' => DeviceType::class, - 'status' => DeviceStatus::class, ]; // RELATIONS /////////////////////////////////////////////////////////////////////////////////// @@ -69,40 +73,4 @@ public function tickets(): HasMany { return $this->hasMany(Ticket::class); } - - // SCOPES ////////////////////////////////////////////////////////////////////////////////////// - - /** - * Scope a query to only include devices with warranty. - */ - public function scopeWithWarranty(Builder $query): void - { - $query->where('warranty_expire_date', '>', now()); - } - - /** - * Scope a query to only include devices of a given type. - */ - public function scopeOfType(Builder $query, DeviceType $type): void - { - $query->where('type', $type->value); - } - - /** - * Scope a query to only include devices of a given status. - */ - public function scopeOfStatus(Builder $query, DeviceStatus $status): void - { - $query->where('status', $status->value); - } - - // METHODS ///////////////////////////////////////////////////////////////////////////////////// - - /** - * Determine if the device has valid warranty. - */ - public function hasWarranty(): bool - { - return $this->warranty_expire_date > now(); - } } diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index d06d395..b01e0c9 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -3,7 +3,9 @@ namespace App\Models; use App\Enums\InvoiceStatus; -use Illuminate\Database\Eloquent\Builder; +use App\Models\Concerns\HasDueDate; +use App\Models\Concerns\HasProgress; +use App\Models\Concerns\HasStatus; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -11,8 +13,11 @@ class Invoice extends Model { - /** @use HasFactory<\Database\Factories\InvoiceFactory> */ - use HasFactory; + /** + * @use HasFactory<\Database\Factories\InvoiceFactory> + * @use HasStatus<\App\Enums\InvoiceStatus> + */ + use HasDueDate, HasFactory, HasProgress, HasStatus; /** * The model's default values for attributes. @@ -49,8 +54,6 @@ class Invoice extends Model 'discount_amount' => 'float', 'paid_amount' => 'float', 'refunded_amount' => 'float', - 'due_date' => 'date', - 'status' => InvoiceStatus::class, ]; // RELATIONS /////////////////////////////////////////////////////////////////////////////////// @@ -86,41 +89,4 @@ public function transactions(): HasMany { return $this->hasMany(Transaction::class); } - - // SCOPES ////////////////////////////////////////////////////////////////////////////////////// - - /** - * Scope a query to only include invoices with the specified status. - */ - public function scopeOfStatus(Builder $query, InvoiceStatus $status): void - { - $query->where('status', $status->value); - } - - /** - * Scope a query to only include tickets that are overdue. - */ - public function scopeOverdue(Builder $query): void - { - $query - ->where('due_date', '<', now()) - ->whereIn('status', [ - InvoiceStatus::Issued->value, - InvoiceStatus::Sent->value, - ]); - } - - // METHODS ///////////////////////////////////////////////////////////////////////////////////// - - /** - * Determine if the invoice is overdue. - */ - public function isOverdue(): bool - { - return $this->due_date->isPast() - && in_array($this->status, [ - InvoiceStatus::Issued, - InvoiceStatus::Sent, - ]); - } } diff --git a/app/Models/Order.php b/app/Models/Order.php index d63cedf..96c6867 100644 --- a/app/Models/Order.php +++ b/app/Models/Order.php @@ -3,14 +3,20 @@ namespace App\Models; use App\Enums\OrderStatus; -use Illuminate\Database\Eloquent\Builder; +use App\Models\Concerns\Billable; +use App\Models\Concerns\HasApproval; +use App\Models\Concerns\HasProgress; +use App\Models\Concerns\HasStatus; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Order extends Model { - /** @use HasFactory<\Database\Factories\OrderFactory> */ - use HasFactory; + /** + * @use HasFactory<\Database\Factories\OrderFactory> + * @use HasStatus<\App\Enums\OrderStatus> + */ + use Billable, HasApproval, HasFactory, HasProgress, HasStatus; /** * The model's default values for attributes. @@ -19,7 +25,6 @@ class Order extends Model */ protected $attributes = [ 'quantity' => 1, - 'is_billable' => true, 'status' => OrderStatus::New, ]; @@ -47,9 +52,6 @@ class Order extends Model protected $casts = [ 'quantity' => 'integer', 'cost' => 'float', - 'is_billable' => 'boolean', - 'status' => OrderStatus::class, - 'approved_at' => 'datetime', ]; // RELATIONS /////////////////////////////////////////////////////////////////////////////////// @@ -61,30 +63,4 @@ public function ticket() { return $this->belongsTo(Ticket::class); } - - // SCOPES ////////////////////////////////////////////////////////////////////////////////////// - - /** - * Scope a query to only include orders that are billable. - */ - public function scopeBillable(Builder $query): void - { - $query->where('is_billable', true); - } - - /** - * Scope a query to only include orders of a given status. - */ - public function scopeOfStatus(Builder $query, OrderStatus $status): void - { - $query->where('status', $status->value); - } - - /** - * Scope a query to only include approved orders. - */ - public function scopeApproved(Builder $query): void - { - $query->whereNotNull('approved_at'); - } } diff --git a/app/Models/Task.php b/app/Models/Task.php index 73e8a86..a38c1ad 100644 --- a/app/Models/Task.php +++ b/app/Models/Task.php @@ -4,14 +4,22 @@ use App\Enums\TaskStatus; use App\Enums\TaskType; -use Illuminate\Database\Eloquent\Builder; +use App\Models\Concerns\Billable; +use App\Models\Concerns\HasApproval; +use App\Models\Concerns\HasProgress; +use App\Models\Concerns\HasStatus; +use App\Models\Concerns\HasType; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Task extends Model { - /** @use HasFactory<\Database\Factories\TaskFactory> */ - use HasFactory; + /** + * @use HasFactory<\Database\Factories\TaskFactory> + * @use HasStatus<\App\Enums\TaskStatus> + * @use HasType<\App\Enums\TaskType> + */ + use Billable, HasApproval, HasFactory, HasProgress, HasStatus, HasType; /** * The model's default values for attributes. @@ -19,7 +27,6 @@ class Task extends Model * @var array */ protected $attributes = [ - 'is_billable' => true, 'type' => TaskType::Repair, 'status' => TaskStatus::New, ]; @@ -45,10 +52,6 @@ class Task extends Model */ protected $casts = [ 'cost' => 'float', - 'is_billable' => 'boolean', - 'type' => TaskType::class, - 'status' => TaskStatus::class, - 'approved_at' => 'datetime', ]; // RELATIONS /////////////////////////////////////////////////////////////////////////////////// @@ -60,38 +63,4 @@ public function ticket() { return $this->belongsTo(Ticket::class); } - - // SCOPES ////////////////////////////////////////////////////////////////////////////////////// - - /** - * Scope a query to only include tasks that are billable. - */ - public function scopeBillable(Builder $query): void - { - $query->where('is_billable', true); - } - - /** - * Scope a query to only include tasks of a given type. - */ - public function scopeOfType(Builder $query, TaskType $type): void - { - $query->where('type', $type->value); - } - - /** - * Scope a query to only include tasks of a given status. - */ - public function scopeOfStatus(Builder $query, TaskStatus $status): void - { - $query->where('status', $status->value); - } - - /** - * Scope a query to only include approved tasks. - */ - public function scopeApproved(Builder $query): void - { - $query->whereNotNull('approved_at'); - } } diff --git a/app/Models/Ticket.php b/app/Models/Ticket.php index ab8a742..fa63f7f 100644 --- a/app/Models/Ticket.php +++ b/app/Models/Ticket.php @@ -2,9 +2,12 @@ namespace App\Models; -use App\Enums\TicketPriority; use App\Enums\TicketStatus; -use Illuminate\Database\Eloquent\Builder; +use App\Models\Concerns\Assignable; +use App\Models\Concerns\HasDueDate; +use App\Models\Concerns\HasPriority; +use App\Models\Concerns\HasProgress; +use App\Models\Concerns\HasStatus; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; @@ -13,8 +16,11 @@ class Ticket extends Model { - /** @use HasFactory<\Database\Factories\TicketFactory> */ - use HasFactory; + /** + * @use HasFactory<\Database\Factories\TicketFactory> + * @use HasStatus<\App\Enums\TicketStatus> + */ + use Assignable, HasDueDate, HasFactory, HasPriority, HasProgress, HasStatus; /** * The model's default values for attributes. @@ -22,7 +28,6 @@ class Ticket extends Model * @var array */ protected $attributes = [ - 'priority' => TicketPriority::Normal, 'status' => TicketStatus::New, ]; @@ -45,21 +50,10 @@ class Ticket extends Model * @var array */ protected $casts = [ - 'priority' => TicketPriority::class, - 'status' => TicketStatus::class, - 'due_date' => 'date', ]; // RELATIONS /////////////////////////////////////////////////////////////////////////////////// - /** - * Get the assignee (user) for the ticket. - */ - public function assignee(): BelongsTo - { - return $this->belongsTo(User::class, 'assignee_id'); - } - /** * Get the device for the ticket. */ @@ -99,75 +93,4 @@ public function invoice(): HasOne { return $this->hasOne(Invoice::class); } - - // SCOPES ////////////////////////////////////////////////////////////////////////////////////// - - /** - * Scope a query to only include tickets that are assignable. - */ - public function scopeAssignable(Builder $query): void - { - $query->whereNull('assignee_id'); - } - - /** - * Scope a query to only include tickets with the specified priority. - */ - public function scopeOfPriority(Builder $query, TicketPriority $priority): void - { - $query->where('priority', $priority->value); - } - - /** - * Scope a query to only include tickets with the specified status. - */ - public function scopeOfStatus(Builder $query, TicketStatus $status): void - { - $query->where('status', $status->value); - } - - /** - * Scope a query to only include tickets that are overdue. - */ - public function scopeOverdue(Builder $query): void - { - $query - ->where('due_date', '<', now()) - ->whereNot('status', TicketStatus::Closed->value); - } - - // METHODS ///////////////////////////////////////////////////////////////////////////////////// - - /** - * Determine if the ticket is assignable to a user. - */ - public function isAssignable(): bool - { - return ! $this->assignee()->exists(); - } - - /** - * Assign the ticket to a user. - */ - public function assignTo(User $user): void - { - $this->assignee()->associate($user)->save(); - } - - /** - * Unassign the ticket from a user. - */ - public function unassign(): void - { - $this->assignee()->dissociate()->save(); - } - - /** - * Determine if the ticket is overdue. - */ - public function isOverdue(): bool - { - return $this->due_date->isPast() - && $this->status !== TicketStatus::Closed; - } } diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index 77d0a20..7cbcb50 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -4,6 +4,7 @@ use App\Enums\TransactionMethod; use App\Enums\TransactionType; +use App\Models\Concerns\HasType; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -11,8 +12,11 @@ class Transaction extends Model { - /** @use HasFactory<\Database\Factories\TransactionFactory> */ - use HasFactory; + /** + * @use HasFactory<\Database\Factories\TransactionFactory> + * @use HasType<\App\Enums\TransactionType> + */ + use HasFactory, HasType; /** * The model's default values for attributes. @@ -44,7 +48,6 @@ class Transaction extends Model protected $casts = [ 'amount' => 'float', 'method' => TransactionMethod::class, - 'type' => TransactionType::class, ]; // RELATIONS /////////////////////////////////////////////////////////////////////////////////// @@ -66,12 +69,4 @@ public function scopeOfMethod(Builder $query, TransactionMethod $method): void { $query->where('method', $method->value); } - - /** - * Scope a query to only include transactions of a given type. - */ - public function scopeOfType(Builder $query, TransactionType $type): void - { - $query->where('type', $type->value); - } } diff --git a/app/Models/User.php b/app/Models/User.php index 5ea0832..e8534b9 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -4,6 +4,8 @@ use App\Enums\UserRole; use App\Enums\UserStatus; +use App\Models\Concerns\Contactable; +use App\Models\Concerns\HasStatus; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Relations\HasMany; @@ -12,8 +14,11 @@ class User extends Authenticatable { - /** @use HasFactory<\Database\Factories\UserFactory> */ - use HasFactory, Notifiable; + /** + * @use HasFactory<\Database\Factories\UserFactory> + * @use HasStatus<\App\Enums\UserStatus> + */ + use Contactable, HasFactory, HasStatus, Notifiable; /** * The model's default values for attributes. @@ -58,7 +63,6 @@ class User extends Authenticatable 'email_verified_at' => 'datetime', 'password' => 'hashed', 'role' => UserRole::class, - 'status' => UserStatus::class, ]; // RELATIONS /////////////////////////////////////////////////////////////////////////////////// @@ -81,14 +85,6 @@ public function scopeOfRole(Builder $query, UserRole $role): void $query->where('role', $role->value); } - /** - * Scope a query to only include users with the specified status. - */ - public function scopeOfStatus(Builder $query, UserStatus $status): void - { - $query->where('status', $status->value); - } - // METHODS ///////////////////////////////////////////////////////////////////////////////////// /** diff --git a/composer.json b/composer.json index 1516d41..5812e12 100644 --- a/composer.json +++ b/composer.json @@ -31,6 +31,7 @@ "autoload": { "psr-4": { "App\\": "app/", + "Database\\Factories\\States\\": "database/factories/states/", "Database\\Factories\\": "database/factories/", "Database\\Seeders\\": "database/seeders/" } diff --git a/database/factories/CustomerFactory.php b/database/factories/CustomerFactory.php index a2defaf..3f14af5 100644 --- a/database/factories/CustomerFactory.php +++ b/database/factories/CustomerFactory.php @@ -2,6 +2,7 @@ namespace Database\Factories; +use Database\Factories\States\HasContactableStates; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -11,6 +12,8 @@ */ class CustomerFactory extends Factory { + use HasContactableStates; + public function definition(): array { return [ @@ -37,26 +40,6 @@ public function withoutCompany(): self ]); } - /** - * Indicate that the customer has no email. - */ - public function withoutEmail(): self - { - return $this->state(fn (array $attributes) => [ - 'email' => null, - ]); - } - - /** - * Indicate that the customer has no phone number. - */ - public function withoutPhone(): self - { - return $this->state(fn (array $attributes) => [ - 'phone' => null, - ]); - } - /** * Indicate that the customer has no address. */ diff --git a/database/factories/DeviceFactory.php b/database/factories/DeviceFactory.php index 4814e8c..81cb429 100644 --- a/database/factories/DeviceFactory.php +++ b/database/factories/DeviceFactory.php @@ -5,6 +5,10 @@ use App\Enums\DeviceStatus; use App\Enums\DeviceType; use App\Models\Customer; +use Database\Factories\States\HasProgressStates; +use Database\Factories\States\HasStatusStates; +use Database\Factories\States\HasTypeStates; +use Database\Factories\States\HasWarrantyStates; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -14,6 +18,8 @@ */ class DeviceFactory extends Factory { + use HasProgressStates, HasStatusStates, HasTypeStates, HasWarrantyStates; + const VERSIONS = [ 'iMac' => ['21"', '27"'], 'Mac' => ['Mini', 'Pro', 'Studio'], @@ -68,6 +74,11 @@ class DeviceFactory extends Factory 'Go Comfort' => DeviceType::Other, ]; + /** + * Define the model's default state. + * + * @return array + */ public function definition(): array { // Generate random model name and version @@ -82,7 +93,7 @@ public function definition(): array 'purchase_date' => now()->subYears(1), 'warranty_expire_date' => now()->addYears(1), 'type' => self::TYPES[$name], - 'status' => fake()->randomElement(DeviceStatus::values()), + 'status' => DeviceStatus::Received, ]; } @@ -131,35 +142,4 @@ public function withoutPurchaseDate(): self 'warranty_expire_date' => null, ]); } - - /** - * Indicate that the device is out of warranty. - */ - public function outOfWarranty(): self - { - return $this->state(fn (array $attributes) => [ - 'purchase_date' => now()->subYears(2), - 'warranty_expire_date' => now()->subYear(), - ]); - } - - /** - * Indicate that the device is of the given type. - */ - public function ofType(DeviceType $type): self - { - return $this->state(fn (array $attributes) => [ - 'type' => $type, - ]); - } - - /** - * Indicate that the device is of the given status. - */ - public function ofStatus(DeviceStatus $status): self - { - return $this->state(fn (array $attributes) => [ - 'status' => $status, - ]); - } } diff --git a/database/factories/InvoiceFactory.php b/database/factories/InvoiceFactory.php index b991a2f..4fd293f 100644 --- a/database/factories/InvoiceFactory.php +++ b/database/factories/InvoiceFactory.php @@ -4,6 +4,9 @@ use App\Enums\InvoiceStatus; use App\Models\Ticket; +use Database\Factories\States\HasDueDateStates; +use Database\Factories\States\HasProgressStates; +use Database\Factories\States\HasStatusStates; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -13,6 +16,8 @@ */ class InvoiceFactory extends Factory { + use HasDueDateStates, HasProgressStates, HasStatusStates; + /** * Define the model's default state. * @@ -62,16 +67,6 @@ public function forTicket(Ticket $ticket): static // STATES ////////////////////////////////////////////////////////////////////////////////////// - /** - * Indicate that the invoice is of a specified status. - */ - public function ofStatus(InvoiceStatus $status): self - { - return $this->state(fn (array $attributes) => [ - 'status' => $status, - ]); - } - /** * Indicate that the invoice has been paid. */ @@ -93,15 +88,4 @@ public function refunded(): static 'refunded_amount' => $attributes['total'] ?? 0, ]); } - - /** - * Indicate that the invoice is overdue. - */ - public function overdue(): static - { - return $this->state(fn (array $attributes) => [ - 'due_date' => now()->subDays(1), - 'status' => InvoiceStatus::Issued, - ]); - } } diff --git a/database/factories/OrderFactory.php b/database/factories/OrderFactory.php index dbee247..800643f 100644 --- a/database/factories/OrderFactory.php +++ b/database/factories/OrderFactory.php @@ -4,6 +4,10 @@ use App\Enums\OrderStatus; use App\Models\Ticket; +use Database\Factories\States\HasApprovalStates; +use Database\Factories\States\HasBillableStates; +use Database\Factories\States\HasProgressStates; +use Database\Factories\States\HasStatusStates; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -11,6 +15,8 @@ */ class OrderFactory extends Factory { + use HasApprovalStates, HasBillableStates, HasProgressStates, HasStatusStates; + const PARTS = [ 'Dell XPS 13 Battery', 'MacBook Pro Retina Screen', @@ -64,36 +70,4 @@ public function forTicket(Ticket $ticket): self 'created_at' => $ticket->created_at, ]); } - - // STATES ////////////////////////////////////////////////////////////////////////////////////// - - /** - * Indicate that the order is non-billable. - */ - public function nonBillable(): self - { - return $this->state(fn (array $attributes) => [ - 'is_billable' => false, - ]); - } - - /** - * Indicate that the order is of a specified status. - */ - public function ofStatus(OrderStatus $status): self - { - return $this->state(fn (array $attributes) => [ - 'status' => $status, - ]); - } - - /** - * Indicate that the order needs customer approval. - */ - public function needsApproval(): self - { - return $this->state(fn (array $attributes) => [ - 'approved_at' => null, - ]); - } } diff --git a/database/factories/TaskFactory.php b/database/factories/TaskFactory.php index d18ff37..e00b679 100644 --- a/database/factories/TaskFactory.php +++ b/database/factories/TaskFactory.php @@ -5,6 +5,11 @@ use App\Enums\TaskStatus; use App\Enums\TaskType; use App\Models\Ticket; +use Database\Factories\States\HasApprovalStates; +use Database\Factories\States\HasBillableStates; +use Database\Factories\States\HasProgressStates; +use Database\Factories\States\HasStatusStates; +use Database\Factories\States\HasTypeStates; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -12,6 +17,8 @@ */ class TaskFactory extends Factory { + use HasApprovalStates, HasBillableStates, HasProgressStates, HasStatusStates, HasTypeStates; + /** * Define the model's default state. * @@ -42,46 +49,4 @@ public function forTicket(Ticket $ticket): self 'created_at' => $ticket->created_at, ]); } - - // STATES ////////////////////////////////////////////////////////////////////////////////////// - - /** - * Indicate that the task is non-billable. - */ - public function nonBillable(): self - { - return $this->state(fn (array $attributes) => [ - 'is_billable' => false, - ]); - } - - /** - * Indicate that the task is of a specific type. - */ - public function ofType(TaskType $type): self - { - return $this->state(fn (array $attributes) => [ - 'type' => $type, - ]); - } - - /** - * Indicate that the task is of a specific status. - */ - public function ofStatus(TaskStatus $status): self - { - return $this->state(fn (array $attributes) => [ - 'status' => $status, - ]); - } - - /** - * Indicate that the task needs customer approval. - */ - public function needsApproval(): self - { - return $this->state(fn (array $attributes) => [ - 'approved_at' => null, - ]); - } } diff --git a/database/factories/TicketFactory.php b/database/factories/TicketFactory.php index 84373af..7e625d2 100644 --- a/database/factories/TicketFactory.php +++ b/database/factories/TicketFactory.php @@ -2,10 +2,14 @@ namespace Database\Factories; -use App\Enums\TicketPriority; +use App\Enums\Priority; use App\Enums\TicketStatus; use App\Models\Device; -use App\Models\User; +use Database\Factories\States\HasAssignableStates; +use Database\Factories\States\HasDueDateStates; +use Database\Factories\States\HasPriorityStates; +use Database\Factories\States\HasProgressStates; +use Database\Factories\States\HasStatusStates; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -16,6 +20,8 @@ */ class TicketFactory extends Factory { + use HasAssignableStates, HasDueDateStates, HasPriorityStates, HasProgressStates, HasStatusStates; + /** * Define the model's default state. * @@ -27,7 +33,7 @@ public function definition(): array 'device_id' => Device::factory(), 'title' => fake()->sentence(), 'description' => fake()->paragraph(), - 'priority' => TicketPriority::Normal, + 'priority' => Priority::Medium, 'status' => TicketStatus::New, 'due_date' => now()->addWeek(), ]; @@ -35,16 +41,6 @@ public function definition(): array // RELATIONS /////////////////////////////////////////////////////////////////////////////////// - /** - * Indicate that the ticket is assigned to a specific user. - */ - public function forAssignee(User $user): self - { - return $this->state(fn (array $attributes) => [ - 'assignee_id' => $user->id, - ]); - } - /** * Indicate that the ticket belongs to a specific device. */ @@ -56,37 +52,4 @@ public function forDevice(Device $device): self 'due_date' => $device->created_at->addWeek(), ]); } - - // STATES ////////////////////////////////////////////////////////////////////////////////////// - - /** - * Indicate that the ticket is of the given priority. - */ - public function ofPriority(TicketPriority $priority): self - { - return $this->state(fn (array $attributes) => [ - 'priority' => $priority, - ]); - } - - /** - * Indicate that the ticket is of the given status. - */ - public function ofStatus(TicketStatus $status): self - { - return $this->state(fn (array $attributes) => [ - 'status' => $status, - ]); - } - - /** - * Indicate that the ticket is overdue. - */ - public function overdue(): self - { - return $this->state(fn (array $attributes) => [ - 'due_date' => now()->subDay(), - 'status' => TicketStatus::New, - ]); - } } diff --git a/database/factories/TransactionFactory.php b/database/factories/TransactionFactory.php index ba561d1..f5a72e2 100644 --- a/database/factories/TransactionFactory.php +++ b/database/factories/TransactionFactory.php @@ -5,6 +5,7 @@ use App\Enums\TransactionMethod; use App\Enums\TransactionType; use App\Models\Invoice; +use Database\Factories\States\HasTypeStates; use Illuminate\Database\Eloquent\Factories\Factory; /** @@ -12,6 +13,8 @@ */ class TransactionFactory extends Factory { + use HasTypeStates; + /** * Define the model's default state. * diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index 9872f4d..47d8830 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -4,6 +4,8 @@ use App\Enums\UserRole; use App\Enums\UserStatus; +use Database\Factories\States\HasContactableStates; +use Database\Factories\States\HasStatusStates; use Illuminate\Database\Eloquent\Factories\Factory; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Str; @@ -13,6 +15,12 @@ */ class UserFactory extends Factory { + use HasContactableStates { + mailable as private; + notMailable as private; + } + use HasStatusStates; + /** * The current password being used by the factory. */ @@ -39,16 +47,6 @@ public function definition(): array // STATES ////////////////////////////////////////////////////////////////////////////////////// - /** - * Indicate that the user has no phone number. - */ - public function withoutPhone(): self - { - return $this->state(fn (array $attributes) => [ - 'phone' => null, - ]); - } - /** * Indicate that the user has the specified role. */ @@ -75,16 +73,6 @@ public function manager(): self return $this->ofRole(UserRole::Manager); } - /** - * Indicate that the user has the specified status. - */ - public function ofStatus(UserStatus $status): self - { - return $this->state(fn (array $attributes) => [ - 'status' => $status, - ]); - } - /** * Indicate that the user's email address is unverified. */ diff --git a/database/factories/states/HasApprovalStates.php b/database/factories/states/HasApprovalStates.php new file mode 100644 index 0000000..6dc6cf7 --- /dev/null +++ b/database/factories/states/HasApprovalStates.php @@ -0,0 +1,28 @@ +state(fn (array $attributes) => [ + static::APPROVED_AT => now(), + ]); + } + + /** + * Indicate that the model is unapproved. + */ + public function unapproved(): self + { + return $this->state(fn (array $attributes) => [ + static::APPROVED_AT => null, + ]); + } +} diff --git a/database/factories/states/HasAssignableStates.php b/database/factories/states/HasAssignableStates.php new file mode 100644 index 0000000..494932c --- /dev/null +++ b/database/factories/states/HasAssignableStates.php @@ -0,0 +1,38 @@ +state(fn (array $attributes) => [ + 'assignee_id' => $user->id, + ]); + } + + /** + * Indicate that the model is assigned to a user. + */ + public function assigned(): self + { + return $this->state(fn (array $attributes) => [ + 'assignee_id' => User::factory(), + ]); + } + + /** + * Indicate that the model is not assigned to any user. + */ + public function unassigned(): self + { + return $this->state(fn (array $attributes) => [ + 'assignee_id' => null, + ]); + } +} diff --git a/database/factories/states/HasBillableStates.php b/database/factories/states/HasBillableStates.php new file mode 100644 index 0000000..1f17ed1 --- /dev/null +++ b/database/factories/states/HasBillableStates.php @@ -0,0 +1,28 @@ +state(fn (array $attributes) => [ + static::IS_BILLABLE => true, + ]); + } + + /** + * Indicate that the model is not billable. + */ + public function notBillable(): self + { + return $this->state(fn (array $attributes) => [ + static::IS_BILLABLE => false, + ]); + } +} diff --git a/database/factories/states/HasContactableStates.php b/database/factories/states/HasContactableStates.php new file mode 100644 index 0000000..0ebe261 --- /dev/null +++ b/database/factories/states/HasContactableStates.php @@ -0,0 +1,66 @@ +state(fn (array $attributes) => [ + static::EMAIL => fake()->unique()->safeEmail(), + ]); + } + + /** + * Indicate that the model has no email address. + */ + public function notMailable(): self + { + return $this->state(fn (array $attributes) => [ + static::EMAIL => null, + ]); + } + + /** + * Indicate that the model has a phone number. + */ + public function callable(): self + { + return $this->state(fn (array $attributes) => [ + static::PHONE => fake()->unique()->phoneNumber(), + ]); + } + + /** + * Indicate that the model has no phone number. + */ + public function notCallable(): self + { + return $this->state(fn (array $attributes) => [ + static::PHONE => null, + ]); + } + + /** + * Indicate that the model is contactable (has email or phone). + */ + public function contactable(): self + { + return $this->mailable()->callable(); + } + + /** + * Indicate that the model is not contactable (no email or phone). + */ + public function notContactable(): self + { + return $this->notMailable()->notCallable(); + } +} diff --git a/database/factories/states/HasDueDateStates.php b/database/factories/states/HasDueDateStates.php new file mode 100644 index 0000000..beeaded --- /dev/null +++ b/database/factories/states/HasDueDateStates.php @@ -0,0 +1,27 @@ +state(fn (array $attributes) => [ + static::DUE_DATE => now()->addDays($days)->startOfDay(), + ]); + } + + /** + * Indicate that the model is overdue. + */ + public function overdue(): static + { + return $this->dueInDays(-1)->pending(); + } +} diff --git a/database/factories/states/HasPriorityStates.php b/database/factories/states/HasPriorityStates.php new file mode 100644 index 0000000..6955cb4 --- /dev/null +++ b/database/factories/states/HasPriorityStates.php @@ -0,0 +1,52 @@ +state(fn (array $attributes) => [ + static::PRIORITY => $priority, + ]); + } + + /** + * Indicate that the model has low priority. + */ + public function lowPriority(): self + { + return $this->ofPriority(Priority::Low); + } + + /** + * Indicate that the model has medium priority. + */ + public function mediumPriority(): self + { + return $this->ofPriority(Priority::Medium); + } + + /** + * Indicate that the model has high priority. + */ + public function highPriority(): self + { + return $this->ofPriority(Priority::High); + } + + /** + * Indicate that the model has urgent priority. + */ + public function urgent(): self + { + return $this->ofPriority(Priority::Urgent); + } +} diff --git a/database/factories/states/HasProgressStates.php b/database/factories/states/HasProgressStates.php new file mode 100644 index 0000000..83dd787 --- /dev/null +++ b/database/factories/states/HasProgressStates.php @@ -0,0 +1,30 @@ +state(fn (array $attributes) => [ + static::STATUS => Arr::first($attributes[static::STATUS]::pendingCases()), + ]); + } + + /** + * Indicate that the model is complete. + */ + public function complete(): self + { + return $this->state(fn (array $attributes) => [ + static::STATUS => Arr::first($attributes[static::STATUS]::completeCases()), + ]); + } +} diff --git a/database/factories/states/HasStatusStates.php b/database/factories/states/HasStatusStates.php new file mode 100644 index 0000000..457c273 --- /dev/null +++ b/database/factories/states/HasStatusStates.php @@ -0,0 +1,20 @@ +state(fn (array $attributes) => [ + static::STATUS => $status, + ]); + } +} diff --git a/database/factories/states/HasTypeStates.php b/database/factories/states/HasTypeStates.php new file mode 100644 index 0000000..2729970 --- /dev/null +++ b/database/factories/states/HasTypeStates.php @@ -0,0 +1,20 @@ +state(fn (array $attributes) => [ + static::TYPE => $type, + ]); + } +} diff --git a/database/factories/states/HasWarrantyStates.php b/database/factories/states/HasWarrantyStates.php new file mode 100644 index 0000000..e25ef31 --- /dev/null +++ b/database/factories/states/HasWarrantyStates.php @@ -0,0 +1,28 @@ +state(fn (array $attributes) => [ + static::WARRANTY_EXPIRE_DATE => now()->addYear(), + ]); + } + + /** + * Indicate that the model has no warranty. + */ + public function withoutWarranty(): self + { + return $this->state(fn (array $attributes) => [ + static::WARRANTY_EXPIRE_DATE => null, + ]); + } +} diff --git a/database/migrations/2025_06_30_215857_create_tickets_table.php b/database/migrations/2025_06_30_215857_create_tickets_table.php index 4ee4113..0aea0a3 100644 --- a/database/migrations/2025_06_30_215857_create_tickets_table.php +++ b/database/migrations/2025_06_30_215857_create_tickets_table.php @@ -1,6 +1,6 @@ foreignId('device_id')->constrained()->cascadeOnDelete(); $table->string('title'); $table->text('description'); - $table->enum('priority', TicketPriority::values())->default(TicketPriority::Normal); + $table->enum('priority', Priority::values())->default(Priority::Medium); $table->enum('status', TicketStatus::values())->default(TicketStatus::New); $table->date('due_date')->nullable(); $table->timestamps(); diff --git a/database/seeders/CustomerSeeder.php b/database/seeders/CustomerSeeder.php index 4e9dcaf..c912044 100644 --- a/database/seeders/CustomerSeeder.php +++ b/database/seeders/CustomerSeeder.php @@ -28,12 +28,12 @@ public function run(): void ]); // Create a customer without email - Customer::factory()->withoutEmail()->create([ + Customer::factory()->notMailable()->create([ 'created_at' => $startDate, ]); // Create a customer without phone - Customer::factory()->withoutPhone()->create([ + Customer::factory()->notCallable()->create([ 'created_at' => $startDate, ]); diff --git a/database/seeders/DeviceSeeder.php b/database/seeders/DeviceSeeder.php index a4944f3..8c3a877 100644 --- a/database/seeders/DeviceSeeder.php +++ b/database/seeders/DeviceSeeder.php @@ -23,7 +23,7 @@ public function run(): void 2 => $deviceFactory->withoutBrand()->create(), 3 => $deviceFactory->withoutSerialNumber()->create(), 4 => $deviceFactory->withoutPurchaseDate()->create(), - 5 => $deviceFactory->outOfWarranty()->create(), + 5 => $deviceFactory->withoutWarranty()->create(), }; }); } diff --git a/database/seeders/OrderSeeder.php b/database/seeders/OrderSeeder.php index 733783e..1ab69fb 100644 --- a/database/seeders/OrderSeeder.php +++ b/database/seeders/OrderSeeder.php @@ -17,7 +17,7 @@ public function run(): void Order::factory()->forTicket($ticket)->create(); // Create an order that needs approval - Order::factory()->forTicket($ticket)->needsApproval()->create(); + Order::factory()->forTicket($ticket)->unapproved()->create(); }); // Mark some orders as non-billable diff --git a/database/seeders/TaskSeeder.php b/database/seeders/TaskSeeder.php index 2d4cb9a..58fd183 100644 --- a/database/seeders/TaskSeeder.php +++ b/database/seeders/TaskSeeder.php @@ -18,7 +18,7 @@ public function run(): void Task::factory()->forTicket($ticket)->create(); // Create a task that needs approval - Task::factory()->forTicket($ticket)->needsApproval()->create(); + Task::factory()->forTicket($ticket)->unapproved()->create(); }); // Mark some tasks as non-billable diff --git a/docs/development-roadmap.md b/docs/development-roadmap.md index cff60e8..d989790 100644 --- a/docs/development-roadmap.md +++ b/docs/development-roadmap.md @@ -136,7 +136,7 @@ php artisan test --filter=User #### Tasks: - [ ] **Ticket Migration & Model** - [ ] Create tickets migration with foreign keys (customer_id, device_id, assigned_to) - - [ ] Create `TicketPriority` and `TicketStatus` enums + - [ ] Create `Priority` and `TicketStatus` enums - [ ] Add computed columns (progress_percentage, is_overdue) - [ ] Add counter fields (total_tasks, completed_tasks, etc.) diff --git a/tests/Unit/Models/Concerns/AssignableTest.php b/tests/Unit/Models/Concerns/AssignableTest.php new file mode 100644 index 0000000..51418a0 --- /dev/null +++ b/tests/Unit/Models/Concerns/AssignableTest.php @@ -0,0 +1,80 @@ + [Ticket::class], +]); + +it('can determine if model is assignable', function (string $modelClass) { + // Arrange + $model = $modelClass::factory()->assigned()->create(); + + // Assert + expect($model->isAssigned())->toBeTrue(); + expect($model->assignee_id)->not->toBeNull(); +})->with('models'); + +it('has an assignee relation', function (string $modelClass) { + // Arrange + $user = User::factory()->create(); + $model = $modelClass::factory()->forAssignee($user)->create(); + + // Assert + expect($model->assignee)->toBeInstanceOf(User::class); + expect($model->assignee->id)->toBe($user->id); +})->with('models'); + +it('can assign a model to a user', function (string $modelClass) { + // Arrange + $user = User::factory()->create(); + $model = $modelClass::factory()->unassigned()->create(); + + // Act + $model->assignTo($user); + + // Assert + expect($model->assignee)->toBeInstanceOf(User::class); + expect($model->assignee->id)->toBe($user->id); +})->with('models'); + +it('can unassign a model from a user', function (string $modelClass) { + // Arrange + $model = $modelClass::factory()->assigned()->create(); + + // Act + $model->unassign(); + + // Assert + expect($model->assignee)->toBeNull(); +})->with('models'); + +it('can filter records by assigned scope', function (string $modelClass) { + // Arrange + $modelClass::factory(2)->assigned()->create(); + $modelClass::factory(1)->unassigned()->create(); + + // Act + $assignedModels = $modelClass::query()->assigned()->get(); + + // Assert + expect($assignedModels)->toHaveCount(2); + expect($assignedModels->first()->isAssigned())->toBeTrue(); +})->with('models'); + +it('can filter records by unassigned scope', function (string $modelClass) { + // Arrange + $modelClass::factory(2)->unassigned()->create(); + $modelClass::factory(1)->assigned()->create(); + + // Act + $unassignedModels = $modelClass::query()->unassigned()->get(); + + // Assert + expect($unassignedModels)->toHaveCount(2); + expect($unassignedModels->first()->isAssigned())->toBeFalse(); +})->with('models'); diff --git a/tests/Unit/Models/Concerns/BillableTest.php b/tests/Unit/Models/Concerns/BillableTest.php new file mode 100644 index 0000000..b1e5443 --- /dev/null +++ b/tests/Unit/Models/Concerns/BillableTest.php @@ -0,0 +1,56 @@ + [Order::class], + 'Task' => [Task::class], +]); + +it('initializes model properties correctly', function (string $modelClass) { + // Arrange + $model = new $modelClass; + + // Assert + expect($model->getCasts())->toHaveKey('is_billable', 'boolean'); + expect($model->getAttributes())->toHaveKey('is_billable', true); +})->with('models'); + +it('can determine if model is billable', function (string $modelClass) { + // Arrange + $model = $modelClass::factory()->billable()->create(); + + // Assert + expect($model->isBillable())->toBeTrue(); + expect($model->is_billable)->toBeTrue(); +})->with('models'); + +it('can filter records by billable scope', function (string $modelClass) { + // Arrange + $modelClass::factory(2)->billable()->create(); + $modelClass::factory(1)->notBillable()->create(); + + // Act + $billableModels = $modelClass::query()->billable()->get(); + + // Assert + expect($billableModels)->toHaveCount(2); + expect($billableModels->first()->isBillable())->toBeTrue(); +})->with('models'); + +it('can filter records by not billable scope', function (string $modelClass) { + // Arrange + $modelClass::factory(2)->notBillable()->create(); + $modelClass::factory(1)->billable()->create(); + + // Act + $notBillableModels = $modelClass::query()->notBillable()->get(); + + // Assert + expect($notBillableModels)->toHaveCount(2); + expect($notBillableModels->first()->isBillable())->toBeFalse(); +})->with('models'); diff --git a/tests/Unit/Models/Concerns/ContactableTest.php b/tests/Unit/Models/Concerns/ContactableTest.php new file mode 100644 index 0000000..b3871a8 --- /dev/null +++ b/tests/Unit/Models/Concerns/ContactableTest.php @@ -0,0 +1,91 @@ + [User::class], + 'Customer' => [Customer::class], +]); + +dataset('optional_models', [ + // User email is not nullable, so we skip it for + // mailable tests and contactable which makes use of email + 'Customer' => [Customer::class], +]); + +it('can determine if model is mailable', function (string $modelClass) { + // Arrange + $mailableModel = $modelClass::factory()->mailable()->create(); + $notMailableModel = $modelClass::factory()->notMailable()->create(); + + // Assert + expect($mailableModel->isMailable())->toBeTrue(); + expect($mailableModel->email)->not->toBeNull(); + expect($notMailableModel->isMailable())->toBeFalse(); + expect($notMailableModel->email)->toBeNull(); +})->with('optional_models'); + +it('can determine if model is callable', function (string $modelClass) { + // Arrange + $callableModel = $modelClass::factory()->callable()->create(); + $notCallableModel = $modelClass::factory()->notCallable()->create(); + + // Assert + expect($callableModel->isCallable())->toBeTrue(); + expect($callableModel->phone)->not->toBeNull(); + expect($notCallableModel->isCallable())->toBeFalse(); + expect($notCallableModel->phone)->toBeNull(); +})->with('models'); + +it('can determine if model is contactable', function (string $modelClass) { + // Arrange + $contactableModel = $modelClass::factory()->contactable()->create(); + $notContactableModel = $modelClass::factory()->notContactable()->create(); + + // Assert + expect($contactableModel->isContactable())->toBeTrue(); + expect($notContactableModel->isContactable())->toBeFalse(); +})->with('optional_models'); + +it('can filter records by mailable scope', function (string $modelClass) { + // Arrange + $modelClass::factory(2)->mailable()->create(); + $modelClass::factory(1)->notMailable()->create(); + + // Act + $mailableModels = $modelClass::query()->mailable()->get(); + + // Assert + expect($mailableModels)->toHaveCount(2); + expect($mailableModels->first()->isMailable())->toBeTrue(); +})->with('optional_models'); + +it('can filter records by callable scope', function (string $modelClass) { + // Arrange + $modelClass::factory(2)->callable()->create(); + $modelClass::factory(1)->notCallable()->create(); + + // Act + $callableModels = $modelClass::query()->callable()->get(); + + // Assert + expect($callableModels)->toHaveCount(2); + expect($callableModels->first()->isCallable())->toBeTrue(); +})->with('models'); + +it('can filter records by contactable scope', function (string $modelClass) { + // Arrange + $modelClass::factory(2)->contactable()->create(); + $modelClass::factory(1)->notContactable()->create(); + + // Act + $contactableModels = $modelClass::query()->contactable()->get(); + + // Assert + expect($contactableModels)->toHaveCount(2); + expect($contactableModels->first()->isContactable())->toBeTrue(); +})->with('optional_models'); diff --git a/tests/Unit/Models/Concerns/HasApprovalTest.php b/tests/Unit/Models/Concerns/HasApprovalTest.php new file mode 100644 index 0000000..e6ac4b2 --- /dev/null +++ b/tests/Unit/Models/Concerns/HasApprovalTest.php @@ -0,0 +1,55 @@ + [Order::class], + 'Task' => [Task::class], +]); + +it('initializes model properties correctly', function (string $modelClass) { + // Arrange + $model = new $modelClass; + + // Assert + expect($model->getCasts())->toHaveKey('approved_at', 'datetime'); +})->with('models'); + +it('can determine if model is approved', function (string $modelClass) { + // Arrange + $model = $modelClass::factory()->approved()->create(); + + // Assert + expect($model->isApproved())->toBeTrue(); + expect($model->approved_at)->not->toBeNull(); +})->with('models'); + +it('can filter records by approved scope', function (string $modelClass) { + // Arrange + $modelClass::factory(2)->approved()->create(); + $modelClass::factory(1)->unapproved()->create(); + + // Act + $approvedModels = $modelClass::query()->approved()->get(); + + // Assert + expect($approvedModels)->toHaveCount(2); + expect($approvedModels->first()->isApproved())->toBeTrue(); +})->with('models'); + +it('can filter records by unapproved scope', function (string $modelClass) { + // Arrange + $modelClass::factory(2)->unapproved()->create(); + $modelClass::factory(1)->approved()->create(); + + // Act + $unapprovedModels = $modelClass::query()->unapproved()->get(); + + // Assert + expect($unapprovedModels)->toHaveCount(2); + expect($unapprovedModels->first()->isApproved())->toBeFalse(); +})->with('models'); diff --git a/tests/Unit/Models/Concerns/HasDueDateTest.php b/tests/Unit/Models/Concerns/HasDueDateTest.php new file mode 100644 index 0000000..d5eb0d5 --- /dev/null +++ b/tests/Unit/Models/Concerns/HasDueDateTest.php @@ -0,0 +1,58 @@ + [Ticket::class], + 'Invoice' => [Invoice::class], +]); + +it('initializes model properties correctly', function (string $modelClass) { + // Arrange + $model = new $modelClass; + + // Assert + expect($model->getCasts())->toHaveKey('due_date', 'date'); +})->with('models'); + +it('can determine if model is overdue', function (string $modelClass) { + // Arrange + $pendingModel = $modelClass::factory()->overdue()->pending()->create(); + $completedModel = $modelClass::factory()->overdue()->complete()->create(); + + // Assert + expect($pendingModel->isOverdue())->toBeTrue(); + expect($pendingModel->due_date->isPast())->toBeTrue(); + expect($completedModel->isOverdue())->toBeFalse(); + expect($completedModel->due_date->isPast())->toBeTrue(); +})->with('models'); + +it('can filter records by overdue scope', function (string $modelClass) { + // Arrange + $modelClass::factory(2)->overdue()->create(); + $modelClass::factory(1)->dueInDays()->create(); + + // Act + $overdueModels = $modelClass::query()->overdue()->get(); + + // Assert + expect($overdueModels)->toHaveCount(2); + expect($overdueModels->first()->isOverdue())->toBeTrue(); +})->with('models'); + +it('can filter records by not overdue scope', function (string $modelClass) { + // Arrange + $modelClass::factory(2)->dueInDays()->create(); + $modelClass::factory(1)->overdue()->create(); + + // Act + $notOverdueModels = $modelClass::query()->notOverdue()->get(); + + // Assert + expect($notOverdueModels)->toHaveCount(2); + expect($notOverdueModels->first()->isOverdue())->toBeFalse(); +})->with('models'); diff --git a/tests/Unit/Models/Concerns/HasPriorityTest.php b/tests/Unit/Models/Concerns/HasPriorityTest.php new file mode 100644 index 0000000..6d44088 --- /dev/null +++ b/tests/Unit/Models/Concerns/HasPriorityTest.php @@ -0,0 +1,73 @@ + [Ticket::class], +]); + +it('initializes model properties correctly', function (string $modelClass) { + // Arrange + $model = new $modelClass; + + // Assert + expect($model->getCasts())->toHaveKey('priority', Priority::class); + expect($model->getAttributes())->toHaveKey('priority', Priority::Medium); +})->with('models'); + +it('can determine if model is urgent', function (string $modelClass) { + // Arrange + $urgentModel = $modelClass::factory()->urgent()->create(); + $nonUrgentModel = $modelClass::factory()->lowPriority()->create(); + + // Assert + expect($urgentModel->isUrgent())->toBeTrue(); + expect($urgentModel->priority)->toBe(Priority::Urgent); + expect($nonUrgentModel->isUrgent())->toBeFalse(); + expect($nonUrgentModel->priority)->toBe(Priority::Low); +})->with('models'); + +it('can filter records by priority scope', function (string $modelClass, Priority $priority) { + // Arrange + $modelClass::factory()->ofPriority($priority)->create(); + + // Act + $models = $modelClass::query()->ofPriority($priority)->get(); + + // Assert + expect($models)->toHaveCount(1); + expect($models->first()->priority)->toBe($priority); +})->with('models')->with(Priority::cases()); + +it('can filter records by urgent scope', function (string $modelClass) { + // Arrange + $modelClass::factory(2)->urgent()->create(); + $modelClass::factory(1)->mediumPriority()->create(); + + // Act + $models = $modelClass::query()->urgent()->get(); + + // Assert + expect($models)->toHaveCount(2); + expect($models->first()->isUrgent())->toBeTrue(); +})->with('models'); + +it('can order records by priority', function (string $modelClass) { + // Arrange + $modelClass::factory()->mediumPriority()->create(); + $modelClass::factory()->lowPriority()->create(); + $modelClass::factory()->urgent()->create(); + $modelClass::factory()->highPriority()->create(); + + // Act + $models = $modelClass::query()->prioritized()->get(); + + // Assert + expect($models)->toHaveCount(4); + expect($models->first()->priority)->toBe(Priority::Urgent); + expect($models->last()->priority)->toBe(Priority::Low); +})->with('models'); diff --git a/tests/Unit/Models/Concerns/HasProgressTest.php b/tests/Unit/Models/Concerns/HasProgressTest.php new file mode 100644 index 0000000..72d3e92 --- /dev/null +++ b/tests/Unit/Models/Concerns/HasProgressTest.php @@ -0,0 +1,60 @@ + [Device::class], + 'Ticket' => [Ticket::class], + 'Task' => [Task::class], + 'Order' => [Order::class], + 'Invoice' => [Invoice::class], +]); + +it('can determine if model is pending', function (string $modelClass) { + // Arrange + $model = $modelClass::factory()->pending()->create(); + + // Assert + expect($model->isPending())->toBeTrue(); +})->with('models'); + +it('can determine if model is complete', function (string $modelClass) { + // Arrange + $model = $modelClass::factory()->complete()->create(); + + // Assert + expect($model->isComplete())->toBeTrue(); +})->with('models'); + +it('can filter records by pending scope', function (string $modelClass) { + // Arrange + $modelClass::factory(2)->pending()->create(); + $modelClass::factory(1)->complete()->create(); + + // Act + $pendingModels = $modelClass::query()->pending()->get(); + + // Assert + expect($pendingModels)->toHaveCount(2); + expect($pendingModels->first()->isPending())->toBeTrue(); +})->with('models'); + +it('can filter records by complete scope', function (string $modelClass) { + // Arrange + $modelClass::factory(2)->complete()->create(); + $modelClass::factory(1)->pending()->create(); + + // Act + $completeModels = $modelClass::query()->complete()->get(); + + // Assert + expect($completeModels)->toHaveCount(2); + expect($completeModels->first()->isComplete())->toBeTrue(); +})->with('models'); diff --git a/tests/Unit/Models/Concerns/HasStatusTest.php b/tests/Unit/Models/Concerns/HasStatusTest.php new file mode 100644 index 0000000..bf9c7c4 --- /dev/null +++ b/tests/Unit/Models/Concerns/HasStatusTest.php @@ -0,0 +1,39 @@ + [User::class, UserStatus::Active, UserStatus::Terminated], + 'Device' => [Device::class, DeviceStatus::Received, DeviceStatus::OnHold], + 'Ticket' => [Ticket::class, TicketStatus::New, TicketStatus::InProgress], + 'Task' => [Task::class, TaskStatus::New, TaskStatus::Cancelled], + 'Order' => [Order::class, OrderStatus::New, OrderStatus::Cancelled], + 'Invoice' => [Invoice::class, InvoiceStatus::Draft, InvoiceStatus::Sent], +]); + +it('can filter records by status scope', function (string $modelClass, BackedEnum $status, BackedEnum $otherStatus) { + // Arrange + $modelClass::factory(2)->ofStatus($status)->create(); + $modelClass::factory(1)->ofStatus($otherStatus)->create(); + + // Act + $results = $modelClass::query()->ofStatus($status)->get(); + + // Assert + expect($results)->toHaveCount(2); + expect($results->first()->status)->toBe($status); +})->with('models'); diff --git a/tests/Unit/Models/Concerns/HasTypeTest.php b/tests/Unit/Models/Concerns/HasTypeTest.php new file mode 100644 index 0000000..f9ba926 --- /dev/null +++ b/tests/Unit/Models/Concerns/HasTypeTest.php @@ -0,0 +1,30 @@ + [Device::class, DeviceType::Phone, DeviceType::Laptop], + 'Task' => [Task::class, TaskType::Repair, TaskType::Maintenance], + 'Transaction' => [Transaction::class, TransactionType::Payment, TransactionType::Refund], +]); + +it('can filter records by type scope', function (string $modelClass, BackedEnum $type, BackedEnum $otherType) { + // Arrange + $modelClass::factory(2)->ofType($type)->create(); + $modelClass::factory(1)->ofType($otherType)->create(); + + // Act + $results = $modelClass::query()->ofType($type)->get(); + + // Assert + expect($results)->toHaveCount(2); + expect($results->first()->type)->toBe($type); +})->with('models'); diff --git a/tests/Unit/Models/Concerns/HasWarrantyTest.php b/tests/Unit/Models/Concerns/HasWarrantyTest.php new file mode 100644 index 0000000..cba3734 --- /dev/null +++ b/tests/Unit/Models/Concerns/HasWarrantyTest.php @@ -0,0 +1,40 @@ + [Device::class], +]); + +it('initializes model properties correctly', function (string $modelClass) { + // Arrange + $model = new $modelClass; + + // Assert + expect($model->getCasts())->toHaveKey('warranty_expire_date', 'date'); +})->with('models'); + +it('can determine if model has warranty', function (string $modelClass) { + // Arrange + $model = $modelClass::factory()->withWarranty()->create(); + + // Assert + expect($model->hasWarranty())->toBeTrue(); + expect($model->warranty_expire_date->isFuture())->toBeTrue(); +})->with('models'); + +it('can filter records by with warranty scope', function (string $modelClass) { + // Arrange + $modelClass::factory(2)->withWarranty()->create(); + $modelClass::factory(1)->withoutWarranty()->create(); + + // Act + $warrantyModels = $modelClass::query()->withWarranty()->get(); + + // Assert + expect($warrantyModels)->toHaveCount(2); + expect($warrantyModels->first()->hasWarranty())->toBeTrue(); +})->with('models'); diff --git a/tests/Unit/Models/CustomerTest.php b/tests/Unit/Models/CustomerTest.php index 502fe7b..e03e783 100644 --- a/tests/Unit/Models/CustomerTest.php +++ b/tests/Unit/Models/CustomerTest.php @@ -16,22 +16,6 @@ expect($customer->address)->not->toBeEmpty(); }); -it('can create customer without email', function () { - // Act - $customer = Customer::factory()->withoutEmail()->create(); - - // Assert - expect($customer->email)->toBeNull(); -}); - -it('can create customer without phone', function () { - // Act - $customer = Customer::factory()->withoutPhone()->create(); - - // Assert - expect($customer->phone)->toBeNull(); -}); - it('can create customer without address', function () { // Act $customer = Customer::factory()->withoutAddress()->create(); diff --git a/tests/Unit/Models/DeviceTest.php b/tests/Unit/Models/DeviceTest.php index 504a852..b3400bc 100644 --- a/tests/Unit/Models/DeviceTest.php +++ b/tests/Unit/Models/DeviceTest.php @@ -44,31 +44,6 @@ expect($device->warranty_expire_date)->toBeNull(); }); -it('can create a device out of warranty', function () { - // Arrange - $device = Device::factory()->outOfWarranty()->create(); - - // Assert - expect($device->purchase_date->isPast())->toBeTrue(); - expect($device->warranty_expire_date->isPast())->toBeTrue(); -}); - -it('can create a device of type', function (DeviceType $type) { - // Arrange - $device = Device::factory()->ofType($type)->create(); - - // Assert - expect($device->type)->toBe($type); -})->with(DeviceType::cases()); - -it('can create a device of status', function (DeviceStatus $status) { - // Arrange - $device = Device::factory()->ofStatus($status)->create(); - - // Assert - expect($device->status)->toBe($status); -})->with(DeviceStatus::cases()); - it('can update a device', function () { // Arrange $device = Device::factory()->create(); @@ -111,22 +86,6 @@ expect($device->tickets)->toHaveCount(2); }); -it('can determine if device has warranty', function () { - // Arrange - $device = Device::factory()->outOfWarranty()->create(); - - // Assert - expect($device->hasWarranty())->toBeFalse(); - - // Act - $device->purchase_date = now()->subMonths(3); - $device->warranty_expire_date = now()->addMonths(9); - $device->save(); - - // Assert - expect($device->hasWarranty())->toBeTrue(); -}); - it('can get the customer that owns the device', function () { // Arrange $customer = Customer::factory()->create(); @@ -135,43 +94,3 @@ // Assert expect($device->customer->id)->toBe($customer->id); }); - -it('can filter devices with warranty scope', function () { - // Arrange - Device::factory()->outOfWarranty()->create(); - $deviceWithWarranty = Device::factory()->create([ - 'purchase_date' => now()->subMonths(3), - 'warranty_expire_date' => now()->addMonths(9), - ]); - - // Act - $devicesWithWarranty = Device::withWarranty()->get(); - - // Assert - expect($devicesWithWarranty)->toHaveCount(1); - expect($devicesWithWarranty->first()->id)->toBe($deviceWithWarranty->id); -}); - -it('can filter devices by type scope', function (DeviceType $type) { - // Arrange - Device::factory()->ofType($type)->create(); - - // Act - $devices = Device::ofType($type)->get(); - - // Assert - expect($devices)->toHaveCount(1); - expect($devices->first()->type)->toBe($type); -})->with(DeviceType::cases()); - -it('can filter devices by status scope', function (DeviceStatus $status) { - // Arrange - Device::factory()->ofStatus($status)->create(); - - // Act - $devices = Device::ofStatus($status)->get(); - - // Assert - expect($devices)->toHaveCount(1); - expect($devices->first()->status)->toBe($status); -})->with(DeviceStatus::cases()); diff --git a/tests/Unit/Models/InvoiceTest.php b/tests/Unit/Models/InvoiceTest.php index eba4665..64e3598 100644 --- a/tests/Unit/Models/InvoiceTest.php +++ b/tests/Unit/Models/InvoiceTest.php @@ -51,23 +51,6 @@ expect($invoice->refunded_amount)->toBe($invoice->total); }); -it('can create an overdue invoice', function () { - // Arrange - $invoice = Invoice::factory()->overdue()->create(); - - // Assert - expect($invoice->isOverdue())->toBeTrue(); - expect($invoice->status)->toBe(InvoiceStatus::Issued); -}); - -it('can create an invoice of status', function (InvoiceStatus $status) { - // Arrange & Act - $invoice = Invoice::factory()->ofStatus($status)->create(); - - // Assert - expect($invoice->status)->toBe($status); -})->with(InvoiceStatus::cases()); - it('can update an invoice', function () { // Arrange $invoice = Invoice::factory()->create(); @@ -128,36 +111,3 @@ expect($invoice->ticket_id)->toBe($ticket->id); expect($invoice->customer->id)->toBe($customer->id); }); - -it('can filter invoices by status scope', function (InvoiceStatus $status) { - // Arrange - Invoice::factory(2)->ofStatus($status)->create(); - - // Act - $invoices = Invoice::query()->ofStatus($status)->get(); - - // Assert - expect($invoices)->toHaveCount(2); - expect($invoices->first()->status)->toBe($status); -})->with(InvoiceStatus::cases()); - -it('can filter invoices by overdue scope', function () { - // Arrange - Invoice::factory()->create(); - Invoice::factory()->overdue()->create(); - - // Act - $overdueInvoices = Invoice::query()->overdue()->get(); - - // Assert - expect($overdueInvoices)->toHaveCount(1); - expect($overdueInvoices->first()->isOverdue())->toBeTrue(); -}); - -it('can determine if an invoice is overdue', function () { - // Arrange - $invoice = Invoice::factory()->overdue()->create(); - - // Assert - expect($invoice->isOverdue())->toBeTrue(); -}); diff --git a/tests/Unit/Models/OrderTest.php b/tests/Unit/Models/OrderTest.php index 0ca5152..d52f158 100644 --- a/tests/Unit/Models/OrderTest.php +++ b/tests/Unit/Models/OrderTest.php @@ -33,14 +33,6 @@ expect($order->ticket->id)->toBe($ticket->id); }); -it('can create an order of status', function (OrderStatus $status) { - // Arrange & Act - $order = Order::factory()->ofStatus($status)->create(); - - // Assert - expect($order->status)->toBe($status); -})->with(OrderStatus::cases()); - it('can update an order', function () { // Arrange $order = Order::factory()->create(); @@ -88,49 +80,3 @@ expect($order->ticket)->toBeInstanceOf(Ticket::class); expect($order->ticket->id)->toBe($ticket->id); }); - -it('can determine if an order is billable', function () { - // Arrange & Act & Assert - $order = Order::factory()->make(['is_billable' => true]); - expect($order->is_billable)->toBeTrue(); - - $order = Order::factory()->make(['is_billable' => false]); - expect($order->is_billable)->toBeFalse(); -}); - -it('can filter orders by billable scope', function () { - // Arrange - Order::factory()->create(); - Order::factory()->nonBillable()->create(); - - // Act - $orders = Order::query()->billable()->get(); - - // Assert - expect($orders)->toHaveCount(1); - expect($orders->first()->is_billable)->toBeTrue(); -}); - -it('can filter orders by status scope', function (OrderStatus $status) { - // Arrange - Order::factory()->ofStatus($status)->create(); - - // Act - $orders = Order::query()->ofStatus($status)->get(); - - // Assert - expect($orders)->toHaveCount(1); - expect($orders->first()->status)->toBe($status); -})->with(OrderStatus::cases()); - -it('can filter orders by approved scope', function () { - // Arrange - Order::factory()->create(); - - // Act - $orders = Order::query()->approved()->get(); - - // Assert - expect($orders)->toHaveCount(1); - expect($orders->first()->approved_at)->not->toBeNull(); -}); diff --git a/tests/Unit/Models/TaskTest.php b/tests/Unit/Models/TaskTest.php index 54acd7e..e1bec44 100644 --- a/tests/Unit/Models/TaskTest.php +++ b/tests/Unit/Models/TaskTest.php @@ -5,6 +5,7 @@ use App\Models\Task; use App\Models\Ticket; use Illuminate\Foundation\Testing\RefreshDatabase; +use Illuminate\Support\Carbon; uses(RefreshDatabase::class); @@ -16,8 +17,10 @@ expect($task->ticket_id)->not->toBeNull(); expect($task->description)->not->toBeEmpty(); expect($task->cost)->toBeGreaterThan(0); + expect($task->is_billable)->toBeTrue(); expect($task->status)->toBeInstanceOf(TaskStatus::class); expect($task->type)->toBeInstanceOf(TaskType::class); + expect($task->approved_at)->toBeInstanceOf(Carbon::class); }); it('can create a task for a ticket', function () { @@ -31,22 +34,6 @@ expect($task->ticket->id)->toBe($ticket->id); }); -it('can create a task of type', function (TaskType $type) { - // Arrange & Act - $task = Task::factory()->ofType($type)->create(); - - // Assert - expect($task->type)->toBe($type); -})->with(TaskType::cases()); - -it('can create a task of status', function (TaskStatus $status) { - // Arrange & Act - $task = Task::factory()->ofStatus($status)->create(); - - // Assert - expect($task->status)->toBe($status); -})->with(TaskStatus::cases()); - it('can update a task', function () { // Arrange $task = Task::factory()->create(); @@ -58,6 +45,7 @@ 'is_billable' => false, 'type' => TaskType::Inspection, 'status' => TaskStatus::Completed, + 'approved_at' => now(), ]); // Assert @@ -66,6 +54,7 @@ expect($task->is_billable)->toBeFalse(); expect($task->type)->toBe(TaskType::Inspection); expect($task->status)->toBe(TaskStatus::Completed); + expect($task->approved_at)->toBeInstanceOf(Carbon::class); }); it('can delete a task', function () { @@ -86,61 +75,3 @@ expect($task->ticket)->toBeInstanceOf(Ticket::class); expect($task->ticket->id)->toBe($ticket->id); }); - -it('can determine if a task is billable', function () { - // Arrange & Act & Assert - $task = Task::factory()->make(['is_billable' => true]); - expect($task->is_billable)->toBeTrue(); - - $task = Task::factory()->make(['is_billable' => false]); - expect($task->is_billable)->toBeFalse(); -}); - -it('can filter tasks by billable scope', function () { - // Arrange - Task::factory()->create(); - Task::factory()->nonBillable()->create(); - - // Act - $tasks = Task::query()->billable()->get(); - - // Assert - expect($tasks)->toHaveCount(1); - expect($tasks->first()->is_billable)->toBeTrue(); -}); - -it('can filter tasks by type scope', function (TaskType $type) { - // Arrange - Task::factory()->ofType($type)->create(); - - // Act - $tasks = Task::query()->ofType($type)->get(); - - // Assert - expect($tasks)->toHaveCount(1); - expect($tasks->first()->type)->toBe($type); -})->with(TaskType::cases()); - -it('can filter tasks by status scope', function (TaskStatus $status) { - // Arrange - Task::factory()->ofStatus($status)->create(); - - // Act - $tasks = Task::query()->ofStatus($status)->get(); - - // Assert - expect($tasks)->tohavecount(1); - expect($tasks->first()->status)->toBe($status); -})->with(TaskStatus::cases()); - -it('can filter tasks by approved scope', function () { - // Arrange - Task::factory()->create(); - - // Act - $tasks = Task::query()->approved()->get(); - - // Assert - expect($tasks)->toHaveCount(1); - expect($tasks->first()->approved_at)->not->toBeNull(); -}); diff --git a/tests/Unit/Models/TicketTest.php b/tests/Unit/Models/TicketTest.php index 12ae90b..01b8fb8 100644 --- a/tests/Unit/Models/TicketTest.php +++ b/tests/Unit/Models/TicketTest.php @@ -1,11 +1,10 @@ device_id)->not->toBeNull(); expect($ticket->title)->not->toBeEmpty(); expect($ticket->description)->not->toBeEmpty(); - expect($ticket->priority)->toBeInstanceOf(TicketPriority::class); + expect($ticket->priority)->toBeInstanceOf(Priority::class); expect($ticket->status)->toBeInstanceOf(TicketStatus::class); expect($ticket->due_date)->not->toBeNull(); }); -it('can create a ticket with an assignee', function () { - // Arrange - $user = User::factory()->create(); - $ticket = Ticket::factory()->forAssignee($user)->create(); - - // Assert - expect($ticket->assignee_id)->toBe($user->id); - expect($ticket->assignee)->toBeInstanceOf(User::class); -}); - -it('can create a ticket of priority', function () { - // Arrange - $ticket = Ticket::factory()->ofPriority(TicketPriority::High)->create(); - - // Assert - expect($ticket->priority)->toBe(TicketPriority::High); -}); - -it('can create a ticket of status', function () { - // Arrange - $ticket = Ticket::factory()->ofStatus(TicketStatus::InProgress)->create(); - - // Assert - expect($ticket->status)->toBe(TicketStatus::InProgress); -}); - -it('can create an overdue ticket', function () { - // Arrange - $ticket = Ticket::factory()->overdue()->create(); - - // Assert - expect($ticket->due_date->isPast())->toBeTrue(); - expect($ticket->status)->not->toBe(TicketStatus::Closed); -}); - it('can update a ticket', function () { // Arrange $ticket = Ticket::factory()->create(); @@ -66,7 +30,7 @@ $ticket->update([ 'title' => 'Updated Ticket Title', 'description' => 'Updated ticket description', - 'priority' => TicketPriority::High, + 'priority' => Priority::High, 'status' => TicketStatus::InProgress, 'due_date' => now()->addMonth(), ]); @@ -74,7 +38,7 @@ // Assert expect($ticket->title)->toBe('Updated Ticket Title'); expect($ticket->description)->toBe('Updated ticket description'); - expect($ticket->priority)->toBe(TicketPriority::High); + expect($ticket->priority)->toBe(Priority::High); expect($ticket->status)->toBe(TicketStatus::InProgress); expect($ticket->due_date->isFuture())->toBeTrue(); }); @@ -118,101 +82,3 @@ expect($ticket->orders)->toHaveCount(2); }); - -it('can assign a ticket to a user', function () { - // Arrange - $user = User::factory()->create(); - $ticket = Ticket::factory()->create(); - - // Act - $ticket->assignTo($user); - - // Assert - expect($ticket->assignee_id)->toBe($user->id); - expect($ticket->assignee)->toBeInstanceOf(User::class); -}); - -it('can unassign a ticket from a user', function () { - // Arrange - $user = User::factory()->create(); - $ticket = Ticket::factory()->forAssignee($user)->create(); - - // Act - $ticket->unassign(); - - // Assert - expect($ticket->assignee_id)->toBeNull(); - expect($ticket->assignee)->toBeNull(); -}); - -it('can determine if a ticket is assignable', function () { - // Arrange - $ticket = Ticket::factory()->create(); - - // Act - $isAssignable = $ticket->isAssignable(); - - // Assert - expect($isAssignable)->toBeTrue(); -}); - -it('can filter tickets by assignable scope', function () { - // Arrange - $user = User::factory()->create(); - Ticket::factory()->forAssignee($user)->create(); - Ticket::factory()->create(); - - // Act - $tickets = Ticket::assignable()->get(); - - // Assert - expect($tickets->count())->toBe(1); - expect($tickets->first()->assignee_id)->toBeNull(); -}); - -it('can filter tickets by priority scope', function (TicketPriority $priority) { - // Arrange - Ticket::factory()->ofPriority($priority)->create(); - - // Act - $tickets = Ticket::ofPriority($priority)->get(); - - // Assert - expect($tickets->count())->toBe(1); - expect($tickets->first()->priority)->toBe($priority); -})->with(TicketPriority::cases()); - -it('can filter tickets by status scope', function (TicketStatus $status) { - // Arrange - Ticket::factory()->ofStatus($status)->create(); - - // Act - $tickets = Ticket::ofStatus($status)->get(); - - // Assert - expect($tickets->count())->toBe(1); - expect($tickets->first()->status)->toBe($status); -})->with(TicketStatus::cases()); - -it('can determine if a ticket is overdue', function () { - // Arrange - $ticket = Ticket::factory()->overdue()->create(); - - // Assert - expect($ticket->isOverdue())->toBeTrue(); - expect($ticket->due_date->isPast())->toBeTrue(); - expect($ticket->status)->not->toBe(TicketStatus::Closed); -}); - -it('can filter tickets by overdue scope', function () { - // Arrange - Ticket::factory()->create(); - Ticket::factory()->overdue()->create(); - - // Act - $tickets = Ticket::overdue()->get(); - - // Assert - expect($tickets->count())->toBe(1); - expect($tickets->first()->due_date->isPast())->toBeTrue(); -}); diff --git a/tests/Unit/Models/TransactionTest.php b/tests/Unit/Models/TransactionTest.php index 5d89e21..dd86956 100644 --- a/tests/Unit/Models/TransactionTest.php +++ b/tests/Unit/Models/TransactionTest.php @@ -46,14 +46,6 @@ expect($transaction->method)->toBe($method); })->with(TransactionMethod::cases()); -it('can create a transaction of type', function (TransactionType $type) { - // Arrange & Act - $transaction = Transaction::factory()->ofType($type)->create(); - - // Assert - expect($transaction->type)->toBe($type); -})->with(TransactionType::cases()); - it('can update a transaction', function () { // Arrange $transaction = Transaction::factory()->create(); @@ -105,15 +97,3 @@ expect($transactions)->toHaveCount(1); expect($transactions->first()->method)->toBe($method); })->with(TransactionMethod::cases()); - -it('can filter transactions by type scope', function (TransactionType $type) { - // Arrange - Transaction::factory()->ofType($type)->create(); - - // Act - $transactions = Transaction::ofType($type)->get(); - - // Assert - expect($transactions)->toHaveCount(1); - expect($transactions->first()->type)->toBe($type); -})->with(TransactionType::cases()); diff --git a/tests/Unit/Models/UserTest.php b/tests/Unit/Models/UserTest.php index f62dd5b..50c4359 100644 --- a/tests/Unit/Models/UserTest.php +++ b/tests/Unit/Models/UserTest.php @@ -19,14 +19,6 @@ expect($user->status)->toBeInstanceOf(UserStatus::class); }); -it('can create user without phone number', function () { - // Act - $user = User::factory()->withoutPhone()->create(); - - // Assert - expect($user->phone)->toBeNull(); -}); - it('can create user of role', function (UserRole $role) { // Act $user = User::factory()->ofRole($role)->create(); @@ -35,14 +27,6 @@ expect($user->role)->toBe($role); })->with(UserRole::cases()); -it('can create user of status', function (UserStatus $status) { - // Act - $user = User::factory()->ofStatus($status)->create(); - - // Assert - expect($user->status)->toBe($status); -})->with(UserStatus::cases()); - it('can create user unverified', function () { // Act $user = User::factory()->unverified()->create(); @@ -114,15 +98,3 @@ expect($activeUser->isActive())->toBeTrue(); expect($suspendedUser->isActive())->toBeFalse(); }); - -it('can filter users by status scope', function (UserStatus $status) { - // Arrange - User::factory()->ofStatus($status)->create(); - - // Act - $users = User::ofStatus($status); - - // Assert - expect($users->count())->toBe(1); - expect($users->first()->status)->toBe($status); -})->with(UserStatus::cases());