# Story 3.4: Booking Request Submission ## Status **Draft** ## Epic Reference **Epic 3:** Booking & Consultation System ## Story **As a** client, **I want** to submit a consultation booking request, **so that** I can schedule a meeting with the lawyer. ## Acceptance Criteria ### Booking Form (AC1-5) 1. Client must be logged in 2. Select date from availability calendar 3. Select available time slot 4. Problem summary field (required, textarea, min 20 characters) 5. Confirmation before submission ### Validation & Constraints (AC6-9) 6. Validate: no more than 1 booking per day for this client 7. Validate: selected slot is still available 8. Validate: problem summary is not empty 9. Show clear error messages for violations ### Submission Flow (AC10-14) 10. Booking enters "pending" status 11. Client sees "Pending Review" confirmation 12. Admin receives email notification 13. Client receives submission confirmation email 14. Redirect to consultations list after submission ### UI/UX (AC15-18) 15. Clear step-by-step flow 16. Loading state during submission 17. Success message with next steps 18. Bilingual labels and messages ### Quality Requirements (AC19-22) 19. Prevent double-booking (race condition) 20. Audit log entry for booking creation 21. Tests for submission flow 22. Tests for validation rules ## Tasks / Subtasks - [ ] **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 `` - [ ] **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 // 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([ 'user_id' => auth()->id(), 'booking_date' => $this->selectedDate, 'booking_time' => $this->selectedTime, 'problem_summary' => $this->problemSummary, 'status' => ConsultationStatus::Pending, 'payment_status' => PaymentStatus::NotApplicable, // consultation_type and payment_amount left null - admin sets later ]); ``` ### Volt Component Structure ```php selectedDate = $date; $this->selectedTime = $time; } public function clearSelection(): void { $this->selectedDate = null; $this->selectedTime = null; } public function showConfirm(): void { $this->validate([ 'selectedDate' => ['required', 'date', 'after_or_equal:today'], 'selectedTime' => ['required'], 'problemSummary' => ['required', 'string', 'min:20', 'max:2000'], ]); // Check 1-per-day limit $existingBooking = Consultation::query() ->where('user_id', auth()->id()) ->whereDate('booking_date', $this->selectedDate) ->whereIn('status', [ConsultationStatus::Pending, ConsultationStatus::Approved]) ->exists(); if ($existingBooking) { $this->addError('selectedDate', __('booking.already_booked_this_day')); return; } // Verify slot still available $service = app(AvailabilityService::class); $availableSlots = $service->getAvailableSlots(Carbon::parse($this->selectedDate)); if (!in_array($this->selectedTime, $availableSlots)) { $this->addError('selectedTime', __('booking.slot_no_longer_available')); return; } $this->showConfirmation = true; } public function submit(): void { try { DB::transaction(function () { // Check slot one more time with lock $slotTaken = Consultation::query() ->whereDate('booking_date', $this->selectedDate) ->where('booking_time', $this->selectedTime) ->whereIn('status', [ConsultationStatus::Pending, ConsultationStatus::Approved]) ->lockForUpdate() ->exists(); if ($slotTaken) { throw new \Exception(__('booking.slot_taken')); } // Check 1-per-day again with lock $userHasBooking = Consultation::query() ->where('user_id', auth()->id()) ->whereDate('booking_date', $this->selectedDate) ->whereIn('status', [ConsultationStatus::Pending, ConsultationStatus::Approved]) ->lockForUpdate() ->exists(); if ($userHasBooking) { throw new \Exception(__('booking.already_booked_this_day')); } // Create booking $consultation = Consultation::create([ 'user_id' => auth()->id(), 'booking_date' => $this->selectedDate, 'booking_time' => $this->selectedTime, 'problem_summary' => $this->problemSummary, 'status' => ConsultationStatus::Pending, 'payment_status' => PaymentStatus::NotApplicable, ]); // Send email to client Mail::to(auth()->user())->queue( new \App\Mail\BookingSubmittedMail($consultation) ); // Send email to admin $admin = User::query()->where('user_type', 'admin')->first(); if ($admin) { Mail::to($admin)->queue( new \App\Mail\NewBookingRequestMail($consultation) ); } // Log action AdminLog::create([ 'admin_id' => null, // Client action 'action' => 'create', 'target_type' => 'consultation', 'target_id' => $consultation->id, 'new_values' => $consultation->toArray(), 'ip_address' => request()->ip(), 'created_at' => now(), ]); }); session()->flash('success', __('booking.submitted_successfully')); $this->redirect(route('client.consultations.index')); } catch (\Exception $e) { $this->addError('selectedTime', $e->getMessage()); $this->showConfirmation = false; } } }; ?> ``` ### Blade Template ```blade
{{ __('booking.request_consultation') }} @if(!$selectedDate || !$selectedTime)

