1269 lines
42 KiB
Markdown
1269 lines
42 KiB
Markdown
# Story 11.3: Guest Email Notifications & Admin Integration
|
|
|
|
## Epic Reference
|
|
**Epic 11:** Guest Booking
|
|
|
|
## Story Context
|
|
This story completes the guest booking workflow by implementing email notifications for guests and updating the admin interface to properly display and manage guest bookings alongside client bookings.
|
|
|
|
## User Story
|
|
As a **guest** who submitted a booking,
|
|
I want **to receive email confirmations about my booking status**,
|
|
So that **I know my request was received and can track its progress**.
|
|
|
|
As an **admin**,
|
|
I want **to see guest contact information when reviewing bookings**,
|
|
So that **I can contact them and manage their appointments**.
|
|
|
|
## Acceptance Criteria
|
|
|
|
### Guest Email Notifications
|
|
- [ ] Guest receives confirmation email when booking submitted
|
|
- [ ] Guest receives approval email when booking approved (with date/time details)
|
|
- [ ] Guest receives rejection email when booking rejected
|
|
- [ ] All emails use existing email template/branding
|
|
- [ ] Emails sent to guest_email address
|
|
- [ ] Bilingual support based on site locale at submission time
|
|
|
|
### Admin Pending Bookings View
|
|
- [ ] Guest bookings appear in pending list alongside client bookings
|
|
- [ ] Guest bookings show "Guest" badge/indicator
|
|
- [ ] Guest name, email, phone displayed in list
|
|
- [ ] Click through to booking review shows full guest details
|
|
|
|
### Admin Booking Review Page
|
|
- [ ] Guest contact info displayed prominently
|
|
- [ ] Guest name shown instead of user name
|
|
- [ ] Guest email shown with mailto link
|
|
- [ ] Guest phone shown with tel link
|
|
- [ ] Approve/reject workflow works for guest bookings
|
|
- [ ] Email notifications sent to guest on status change
|
|
|
|
### Existing Admin Email
|
|
- [ ] `NewBookingAdminEmail` updated to handle guest bookings
|
|
- [ ] Admin email shows guest contact info for guest bookings
|
|
- [ ] Admin email shows client info for client bookings
|
|
|
|
## Technical Context
|
|
|
|
### Existing Email Architecture
|
|
The current codebase uses two approaches for sending emails:
|
|
1. **Notifications** (`app/Notifications/`) - Used for client bookings via `$user->notify()`
|
|
2. **Mailables** (`app/Mail/`) - Used for direct email sending via `Mail::to()`
|
|
|
|
For **guest bookings**, we use Mailables (not Notifications) because guests don't have User accounts.
|
|
|
|
### Existing Admin Email Structure
|
|
The `NewBookingAdminEmail` uses locale-specific markdown templates:
|
|
- `resources/views/emails/admin/new-booking/en.blade.php`
|
|
- `resources/views/emails/admin/new-booking/ar.blade.php`
|
|
|
|
### Existing Admin Components
|
|
- `pending.blade.php` uses `$booking` variable (not `$consultation`)
|
|
- `review.blade.php` uses modal-based approve/reject flow (`openApproveModal()` -> `approve()`)
|
|
- Client notifications use `BookingApproved` and `BookingRejected` Notifications
|
|
|
|
## Implementation Steps
|
|
|
|
### Step 1: Create Guest Booking Submitted Email
|
|
Create `app/Mail/GuestBookingSubmittedMail.php`:
|
|
|
|
```php
|
|
<?php
|
|
|
|
namespace App\Mail;
|
|
|
|
use App\Models\Consultation;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Mail\Mailable;
|
|
use Illuminate\Mail\Mailables\Content;
|
|
use Illuminate\Mail\Mailables\Envelope;
|
|
use Illuminate\Queue\SerializesModels;
|
|
|
|
class GuestBookingSubmittedMail extends Mailable implements ShouldQueue
|
|
{
|
|
use Queueable, SerializesModels;
|
|
|
|
public function __construct(
|
|
public Consultation $consultation,
|
|
public string $locale = 'en'
|
|
) {}
|
|
|
|
public function envelope(): Envelope
|
|
{
|
|
return new Envelope(
|
|
subject: $this->locale === 'ar'
|
|
? 'تم استلام طلب الحجز - مكتب ليبرا للمحاماة'
|
|
: 'Booking Request Received - Libra Law Firm',
|
|
);
|
|
}
|
|
|
|
public function content(): Content
|
|
{
|
|
return new Content(
|
|
markdown: 'emails.guest.booking-submitted.' . $this->locale,
|
|
with: [
|
|
'consultation' => $this->consultation,
|
|
'guestName' => $this->consultation->guest_name,
|
|
'formattedDate' => $this->getFormattedDate(),
|
|
'formattedTime' => $this->getFormattedTime(),
|
|
],
|
|
);
|
|
}
|
|
|
|
private function getFormattedDate(): string
|
|
{
|
|
$date = $this->consultation->booking_date;
|
|
return $this->locale === 'ar'
|
|
? $date->format('d/m/Y')
|
|
: $date->format('m/d/Y');
|
|
}
|
|
|
|
private function getFormattedTime(): string
|
|
{
|
|
return Carbon::parse($this->consultation->booking_time)->format('h:i A');
|
|
}
|
|
}
|
|
```
|
|
|
|
### Step 2: Create Guest Booking Submitted Email Templates
|
|
Create `resources/views/emails/guest/booking-submitted/en.blade.php`:
|
|
|
|
```blade
|
|
<x-mail::message>
|
|
# Your Booking Request Has Been Received
|
|
|
|
Dear {{ $guestName }},
|
|
|
|
Thank you for your consultation request. We have received your booking and our team will review it shortly.
|
|
|
|
**Requested Date:** {{ $formattedDate }}
|
|
|
|
**Requested Time:** {{ $formattedTime }}
|
|
|
|
**Duration:** 45 minutes
|
|
|
|
You will receive another email once your booking has been reviewed. If approved, you will receive the consultation details and a calendar invitation.
|
|
|
|
Thanks,<br>
|
|
{{ config('app.name') }}
|
|
</x-mail::message>
|
|
```
|
|
|
|
Create `resources/views/emails/guest/booking-submitted/ar.blade.php`:
|
|
|
|
```blade
|
|
<x-mail::message>
|
|
# تم استلام طلب الحجز الخاص بك
|
|
|
|
عزيزي/عزيزتي {{ $guestName }}،
|
|
|
|
شكراً لطلب الاستشارة. لقد تلقينا حجزك وسيقوم فريقنا بمراجعته قريباً.
|
|
|
|
**التاريخ المطلوب:** {{ $formattedDate }}
|
|
|
|
**الوقت المطلوب:** {{ $formattedTime }}
|
|
|
|
**المدة:** 45 دقيقة
|
|
|
|
ستتلقى رسالة أخرى عند مراجعة حجزك. في حال الموافقة، ستتلقى تفاصيل الاستشارة ودعوة تقويم.
|
|
|
|
شكراً،<br>
|
|
{{ config('app.name') }}
|
|
</x-mail::message>
|
|
```
|
|
|
|
### Step 3: Create Guest Booking Approved Email
|
|
Create `app/Mail/GuestBookingApprovedMail.php`:
|
|
|
|
```php
|
|
<?php
|
|
|
|
namespace App\Mail;
|
|
|
|
use App\Models\Consultation;
|
|
use App\Services\CalendarService;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Mail\Mailable;
|
|
use Illuminate\Mail\Mailables\Attachment;
|
|
use Illuminate\Mail\Mailables\Content;
|
|
use Illuminate\Mail\Mailables\Envelope;
|
|
use Illuminate\Queue\SerializesModels;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class GuestBookingApprovedMail extends Mailable implements ShouldQueue
|
|
{
|
|
use Queueable, SerializesModels;
|
|
|
|
public function __construct(
|
|
public Consultation $consultation,
|
|
public string $locale = 'en',
|
|
public ?string $paymentInstructions = null
|
|
) {}
|
|
|
|
public function envelope(): Envelope
|
|
{
|
|
return new Envelope(
|
|
subject: $this->locale === 'ar'
|
|
? 'تأكيد الحجز - مكتب ليبرا للمحاماة'
|
|
: 'Booking Confirmed - Libra Law Firm',
|
|
);
|
|
}
|
|
|
|
public function content(): Content
|
|
{
|
|
return new Content(
|
|
markdown: 'emails.guest.booking-approved.' . $this->locale,
|
|
with: [
|
|
'consultation' => $this->consultation,
|
|
'guestName' => $this->consultation->guest_name,
|
|
'formattedDate' => $this->getFormattedDate(),
|
|
'formattedTime' => $this->getFormattedTime(),
|
|
'isPaid' => $this->consultation->consultation_type?->value === 'paid',
|
|
'paymentAmount' => $this->consultation->payment_amount,
|
|
'paymentInstructions' => $this->paymentInstructions,
|
|
],
|
|
);
|
|
}
|
|
|
|
public function attachments(): array
|
|
{
|
|
try {
|
|
$calendarService = app(CalendarService::class);
|
|
$icsContent = $calendarService->generateIcs($this->consultation);
|
|
|
|
return [
|
|
Attachment::fromData(fn () => $icsContent, 'consultation.ics')
|
|
->withMime('text/calendar'),
|
|
];
|
|
} catch (\Exception $e) {
|
|
Log::error('Failed to generate calendar attachment for guest email', [
|
|
'consultation_id' => $this->consultation->id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
private function getFormattedDate(): string
|
|
{
|
|
$date = $this->consultation->booking_date;
|
|
return $this->locale === 'ar'
|
|
? $date->format('d/m/Y')
|
|
: $date->format('m/d/Y');
|
|
}
|
|
|
|
private function getFormattedTime(): string
|
|
{
|
|
return Carbon::parse($this->consultation->booking_time)->format('h:i A');
|
|
}
|
|
}
|
|
```
|
|
|
|
### Step 4: Create Guest Booking Approved Email Templates
|
|
Create `resources/views/emails/guest/booking-approved/en.blade.php`:
|
|
|
|
```blade
|
|
<x-mail::message>
|
|
# Your Booking Has Been Confirmed
|
|
|
|
Dear {{ $guestName }},
|
|
|
|
Great news! Your consultation request has been approved.
|
|
|
|
**Date:** {{ $formattedDate }}
|
|
|
|
**Time:** {{ $formattedTime }}
|
|
|
|
**Duration:** 45 minutes
|
|
|
|
**Type:** {{ $isPaid ? 'Paid Consultation' : 'Free Consultation' }}
|
|
|
|
@if($isPaid && $paymentAmount)
|
|
**Amount Due:** {{ number_format($paymentAmount, 2) }} ILS
|
|
|
|
@if($paymentInstructions)
|
|
**Payment Instructions:**
|
|
{{ $paymentInstructions }}
|
|
@endif
|
|
@endif
|
|
|
|
A calendar invitation (.ics file) is attached to this email. You can add it to your calendar application.
|
|
|
|
We look forward to meeting with you.
|
|
|
|
Thanks,<br>
|
|
{{ config('app.name') }}
|
|
</x-mail::message>
|
|
```
|
|
|
|
Create `resources/views/emails/guest/booking-approved/ar.blade.php`:
|
|
|
|
```blade
|
|
<x-mail::message>
|
|
# تم تأكيد حجزك
|
|
|
|
عزيزي/عزيزتي {{ $guestName }}،
|
|
|
|
أخبار سارة! تمت الموافقة على طلب الاستشارة الخاص بك.
|
|
|
|
**التاريخ:** {{ $formattedDate }}
|
|
|
|
**الوقت:** {{ $formattedTime }}
|
|
|
|
**المدة:** 45 دقيقة
|
|
|
|
**النوع:** {{ $isPaid ? 'استشارة مدفوعة' : 'استشارة مجانية' }}
|
|
|
|
@if($isPaid && $paymentAmount)
|
|
**المبلغ المستحق:** {{ number_format($paymentAmount, 2) }} شيكل
|
|
|
|
@if($paymentInstructions)
|
|
**تعليمات الدفع:**
|
|
{{ $paymentInstructions }}
|
|
@endif
|
|
@endif
|
|
|
|
مرفق بهذه الرسالة ملف دعوة تقويم (.ics). يمكنك إضافته إلى تطبيق التقويم الخاص بك.
|
|
|
|
نتطلع للقائك.
|
|
|
|
شكراً،<br>
|
|
{{ config('app.name') }}
|
|
</x-mail::message>
|
|
```
|
|
|
|
### Step 5: Create Guest Booking Rejected Email
|
|
Create `app/Mail/GuestBookingRejectedMail.php`:
|
|
|
|
```php
|
|
<?php
|
|
|
|
namespace App\Mail;
|
|
|
|
use App\Models\Consultation;
|
|
use Carbon\Carbon;
|
|
use Illuminate\Bus\Queueable;
|
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
use Illuminate\Mail\Mailable;
|
|
use Illuminate\Mail\Mailables\Content;
|
|
use Illuminate\Mail\Mailables\Envelope;
|
|
use Illuminate\Queue\SerializesModels;
|
|
|
|
class GuestBookingRejectedMail extends Mailable implements ShouldQueue
|
|
{
|
|
use Queueable, SerializesModels;
|
|
|
|
public function __construct(
|
|
public Consultation $consultation,
|
|
public string $locale = 'en',
|
|
public ?string $reason = null
|
|
) {}
|
|
|
|
public function envelope(): Envelope
|
|
{
|
|
return new Envelope(
|
|
subject: $this->locale === 'ar'
|
|
? 'تحديث الحجز - مكتب ليبرا للمحاماة'
|
|
: 'Booking Update - Libra Law Firm',
|
|
);
|
|
}
|
|
|
|
public function content(): Content
|
|
{
|
|
return new Content(
|
|
markdown: 'emails.guest.booking-rejected.' . $this->locale,
|
|
with: [
|
|
'consultation' => $this->consultation,
|
|
'guestName' => $this->consultation->guest_name,
|
|
'formattedDate' => $this->getFormattedDate(),
|
|
'formattedTime' => $this->getFormattedTime(),
|
|
'reason' => $this->reason,
|
|
'hasReason' => !empty($this->reason),
|
|
],
|
|
);
|
|
}
|
|
|
|
private function getFormattedDate(): string
|
|
{
|
|
$date = $this->consultation->booking_date;
|
|
return $this->locale === 'ar'
|
|
? $date->format('d/m/Y')
|
|
: $date->format('m/d/Y');
|
|
}
|
|
|
|
private function getFormattedTime(): string
|
|
{
|
|
return Carbon::parse($this->consultation->booking_time)->format('h:i A');
|
|
}
|
|
}
|
|
```
|
|
|
|
### Step 6: Create Guest Booking Rejected Email Templates
|
|
Create `resources/views/emails/guest/booking-rejected/en.blade.php`:
|
|
|
|
```blade
|
|
<x-mail::message>
|
|
# Booking Update
|
|
|
|
Dear {{ $guestName }},
|
|
|
|
We regret to inform you that we are unable to accommodate your consultation request for the following time:
|
|
|
|
**Requested Date:** {{ $formattedDate }}
|
|
|
|
**Requested Time:** {{ $formattedTime }}
|
|
|
|
@if($hasReason)
|
|
**Reason:** {{ $reason }}
|
|
@endif
|
|
|
|
We apologize for any inconvenience. Please feel free to submit a new booking request for a different time slot.
|
|
|
|
Thanks,<br>
|
|
{{ config('app.name') }}
|
|
</x-mail::message>
|
|
```
|
|
|
|
Create `resources/views/emails/guest/booking-rejected/ar.blade.php`:
|
|
|
|
```blade
|
|
<x-mail::message>
|
|
# تحديث الحجز
|
|
|
|
عزيزي/عزيزتي {{ $guestName }}،
|
|
|
|
نأسف لإبلاغك بأننا غير قادرين على استيعاب طلب الاستشارة الخاص بك في الوقت التالي:
|
|
|
|
**التاريخ المطلوب:** {{ $formattedDate }}
|
|
|
|
**الوقت المطلوب:** {{ $formattedTime }}
|
|
|
|
@if($hasReason)
|
|
**السبب:** {{ $reason }}
|
|
@endif
|
|
|
|
نعتذر عن أي إزعاج. لا تتردد في تقديم طلب حجز جديد لفترة زمنية مختلفة.
|
|
|
|
شكراً،<br>
|
|
{{ config('app.name') }}
|
|
</x-mail::message>
|
|
```
|
|
|
|
### Step 7: Update NewBookingAdminEmail
|
|
Update `app/Mail/NewBookingAdminEmail.php` to handle guest bookings.
|
|
|
|
Modify the `content()` method to use the Consultation model's helper methods:
|
|
|
|
```php
|
|
public function content(): Content
|
|
{
|
|
$admin = $this->getAdminUser();
|
|
$locale = $admin?->preferred_language ?? 'en';
|
|
|
|
return new Content(
|
|
markdown: 'emails.admin.new-booking.'.$locale,
|
|
with: [
|
|
'consultation' => $this->consultation,
|
|
'client' => $this->consultation->user,
|
|
'clientName' => $this->consultation->getClientName(),
|
|
'clientEmail' => $this->consultation->getClientEmail(),
|
|
'clientPhone' => $this->consultation->getClientPhone(),
|
|
'isGuest' => $this->consultation->isGuest(),
|
|
'formattedDate' => $this->getFormattedDate($locale),
|
|
'formattedTime' => $this->getFormattedTime(),
|
|
'reviewUrl' => $this->getReviewUrl(),
|
|
],
|
|
);
|
|
}
|
|
```
|
|
|
|
### Step 8: Update Admin New Booking Email Templates
|
|
Update `resources/views/emails/admin/new-booking/en.blade.php` to show guest indicator:
|
|
|
|
Add after the title section:
|
|
```blade
|
|
@if($isGuest)
|
|
**Booking Type:** Guest (No Account)
|
|
|
|
@endif
|
|
**Client Name:** {{ $clientName }}
|
|
|
|
**Client Email:** {{ $clientEmail }}
|
|
|
|
**Client Phone:** {{ $clientPhone ?? 'Not provided' }}
|
|
```
|
|
|
|
Update `resources/views/emails/admin/new-booking/ar.blade.php` similarly:
|
|
```blade
|
|
@if($isGuest)
|
|
**نوع الحجز:** زائر (بدون حساب)
|
|
|
|
@endif
|
|
**اسم العميل:** {{ $clientName }}
|
|
|
|
**بريد العميل:** {{ $clientEmail }}
|
|
|
|
**هاتف العميل:** {{ $clientPhone ?? 'غير متوفر' }}
|
|
```
|
|
|
|
### Step 9: Update Admin Pending Bookings List
|
|
Update `resources/views/livewire/admin/bookings/pending.blade.php`.
|
|
|
|
In the booking info section (around line 194), replace the client name display:
|
|
|
|
```blade
|
|
<!-- Booking Info -->
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-3 mb-2">
|
|
@if($booking->isGuest())
|
|
<flux:badge color="amber" size="sm">{{ __('admin.guest') }}</flux:badge>
|
|
@endif
|
|
<span class="font-semibold text-zinc-900 dark:text-zinc-100">
|
|
{{ $booking->getClientName() }}
|
|
</span>
|
|
<flux:badge variant="warning" size="sm">
|
|
{{ $booking->status->label() }}
|
|
</flux:badge>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2 text-sm text-zinc-600 dark:text-zinc-400">
|
|
<div class="flex items-center gap-2">
|
|
<flux:icon name="calendar" class="w-4 h-4" />
|
|
{{ \Carbon\Carbon::parse($booking->booking_date)->translatedFormat('l, d M Y') }}
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<flux:icon name="clock" class="w-4 h-4" />
|
|
{{ \Carbon\Carbon::parse($booking->booking_time)->format('g:i A') }}
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<flux:icon name="envelope" class="w-4 h-4" />
|
|
<a href="mailto:{{ $booking->getClientEmail() }}" class="hover:underline">
|
|
{{ $booking->getClientEmail() }}
|
|
</a>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<flux:icon name="document-text" class="w-4 h-4" />
|
|
{{ __('admin.submitted') }}: {{ $booking->created_at->translatedFormat('d M Y') }}
|
|
</div>
|
|
</div>
|
|
|
|
<p class="mt-3 text-sm text-zinc-600 dark:text-zinc-400 line-clamp-2">
|
|
{{ Str::limit($booking->problem_summary, 150) }}
|
|
</p>
|
|
</div>
|
|
```
|
|
|
|
Also update the `quickApprove` and `quickReject` methods to handle guest bookings:
|
|
|
|
```php
|
|
public function quickApprove(int $id): void
|
|
{
|
|
$consultation = Consultation::with('user')->findOrFail($id);
|
|
|
|
// ... existing validation and update logic ...
|
|
|
|
// Send appropriate notification/email based on guest/client
|
|
if ($consultation->isGuest()) {
|
|
Mail::to($consultation->guest_email)->queue(
|
|
new \App\Mail\GuestBookingApprovedMail(
|
|
$consultation,
|
|
app()->getLocale()
|
|
)
|
|
);
|
|
} elseif ($consultation->user) {
|
|
$consultation->user->notify(
|
|
new BookingApproved($consultation, $icsContent ?? '', null)
|
|
);
|
|
}
|
|
|
|
// ... rest of logging ...
|
|
}
|
|
|
|
public function quickReject(int $id): void
|
|
{
|
|
$consultation = Consultation::with('user')->findOrFail($id);
|
|
|
|
// ... existing validation and update logic ...
|
|
|
|
// Send appropriate notification/email based on guest/client
|
|
if ($consultation->isGuest()) {
|
|
Mail::to($consultation->guest_email)->queue(
|
|
new \App\Mail\GuestBookingRejectedMail(
|
|
$consultation,
|
|
app()->getLocale(),
|
|
null
|
|
)
|
|
);
|
|
} elseif ($consultation->user) {
|
|
$consultation->user->notify(
|
|
new BookingRejected($consultation, null)
|
|
);
|
|
}
|
|
|
|
// ... rest of logging ...
|
|
}
|
|
```
|
|
|
|
Add the required imports at the top:
|
|
```php
|
|
use App\Mail\GuestBookingApprovedMail;
|
|
use App\Mail\GuestBookingRejectedMail;
|
|
use Illuminate\Support\Facades\Mail;
|
|
```
|
|
|
|
### Step 10: Update Admin Booking Review Page
|
|
Update `resources/views/livewire/admin/bookings/review.blade.php`.
|
|
|
|
Replace the "Client Information" section (around line 188-218):
|
|
|
|
```blade
|
|
<!-- Client/Guest Information -->
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg p-6 border border-zinc-200 dark:border-zinc-700 mb-6">
|
|
<div class="flex items-center gap-2 mb-4">
|
|
<flux:heading size="lg">{{ __('admin.client_information') }}</flux:heading>
|
|
@if($consultation->isGuest())
|
|
<flux:badge color="amber" size="sm">{{ __('admin.guest') }}</flux:badge>
|
|
@endif
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.client_name') }}</p>
|
|
<p class="font-medium text-zinc-900 dark:text-zinc-100">
|
|
{{ $consultation->getClientName() }}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.client_email') }}</p>
|
|
<p class="font-medium text-zinc-900 dark:text-zinc-100">
|
|
<a href="mailto:{{ $consultation->getClientEmail() }}" class="text-primary hover:underline">
|
|
{{ $consultation->getClientEmail() }}
|
|
</a>
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.client_phone') }}</p>
|
|
<p class="font-medium text-zinc-900 dark:text-zinc-100">
|
|
@if($consultation->getClientPhone())
|
|
<a href="tel:{{ $consultation->getClientPhone() }}" class="text-primary hover:underline">
|
|
{{ $consultation->getClientPhone() }}
|
|
</a>
|
|
@else
|
|
-
|
|
@endif
|
|
</p>
|
|
</div>
|
|
@unless($consultation->isGuest())
|
|
<div>
|
|
<p class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.client_type') }}</p>
|
|
<p class="font-medium text-zinc-900 dark:text-zinc-100">
|
|
{{ $consultation->user?->user_type?->label() ?? '-' }}
|
|
</p>
|
|
</div>
|
|
@endunless
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
Update the `approve()` method in the PHP section:
|
|
|
|
```php
|
|
public function approve(): void
|
|
{
|
|
if ($this->consultation->status !== ConsultationStatus::Pending) {
|
|
session()->flash('error', __('admin.booking_already_processed'));
|
|
$this->showApproveModal = false;
|
|
return;
|
|
}
|
|
|
|
$this->validate([
|
|
'consultationType' => ['required', 'in:free,paid'],
|
|
'paymentAmount' => ['required_if:consultationType,paid', 'nullable', 'numeric', 'min:0'],
|
|
'paymentInstructions' => ['nullable', 'string', 'max:1000'],
|
|
]);
|
|
|
|
$oldStatus = $this->consultation->status->value;
|
|
$type = $this->consultationType === 'paid' ? ConsultationType::Paid : ConsultationType::Free;
|
|
|
|
$this->consultation->update([
|
|
'status' => ConsultationStatus::Approved,
|
|
'consultation_type' => $type,
|
|
'payment_amount' => $type === ConsultationType::Paid ? $this->paymentAmount : null,
|
|
'payment_status' => $type === ConsultationType::Paid ? PaymentStatus::Pending : PaymentStatus::NotApplicable,
|
|
]);
|
|
|
|
// Generate calendar file
|
|
$icsContent = null;
|
|
try {
|
|
$calendarService = app(CalendarService::class);
|
|
$icsContent = $calendarService->generateIcs($this->consultation);
|
|
} catch (\Exception $e) {
|
|
Log::error('Failed to generate calendar file', [
|
|
'consultation_id' => $this->consultation->id,
|
|
'error' => $e->getMessage(),
|
|
]);
|
|
}
|
|
|
|
// Send appropriate notification/email based on guest/client
|
|
if ($this->consultation->isGuest()) {
|
|
Mail::to($this->consultation->guest_email)->queue(
|
|
new GuestBookingApprovedMail(
|
|
$this->consultation,
|
|
app()->getLocale(),
|
|
$this->paymentInstructions ?: null
|
|
)
|
|
);
|
|
} elseif ($this->consultation->user) {
|
|
$this->consultation->user->notify(
|
|
new BookingApproved(
|
|
$this->consultation,
|
|
$icsContent ?? '',
|
|
$this->paymentInstructions ?: null
|
|
)
|
|
);
|
|
}
|
|
|
|
// Log action
|
|
AdminLog::create([
|
|
'admin_id' => auth()->id(),
|
|
'action' => 'approve',
|
|
'target_type' => 'consultation',
|
|
'target_id' => $this->consultation->id,
|
|
'old_values' => ['status' => $oldStatus],
|
|
'new_values' => [
|
|
'status' => ConsultationStatus::Approved->value,
|
|
'consultation_type' => $type->value,
|
|
'payment_amount' => $this->paymentAmount,
|
|
],
|
|
'ip_address' => request()->ip(),
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
session()->flash('success', __('admin.booking_approved'));
|
|
$this->redirect(route('admin.bookings.pending'), navigate: true);
|
|
}
|
|
```
|
|
|
|
Update the `reject()` method similarly:
|
|
|
|
```php
|
|
public function reject(): void
|
|
{
|
|
if ($this->consultation->status !== ConsultationStatus::Pending) {
|
|
session()->flash('error', __('admin.booking_already_processed'));
|
|
$this->showRejectModal = false;
|
|
return;
|
|
}
|
|
|
|
$this->validate([
|
|
'rejectionReason' => ['nullable', 'string', 'max:1000'],
|
|
]);
|
|
|
|
$oldStatus = $this->consultation->status->value;
|
|
|
|
$this->consultation->update([
|
|
'status' => ConsultationStatus::Rejected,
|
|
]);
|
|
|
|
// Send appropriate notification/email based on guest/client
|
|
if ($this->consultation->isGuest()) {
|
|
Mail::to($this->consultation->guest_email)->queue(
|
|
new GuestBookingRejectedMail(
|
|
$this->consultation,
|
|
app()->getLocale(),
|
|
$this->rejectionReason ?: null
|
|
)
|
|
);
|
|
} elseif ($this->consultation->user) {
|
|
$this->consultation->user->notify(
|
|
new BookingRejected($this->consultation, $this->rejectionReason ?: null)
|
|
);
|
|
}
|
|
|
|
// Log action
|
|
AdminLog::create([
|
|
'admin_id' => auth()->id(),
|
|
'action' => 'reject',
|
|
'target_type' => 'consultation',
|
|
'target_id' => $this->consultation->id,
|
|
'old_values' => ['status' => $oldStatus],
|
|
'new_values' => [
|
|
'status' => ConsultationStatus::Rejected->value,
|
|
'reason' => $this->rejectionReason,
|
|
],
|
|
'ip_address' => request()->ip(),
|
|
'created_at' => now(),
|
|
]);
|
|
|
|
session()->flash('success', __('admin.booking_rejected'));
|
|
$this->redirect(route('admin.bookings.pending'), navigate: true);
|
|
}
|
|
```
|
|
|
|
Add required imports at the top of the PHP section:
|
|
```php
|
|
use App\Mail\GuestBookingApprovedMail;
|
|
use App\Mail\GuestBookingRejectedMail;
|
|
use Illuminate\Support\Facades\Mail;
|
|
```
|
|
|
|
Update the `with()` method to handle guest consultations (they have no history):
|
|
```php
|
|
public function with(): array
|
|
{
|
|
// Guest bookings don't have consultation history
|
|
if ($this->consultation->isGuest()) {
|
|
return ['consultationHistory' => collect()];
|
|
}
|
|
|
|
return [
|
|
'consultationHistory' => Consultation::query()
|
|
->where('user_id', $this->consultation->user_id)
|
|
->where('id', '!=', $this->consultation->id)
|
|
->orderBy('booking_date', 'desc')
|
|
->limit(5)
|
|
->get(),
|
|
];
|
|
}
|
|
```
|
|
|
|
Update the modal summary sections to use helper methods:
|
|
|
|
In the Approve Modal (around line 314):
|
|
```blade
|
|
<div class="bg-zinc-50 dark:bg-zinc-700 p-4 rounded-lg">
|
|
<p><strong>{{ __('admin.client') }}:</strong> {{ $consultation->getClientName() }}</p>
|
|
<p><strong>{{ __('admin.date') }}:</strong> {{ \Carbon\Carbon::parse($consultation->booking_date)->translatedFormat('l, d M Y') }}</p>
|
|
<p><strong>{{ __('admin.time') }}:</strong> {{ \Carbon\Carbon::parse($consultation->booking_time)->format('g:i A') }}</p>
|
|
</div>
|
|
```
|
|
|
|
In the Reject Modal (around line 369):
|
|
```blade
|
|
<div class="bg-zinc-50 dark:bg-zinc-700 p-4 rounded-lg">
|
|
<p><strong>{{ __('admin.client') }}:</strong> {{ $consultation->getClientName() }}</p>
|
|
<p><strong>{{ __('admin.date') }}:</strong> {{ \Carbon\Carbon::parse($consultation->booking_date)->translatedFormat('l, d M Y') }}</p>
|
|
<p><strong>{{ __('admin.time') }}:</strong> {{ \Carbon\Carbon::parse($consultation->booking_time)->format('g:i A') }}</p>
|
|
</div>
|
|
```
|
|
|
|
### Step 11: Add Translation Keys
|
|
Add to `lang/en/admin.php`:
|
|
```php
|
|
'guest' => 'Guest',
|
|
'client_information' => 'Client Information',
|
|
'client_type' => 'Client Type',
|
|
```
|
|
|
|
Add to `lang/ar/admin.php`:
|
|
```php
|
|
'guest' => 'زائر',
|
|
'client_information' => 'معلومات العميل',
|
|
'client_type' => 'نوع العميل',
|
|
```
|
|
|
|
## Testing Requirements
|
|
|
|
### Email Tests
|
|
Create `tests/Feature/GuestEmailNotificationTest.php`:
|
|
|
|
```php
|
|
<?php
|
|
|
|
use App\Enums\ConsultationStatus;
|
|
use App\Mail\GuestBookingApprovedMail;
|
|
use App\Mail\GuestBookingRejectedMail;
|
|
use App\Mail\GuestBookingSubmittedMail;
|
|
use App\Mail\NewBookingAdminEmail;
|
|
use App\Models\Consultation;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\Mail;
|
|
|
|
test('guest receives confirmation email on booking submission', function () {
|
|
Mail::fake();
|
|
|
|
$consultation = Consultation::factory()->guest()->create([
|
|
'status' => ConsultationStatus::Pending,
|
|
]);
|
|
|
|
Mail::to($consultation->guest_email)->send(
|
|
new GuestBookingSubmittedMail($consultation, 'en')
|
|
);
|
|
|
|
Mail::assertSent(GuestBookingSubmittedMail::class, function ($mail) use ($consultation) {
|
|
return $mail->hasTo($consultation->guest_email);
|
|
});
|
|
});
|
|
|
|
test('guest receives approval email with calendar attachment', function () {
|
|
Mail::fake();
|
|
|
|
$consultation = Consultation::factory()->guest()->create([
|
|
'status' => ConsultationStatus::Approved,
|
|
]);
|
|
|
|
Mail::to($consultation->guest_email)->send(
|
|
new GuestBookingApprovedMail($consultation, 'en')
|
|
);
|
|
|
|
Mail::assertSent(GuestBookingApprovedMail::class, function ($mail) {
|
|
return count($mail->attachments()) === 1;
|
|
});
|
|
});
|
|
|
|
test('guest receives rejection email', function () {
|
|
Mail::fake();
|
|
|
|
$consultation = Consultation::factory()->guest()->create([
|
|
'status' => ConsultationStatus::Rejected,
|
|
]);
|
|
|
|
Mail::to($consultation->guest_email)->send(
|
|
new GuestBookingRejectedMail($consultation, 'en', 'Not available')
|
|
);
|
|
|
|
Mail::assertSent(GuestBookingRejectedMail::class, function ($mail) use ($consultation) {
|
|
return $mail->hasTo($consultation->guest_email);
|
|
});
|
|
});
|
|
|
|
test('admin email shows guest indicator for guest bookings', function () {
|
|
Mail::fake();
|
|
|
|
$consultation = Consultation::factory()->guest()->create();
|
|
$admin = User::factory()->admin()->create();
|
|
|
|
Mail::to($admin)->send(new NewBookingAdminEmail($consultation));
|
|
|
|
Mail::assertSent(NewBookingAdminEmail::class);
|
|
});
|
|
```
|
|
|
|
### Admin Interface Tests
|
|
Create `tests/Feature/Admin/GuestBookingManagementTest.php`:
|
|
|
|
```php
|
|
<?php
|
|
|
|
use App\Enums\ConsultationStatus;
|
|
use App\Mail\GuestBookingApprovedMail;
|
|
use App\Mail\GuestBookingRejectedMail;
|
|
use App\Models\Consultation;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Livewire\Volt\Volt;
|
|
|
|
test('admin can see guest bookings in pending list', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$guestConsultation = Consultation::factory()->guest()->pending()->create();
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.bookings.pending')
|
|
->assertSee($guestConsultation->guest_name)
|
|
->assertSee(__('admin.guest'));
|
|
});
|
|
|
|
test('admin can view guest booking details in review page', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->guest()->pending()->create([
|
|
'guest_name' => 'Test Guest',
|
|
'guest_email' => 'test@example.com',
|
|
]);
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.bookings.review', ['consultation' => $consultation])
|
|
->assertSee('Test Guest')
|
|
->assertSee('test@example.com')
|
|
->assertSee(__('admin.guest'));
|
|
});
|
|
|
|
test('admin can approve guest booking via modal', function () {
|
|
Mail::fake();
|
|
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->guest()->pending()->create();
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.bookings.review', ['consultation' => $consultation])
|
|
->call('openApproveModal')
|
|
->assertSet('showApproveModal', true)
|
|
->set('consultationType', 'free')
|
|
->call('approve');
|
|
|
|
expect($consultation->fresh()->status)->toBe(ConsultationStatus::Approved);
|
|
Mail::assertQueued(GuestBookingApprovedMail::class);
|
|
});
|
|
|
|
test('admin can reject guest booking via modal', function () {
|
|
Mail::fake();
|
|
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->guest()->pending()->create();
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.bookings.review', ['consultation' => $consultation])
|
|
->call('openRejectModal')
|
|
->assertSet('showRejectModal', true)
|
|
->set('rejectionReason', 'Not available for this time')
|
|
->call('reject');
|
|
|
|
expect($consultation->fresh()->status)->toBe(ConsultationStatus::Rejected);
|
|
Mail::assertQueued(GuestBookingRejectedMail::class);
|
|
});
|
|
|
|
test('admin can quick approve guest booking from pending list', function () {
|
|
Mail::fake();
|
|
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->guest()->pending()->create();
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.bookings.pending')
|
|
->call('quickApprove', $consultation->id);
|
|
|
|
expect($consultation->fresh()->status)->toBe(ConsultationStatus::Approved);
|
|
Mail::assertQueued(GuestBookingApprovedMail::class);
|
|
});
|
|
|
|
test('admin can quick reject guest booking from pending list', function () {
|
|
Mail::fake();
|
|
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->guest()->pending()->create();
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.bookings.pending')
|
|
->call('quickReject', $consultation->id);
|
|
|
|
expect($consultation->fresh()->status)->toBe(ConsultationStatus::Rejected);
|
|
Mail::assertQueued(GuestBookingRejectedMail::class);
|
|
});
|
|
|
|
test('guest booking review shows no consultation history', function () {
|
|
$admin = User::factory()->admin()->create();
|
|
$consultation = Consultation::factory()->guest()->pending()->create();
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.bookings.review', ['consultation' => $consultation])
|
|
->assertDontSee(__('admin.consultation_history'));
|
|
});
|
|
|
|
test('client booking still uses notification system', function () {
|
|
Notification::fake();
|
|
|
|
$admin = User::factory()->admin()->create();
|
|
$client = User::factory()->client()->create();
|
|
$consultation = Consultation::factory()->pending()->create(['user_id' => $client->id]);
|
|
|
|
$this->actingAs($admin);
|
|
|
|
Volt::test('admin.bookings.review', ['consultation' => $consultation])
|
|
->call('openApproveModal')
|
|
->set('consultationType', 'free')
|
|
->call('approve');
|
|
|
|
Notification::assertSentTo($client, BookingApproved::class);
|
|
});
|
|
```
|
|
|
|
## Dependencies
|
|
- Story 11.1 (Database Schema & Model Updates) - provides `isGuest()`, `getClientName()`, `getClientEmail()`, `getClientPhone()` methods
|
|
- Story 11.2 (Public Booking Form) - uses the guest email classes created in this story
|
|
|
|
## Definition of Done
|
|
- [x] Guest booking submitted email created and working (en/ar templates)
|
|
- [x] Guest booking approved email created with calendar attachment (en/ar templates)
|
|
- [x] Guest booking rejected email created (en/ar templates)
|
|
- [x] Admin new booking email updated for guests (shows guest indicator)
|
|
- [x] Admin pending bookings shows guest badge and uses helper methods
|
|
- [x] Admin booking review shows guest contact info with mailto/tel links
|
|
- [x] Quick approve/reject sends correct email (guest Mail vs client Notification)
|
|
- [x] Modal approve/reject sends correct email (guest Mail vs client Notification)
|
|
- [x] All translations in place (Arabic/English)
|
|
- [x] All email tests pass
|
|
- [x] All admin interface tests pass
|
|
- [x] Existing client booking notifications unchanged
|
|
|
|
---
|
|
|
|
## Dev Agent Record
|
|
|
|
### Agent Model Used
|
|
Claude Opus 4.5
|
|
|
|
### Completion Notes
|
|
- GuestBookingSubmittedMail already existed from story 11.2 - used that
|
|
- Created GuestBookingApprovedMail with calendar attachment and payment info support
|
|
- Created GuestBookingRejectedMail with optional reason support
|
|
- Updated NewBookingAdminEmail to include guest helper methods (isGuest, getClientName, getClientEmail, getClientPhone)
|
|
- Updated admin new-booking email templates (en/ar) to show guest indicator
|
|
- Updated admin pending bookings view with guest badge, mailto links, and helper methods
|
|
- Updated admin booking review page with guest badge, mailto/tel links, client type hidden for guests
|
|
- Updated approve/reject methods to send Mail for guests and Notification for clients
|
|
- Added 'guest' translation key to en/ar admin translations
|
|
- Note: Changed `$locale` to `$emailLocale` in mail classes due to Laravel 12 Mailable base class conflict
|
|
|
|
### File List
|
|
**New Files:**
|
|
- `app/Mail/GuestBookingApprovedMail.php`
|
|
- `app/Mail/GuestBookingRejectedMail.php`
|
|
- `resources/views/emails/booking/guest-approved/en.blade.php`
|
|
- `resources/views/emails/booking/guest-approved/ar.blade.php`
|
|
- `resources/views/emails/booking/guest-rejected/en.blade.php`
|
|
- `resources/views/emails/booking/guest-rejected/ar.blade.php`
|
|
- `tests/Feature/GuestEmailNotificationTest.php`
|
|
- `tests/Feature/Admin/GuestBookingManagementTest.php`
|
|
|
|
**Modified Files:**
|
|
- `app/Mail/NewBookingAdminEmail.php` - Added guest helper methods to content()
|
|
- `resources/views/emails/admin/new-booking/en.blade.php` - Guest indicator support
|
|
- `resources/views/emails/admin/new-booking/ar.blade.php` - Guest indicator support
|
|
- `resources/views/livewire/admin/bookings/pending.blade.php` - Guest badge, mailto links, guest email notifications
|
|
- `resources/views/livewire/admin/bookings/review.blade.php` - Guest badge, mailto/tel links, guest email notifications
|
|
- `lang/en/admin.php` - Added 'guest' key
|
|
- `lang/ar/admin.php` - Added 'guest' key
|
|
|
|
### Change Log
|
|
| Date | Change |
|
|
|------|--------|
|
|
| 2026-01-03 | Implemented story 11.3 - Guest email notifications and admin integration |
|
|
|
|
### Status
|
|
Ready for Review
|
|
|
|
---
|
|
|
|
## QA Results
|
|
|
|
### Review Date: 2026-01-03
|
|
|
|
### Reviewed By: Quinn (Test Architect)
|
|
|
|
### Code Quality Assessment
|
|
|
|
The implementation is **well-structured and follows Laravel best practices**. The code demonstrates:
|
|
|
|
1. **Good separation of concerns**: Mailables are properly separated from Notifications - Guests use `Mail` while authenticated users use `Notification` system
|
|
2. **Consistent patterns**: Both `GuestBookingApprovedMail` and `GuestBookingRejectedMail` follow the same structure as existing mailables
|
|
3. **Proper error handling**: Calendar attachment generation uses try-catch with logging
|
|
4. **Clean helper methods**: The Consultation model's `isGuest()`, `getClientName()`, `getClientEmail()`, `getClientPhone()` methods provide clean abstraction
|
|
5. **Bilingual support**: Both English and Arabic templates are properly implemented with RTL support
|
|
|
|
### Refactoring Performed
|
|
|
|
No refactoring was needed. The implementation is clean and follows established patterns.
|
|
|
|
### Compliance Check
|
|
|
|
- Coding Standards: ✓ Code follows Laravel 12 / Livewire 3 patterns with proper Volt component structure
|
|
- Project Structure: ✓ Files placed correctly (`app/Mail/`, `resources/views/emails/`, appropriate test directories)
|
|
- Testing Strategy: ✓ Good coverage with 29 tests covering email sending, admin interface, and edge cases
|
|
- All ACs Met: ✓ All 18 acceptance criteria verified (see traceability below)
|
|
|
|
### Requirements Traceability
|
|
|
|
**Guest Email Notifications (AC 1-6):**
|
|
| AC | Description | Test Coverage |
|
|
|----|-------------|---------------|
|
|
| 1 | Guest receives confirmation email when booking submitted | `GuestBookingSubmittedMail` - tested in `test('guest receives confirmation email on booking submission')` |
|
|
| 2 | Guest receives approval email when booking approved | Tested in `test('guest receives approval email with calendar attachment')` |
|
|
| 3 | Guest receives rejection email when booking rejected | Tested in `test('guest receives rejection email')` |
|
|
| 4 | All emails use existing email template/branding | Uses `<x-mail::message>` component |
|
|
| 5 | Emails sent to guest_email address | Verified via `Mail::assertQueued()` with `hasTo()` |
|
|
| 6 | Bilingual support | Tested in `test('guest booking approved mail uses correct arabic locale')` and `test('guest booking approved mail uses correct english locale')` |
|
|
|
|
**Admin Pending Bookings View (AC 7-10):**
|
|
| AC | Description | Test Coverage |
|
|
|----|-------------|---------------|
|
|
| 7 | Guest bookings appear in pending list | `test('admin can see guest bookings in pending list')` |
|
|
| 8 | Guest bookings show "Guest" badge | `assertSee(__('admin.guest'))` |
|
|
| 9 | Guest name, email, phone displayed | `test('pending list shows guest email with mailto link')` |
|
|
| 10 | Click through to review shows details | `test('admin can view guest booking details in review page')` |
|
|
|
|
**Admin Booking Review Page (AC 11-16):**
|
|
| AC | Description | Test Coverage |
|
|
|----|-------------|---------------|
|
|
| 11 | Guest contact info displayed | `assertSee('testguest@example.com')` |
|
|
| 12 | Guest name shown | `assertSee('Test Guest')` |
|
|
| 13 | Guest email with mailto link | `test('review page shows guest email with mailto link')` |
|
|
| 14 | Guest phone with tel link | `test('review page shows guest phone with tel link')` |
|
|
| 15 | Approve/reject workflow works | `test('admin can approve guest booking via modal')` and `test('admin can reject guest booking via modal')` |
|
|
| 16 | Email notifications on status change | `Mail::assertQueued(GuestBookingApprovedMail::class)` |
|
|
|
|
**Existing Admin Email (AC 17-18):**
|
|
| AC | Description | Test Coverage |
|
|
|----|-------------|---------------|
|
|
| 17 | NewBookingAdminEmail updated for guests | `test('admin email shows guest indicator for guest bookings')` |
|
|
| 18 | Admin email shows client info for clients | `test('admin email shows client info for client bookings')` |
|
|
|
|
### Improvements Checklist
|
|
|
|
All implementation tasks completed correctly:
|
|
|
|
- [x] Guest email mailables created with correct structure
|
|
- [x] Email templates for approved/rejected in both locales
|
|
- [x] Admin email updated with guest helper methods
|
|
- [x] Pending bookings view shows guest badge and uses helper methods
|
|
- [x] Review page shows guest contact info with mailto/tel links
|
|
- [x] Quick approve/reject sends correct email type (Mail vs Notification)
|
|
- [x] Modal approve/reject sends correct email type
|
|
- [x] Translation keys added for 'guest', 'client_information', 'client_type'
|
|
- [x] All 29 tests passing
|
|
|
|
### Security Review
|
|
|
|
**Status: PASS**
|
|
- No security vulnerabilities identified
|
|
- Email addresses are properly handled through established patterns
|
|
- No user input is directly rendered in emails without proper escaping
|
|
- The `$emailLocale` parameter only accepts 'en' or 'ar' values (controlled by code, not user input)
|
|
- Admin authorization is handled by existing middleware
|
|
|
|
### Performance Considerations
|
|
|
|
**Status: PASS**
|
|
- Emails are queued via `ShouldQueue` interface (no blocking requests)
|
|
- Calendar attachment generation has proper exception handling
|
|
- Database queries are efficient with eager loading (`Consultation::with('user')`)
|
|
|
|
### Test Architecture Assessment
|
|
|
|
**Strengths:**
|
|
- Comprehensive coverage: 29 tests with 58 assertions
|
|
- Good use of Mail::fake() and Notification::fake()
|
|
- Tests verify actual email content and recipients
|
|
- Edge cases covered (missing reason, payment instructions, mixed guest/client lists)
|
|
- Proper use of factory states (`->guest()`, `->pending()`, `->admin()`)
|
|
|
|
**Test Level Appropriateness:**
|
|
- Feature tests are appropriate for email/admin interface testing
|
|
- Tests verify integration between Livewire components and email sending
|
|
- Tests verify correct dispatch of Mail vs Notification based on booking type
|
|
|
|
### Files Modified During Review
|
|
|
|
No files were modified during this review.
|
|
|
|
### Gate Status
|
|
|
|
Gate: **PASS** → docs/qa/gates/11.3-guest-notifications-admin.yml
|
|
Risk profile: Low - Standard CRUD operations with email sending
|
|
|
|
### Recommended Status
|
|
|
|
✓ Ready for Done
|
|
|
|
All acceptance criteria met, tests passing, code quality excellent. The implementation correctly handles the distinction between guest email notifications (using Mailables) and client notifications (using the Notification system).
|