7.1 KiB
7.1 KiB
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_idcolumn changed to nullable onconsultationstableguest_namecolumn added (varchar 255, nullable)guest_emailcolumn added (varchar 255, nullable)guest_phonecolumn added (varchar 50, nullable)- Index added on
guest_emailfor 1-per-day lookup performance - Migration is reversible without data loss
- Existing consultations with
user_idunaffected
Model Updates
Consultationmodel updated with new fillable fieldsisGuest()helper method returns true whenuser_idis nullgetClientName()helper returns guest_name or user->namegetClientEmail()helper returns guest_email or user->emailgetClientPhone()helper returns guest_phone or user->phone- Model validation ensures either user_id OR guest fields are present
Factory Updates
ConsultationFactoryupdated withguest()state- Guest state generates fake guest_name, guest_email, guest_phone
- Guest state sets user_id to null
Implementation Steps
Step 1: Create Migration
php artisan make:migration add_guest_fields_to_consultations_table
Migration content:
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:
// 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:
/**
* 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:
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:
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
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)