242 lines
7.1 KiB
Markdown
242 lines
7.1 KiB
Markdown
# Story 11.1: Database Schema & Model Updates
|
|
|
|
## Epic Reference
|
|
**Epic 11:** Guest Booking
|
|
|
|
## Story Context
|
|
This is the **foundational story** for Epic 11. All subsequent guest booking stories depend on these database and model changes being complete. This story prepares the data layer to support consultations without a linked user account.
|
|
|
|
## User Story
|
|
As a **developer**,
|
|
I want **to extend the consultation system to support guest bookings**,
|
|
So that **consultations can be created for both registered clients and anonymous guests**.
|
|
|
|
## Acceptance Criteria
|
|
|
|
### Database Migration
|
|
- [ ] `user_id` column changed to nullable on `consultations` table
|
|
- [ ] `guest_name` column added (varchar 255, nullable)
|
|
- [ ] `guest_email` column added (varchar 255, nullable)
|
|
- [ ] `guest_phone` column added (varchar 50, nullable)
|
|
- [ ] Index added on `guest_email` for 1-per-day lookup performance
|
|
- [ ] Migration is reversible without data loss
|
|
- [ ] Existing consultations with `user_id` unaffected
|
|
|
|
### Model Updates
|
|
- [ ] `Consultation` model updated with new fillable fields
|
|
- [ ] `isGuest()` helper method returns true when `user_id` is null
|
|
- [ ] `getClientName()` helper returns guest_name or user->name
|
|
- [ ] `getClientEmail()` helper returns guest_email or user->email
|
|
- [ ] `getClientPhone()` helper returns guest_phone or user->phone
|
|
- [ ] Model validation ensures either user_id OR guest fields are present
|
|
|
|
### Factory Updates
|
|
- [ ] `ConsultationFactory` updated with `guest()` state
|
|
- [ ] Guest state generates fake guest_name, guest_email, guest_phone
|
|
- [ ] Guest state sets user_id to null
|
|
|
|
## Implementation Steps
|
|
|
|
### Step 1: Create Migration
|
|
```bash
|
|
php artisan make:migration add_guest_fields_to_consultations_table
|
|
```
|
|
|
|
Migration content:
|
|
```php
|
|
public function up(): void
|
|
{
|
|
Schema::table('consultations', function (Blueprint $table) {
|
|
// Make user_id nullable
|
|
$table->foreignId('user_id')->nullable()->change();
|
|
|
|
// Add guest fields
|
|
$table->string('guest_name', 255)->nullable()->after('user_id');
|
|
$table->string('guest_email', 255)->nullable()->after('guest_name');
|
|
$table->string('guest_phone', 50)->nullable()->after('guest_email');
|
|
|
|
// Index for 1-per-day lookup
|
|
$table->index('guest_email');
|
|
});
|
|
}
|
|
|
|
public function down(): void
|
|
{
|
|
Schema::table('consultations', function (Blueprint $table) {
|
|
$table->dropIndex(['guest_email']);
|
|
$table->dropColumn(['guest_name', 'guest_email', 'guest_phone']);
|
|
// Note: Cannot safely restore NOT NULL if guest records exist
|
|
});
|
|
}
|
|
```
|
|
|
|
### Step 2: Update Consultation Model
|
|
Add to `app/Models/Consultation.php`:
|
|
|
|
```php
|
|
// Add to fillable array
|
|
protected $fillable = [
|
|
// ... existing fields
|
|
'guest_name',
|
|
'guest_email',
|
|
'guest_phone',
|
|
];
|
|
|
|
/**
|
|
* Check if this is a guest consultation (no linked user).
|
|
*/
|
|
public function isGuest(): bool
|
|
{
|
|
return is_null($this->user_id);
|
|
}
|
|
|
|
/**
|
|
* Get the client's display name.
|
|
*/
|
|
public function getClientName(): string
|
|
{
|
|
return $this->isGuest()
|
|
? $this->guest_name
|
|
: $this->user->name;
|
|
}
|
|
|
|
/**
|
|
* Get the client's email address.
|
|
*/
|
|
public function getClientEmail(): string
|
|
{
|
|
return $this->isGuest()
|
|
? $this->guest_email
|
|
: $this->user->email;
|
|
}
|
|
|
|
/**
|
|
* Get the client's phone number.
|
|
*/
|
|
public function getClientPhone(): ?string
|
|
{
|
|
return $this->isGuest()
|
|
? $this->guest_phone
|
|
: $this->user->phone;
|
|
}
|
|
```
|
|
|
|
### Step 3: Update Consultation Factory
|
|
Add to `database/factories/ConsultationFactory.php`:
|
|
|
|
```php
|
|
/**
|
|
* Create a guest consultation (no user account).
|
|
*/
|
|
public function guest(): static
|
|
{
|
|
return $this->state(fn (array $attributes) => [
|
|
'user_id' => null,
|
|
'guest_name' => fake()->name(),
|
|
'guest_email' => fake()->unique()->safeEmail(),
|
|
'guest_phone' => fake()->phoneNumber(),
|
|
]);
|
|
}
|
|
```
|
|
|
|
### Step 4: Add Model Validation (Optional Boot Method)
|
|
Consider adding validation in model boot to ensure data integrity:
|
|
|
|
```php
|
|
protected static function booted(): void
|
|
{
|
|
static::saving(function (Consultation $consultation) {
|
|
// Either user_id or guest fields must be present
|
|
if (is_null($consultation->user_id)) {
|
|
if (empty($consultation->guest_name) ||
|
|
empty($consultation->guest_email) ||
|
|
empty($consultation->guest_phone)) {
|
|
throw new \InvalidArgumentException(
|
|
'Guest consultations require guest_name, guest_email, and guest_phone'
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
```
|
|
|
|
## Technical Notes
|
|
|
|
### Foreign Key Consideration
|
|
The existing foreign key `consultations_user_id_foreign` cascades on delete. With nullable user_id:
|
|
- Guest consultations won't be affected by user deletions (they have no user)
|
|
- Client consultations still cascade delete when user is deleted
|
|
|
|
### Query Scopes
|
|
Consider adding scopes for filtering:
|
|
```php
|
|
public function scopeGuests(Builder $query): Builder
|
|
{
|
|
return $query->whereNull('user_id');
|
|
}
|
|
|
|
public function scopeClients(Builder $query): Builder
|
|
{
|
|
return $query->whereNotNull('user_id');
|
|
}
|
|
```
|
|
|
|
## Testing Requirements
|
|
|
|
### Unit Tests
|
|
```php
|
|
test('consultation can be created as guest', function () {
|
|
$consultation = Consultation::factory()->guest()->create();
|
|
|
|
expect($consultation->isGuest())->toBeTrue();
|
|
expect($consultation->user_id)->toBeNull();
|
|
expect($consultation->guest_name)->not->toBeNull();
|
|
expect($consultation->guest_email)->not->toBeNull();
|
|
expect($consultation->guest_phone)->not->toBeNull();
|
|
});
|
|
|
|
test('consultation can be created for client', function () {
|
|
$user = User::factory()->client()->create();
|
|
$consultation = Consultation::factory()->create(['user_id' => $user->id]);
|
|
|
|
expect($consultation->isGuest())->toBeFalse();
|
|
expect($consultation->user_id)->toBe($user->id);
|
|
});
|
|
|
|
test('getClientName returns guest name for guest consultation', function () {
|
|
$consultation = Consultation::factory()->guest()->create([
|
|
'guest_name' => 'John Doe',
|
|
]);
|
|
|
|
expect($consultation->getClientName())->toBe('John Doe');
|
|
});
|
|
|
|
test('getClientName returns user name for client consultation', function () {
|
|
$user = User::factory()->client()->create(['name' => 'Jane Smith']);
|
|
$consultation = Consultation::factory()->create(['user_id' => $user->id]);
|
|
|
|
expect($consultation->getClientName())->toBe('Jane Smith');
|
|
});
|
|
|
|
test('existing consultations are not affected by migration', function () {
|
|
// Verify existing consultations still work
|
|
$user = User::factory()->client()->create();
|
|
$consultation = Consultation::factory()->create(['user_id' => $user->id]);
|
|
|
|
expect($consultation->user)->toBeInstanceOf(User::class);
|
|
expect($consultation->isGuest())->toBeFalse();
|
|
});
|
|
```
|
|
|
|
## Dependencies
|
|
- None (foundational story)
|
|
|
|
## Definition of Done
|
|
- [ ] Migration created and runs successfully
|
|
- [ ] Migration is reversible
|
|
- [ ] Consultation model updated with guest fields and helpers
|
|
- [ ] Factory updated with guest state
|
|
- [ ] All existing consultation tests still pass
|
|
- [ ] New unit tests for guest functionality pass
|
|
- [ ] Code follows project patterns (Pint formatted)
|