# 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, notifications - **Technology:** Livewire Volt, form validation - **Follows pattern:** Form submission with confirmation - **Touch points:** Client dashboard, admin notifications ## 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 // Custom validation rule 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')); } } } ``` ## 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 - **Epic 2:** User authentication - **Epic 8:** Email notifications (partial) ## 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