17 KiB
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
NewBookingAdminEmailupdated to handle guest bookings- Admin email shows guest contact info for guest bookings
- Admin email shows client info for client bookings
Implementation Steps
Step 1: Create Guest Booking Submitted Email
Create app/Mail/GuestBookingSubmittedMail.php:
<?php
namespace App\Mail;
use App\Models\Consultation;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class GuestBookingSubmittedMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(
public Consultation $consultation
) {}
public function envelope(): Envelope
{
return new Envelope(
subject: __('emails.guest_booking_submitted_subject'),
);
}
public function content(): Content
{
return new Content(
view: 'emails.guest-booking-submitted',
with: [
'consultation' => $this->consultation,
'guestName' => $this->consultation->guest_name,
],
);
}
}
Step 2: Create Guest Booking Submitted Email Template
Create resources/views/emails/guest-booking-submitted.blade.php:
<x-mail::message>
# {{ __('emails.guest_booking_submitted_title') }}
{{ __('emails.guest_booking_submitted_greeting', ['name' => $guestName]) }}
{{ __('emails.guest_booking_submitted_body') }}
**{{ __('booking.date') }}:** {{ $consultation->booking_date->translatedFormat('l, d M Y') }}
**{{ __('booking.time') }}:** {{ \Carbon\Carbon::parse($consultation->booking_time)->format('g:i A') }}
**{{ __('booking.duration') }}:** 45 {{ __('common.minutes') }}
{{ __('emails.guest_booking_submitted_next_steps') }}
{{ __('emails.signature') }},<br>
{{ config('app.name') }}
</x-mail::message>
Step 3: Create Guest Booking Approved Email
Create app/Mail/GuestBookingApprovedMail.php:
<?php
namespace App\Mail;
use App\Models\Consultation;
use App\Services\CalendarService;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Attachment;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class GuestBookingApprovedMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(
public Consultation $consultation
) {}
public function envelope(): Envelope
{
return new Envelope(
subject: __('emails.guest_booking_approved_subject'),
);
}
public function content(): Content
{
return new Content(
view: 'emails.guest-booking-approved',
with: [
'consultation' => $this->consultation,
'guestName' => $this->consultation->guest_name,
'isPaid' => $this->consultation->consultation_type?->value === 'paid',
],
);
}
public function attachments(): array
{
$calendarService = app(CalendarService::class);
$icsContent = $calendarService->generateIcs($this->consultation);
return [
Attachment::fromData(fn () => $icsContent, 'consultation.ics')
->withMime('text/calendar'),
];
}
}
Step 4: Create Guest Booking Rejected Email
Create app/Mail/GuestBookingRejectedMail.php:
<?php
namespace App\Mail;
use App\Models\Consultation;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class GuestBookingRejectedMail extends Mailable
{
use Queueable, SerializesModels;
public function __construct(
public Consultation $consultation,
public ?string $reason = null
) {}
public function envelope(): Envelope
{
return new Envelope(
subject: __('emails.guest_booking_rejected_subject'),
);
}
public function content(): Content
{
return new Content(
view: 'emails.guest-booking-rejected',
with: [
'consultation' => $this->consultation,
'guestName' => $this->consultation->guest_name,
'reason' => $this->reason,
],
);
}
}
Step 5: Update NewBookingAdminEmail
Update app/Mail/NewBookingAdminEmail.php to handle guests:
public function content(): Content
{
return new Content(
view: 'emails.new-booking-admin',
with: [
'consultation' => $this->consultation,
'clientName' => $this->consultation->getClientName(),
'clientEmail' => $this->consultation->getClientEmail(),
'clientPhone' => $this->consultation->getClientPhone(),
'isGuest' => $this->consultation->isGuest(),
],
);
}
Step 6: Update Admin New Booking Email Template
Update resources/views/emails/new-booking-admin.blade.php:
<x-mail::message>
# {{ __('emails.new_booking_admin_title') }}
{{ __('emails.new_booking_admin_body') }}
@if($isGuest)
**{{ __('emails.booking_type') }}:** {{ __('emails.guest_booking') }}
@endif
**{{ __('booking.client_name') }}:** {{ $clientName }}
**{{ __('booking.client_email') }}:** {{ $clientEmail }}
**{{ __('booking.client_phone') }}:** {{ $clientPhone }}
**{{ __('booking.date') }}:** {{ $consultation->booking_date->translatedFormat('l, d M Y') }}
**{{ __('booking.time') }}:** {{ \Carbon\Carbon::parse($consultation->booking_time)->format('g:i A') }}
**{{ __('booking.problem_summary') }}:**
{{ $consultation->problem_summary }}
<x-mail::button :url="route('admin.bookings.review', $consultation)">
{{ __('emails.review_booking') }}
</x-mail::button>
{{ __('emails.signature') }},<br>
{{ config('app.name') }}
</x-mail::message>
Step 7: Update Admin Pending Bookings List
Update resources/views/livewire/admin/bookings/pending.blade.php to show guest indicator:
In the table row, add guest badge:
<td>
@if($consultation->isGuest())
<flux:badge size="sm" color="amber" class="mb-1">{{ __('admin.guest') }}</flux:badge><br>
{{ $consultation->guest_name }}
@else
{{ $consultation->user->name }}
@endif
</td>
<td>
@if($consultation->isGuest())
<a href="mailto:{{ $consultation->guest_email }}" class="text-primary hover:underline">
{{ $consultation->guest_email }}
</a>
@else
<a href="mailto:{{ $consultation->user->email }}" class="text-primary hover:underline">
{{ $consultation->user->email }}
</a>
@endif
</td>
Step 8: Update Admin Booking Review Page
Update resources/views/livewire/admin/bookings/review.blade.php:
{{-- Client/Guest Information Section --}}
<div class="mb-6 p-4 bg-zinc-50 dark:bg-zinc-800 rounded-lg">
<flux:heading size="sm" class="mb-3">
{{ __('admin.client_information') }}
@if($consultation->isGuest())
<flux:badge size="sm" color="amber" class="ml-2">{{ __('admin.guest') }}</flux:badge>
@endif
</flux:heading>
<dl class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<dt class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('booking.client_name') }}</dt>
<dd class="font-medium">{{ $consultation->getClientName() }}</dd>
</div>
<div>
<dt class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('booking.client_email') }}</dt>
<dd>
<a href="mailto:{{ $consultation->getClientEmail() }}" class="text-primary hover:underline">
{{ $consultation->getClientEmail() }}
</a>
</dd>
</div>
<div>
<dt class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('booking.client_phone') }}</dt>
<dd>
<a href="tel:{{ $consultation->getClientPhone() }}" class="text-primary hover:underline">
{{ $consultation->getClientPhone() }}
</a>
</dd>
</div>
@unless($consultation->isGuest())
<div>
<dt class="text-sm text-zinc-500 dark:text-zinc-400">{{ __('admin.client_type') }}</dt>
<dd>{{ $consultation->user->user_type->label() }}</dd>
</div>
@endunless
</dl>
</div>
Step 9: Update Booking Approval Logic
Update the approve method in the booking review component to send guest emails:
public function approve(): void
{
// ... existing approval logic ...
// Send appropriate email based on guest/client
if ($this->consultation->isGuest()) {
Mail::to($this->consultation->guest_email)->queue(
new GuestBookingApprovedMail($this->consultation)
);
} else {
Mail::to($this->consultation->user)->queue(
new BookingApprovedMail($this->consultation)
);
}
}
public function reject(): void
{
// ... existing rejection logic ...
// Send appropriate email based on guest/client
if ($this->consultation->isGuest()) {
Mail::to($this->consultation->guest_email)->queue(
new GuestBookingRejectedMail($this->consultation, $this->rejectionReason)
);
} else {
Mail::to($this->consultation->user)->queue(
new BookingRejectedMail($this->consultation, $this->rejectionReason)
);
}
}
Step 10: Add Translation Keys
Add to lang/en/emails.php:
'guest_booking_submitted_subject' => 'Booking Request Received - Libra Law Firm',
'guest_booking_submitted_title' => 'Your Booking Request Has Been Received',
'guest_booking_submitted_greeting' => 'Dear :name,',
'guest_booking_submitted_body' => 'Thank you for your consultation request. We have received your booking and our team will review it shortly.',
'guest_booking_submitted_next_steps' => 'You will receive another email once your booking has been reviewed. If approved, you will receive the consultation details and a calendar invitation.',
'guest_booking_approved_subject' => 'Booking Confirmed - Libra Law Firm',
'guest_booking_rejected_subject' => 'Booking Update - Libra Law Firm',
'booking_type' => 'Booking Type',
'guest_booking' => 'Guest (No Account)',
Add to lang/ar/emails.php:
'guest_booking_submitted_subject' => 'تم استلام طلب الحجز - مكتب ليبرا للمحاماة',
'guest_booking_submitted_title' => 'تم استلام طلب الحجز الخاص بك',
'guest_booking_submitted_greeting' => 'عزيزي/عزيزتي :name،',
'guest_booking_submitted_body' => 'شكراً لطلب الاستشارة. لقد تلقينا حجزك وسيقوم فريقنا بمراجعته قريباً.',
'guest_booking_submitted_next_steps' => 'ستتلقى رسالة أخرى عند مراجعة حجزك. في حال الموافقة، ستتلقى تفاصيل الاستشارة ودعوة تقويم.',
'guest_booking_approved_subject' => 'تأكيد الحجز - مكتب ليبرا للمحاماة',
'guest_booking_rejected_subject' => 'تحديث الحجز - مكتب ليبرا للمحاماة',
'booking_type' => 'نوع الحجز',
'guest_booking' => 'زائر (بدون حساب)',
Add to lang/en/admin.php:
'guest' => 'Guest',
'client_information' => 'Client Information',
'client_type' => 'Client Type',
Add to lang/ar/admin.php:
'guest' => 'زائر',
'client_information' => 'معلومات العميل',
'client_type' => 'نوع العميل',
Testing Requirements
Email Tests
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)
);
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)
);
Mail::assertSent(GuestBookingApprovedMail::class, function ($mail) {
return count($mail->attachments()) === 1;
});
});
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
test('admin can see guest bookings in pending list', function () {
$admin = User::factory()->admin()->create();
$guestConsultation = Consultation::factory()->guest()->pending()->create();
Volt::test('admin.bookings.pending')
->actingAs($admin)
->assertSee($guestConsultation->guest_name)
->assertSee(__('admin.guest'));
});
test('admin can view guest booking details', function () {
$admin = User::factory()->admin()->create();
$consultation = Consultation::factory()->guest()->create([
'guest_name' => 'Test Guest',
'guest_email' => 'test@example.com',
]);
Volt::test('admin.bookings.review', ['consultation' => $consultation])
->actingAs($admin)
->assertSee('Test Guest')
->assertSee('test@example.com')
->assertSee(__('admin.guest'));
});
test('admin can approve guest booking', function () {
Mail::fake();
$admin = User::factory()->admin()->create();
$consultation = Consultation::factory()->guest()->pending()->create();
Volt::test('admin.bookings.review', ['consultation' => $consultation])
->actingAs($admin)
->call('approve');
expect($consultation->fresh()->status)->toBe(ConsultationStatus::Approved);
Mail::assertQueued(GuestBookingApprovedMail::class);
});
test('admin can reject guest booking', function () {
Mail::fake();
$admin = User::factory()->admin()->create();
$consultation = Consultation::factory()->guest()->pending()->create();
Volt::test('admin.bookings.review', ['consultation' => $consultation])
->actingAs($admin)
->set('rejectionReason', 'Not available for this time')
->call('reject');
expect($consultation->fresh()->status)->toBe(ConsultationStatus::Rejected);
Mail::assertQueued(GuestBookingRejectedMail::class);
});
Dependencies
- Story 11.1 (Database Schema & Model Updates)
- Story 11.2 (Public Booking Form)
Definition of Done
- Guest booking submitted email created and working
- Guest booking approved email created with calendar attachment
- Guest booking rejected email created
- Admin new booking email updated for guests
- Admin pending bookings shows guest indicator
- Admin booking review shows guest contact info
- Approve/reject sends correct email (guest vs client)
- All translations in place (Arabic/English)
- All email tests pass
- All admin interface tests pass
- Existing client booking emails unchanged