# Story 3.4: Booking Request Submission ## Epic Reference **Epic 3:** Booking & Consultation System ## User Story As a **client**, I want **to submit a consultation booking request**, 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 ### Booking Form - [ ] Client must be logged in - [ ] Select date from availability calendar - [ ] Select available time slot - [ ] Problem summary field (required, textarea) - [ ] Confirmation before submission ### Validation & Constraints - [ ] Validate: no more than 1 booking per day for this client - [ ] Validate: selected slot is still available - [ ] Validate: problem summary is not empty - [ ] Show clear error messages for violations ### Submission Flow - [ ] Booking enters "pending" status - [ ] Client sees "Pending Review" confirmation - [ ] Admin receives email notification - [ ] Client receives submission confirmation email - [ ] Redirect to consultations list after submission ### UI/UX - [ ] Clear step-by-step flow - [ ] Loading state during submission - [ ] Success message with next steps - [ ] Bilingual labels and messages ### Quality Requirements - [ ] Prevent double-booking (race condition) - [ ] Audit log entry for booking creation - [ ] Tests for submission flow - [ ] Tests for validation rules ## Technical Notes ### Database Record ```php // consultations table fields on creation $consultation = Consultation::create([ 'user_id' => auth()->id(), 'scheduled_date' => $selectedDate, 'scheduled_time' => $selectedTime, 'duration' => 45, // default 'status' => 'pending', 'type' => null, // admin sets this later 'payment_amount' => null, 'payment_status' => 'not_applicable', 'problem_summary' => $problemSummary, ]); ``` ### Volt Component ```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::where('user_id', auth()->id()) ->where('scheduled_date', $this->selectedDate) ->whereIn('status', ['pending', '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 { // Double-check availability with lock DB::transaction(function () { // Check slot one more time with lock $exists = Consultation::where('scheduled_date', $this->selectedDate) ->where('scheduled_time', $this->selectedTime) ->whereIn('status', ['pending', 'approved']) ->lockForUpdate() ->exists(); if ($exists) { throw new \Exception(__('booking.slot_taken')); } // Check 1-per-day again $userBooking = Consultation::where('user_id', auth()->id()) ->where('scheduled_date', $this->selectedDate) ->whereIn('status', ['pending', 'approved']) ->lockForUpdate() ->exists(); if ($userBooking) { throw new \Exception(__('booking.already_booked_this_day')); } // Create booking $consultation = Consultation::create([ 'user_id' => auth()->id(), 'scheduled_date' => $this->selectedDate, 'scheduled_time' => $this->selectedTime, 'duration' => 45, 'status' => 'pending', 'problem_summary' => $this->problemSummary, ]); // Send notifications auth()->user()->notify(new BookingSubmittedClient($consultation)); // Notify admin $admin = User::where('user_type', 'admin')->first(); $admin?->notify(new NewBookingAdmin($consultation)); // Log action AdminLog::create([ 'admin_id' => null, // Client action 'action_type' => 'create', 'target_type' => 'consultation', 'target_id' => $consultation->id, 'new_values' => $consultation->toArray(), 'ip_address' => request()->ip(), ]); }); session()->flash('success', __('booking.submitted_successfully')); $this->redirect(route('client.consultations.index')); } }; ``` ### 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') }}
@endif
@endif
``` ### 1-Per-Day Validation Rule ```php // app/Rules/OneBookingPerDay.php use Illuminate\Contracts\Validation\ValidationRule; class OneBookingPerDay implements ValidationRule { public function validate(string $attribute, mixed $value, Closure $fail): void { $exists = Consultation::where('user_id', auth()->id()) ->where('scheduled_date', $value) ->whereIn('status', ['pending', 'approved']) ->exists(); if ($exists) { $fail(__('booking.already_booked_this_day')); } } } ``` ### Advanced Pattern: Race Condition Prevention The `submit()` method uses `DB::transaction()` with `lockForUpdate()` to prevent race conditions. This is an **advanced pattern** required because: - Multiple clients could attempt to book the same slot simultaneously - 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. ## Files to Create | File | Purpose | |------|---------| | `resources/views/livewire/booking/request.blade.php` | Main Volt component for booking submission | | `app/Rules/OneBookingPerDay.php` | Custom validation rule for 1-per-day limit | | `app/Notifications/BookingSubmittedClient.php` | Email notification to client on submission | | `app/Notifications/NewBookingAdmin.php` | Email notification to admin for new booking | ### Notification Classes Create notifications using artisan: ```bash php artisan make:notification BookingSubmittedClient php artisan make:notification NewBookingAdmin ``` Both notifications should: - Accept `Consultation $consultation` in constructor - Implement `toMail()` for email delivery - Use bilingual subjects based on user's `preferred_language` ## Translation Keys Required Add to `lang/en/booking.php` and `lang/ar/booking.php`: ```php // lang/en/booking.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.', ``` ## Testing Requirements ### 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 - [ ] Can select date from calendar - [ ] Can select time slot - [ ] Problem summary required - [ ] 1-per-day limit enforced - [ ] Race condition prevented - [ ] Confirmation step before submission - [ ] Booking created with "pending" status - [ ] Client notification sent - [ ] Admin notification sent - [ ] Bilingual support complete - [ ] Tests for submission flow - [ ] Code formatted with Pint ## Dependencies - **Story 3.3:** Availability calendar (`docs/stories/story-3.3-availability-calendar-display.md`) - Provides `AvailabilityService` for slot availability checking - Provides `booking.availability-calendar` Livewire component - **Epic 2:** User authentication (`docs/epics/epic-2-user-management.md`) - Client must be logged in to submit bookings - **Epic 8:** Email notifications (partial) - Notification infrastructure for sending emails ## Risk Assessment - **Primary Risk:** Double-booking from concurrent submissions - **Mitigation:** Database transaction with row locking - **Rollback:** Return to calendar with error message ## Estimation **Complexity:** Medium-High **Estimated Effort:** 4-5 hours