generated sotries
This commit is contained in:
@@ -0,0 +1,334 @@
|
||||
# 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
|
||||
<?php
|
||||
|
||||
use App\Models\Consultation;
|
||||
use App\Services\AvailabilityService;
|
||||
use App\Notifications\BookingSubmittedClient;
|
||||
use App\Notifications\NewBookingAdmin;
|
||||
use Livewire\Volt\Component;
|
||||
use Carbon\Carbon;
|
||||
|
||||
new class extends Component {
|
||||
public ?string $selectedDate = null;
|
||||
public ?string $selectedTime = null;
|
||||
public string $problemSummary = '';
|
||||
public bool $showConfirmation = false;
|
||||
|
||||
public function selectSlot(string $date, string $time): void
|
||||
{
|
||||
$this->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
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<flux:heading>{{ __('booking.request_consultation') }}</flux:heading>
|
||||
|
||||
@if(!$selectedDate || !$selectedTime)
|
||||
<!-- Step 1: Calendar Selection -->
|
||||
<div class="mt-6">
|
||||
<p class="mb-4">{{ __('booking.select_date_time') }}</p>
|
||||
<livewire:booking.availability-calendar
|
||||
@slot-selected="selectSlot($event.detail.date, $event.detail.time)"
|
||||
/>
|
||||
</div>
|
||||
@else
|
||||
<!-- Step 2: Problem Summary -->
|
||||
<div class="mt-6">
|
||||
<!-- Selected Time Display -->
|
||||
<div class="bg-gold/10 p-4 rounded-lg mb-6">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<p class="font-semibold">{{ __('booking.selected_time') }}</p>
|
||||
<p>{{ \Carbon\Carbon::parse($selectedDate)->translatedFormat('l, d M Y') }}</p>
|
||||
<p>{{ \Carbon\Carbon::parse($selectedTime)->format('g:i A') }}</p>
|
||||
</div>
|
||||
<flux:button size="sm" wire:click="clearSelection">
|
||||
{{ __('common.change') }}
|
||||
</flux:button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if(!$showConfirmation)
|
||||
<!-- Problem Summary Form -->
|
||||
<flux:field>
|
||||
<flux:label>{{ __('booking.problem_summary') }} *</flux:label>
|
||||
<flux:textarea
|
||||
wire:model="problemSummary"
|
||||
rows="6"
|
||||
placeholder="{{ __('booking.problem_summary_placeholder') }}"
|
||||
/>
|
||||
<flux:description>
|
||||
{{ __('booking.problem_summary_help') }}
|
||||
</flux:description>
|
||||
<flux:error name="problemSummary" />
|
||||
</flux:field>
|
||||
|
||||
<flux:button
|
||||
wire:click="showConfirm"
|
||||
class="mt-4"
|
||||
wire:loading.attr="disabled"
|
||||
>
|
||||
<span wire:loading.remove>{{ __('booking.continue') }}</span>
|
||||
<span wire:loading>{{ __('common.loading') }}</span>
|
||||
</flux:button>
|
||||
@else
|
||||
<!-- Confirmation Step -->
|
||||
<flux:callout>
|
||||
<flux:heading size="sm">{{ __('booking.confirm_booking') }}</flux:heading>
|
||||
<p>{{ __('booking.confirm_message') }}</p>
|
||||
|
||||
<div class="mt-4 space-y-2">
|
||||
<p><strong>{{ __('booking.date') }}:</strong>
|
||||
{{ \Carbon\Carbon::parse($selectedDate)->translatedFormat('l, d M Y') }}</p>
|
||||
<p><strong>{{ __('booking.time') }}:</strong>
|
||||
{{ \Carbon\Carbon::parse($selectedTime)->format('g:i A') }}</p>
|
||||
<p><strong>{{ __('booking.duration') }}:</strong> 45 {{ __('common.minutes') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<p><strong>{{ __('booking.problem_summary') }}:</strong></p>
|
||||
<p class="mt-1 text-sm">{{ $problemSummary }}</p>
|
||||
</div>
|
||||
</flux:callout>
|
||||
|
||||
<div class="flex gap-3 mt-4">
|
||||
<flux:button wire:click="$set('showConfirmation', false)">
|
||||
{{ __('common.back') }}
|
||||
</flux:button>
|
||||
<flux:button
|
||||
wire:click="submit"
|
||||
variant="primary"
|
||||
wire:loading.attr="disabled"
|
||||
>
|
||||
<span wire:loading.remove>{{ __('booking.submit_request') }}</span>
|
||||
<span wire:loading>{{ __('common.submitting') }}</span>
|
||||
</flux:button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
```
|
||||
|
||||
### 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
|
||||
Reference in New Issue
Block a user