libra/docs/stories/story-11.1-database-schema-...

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)