{{ __('booking.select_date_time') }}

@else

{{ __('booking.selected_time') }}

{{ \Carbon\Carbon::parse($selectedDate)->translatedFormat('l, d M Y') }}

{{ \Carbon\Carbon::parse($selectedTime)->format('g:i A') }}

{{ __('common.change') }}
@if(!$showConfirmation) {{ __('booking.problem_summary') }} * {{ __('booking.problem_summary_help') }} {{ __('booking.continue') }} {{ __('common.loading') }} @else {{ __('booking.confirm_booking') }}

{{ __('booking.confirm_message') }}

{{ __('booking.date') }}: {{ \Carbon\Carbon::parse($selectedDate)->translatedFormat('l, d M Y') }}

{{ __('booking.time') }}: {{ \Carbon\Carbon::parse($selectedTime)->format('g:i A') }}

{{ __('booking.duration') }}: 45 {{ __('common.minutes') }}

{{ __('booking.problem_summary') }}:

{{ $problemSummary }}

{{ __('common.back') }} {{ __('booking.submit_request') }} {{ __('common.submitting') }}
@error('selectedTime') {{ $message }} @enderror @endif
@endif
``` ### 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 ```php where('user_id', auth()->id()) ->whereDate('booking_date', $value) ->whereIn('status', [ConsultationStatus::Pending, ConsultationStatus::Approved]) ->exists(); if ($exists) { $fail(__('booking.already_booked_this_day')); } } } ``` ### Testing #### Test File Location `tests/Feature/Client/BookingSubmissionTest.php` #### Required Test Scenarios ```php create([ 'day_of_week' => 1, 'start_time' => '09:00', 'end_time' => '17:00', 'is_active' => true, ]); }); // 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('client.consultations.book') ->actingAs($client) ->call('selectSlot', $monday, '10:00') ->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 | File | Purpose | |------|---------| | `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/Mail/BookingSubmittedMail.php` | Email to client on submission | | `app/Mail/NewBookingRequestMail.php` | Email to admin for new booking | | `tests/Feature/Client/BookingSubmissionTest.php` | Feature tests for booking flow | ### Mail Classes Create mail classes using artisan: ```bash php artisan make:mail BookingSubmittedMail php artisan make:mail NewBookingRequestMail ``` Both mail classes should: - Accept `Consultation $consultation` in constructor - Use bilingual subjects based on recipient's `preferred_language` - Create corresponding email views in `resources/views/emails/` ## Translation Keys Required Add to `lang/en/booking.php`: ```php 'request_consultation' => 'Request Consultation', 'select_date_time' => 'Select a date and time for your consultation', 'selected_time' => 'Selected Time', 'problem_summary' => 'Problem Summary', 'problem_summary_placeholder' => 'Please describe your legal issue or question in detail...', 'problem_summary_help' => 'Minimum 20 characters. This helps the lawyer prepare for your consultation.', 'continue' => 'Continue', 'confirm_booking' => 'Confirm Your Booking', 'confirm_message' => 'Please review your booking details before submitting.', 'date' => 'Date', 'time' => 'Time', 'duration' => 'Duration', 'submit_request' => 'Submit Request', 'submitted_successfully' => 'Your booking request has been submitted. You will receive an email confirmation shortly.', 'already_booked_this_day' => 'You already have a booking on this day.', 'slot_no_longer_available' => 'This time slot is no longer available. Please select another.', 'slot_taken' => 'This slot was just booked. Please select another time.', ``` Add corresponding Arabic translations to `lang/ar/booking.php`. ## Definition of Done - [ ] Volt component created at correct path - [ ] Can select date from calendar - [ ] Can select time slot - [ ] Problem summary required (min 20 chars) - [ ] 1-per-day limit enforced - [ ] Race condition prevented with DB locking - [ ] Confirmation step before submission - [ ] Booking created with "pending" status - [ ] Client email sent - [ ] Admin email sent - [ ] Audit log entry created - [ ] Bilingual support complete - [ ] All tests passing - [ ] Code formatted with Pint ## Dependencies - **Story 3.3:** Availability calendar (COMPLETED) - Provides `AvailabilityService` for slot availability checking - Provides `availability-calendar` Livewire component - **Epic 2:** User authentication (COMPLETED) - Client must be logged in to submit bookings - **Epic 8:** Email notifications (partial - mail classes needed) ## Risk Assessment | Risk | Impact | Mitigation | |------|--------|------------| | 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 **Complexity:** Medium-High **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 |