reviewed epic 8 stories
This commit is contained in:
@@ -3,6 +3,12 @@
|
||||
## Epic Reference
|
||||
**Epic 8:** Email Notification System
|
||||
|
||||
## Dependencies
|
||||
- **Story 8.1:** Email Infrastructure Setup (base templates, SMTP config, queue setup)
|
||||
|
||||
## Story Context
|
||||
This notification is triggered during **Step 2 of the Booking Flow** (PRD Section 5.4). When a client submits a consultation request, it enters the pending queue and the admin must be notified immediately so they can review and respond promptly. This email works alongside Story 8.3 (client confirmation) - both are sent on the same trigger but to different recipients.
|
||||
|
||||
## User Story
|
||||
As an **admin**,
|
||||
I want **to be notified when a client submits a booking request**,
|
||||
@@ -11,64 +17,345 @@ So that **I can review and respond promptly**.
|
||||
## Acceptance Criteria
|
||||
|
||||
### Trigger
|
||||
- [ ] Sent on booking submission by client
|
||||
- [ ] Sent immediately after successful consultation creation (same trigger as Story 8.3)
|
||||
- [ ] Consultation status: pending
|
||||
- [ ] Email queued for async delivery
|
||||
|
||||
### Recipient
|
||||
- [ ] Admin email address
|
||||
- [ ] First user with `user_type = 'admin'` in the database
|
||||
- [ ] If no admin exists, log error but don't fail the booking process
|
||||
|
||||
### Content
|
||||
- [ ] "New Consultation Request"
|
||||
- [ ] Client name (individual or company)
|
||||
- [ ] Requested date and time
|
||||
- [ ] Problem summary (full)
|
||||
- [ ] Client contact info
|
||||
- [ ] "Review Request" button/link
|
||||
- [ ] Subject line: "[Action Required] New Consultation Request" / "[إجراء مطلوب] طلب استشارة جديد"
|
||||
- [ ] "New Consultation Request" heading
|
||||
- [ ] Client name:
|
||||
- Individual: `full_name`
|
||||
- Company: `company_name` (with contact person: `contact_person_name`)
|
||||
- [ ] Requested date and time (formatted per admin's language preference)
|
||||
- [ ] Problem summary (full text, no truncation)
|
||||
- [ ] Client contact information:
|
||||
- Email address
|
||||
- Phone number
|
||||
- Client type indicator (Individual/Company)
|
||||
- [ ] "Review Request" button linking to consultation detail in admin dashboard
|
||||
|
||||
### Priority
|
||||
- [ ] Clear indicator in subject line
|
||||
- [ ] "[Action Required]" prefix in subject line (English)
|
||||
- [ ] "[إجراء مطلوب]" prefix in subject line (Arabic)
|
||||
|
||||
### Language
|
||||
- [ ] Admin language preference (or default)
|
||||
- [ ] Email sent in admin's `preferred_language`
|
||||
- [ ] Default to 'en' (English) if admin has no preference set (admin-facing communications default to English)
|
||||
|
||||
## Technical Notes
|
||||
|
||||
### Files to Create
|
||||
```
|
||||
app/Mail/NewBookingAdminEmail.php
|
||||
resources/views/emails/admin/new-booking/ar.blade.php
|
||||
resources/views/emails/admin/new-booking/en.blade.php
|
||||
```
|
||||
|
||||
### Mailable Implementation
|
||||
Using `Mailable` pattern to align with sibling stories (8.2-8.8):
|
||||
|
||||
```php
|
||||
class NewBookingAdminNotification extends Notification
|
||||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\Models\Consultation;
|
||||
use App\Models\User;
|
||||
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 NewBookingAdminEmail extends Mailable implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
public function __construct(
|
||||
public Consultation $consultation
|
||||
) {}
|
||||
|
||||
public function via(object $notifiable): array
|
||||
public function envelope(): Envelope
|
||||
{
|
||||
return ['mail'];
|
||||
$admin = $this->getAdminUser();
|
||||
$locale = $admin?->preferred_language ?? 'en';
|
||||
|
||||
return new Envelope(
|
||||
subject: $locale === 'ar'
|
||||
? '[إجراء مطلوب] طلب استشارة جديد'
|
||||
: '[Action Required] New Consultation Request',
|
||||
);
|
||||
}
|
||||
|
||||
public function toMail(object $notifiable): MailMessage
|
||||
public function content(): Content
|
||||
{
|
||||
return (new MailMessage)
|
||||
->subject('[Action Required] New Consultation Request')
|
||||
->markdown('emails.admin.new-booking', [
|
||||
$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,
|
||||
]);
|
||||
'formattedDate' => $this->getFormattedDate($locale),
|
||||
'formattedTime' => $this->getFormattedTime(),
|
||||
'reviewUrl' => $this->getReviewUrl(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
private function getAdminUser(): ?User
|
||||
{
|
||||
return User::where('user_type', 'admin')->first();
|
||||
}
|
||||
|
||||
private function getFormattedDate(string $locale): string
|
||||
{
|
||||
$date = $this->consultation->booking_date;
|
||||
return $locale === 'ar'
|
||||
? $date->format('d/m/Y')
|
||||
: $date->format('m/d/Y');
|
||||
}
|
||||
|
||||
private function getFormattedTime(): string
|
||||
{
|
||||
return $this->consultation->booking_time->format('h:i A');
|
||||
}
|
||||
|
||||
private function getReviewUrl(): string
|
||||
{
|
||||
return route('admin.consultations.show', $this->consultation);
|
||||
}
|
||||
}
|
||||
|
||||
// Trigger in booking submission
|
||||
$admin = User::where('user_type', 'admin')->first();
|
||||
$admin?->notify(new NewBookingAdminNotification($consultation));
|
||||
```
|
||||
|
||||
### Dispatch Point
|
||||
Same location as Story 8.3 - after consultation creation:
|
||||
|
||||
```php
|
||||
// In the controller or action handling booking submission
|
||||
// Dispatch AFTER the client confirmation email (Story 8.3)
|
||||
|
||||
use App\Mail\NewBookingAdminEmail;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
// Send admin notification
|
||||
$admin = User::where('user_type', 'admin')->first();
|
||||
|
||||
if ($admin) {
|
||||
Mail::to($admin->email)->send(new NewBookingAdminEmail($consultation));
|
||||
} else {
|
||||
Log::warning('No admin user found to notify about new booking', [
|
||||
'consultation_id' => $consultation->id,
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
||||
### Edge Cases
|
||||
- **No admin user exists:** Log warning, continue without sending (booking should not fail)
|
||||
- **Admin has no email:** Skip sending, log error
|
||||
- **Admin `preferred_language` is null:** Default to 'en' (English)
|
||||
- **Client is company type:** Display company name prominently, include contact person name
|
||||
- **Client is individual type:** Display full name
|
||||
- **Consultation missing `booking_date` or `booking_time`:** Should not happen (validation), but handle gracefully
|
||||
|
||||
### Client Information Display Logic
|
||||
```php
|
||||
// In the email template
|
||||
@if($client->user_type === 'company')
|
||||
<strong>{{ $client->company_name }}</strong>
|
||||
<br>Contact: {{ $client->contact_person_name }}
|
||||
@else
|
||||
<strong>{{ $client->full_name }}</strong>
|
||||
@endif
|
||||
|
||||
Email: {{ $client->email }}
|
||||
Phone: {{ $client->phone }}
|
||||
```
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
### Unit Tests
|
||||
```php
|
||||
<?php
|
||||
|
||||
use App\Mail\NewBookingAdminEmail;
|
||||
use App\Models\Consultation;
|
||||
use App\Models\User;
|
||||
|
||||
test('admin email has action required prefix in English subject', function () {
|
||||
$admin = User::factory()->create([
|
||||
'user_type' => 'admin',
|
||||
'preferred_language' => 'en',
|
||||
]);
|
||||
$client = User::factory()->create(['user_type' => 'individual']);
|
||||
$consultation = Consultation::factory()->create(['user_id' => $client->id]);
|
||||
|
||||
$mailable = new NewBookingAdminEmail($consultation);
|
||||
|
||||
expect($mailable->envelope()->subject)
|
||||
->toBe('[Action Required] New Consultation Request');
|
||||
});
|
||||
|
||||
test('admin email has action required prefix in Arabic subject', function () {
|
||||
$admin = User::factory()->create([
|
||||
'user_type' => 'admin',
|
||||
'preferred_language' => 'ar',
|
||||
]);
|
||||
$client = User::factory()->create(['user_type' => 'individual']);
|
||||
$consultation = Consultation::factory()->create(['user_id' => $client->id]);
|
||||
|
||||
$mailable = new NewBookingAdminEmail($consultation);
|
||||
|
||||
expect($mailable->envelope()->subject)
|
||||
->toBe('[إجراء مطلوب] طلب استشارة جديد');
|
||||
});
|
||||
|
||||
test('admin email defaults to English when admin has no language preference', function () {
|
||||
$admin = User::factory()->create([
|
||||
'user_type' => 'admin',
|
||||
'preferred_language' => null,
|
||||
]);
|
||||
$client = User::factory()->create(['user_type' => 'individual']);
|
||||
$consultation = Consultation::factory()->create(['user_id' => $client->id]);
|
||||
|
||||
$mailable = new NewBookingAdminEmail($consultation);
|
||||
|
||||
expect($mailable->envelope()->subject)
|
||||
->toContain('[Action Required]');
|
||||
});
|
||||
|
||||
test('admin email includes full problem summary', function () {
|
||||
$admin = User::factory()->create(['user_type' => 'admin']);
|
||||
$client = User::factory()->create(['user_type' => 'individual']);
|
||||
$longSummary = str_repeat('Legal issue description. ', 50);
|
||||
$consultation = Consultation::factory()->create([
|
||||
'user_id' => $client->id,
|
||||
'problem_summary' => $longSummary,
|
||||
]);
|
||||
|
||||
$mailable = new NewBookingAdminEmail($consultation);
|
||||
$content = $mailable->content();
|
||||
|
||||
// Full summary passed, not truncated
|
||||
expect($content->with['consultation']->problem_summary)
|
||||
->toBe($longSummary);
|
||||
});
|
||||
|
||||
test('admin email includes review URL', function () {
|
||||
$admin = User::factory()->create(['user_type' => 'admin']);
|
||||
$client = User::factory()->create(['user_type' => 'individual']);
|
||||
$consultation = Consultation::factory()->create(['user_id' => $client->id]);
|
||||
|
||||
$mailable = new NewBookingAdminEmail($consultation);
|
||||
$content = $mailable->content();
|
||||
|
||||
expect($content->with['reviewUrl'])
|
||||
->toContain('consultations')
|
||||
->toContain((string) $consultation->id);
|
||||
});
|
||||
```
|
||||
|
||||
### Feature Tests
|
||||
```php
|
||||
<?php
|
||||
|
||||
use App\Mail\NewBookingAdminEmail;
|
||||
use App\Models\Consultation;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
||||
test('admin email is sent when consultation is created', function () {
|
||||
Mail::fake();
|
||||
|
||||
$admin = User::factory()->create(['user_type' => 'admin']);
|
||||
$client = User::factory()->create(['user_type' => 'individual']);
|
||||
$consultation = Consultation::factory()->create(['user_id' => $client->id]);
|
||||
|
||||
Mail::to($admin->email)->send(new NewBookingAdminEmail($consultation));
|
||||
|
||||
Mail::assertSent(NewBookingAdminEmail::class, function ($mail) use ($admin) {
|
||||
return $mail->hasTo($admin->email);
|
||||
});
|
||||
});
|
||||
|
||||
test('admin email is queued for async delivery', function () {
|
||||
Mail::fake();
|
||||
|
||||
$admin = User::factory()->create(['user_type' => 'admin']);
|
||||
$client = User::factory()->create(['user_type' => 'individual']);
|
||||
$consultation = Consultation::factory()->create(['user_id' => $client->id]);
|
||||
|
||||
Mail::to($admin->email)->send(new NewBookingAdminEmail($consultation));
|
||||
|
||||
Mail::assertQueued(NewBookingAdminEmail::class);
|
||||
});
|
||||
|
||||
test('warning is logged when no admin exists', function () {
|
||||
Log::shouldReceive('warning')
|
||||
->once()
|
||||
->with('No admin user found to notify about new booking', \Mockery::any());
|
||||
|
||||
$client = User::factory()->create(['user_type' => 'individual']);
|
||||
$consultation = Consultation::factory()->create(['user_id' => $client->id]);
|
||||
|
||||
$admin = User::where('user_type', 'admin')->first();
|
||||
|
||||
if (!$admin) {
|
||||
Log::warning('No admin user found to notify about new booking', [
|
||||
'consultation_id' => $consultation->id,
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
test('admin email displays company client information correctly', function () {
|
||||
Mail::fake();
|
||||
|
||||
$admin = User::factory()->create(['user_type' => 'admin']);
|
||||
$companyClient = User::factory()->create([
|
||||
'user_type' => 'company',
|
||||
'company_name' => 'Acme Corp',
|
||||
'contact_person_name' => 'John Doe',
|
||||
]);
|
||||
$consultation = Consultation::factory()->create(['user_id' => $companyClient->id]);
|
||||
|
||||
$mailable = new NewBookingAdminEmail($consultation);
|
||||
|
||||
expect($mailable->content()->with['client']->company_name)->toBe('Acme Corp');
|
||||
expect($mailable->content()->with['client']->contact_person_name)->toBe('John Doe');
|
||||
});
|
||||
```
|
||||
|
||||
## References
|
||||
- **PRD Section 5.4:** Booking Flow - Step 2 "Admin receives email notification at no-reply@libra.ps"
|
||||
- **PRD Section 8.2:** Admin Emails - "New Booking Request - With client details and problem summary"
|
||||
- **Story 8.1:** Base email template structure, SMTP config, queue setup
|
||||
- **Story 8.3:** Similar trigger pattern (booking submission) - client-facing counterpart
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Email sent to admin on new booking
|
||||
- [ ] All client info included
|
||||
- [ ] Problem summary shown
|
||||
- [ ] Review link works
|
||||
- [ ] Priority clear in subject
|
||||
- [ ] Tests pass
|
||||
- [ ] `NewBookingAdminEmail` Mailable class created
|
||||
- [ ] Arabic template created and renders correctly
|
||||
- [ ] English template created and renders correctly
|
||||
- [ ] Email dispatched on consultation creation (after Story 8.3 client email)
|
||||
- [ ] Email queued (implements ShouldQueue)
|
||||
- [ ] Subject contains "[Action Required]" / "[إجراء مطلوب]" prefix
|
||||
- [ ] All client information included (name, email, phone, type)
|
||||
- [ ] Company clients show company name and contact person
|
||||
- [ ] Full problem summary displayed (no truncation)
|
||||
- [ ] Review link navigates to admin consultation detail page
|
||||
- [ ] Date/time formatted per admin language preference
|
||||
- [ ] Graceful handling when no admin exists (log warning, don't fail)
|
||||
- [ ] Unit tests pass
|
||||
- [ ] Feature tests pass
|
||||
- [ ] Code formatted with Pint
|
||||
|
||||
## Estimation
|
||||
**Complexity:** Low | **Effort:** 2 hours
|
||||
**Complexity:** Low | **Effort:** 2-3 hours
|
||||
|
||||
Reference in New Issue
Block a user