540 lines
17 KiB
Markdown
540 lines
17 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
|
|
|
|
## Implementation Steps
|
|
|
|
### Step 1: Create Guest Booking Submitted Email
|
|
Create `app/Mail/GuestBookingSubmittedMail.php`:
|
|
|
|
```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`:
|
|
|
|
```blade
|
|
<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
|
|
<?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
|
|
<?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:
|
|
|
|
```php
|
|
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`:
|
|
|
|
```blade
|
|
<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:
|
|
```blade
|
|
<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`:
|
|
|
|
```blade
|
|
{{-- 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:
|
|
|
|
```php
|
|
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`:
|
|
```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`:
|
|
```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`:
|
|
```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
|
|
```php
|
|
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
|
|
```php
|
|
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
|