libra/docs/stories/story-11.3-guest-notificati...

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).