fixed issues in story 3.4

This commit is contained in:
Naser Mansour 2025-12-26 19:12:56 +02:00
parent eaef242831
commit 1b3bc0a2cf
1 changed files with 469 additions and 246 deletions

View File

@ -1,84 +1,220 @@
# Story 3.4: Booking Request Submission # Story 3.4: Booking Request Submission
## Status
**Draft**
## Epic Reference ## Epic Reference
**Epic 3:** Booking & Consultation System **Epic 3:** Booking & Consultation System
## User Story ## Story
As a **client**, **As a** client,
I want **to submit a consultation booking request**, **I want** to submit a consultation booking request,
So that **I can schedule a meeting with the lawyer**. **so that** I can schedule a meeting with the lawyer.
## Story Context
### Existing System Integration
- **Integrates with:** `consultations` table, availability calendar (Story 3.3), notifications system
- **Technology:** Livewire Volt (class-based), Laravel form validation, DB transactions
- **Follows pattern:** Multi-step form submission with confirmation
- **Touch points:** Client dashboard, admin notifications, audit log
- **Component location:** `resources/views/livewire/booking/request.blade.php`
## Acceptance Criteria ## Acceptance Criteria
### Booking Form ### Booking Form (AC1-5)
- [ ] Client must be logged in 1. Client must be logged in
- [ ] Select date from availability calendar 2. Select date from availability calendar
- [ ] Select available time slot 3. Select available time slot
- [ ] Problem summary field (required, textarea) 4. Problem summary field (required, textarea, min 20 characters)
- [ ] Confirmation before submission 5. Confirmation before submission
### Validation & Constraints ### Validation & Constraints (AC6-9)
- [ ] Validate: no more than 1 booking per day for this client 6. Validate: no more than 1 booking per day for this client
- [ ] Validate: selected slot is still available 7. Validate: selected slot is still available
- [ ] Validate: problem summary is not empty 8. Validate: problem summary is not empty
- [ ] Show clear error messages for violations 9. Show clear error messages for violations
### Submission Flow ### Submission Flow (AC10-14)
- [ ] Booking enters "pending" status 10. Booking enters "pending" status
- [ ] Client sees "Pending Review" confirmation 11. Client sees "Pending Review" confirmation
- [ ] Admin receives email notification 12. Admin receives email notification
- [ ] Client receives submission confirmation email 13. Client receives submission confirmation email
- [ ] Redirect to consultations list after submission 14. Redirect to consultations list after submission
### UI/UX ### UI/UX (AC15-18)
- [ ] Clear step-by-step flow 15. Clear step-by-step flow
- [ ] Loading state during submission 16. Loading state during submission
- [ ] Success message with next steps 17. Success message with next steps
- [ ] Bilingual labels and messages 18. Bilingual labels and messages
### Quality Requirements ### Quality Requirements (AC19-22)
- [ ] Prevent double-booking (race condition) 19. Prevent double-booking (race condition)
- [ ] Audit log entry for booking creation 20. Audit log entry for booking creation
- [ ] Tests for submission flow 21. Tests for submission flow
- [ ] Tests for validation rules 22. Tests for validation rules
## Technical Notes ## Tasks / Subtasks
### Database Record - [ ] **Task 1: Create Volt component file** (AC: 1-5, 15-18)
- [ ] Create `resources/views/livewire/client/consultations/book.blade.php`
- [ ] Implement class-based Volt component with state properties
- [ ] Add `selectSlot()`, `clearSelection()`, `showConfirm()`, `submit()` methods
- [ ] Add validation rules for form fields
- [ ] **Task 2: Implement calendar integration** (AC: 2, 3)
- [ ] Embed `availability-calendar` component
- [ ] Handle slot selection via `$parent.selectSlot()` pattern
- [ ] Display selected date/time with change option
- [ ] **Task 3: Implement problem summary form** (AC: 4, 8)
- [ ] Add textarea with Flux UI components
- [ ] Validate minimum 20 characters, maximum 2000 characters
- [ ] Show validation errors with `<flux:error>`
- [ ] **Task 4: Implement 1-per-day validation** (AC: 6, 9)
- [ ] Create `app/Rules/OneBookingPerDay.php` validation rule
- [ ] Check against `booking_date` with pending/approved status
- [ ] Display clear error message when violated
- [ ] **Task 5: Implement slot availability check** (AC: 7, 9)
- [ ] Use `AvailabilityService::getAvailableSlots()` before confirmation
- [ ] Display error if slot no longer available
- [ ] Refresh calendar on error
- [ ] **Task 6: Implement confirmation step** (AC: 5, 15)
- [ ] Show booking summary before final submission
- [ ] Display date, time, duration (45 min), problem summary
- [ ] Add back button to edit
- [ ] **Task 7: Implement race condition prevention** (AC: 19)
- [ ] Use `DB::transaction()` with `lockForUpdate()` on slot check
- [ ] Re-validate 1-per-day rule inside transaction
- [ ] Throw exception if slot taken, catch and show error
- [ ] **Task 8: Create booking record** (AC: 10)
- [ ] Create Consultation with status `ConsultationStatus::Pending`
- [ ] Set `booking_date`, `booking_time`, `problem_summary`, `user_id`
- [ ] Leave `consultation_type`, `payment_amount` as null (admin sets later)
- [ ] **Task 9: Create email notifications** (AC: 12, 13)
- [ ] Create `app/Mail/BookingSubmittedMail.php` for client
- [ ] Create `app/Mail/NewBookingRequestMail.php` for admin
- [ ] Queue emails via `SendBookingNotification` job
- [ ] Support bilingual content based on user's `preferred_language`
- [ ] **Task 10: Implement audit logging** (AC: 20)
- [ ] Create AdminLog entry on booking creation
- [ ] Set `admin_id` to null (client action)
- [ ] Set `action` to 'create', `target_type` to 'consultation'
- [ ] **Task 11: Implement success flow** (AC: 11, 14, 17)
- [ ] Flash success message to session
- [ ] Redirect to `route('client.consultations.index')`
- [ ] **Task 12: Add route** (AC: 1)
- [ ] Add route in `routes/web.php` under client middleware group
- [ ] Route: `GET /client/consultations/book` → Volt component
- [ ] **Task 13: Add translation keys** (AC: 18)
- [ ] Add keys to `lang/en/booking.php`
- [ ] Add keys to `lang/ar/booking.php`
- [ ] **Task 14: Write tests** (AC: 21, 22)
- [ ] Create `tests/Feature/Client/BookingSubmissionTest.php`
- [ ] Test happy path submission
- [ ] Test validation rules
- [ ] Test 1-per-day constraint
- [ ] Test race condition handling
- [ ] Test notifications sent
- [ ] **Task 15: Run Pint and verify**
- [ ] Run `vendor/bin/pint --dirty`
- [ ] Verify all tests pass
## Dev Notes
### Relevant Source Tree
```
app/
├── Enums/
│ ├── ConsultationStatus.php # Pending, Approved, Completed, Cancelled, NoShow
│ └── PaymentStatus.php # Pending, Received, NotApplicable
├── Mail/
│ ├── BookingSubmittedMail.php # TO CREATE
│ └── NewBookingRequestMail.php # TO CREATE
├── Models/
│ ├── Consultation.php # EXISTS - booking_date, booking_time columns
│ ├── AdminLog.php # EXISTS - action column (not action_type)
│ └── User.php
├── Rules/
│ └── OneBookingPerDay.php # TO CREATE
├── Services/
│ └── AvailabilityService.php # EXISTS - getAvailableSlots() method
└── Jobs/
└── SendBookingNotification.php # TO CREATE (or use Mail directly)
resources/views/livewire/
├── availability-calendar.blade.php # EXISTS - calendar component
└── client/
└── consultations/
└── book.blade.php # TO CREATE - main booking form
lang/
├── en/booking.php # EXISTS - needs new keys added
└── ar/booking.php # EXISTS - needs new keys added
```
### Consultation Model Fields (Actual)
```php ```php
// consultations table fields on creation // app/Models/Consultation.php - $fillable
'user_id',
'booking_date', // NOT scheduled_date
'booking_time', // NOT scheduled_time
'problem_summary',
'consultation_type', // null initially, admin sets later
'payment_amount', // null initially
'payment_status', // PaymentStatus::NotApplicable initially
'status', // ConsultationStatus::Pending
'admin_notes',
```
**Important:** The model does NOT have a `duration` field. Duration (45 min) is a business constant, not stored per-consultation.
### AdminLog Model Fields (Actual)
```php
// app/Models/AdminLog.php - $fillable
'admin_id', // null for client actions
'action', // NOT action_type - values: 'create', 'update', 'delete'
'target_type', // 'consultation', 'user', 'working_hours', etc.
'target_id',
'old_values',
'new_values',
'ip_address',
'created_at',
```
### Database Record Creation
```php
use App\Enums\ConsultationStatus;
use App\Enums\PaymentStatus;
$consultation = Consultation::create([ $consultation = Consultation::create([
'user_id' => auth()->id(), 'user_id' => auth()->id(),
'scheduled_date' => $selectedDate, 'booking_date' => $this->selectedDate,
'scheduled_time' => $selectedTime, 'booking_time' => $this->selectedTime,
'duration' => 45, // default 'problem_summary' => $this->problemSummary,
'status' => 'pending', 'status' => ConsultationStatus::Pending,
'type' => null, // admin sets this later 'payment_status' => PaymentStatus::NotApplicable,
'payment_amount' => null, // consultation_type and payment_amount left null - admin sets later
'payment_status' => 'not_applicable',
'problem_summary' => $problemSummary,
]); ]);
``` ```
### Volt Component ### Volt Component Structure
```php ```php
<?php <?php
use App\Enums\ConsultationStatus;
use App\Enums\PaymentStatus;
use App\Models\AdminLog;
use App\Models\Consultation; use App\Models\Consultation;
use App\Models\User;
use App\Services\AvailabilityService; use App\Services\AvailabilityService;
use App\Notifications\BookingSubmittedClient;
use App\Notifications\NewBookingAdmin;
use Livewire\Volt\Component;
use Carbon\Carbon; use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Livewire\Volt\Component;
new class extends Component { new class extends Component {
public ?string $selectedDate = null; public ?string $selectedDate = null;
@ -107,9 +243,10 @@ new class extends Component {
]); ]);
// Check 1-per-day limit // Check 1-per-day limit
$existingBooking = Consultation::where('user_id', auth()->id()) $existingBooking = Consultation::query()
->where('scheduled_date', $this->selectedDate) ->where('user_id', auth()->id())
->whereIn('status', ['pending', 'approved']) ->whereDate('booking_date', $this->selectedDate)
->whereIn('status', [ConsultationStatus::Pending, ConsultationStatus::Approved])
->exists(); ->exists();
if ($existingBooking) { if ($existingBooking) {
@ -131,62 +268,76 @@ new class extends Component {
public function submit(): void public function submit(): void
{ {
// Double-check availability with lock try {
DB::transaction(function () { DB::transaction(function () {
// Check slot one more time with lock // Check slot one more time with lock
$exists = Consultation::where('scheduled_date', $this->selectedDate) $slotTaken = Consultation::query()
->where('scheduled_time', $this->selectedTime) ->whereDate('booking_date', $this->selectedDate)
->whereIn('status', ['pending', 'approved']) ->where('booking_time', $this->selectedTime)
->lockForUpdate() ->whereIn('status', [ConsultationStatus::Pending, ConsultationStatus::Approved])
->exists(); ->lockForUpdate()
->exists();
if ($exists) { if ($slotTaken) {
throw new \Exception(__('booking.slot_taken')); throw new \Exception(__('booking.slot_taken'));
} }
// Check 1-per-day again // Check 1-per-day again with lock
$userBooking = Consultation::where('user_id', auth()->id()) $userHasBooking = Consultation::query()
->where('scheduled_date', $this->selectedDate) ->where('user_id', auth()->id())
->whereIn('status', ['pending', 'approved']) ->whereDate('booking_date', $this->selectedDate)
->lockForUpdate() ->whereIn('status', [ConsultationStatus::Pending, ConsultationStatus::Approved])
->exists(); ->lockForUpdate()
->exists();
if ($userBooking) { if ($userHasBooking) {
throw new \Exception(__('booking.already_booked_this_day')); throw new \Exception(__('booking.already_booked_this_day'));
} }
// Create booking // Create booking
$consultation = Consultation::create([ $consultation = Consultation::create([
'user_id' => auth()->id(), 'user_id' => auth()->id(),
'scheduled_date' => $this->selectedDate, 'booking_date' => $this->selectedDate,
'scheduled_time' => $this->selectedTime, 'booking_time' => $this->selectedTime,
'duration' => 45, 'problem_summary' => $this->problemSummary,
'status' => 'pending', 'status' => ConsultationStatus::Pending,
'problem_summary' => $this->problemSummary, 'payment_status' => PaymentStatus::NotApplicable,
]); ]);
// Send notifications // Send email to client
auth()->user()->notify(new BookingSubmittedClient($consultation)); Mail::to(auth()->user())->queue(
new \App\Mail\BookingSubmittedMail($consultation)
);
// Notify admin // Send email to admin
$admin = User::where('user_type', 'admin')->first(); $admin = User::query()->where('user_type', 'admin')->first();
$admin?->notify(new NewBookingAdmin($consultation)); if ($admin) {
Mail::to($admin)->queue(
new \App\Mail\NewBookingRequestMail($consultation)
);
}
// Log action // Log action
AdminLog::create([ AdminLog::create([
'admin_id' => null, // Client action 'admin_id' => null, // Client action
'action_type' => 'create', 'action' => 'create',
'target_type' => 'consultation', 'target_type' => 'consultation',
'target_id' => $consultation->id, 'target_id' => $consultation->id,
'new_values' => $consultation->toArray(), 'new_values' => $consultation->toArray(),
'ip_address' => request()->ip(), 'ip_address' => request()->ip(),
]); 'created_at' => now(),
}); ]);
});
session()->flash('success', __('booking.submitted_successfully')); session()->flash('success', __('booking.submitted_successfully'));
$this->redirect(route('client.consultations.index')); $this->redirect(route('client.consultations.index'));
} catch (\Exception $e) {
$this->addError('selectedTime', $e->getMessage());
$this->showConfirmation = false;
}
} }
}; }; ?>
``` ```
### Blade Template ### Blade Template
@ -198,9 +349,7 @@ new class extends Component {
<!-- Step 1: Calendar Selection --> <!-- Step 1: Calendar Selection -->
<div class="mt-6"> <div class="mt-6">
<p class="mb-4">{{ __('booking.select_date_time') }}</p> <p class="mb-4">{{ __('booking.select_date_time') }}</p>
<livewire:booking.availability-calendar <livewire:availability-calendar />
@slot-selected="selectSlot($event.detail.date, $event.detail.time)"
/>
</div> </div>
@else @else
<!-- Step 2: Problem Summary --> <!-- Step 2: Problem Summary -->
@ -275,24 +424,41 @@ new class extends Component {
<span wire:loading>{{ __('common.submitting') }}</span> <span wire:loading>{{ __('common.submitting') }}</span>
</flux:button> </flux:button>
</div> </div>
@error('selectedTime')
<flux:callout variant="danger" class="mt-4">
{{ $message }}
</flux:callout>
@enderror
@endif @endif
</div> </div>
@endif @endif
</div> </div>
``` ```
### Calendar Integration Note
The `availability-calendar` component (from Story 3.3) uses the pattern `$parent.selectSlot()` to communicate slot selection back to the parent component. The parent booking component needs to have the `selectSlot(string $date, string $time)` method to receive this.
### 1-Per-Day Validation Rule ### 1-Per-Day Validation Rule
```php ```php
<?php
// app/Rules/OneBookingPerDay.php // app/Rules/OneBookingPerDay.php
namespace App\Rules;
use App\Enums\ConsultationStatus;
use App\Models\Consultation;
use Closure;
use Illuminate\Contracts\Validation\ValidationRule; use Illuminate\Contracts\Validation\ValidationRule;
class OneBookingPerDay implements ValidationRule class OneBookingPerDay implements ValidationRule
{ {
public function validate(string $attribute, mixed $value, Closure $fail): void public function validate(string $attribute, mixed $value, Closure $fail): void
{ {
$exists = Consultation::where('user_id', auth()->id()) $exists = Consultation::query()
->where('scheduled_date', $value) ->where('user_id', auth()->id())
->whereIn('status', ['pending', 'approved']) ->whereDate('booking_date', $value)
->whereIn('status', [ConsultationStatus::Pending, ConsultationStatus::Approved])
->exists(); ->exists();
if ($exists) { if ($exists) {
@ -302,121 +468,200 @@ class OneBookingPerDay implements ValidationRule
} }
``` ```
### Advanced Pattern: Race Condition Prevention ### Testing
The `submit()` method uses `DB::transaction()` with `lockForUpdate()` to prevent race conditions. This is an **advanced pattern** required because: #### Test File Location
- Multiple clients could attempt to book the same slot simultaneously `tests/Feature/Client/BookingSubmissionTest.php`
- Without locking, both requests could pass validation and create duplicate bookings
The `lockForUpdate()` acquires a row-level lock, ensuring only one transaction completes while others wait and then fail validation. #### Required Test Scenarios
### Cross-Story Task: Update Working Hours Pending Bookings Warning
**Context:** Story 3.1 (Working Hours Configuration) implemented a stubbed `checkPendingBookings()` method that returns an empty array. Now that the `Consultation` model exists, this method should be implemented to warn admins when changing working hours that affect pending bookings.
**File to update:** `resources/views/livewire/admin/settings/working-hours.blade.php`
**Implementation:**
```php ```php
private function checkPendingBookings(): array <?php
{
$affectedBookings = [];
foreach ($this->schedule as $day => $config) { use App\Enums\ConsultationStatus;
$original = WorkingHour::where('day_of_week', $day)->first(); use App\Models\Consultation;
use App\Models\User;
// Check if day is being disabled or hours reduced use App\Models\WorkingHour;
$isBeingDisabled = $original?->is_active && !$config['is_active']; use Livewire\Volt\Volt;
$hoursReduced = $original && (
$config['start_time'] > Carbon::parse($original->start_time)->format('H:i') ||
$config['end_time'] < Carbon::parse($original->end_time)->format('H:i')
);
if ($isBeingDisabled || $hoursReduced) {
$query = Consultation::query()
->where('status', 'pending')
->whereRaw('DAYOFWEEK(scheduled_date) = ?', [$day + 1]); // MySQL DAYOFWEEK is 1-indexed (1=Sunday)
if ($hoursReduced && !$isBeingDisabled) {
$query->where(function ($q) use ($config) {
$q->where('scheduled_time', '<', $config['start_time'])
->orWhere('scheduled_time', '>=', $config['end_time']);
});
}
$bookings = $query->get();
$affectedBookings = array_merge($affectedBookings, $bookings->toArray());
}
}
return $affectedBookings;
}
```
**Also update the save() method** to use the warning message when bookings are affected:
```php
$warnings = $this->checkPendingBookings();
// ... save logic ...
$message = __('messages.working_hours_saved');
if (!empty($warnings)) {
$message .= ' ' . __('messages.pending_bookings_warning', ['count' => count($warnings)]);
}
session()->flash('success', $message);
```
**Test to add:** `tests/Feature/Admin/WorkingHoursTest.php`
```php
test('warning shown when disabling day with pending bookings', function () {
// Create pending consultation for Monday
$consultation = Consultation::factory()->create([
'scheduled_date' => now()->next('Monday'),
'status' => 'pending',
]);
beforeEach(function () {
// Setup working hours for Monday
WorkingHour::factory()->create([ WorkingHour::factory()->create([
'day_of_week' => 1, // Monday 'day_of_week' => 1,
'start_time' => '09:00',
'end_time' => '17:00',
'is_active' => true, 'is_active' => true,
]); ]);
});
$this->actingAs($this->admin); // Happy path
test('authenticated client can submit booking request', function () {
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
Volt::test('admin.settings.working-hours') Volt::test('client.consultations.book')
->set('schedule.1.is_active', false) ->actingAs($client)
->call('save') ->call('selectSlot', $monday, '10:00')
->assertSee(__('messages.pending_bookings_warning', ['count' => 1])); ->set('problemSummary', 'I need legal advice regarding a contract dispute with my employer.')
->call('showConfirm')
->assertSet('showConfirmation', true)
->call('submit')
->assertRedirect(route('client.consultations.index'));
expect(Consultation::where('user_id', $client->id)->exists())->toBeTrue();
});
test('booking is created with pending status', function () {
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
Volt::test('client.consultations.book')
->actingAs($client)
->call('selectSlot', $monday, '10:00')
->set('problemSummary', 'I need legal advice regarding a contract dispute.')
->call('showConfirm')
->call('submit');
$consultation = Consultation::where('user_id', $client->id)->first();
expect($consultation->status)->toBe(ConsultationStatus::Pending);
});
// Validation
test('guest cannot access booking form', function () {
$this->get(route('client.consultations.book'))
->assertRedirect(route('login'));
});
test('problem summary is required', function () {
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
Volt::test('client.consultations.book')
->actingAs($client)
->call('selectSlot', $monday, '10:00')
->set('problemSummary', '')
->call('showConfirm')
->assertHasErrors(['problemSummary' => 'required']);
});
test('problem summary must be at least 20 characters', function () {
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
Volt::test('client.consultations.book')
->actingAs($client)
->call('selectSlot', $monday, '10:00')
->set('problemSummary', 'Too short')
->call('showConfirm')
->assertHasErrors(['problemSummary' => 'min']);
});
// Business rules
test('client cannot book more than once per day', function () {
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
// Create existing booking
Consultation::factory()->pending()->create([
'user_id' => $client->id,
'booking_date' => $monday,
'booking_time' => '09:00',
]);
Volt::test('client.consultations.book')
->actingAs($client)
->call('selectSlot', $monday, '10:00')
->set('problemSummary', 'I need legal advice regarding a contract dispute.')
->call('showConfirm')
->assertHasErrors(['selectedDate']);
});
test('client cannot book unavailable slot', function () {
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
// Create booking that takes the slot
Consultation::factory()->approved()->create([
'booking_date' => $monday,
'booking_time' => '10:00',
]);
Volt::test('client.consultations.book')
->actingAs($client)
->call('selectSlot', $monday, '10:00')
->set('problemSummary', 'I need legal advice regarding a contract dispute.')
->call('showConfirm')
->assertHasErrors(['selectedTime']);
});
// UI flow
test('confirmation step displays before final submission', function () {
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
Volt::test('client.consultations.book')
->actingAs($client)
->call('selectSlot', $monday, '10:00')
->set('problemSummary', 'I need legal advice regarding a contract dispute.')
->assertSet('showConfirmation', false)
->call('showConfirm')
->assertSet('showConfirmation', true);
});
test('user can go back from confirmation to edit', function () {
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
Volt::test('client.consultations.book')
->actingAs($client)
->call('selectSlot', $monday, '10:00')
->set('problemSummary', 'I need legal advice regarding a contract dispute.')
->call('showConfirm')
->assertSet('showConfirmation', true)
->set('showConfirmation', false)
->assertSet('showConfirmation', false);
});
test('success message shown after submission', function () {
$client = User::factory()->individual()->create();
$monday = now()->next('Monday')->format('Y-m-d');
Volt::test('client.consultations.book')
->actingAs($client)
->call('selectSlot', $monday, '10:00')
->set('problemSummary', 'I need legal advice regarding a contract dispute.')
->call('showConfirm')
->call('submit')
->assertSessionHas('success');
}); });
``` ```
---
## Files to Create ## Files to Create
| File | Purpose | | File | Purpose |
|------|---------| |------|---------|
| `resources/views/livewire/booking/request.blade.php` | Main Volt component for booking submission | | `resources/views/livewire/client/consultations/book.blade.php` | Main Volt component for booking submission |
| `app/Rules/OneBookingPerDay.php` | Custom validation rule for 1-per-day limit | | `app/Rules/OneBookingPerDay.php` | Custom validation rule for 1-per-day limit |
| `app/Notifications/BookingSubmittedClient.php` | Email notification to client on submission | | `app/Mail/BookingSubmittedMail.php` | Email to client on submission |
| `app/Notifications/NewBookingAdmin.php` | Email notification to admin for new booking | | `app/Mail/NewBookingRequestMail.php` | Email to admin for new booking |
| `tests/Feature/Client/BookingSubmissionTest.php` | Feature tests for booking flow |
### Notification Classes ### Mail Classes
Create notifications using artisan: Create mail classes using artisan:
```bash ```bash
php artisan make:notification BookingSubmittedClient php artisan make:mail BookingSubmittedMail
php artisan make:notification NewBookingAdmin php artisan make:mail NewBookingRequestMail
``` ```
Both notifications should: Both mail classes should:
- Accept `Consultation $consultation` in constructor - Accept `Consultation $consultation` in constructor
- Implement `toMail()` for email delivery - Use bilingual subjects based on recipient's `preferred_language`
- Use bilingual subjects based on user's `preferred_language` - Create corresponding email views in `resources/views/emails/`
## Translation Keys Required ## Translation Keys Required
Add to `lang/en/booking.php` and `lang/ar/booking.php`: Add to `lang/en/booking.php`:
```php ```php
// lang/en/booking.php
'request_consultation' => 'Request Consultation', 'request_consultation' => 'Request Consultation',
'select_date_time' => 'Select a date and time for your consultation', 'select_date_time' => 'Select a date and time for your consultation',
'selected_time' => 'Selected Time', 'selected_time' => 'Selected Time',
@ -436,74 +681,52 @@ Add to `lang/en/booking.php` and `lang/ar/booking.php`:
'slot_taken' => 'This slot was just booked. Please select another time.', 'slot_taken' => 'This slot was just booked. Please select another time.',
``` ```
## Testing Requirements Add corresponding Arabic translations to `lang/ar/booking.php`.
### Test File Location
`tests/Feature/Booking/BookingSubmissionTest.php`
### Required Test Scenarios
```php
// Happy path
test('authenticated client can submit booking request')
test('booking is created with pending status')
test('client receives confirmation notification')
test('admin receives new booking notification')
// Validation
test('guest cannot access booking form')
test('problem summary is required')
test('problem summary must be at least 20 characters')
test('selected date must be today or future')
// Business rules
test('client cannot book more than once per day')
test('client cannot book unavailable slot')
test('booking fails if slot taken during submission', function () {
// Test race condition prevention
// Create booking for same slot in parallel/before submission completes
})
// UI flow
test('confirmation step displays before final submission')
test('user can go back from confirmation to edit')
test('success message shown after submission')
test('redirects to consultations list after submission')
```
## Definition of Done ## Definition of Done
- [ ] Volt component created at correct path
- [ ] Can select date from calendar - [ ] Can select date from calendar
- [ ] Can select time slot - [ ] Can select time slot
- [ ] Problem summary required - [ ] Problem summary required (min 20 chars)
- [ ] 1-per-day limit enforced - [ ] 1-per-day limit enforced
- [ ] Race condition prevented - [ ] Race condition prevented with DB locking
- [ ] Confirmation step before submission - [ ] Confirmation step before submission
- [ ] Booking created with "pending" status - [ ] Booking created with "pending" status
- [ ] Client notification sent - [ ] Client email sent
- [ ] Admin notification sent - [ ] Admin email sent
- [ ] Audit log entry created
- [ ] Bilingual support complete - [ ] Bilingual support complete
- [ ] Tests for submission flow - [ ] All tests passing
- [ ] Code formatted with Pint - [ ] Code formatted with Pint
- [ ] **Cross-story:** Working Hours `checkPendingBookings()` implemented (see Technical Notes)
## Dependencies ## Dependencies
- **Story 3.3:** Availability calendar (`docs/stories/story-3.3-availability-calendar-display.md`) - **Story 3.3:** Availability calendar (COMPLETED)
- Provides `AvailabilityService` for slot availability checking - Provides `AvailabilityService` for slot availability checking
- Provides `booking.availability-calendar` Livewire component - Provides `availability-calendar` Livewire component
- **Epic 2:** User authentication (`docs/epics/epic-2-user-management.md`) - **Epic 2:** User authentication (COMPLETED)
- Client must be logged in to submit bookings - Client must be logged in to submit bookings
- **Epic 8:** Email notifications (partial) - **Epic 8:** Email notifications (partial - mail classes needed)
- Notification infrastructure for sending emails
## Risk Assessment ## Risk Assessment
- **Primary Risk:** Double-booking from concurrent submissions | Risk | Impact | Mitigation |
- **Mitigation:** Database transaction with row locking |------|--------|------------|
- **Rollback:** Return to calendar with error message | Double-booking race condition | High | DB transaction with `lockForUpdate()` |
| Email delivery failure | Medium | Use queue with retries |
| Timezone confusion | Low | Use `whereDate()` for date comparison |
## Estimation ## Estimation
**Complexity:** Medium-High **Complexity:** Medium-High
**Estimated Effort:** 4-5 hours **Estimated Effort:** 4-5 hours
---
## Change Log
| Date | Version | Description | Author |
|------|---------|-------------|--------|
| 2025-12-26 | 1.0 | Initial draft | - |
| 2025-12-26 | 1.1 | Fixed column names (booking_date/booking_time), removed hallucinated duration field, fixed AdminLog column, removed invalid cross-story task, added Tasks/Subtasks section, aligned with source tree architecture | QA Validation |