'date', 'consultation_type' => ConsultationType::class, 'payment_status' => PaymentStatus::class, 'status' => ConsultationStatus::class, 'payment_amount' => 'decimal:2', 'payment_received_at' => 'datetime', 'admin_notes' => 'array', 'reminder_24h_sent_at' => 'datetime', 'reminder_2h_sent_at' => 'datetime', ]; } protected static function booted(): void { static::saving(function (Consultation $consultation) { // Either user_id or guest fields must be present if (is_null($consultation->user_id)) { if (empty($consultation->guest_name) || empty($consultation->guest_email) || empty($consultation->guest_phone)) { throw new \InvalidArgumentException( 'Guest consultations require guest_name, guest_email, and guest_phone' ); } } }); } /** * Get the user that owns the consultation. */ public function user(): BelongsTo { return $this->belongsTo(User::class); } /** * Check if this is a guest consultation (no linked user). */ public function isGuest(): bool { return is_null($this->user_id); } /** * Get the client's display name. */ public function getClientName(): string { return $this->isGuest() ? $this->guest_name : $this->user->full_name; } /** * Get the client's email address. */ public function getClientEmail(): string { return $this->isGuest() ? $this->guest_email : $this->user->email; } /** * Get the client's phone number. */ public function getClientPhone(): ?string { return $this->isGuest() ? $this->guest_phone : $this->user->phone; } /** * Mark consultation as completed. * * @throws \InvalidArgumentException */ public function markAsCompleted(): void { if (! in_array($this->status, [ConsultationStatus::Approved, ConsultationStatus::NoShow])) { throw new \InvalidArgumentException( __('messages.invalid_status_transition', ['from' => $this->status->value, 'to' => 'completed']) ); } $this->update(['status' => ConsultationStatus::Completed]); } /** * Mark consultation as no-show. * * @throws \InvalidArgumentException */ public function markAsNoShow(): void { if (! in_array($this->status, [ConsultationStatus::Approved, ConsultationStatus::Completed])) { throw new \InvalidArgumentException( __('messages.invalid_status_transition', ['from' => $this->status->value, 'to' => 'no_show']) ); } $this->update(['status' => ConsultationStatus::NoShow]); } /** * Cancel the consultation. * * @throws \InvalidArgumentException */ public function cancel(): void { if (! in_array($this->status, [ConsultationStatus::Pending, ConsultationStatus::Approved])) { throw new \InvalidArgumentException( __('messages.cannot_cancel_consultation') ); } $this->update(['status' => ConsultationStatus::Cancelled]); } /** * Mark payment as received. * * @throws \InvalidArgumentException */ public function markPaymentReceived(): void { if ($this->consultation_type !== ConsultationType::Paid) { throw new \InvalidArgumentException(__('messages.not_paid_consultation')); } if ($this->payment_status === PaymentStatus::Received) { throw new \InvalidArgumentException(__('messages.payment_already_received')); } $this->update([ 'payment_status' => PaymentStatus::Received, 'payment_received_at' => now(), ]); } /** * Reschedule the consultation to a new date and time. */ public function reschedule(string $newDate, string $newTime): void { $this->update([ 'booking_date' => $newDate, 'booking_time' => $newTime, ]); } /** * Add an admin note to the consultation. */ public function addNote(string $note, int $adminId): void { $notes = $this->admin_notes ?? []; $notes[] = [ 'text' => $note, 'admin_id' => $adminId, 'created_at' => now()->toISOString(), ]; $this->update(['admin_notes' => $notes]); } /** * Update an admin note at the given index. */ public function updateNote(int $index, string $newText): void { $notes = $this->admin_notes ?? []; if (isset($notes[$index])) { $notes[$index]['text'] = $newText; $notes[$index]['updated_at'] = now()->toISOString(); $this->update(['admin_notes' => $notes]); } } /** * Delete an admin note at the given index. */ public function deleteNote(int $index): void { $notes = $this->admin_notes ?? []; if (isset($notes[$index])) { array_splice($notes, $index, 1); $this->update(['admin_notes' => array_values($notes)]); } } /** * Scope for pending consultations. */ public function scopePending(Builder $query): Builder { return $query->where('status', ConsultationStatus::Pending); } /** * Scope for approved consultations. */ public function scopeApproved(Builder $query): Builder { return $query->where('status', ConsultationStatus::Approved); } /** * Scope for upcoming approved consultations. */ public function scopeUpcoming(Builder $query): Builder { return $query->where('booking_date', '>=', today()) ->where('status', ConsultationStatus::Approved); } /** * Scope for past consultations. */ public function scopePast(Builder $query): Builder { return $query->where(function ($q) { $q->where('booking_date', '<', today()) ->orWhereIn('status', [ ConsultationStatus::Completed, ConsultationStatus::Cancelled, ConsultationStatus::NoShow, ]); }); } /** * Scope for guest consultations (no linked user). */ public function scopeGuests(Builder $query): Builder { return $query->whereNull('user_id'); } /** * Scope for client consultations (has linked user). */ public function scopeClients(Builder $query): Builder { return $query->whereNotNull('user_id'); } }