From 1b3bc0a2cf0318bce1e625d0da84812282f32a11 Mon Sep 17 00:00:00 2001 From: Naser Mansour Date: Fri, 26 Dec 2025 19:12:56 +0200 Subject: [PATCH] fixed issues in story 3.4 --- .../story-3.4-booking-request-submission.md | 715 ++++++++++++------ 1 file changed, 469 insertions(+), 246 deletions(-) diff --git a/docs/stories/story-3.4-booking-request-submission.md b/docs/stories/story-3.4-booking-request-submission.md index b07f01b..1f5b6c4 100644 --- a/docs/stories/story-3.4-booking-request-submission.md +++ b/docs/stories/story-3.4-booking-request-submission.md @@ -1,84 +1,220 @@ # Story 3.4: Booking Request Submission +## Status +**Draft** + ## 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` +## 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 -- [ ] Client must be logged in -- [ ] Select date from availability calendar -- [ ] Select available time slot -- [ ] Problem summary field (required, textarea) -- [ ] Confirmation before submission +### 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 -- [ ] 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 +### 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 -- [ ] Booking enters "pending" status -- [ ] Client sees "Pending Review" confirmation -- [ ] Admin receives email notification -- [ ] Client receives submission confirmation email -- [ ] Redirect to consultations list after submission +### 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 -- [ ] Clear step-by-step flow -- [ ] Loading state during submission -- [ ] Success message with next steps -- [ ] Bilingual labels and messages +### 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 -- [ ] Prevent double-booking (race condition) -- [ ] Audit log entry for booking creation -- [ ] Tests for submission flow -- [ ] Tests for validation rules +### 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 -## 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 `` + +- [ ] **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 -// 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([ '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, + '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 +### Volt Component Structure ```php id()) - ->where('scheduled_date', $this->selectedDate) - ->whereIn('status', ['pending', 'approved']) + $existingBooking = Consultation::query() + ->where('user_id', auth()->id()) + ->whereDate('booking_date', $this->selectedDate) + ->whereIn('status', [ConsultationStatus::Pending, ConsultationStatus::Approved]) ->exists(); if ($existingBooking) { @@ -131,62 +268,76 @@ new class extends Component { 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(); + 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 ($exists) { - throw new \Exception(__('booking.slot_taken')); - } + if ($slotTaken) { + 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(); + // 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 ($userBooking) { - throw new \Exception(__('booking.already_booked_this_day')); - } + if ($userHasBooking) { + 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, - ]); + // 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 notifications - auth()->user()->notify(new BookingSubmittedClient($consultation)); + // Send email to client + Mail::to(auth()->user())->queue( + new \App\Mail\BookingSubmittedMail($consultation) + ); - // Notify admin - $admin = User::where('user_type', 'admin')->first(); - $admin?->notify(new NewBookingAdmin($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_type' => 'create', - 'target_type' => 'consultation', - 'target_id' => $consultation->id, - 'new_values' => $consultation->toArray(), - 'ip_address' => request()->ip(), - ]); - }); + // 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')); + 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 @@ -198,9 +349,7 @@ new class extends Component {

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

- +
@else @@ -275,24 +424,41 @@ new class extends Component { {{ __('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 +id()) - ->where('scheduled_date', $value) - ->whereIn('status', ['pending', 'approved']) + $exists = Consultation::query() + ->where('user_id', auth()->id()) + ->whereDate('booking_date', $value) + ->whereIn('status', [ConsultationStatus::Pending, ConsultationStatus::Approved]) ->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: -- Multiple clients could attempt to book the same slot simultaneously -- Without locking, both requests could pass validation and create duplicate bookings +#### Test File Location +`tests/Feature/Client/BookingSubmissionTest.php` -The `lockForUpdate()` acquires a row-level lock, ensuring only one transaction completes while others wait and then fail validation. - -### 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:** +#### Required Test Scenarios ```php -private function checkPendingBookings(): array -{ - $affectedBookings = []; +schedule as $day => $config) { - $original = WorkingHour::where('day_of_week', $day)->first(); - - // Check if day is being disabled or hours reduced - $isBeingDisabled = $original?->is_active && !$config['is_active']; - $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', - ]); +use App\Enums\ConsultationStatus; +use App\Models\Consultation; +use App\Models\User; +use App\Models\WorkingHour; +use Livewire\Volt\Volt; +beforeEach(function () { + // Setup working hours for Monday WorkingHour::factory()->create([ - 'day_of_week' => 1, // Monday + 'day_of_week' => 1, + 'start_time' => '09:00', + 'end_time' => '17:00', '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') - ->set('schedule.1.is_active', false) - ->call('save') - ->assertSee(__('messages.pending_bookings_warning', ['count' => 1])); + 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/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/Notifications/BookingSubmittedClient.php` | Email notification to client on submission | -| `app/Notifications/NewBookingAdmin.php` | Email notification to admin for new booking | +| `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 | -### Notification Classes +### Mail Classes -Create notifications using artisan: +Create mail classes using artisan: ```bash -php artisan make:notification BookingSubmittedClient -php artisan make:notification NewBookingAdmin +php artisan make:mail BookingSubmittedMail +php artisan make:mail NewBookingRequestMail ``` -Both notifications should: +Both mail classes should: - Accept `Consultation $consultation` in constructor -- Implement `toMail()` for email delivery -- Use bilingual subjects based on user's `preferred_language` +- 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` and `lang/ar/booking.php`: - +Add to `lang/en/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', @@ -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.', ``` -## 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') -``` +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 +- [ ] Problem summary required (min 20 chars) - [ ] 1-per-day limit enforced -- [ ] Race condition prevented +- [ ] Race condition prevented with DB locking - [ ] Confirmation step before submission - [ ] Booking created with "pending" status -- [ ] Client notification sent -- [ ] Admin notification sent +- [ ] Client email sent +- [ ] Admin email sent +- [ ] Audit log entry created - [ ] Bilingual support complete -- [ ] Tests for submission flow +- [ ] All tests passing - [ ] Code formatted with Pint -- [ ] **Cross-story:** Working Hours `checkPendingBookings()` implemented (see Technical Notes) ## 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 `booking.availability-calendar` Livewire component -- **Epic 2:** User authentication (`docs/epics/epic-2-user-management.md`) + - Provides `availability-calendar` Livewire component +- **Epic 2:** User authentication (COMPLETED) - Client must be logged in to submit bookings -- **Epic 8:** Email notifications (partial) - - Notification infrastructure for sending emails +- **Epic 8:** Email notifications (partial - mail classes needed) ## Risk Assessment -- **Primary Risk:** Double-booking from concurrent submissions -- **Mitigation:** Database transaction with row locking -- **Rollback:** Return to calendar with error message +| 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 |