# 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