https://laravel-zap.com/
laravel zap booking workflow

Definitions

Availability: Time periods in which the Entity is bookable
Blocked: Time periods in which the Entity is NOT bookable
Appointment: Time periods in which a User has booked the Entity.
Schedule:

Basics

There are 3 different things

  • Availability (defined by the Entity itself)
  • Blocked (defined by the Entity itself)
  • Appointment (defined by another user for the Entity)

Any Entity model that can be booked, should have trait

use Zap\Models\Concerns\HasSchedules;
 
class Doctor extends Model
{
    use HasSchedules;

Availability

  • The Entity should define the time-periods in which they can be booked.
  • Different ways to create Availability for any Entity

Fixed Weekly Working Hours

// 1️⃣ Define working hours
Zap::for($doctor)
    ->named('Office Hours')
    ->availability()
    ->forYear(2025)
    ->addPeriod('09:00', '12:00')
    ->addPeriod('14:00', '17:00')
    ->weekly(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'])
    ->save();

Single-Day Availability (Ad-hoc / Special Day)

Zap::for($doctor)
    ->availability()
    ->on('2025-03-15')
    ->addPeriod('09:00', '13:00')
    ->save();

Shift-Based Availability (Multiple Daily Shifts)

Zap::for($nurse)
    ->availability()
    ->named('Morning Shift')
    ->addPeriod('06:00', '14:00')
    ->daily()
    ->save();
 
Zap::for($nurse)
    ->availability()
    ->named('Evening Shift')
    ->addPeriod('14:00', '22:00')
    ->daily()
    ->save();
 

Block

  • There are time-period in which Booking/Appointment is not allowed

  • “Blocked schedules define time-periods in which booking / appointments are NOT allowed, even if availability exists.”
    ✅ Correct - Blocks take precedence over availability.

  • “Blocks always belong to an Entity”
    ✅ Correct - Just like availability, blocks are attached to specific schedulable entities.

  • “Blocks override availability”
    ✅ Correct - This is the most important principle: if there’s a conflict, block wins.

  • “Blocks are policies, not bookings”
    ✅ Correct - Blocks define “when you cannot book” as a rule, not “when someone has already booked.”

  • “Internally, blocks are evaluated using the same recurrence engine as availability”
    ✅ Correct - Same pattern engine, same time logic, just opposite intent.

Fixed Weekly Block (Recurring Breaks)

// 2️⃣ Block lunch break
Zap::for($doctor)
    ->named('Lunch Break')
    ->blocked()
    ->forYear(2025)
    ->addPeriod('12:00', '13:00')
    ->weekly(['monday', 'tuesday', 'wednesday', 'thursday', 'friday'])
    ->save();

Single-Day Block (One-Time Exception)

Zap::for($doctor)
    ->blocked()
    ->on('2025-03-20')
    ->addPeriod('09:00', '18:00')
    ->save();

Full-Day Block (No Periods)

Zap::for($clinic)
    ->named('Public Holiday')
    ->blocked()
    ->on('2025-08-15')
    ->save();

Date-Range Block (Multi-Day Leave)

Zap::for($doctor)
    ->named('Annual Leave')
    ->blocked()
    ->from('2025-06-01')
    ->to('2025-06-10')
    ->save();

Date-Range Block with Time Windows

Zap::for($doctor)
    ->blocked()
    ->from('2025-04-01')
    ->to('2025-04-05')
    ->addPeriod('15:00', '18:00')
    ->save();

Recurring Weekly Block (Specific Days)

Zap::for($doctor)
    ->named('Weekly Off')
    ->blocked()
    ->weekly(['sunday'])
    ->forYear(2025)
    ->save();

Bookable slots

  • Before creating Appointment, we need to see the available slots for any Entity.
  • This will give all possible slots after doing ( Availability - Blocked - Appointments)
  • Example (60 min slots, 15 min buffer)
$slots = $doctor->getBookableSlots('2025-01-15', 60, 15);
// Returns: [['start_time' => '09:00', 'end_time' => '10:00', 'is_available' => true, ...], ...]

Schedule

Schedule in laravel zap

Get all the appointments booked by a user

  • get the current user
use Zap\Models\Schedule;
 
$appointments = Schedule::query()
    ->appointments()
    ->where('metadata->patient_id', auth()->id())
    ->with(['schedulable', 'periods'])
    ->orderBy('start_date')
    ->get();
 

✅ This returns all appointments booked by the logged-in patient, across:

  • doctors
  • clinics
  • resources

Appointment

An Appointment in Laravel ZAP is a schedule of type APPOINTMENT.

It represents:

  • A confirmed booking
  • For a specific schedulable entity
  • On specific dates and times defined by schedule periods
  • With overlap rules enforced by ZAP

Appointments are state, not policy.

1. How a User Books an Appointment for an Entity

Conceptual flow

  1. Your app computes bookable slots from availability minus blocked minus existing appointments
  2. User selects a slot
  3. You create an APPOINTMENT schedule with one or more periods

Canonical example

Zap::for($doctor)
    ->appointment()
    ->named('Patient Appointment')
    ->on('2025-03-15')
    ->addPeriod('10:00', '10:30')
    ->withMetadata([
        'user_id' => $user->id,
    ])
    ->save();
// Book a 30-minute appointment
Zap::for($doctor)
    ->appointment()                 // Creates schedule_type = APPOINTMENT
    ->on('2025-03-20')             // Single occurrence date
    ->addPeriod('14:00', '14:30')  // Appointment time window
    ->withMetadata([               // Store user/reference data
        'patient_id' => $user->id,
        'patient_name' => $user->name,
        'status' => 'confirmed'
    ])
    ->save();

What ZAP enforces automatically:

  • Appointment does not overlap another appointment
  • Appointment does not overlap blocked schedules
  • Appointment is valid within its defined date scope

What your app controls:

  • Slot duration and selection
  • User permissions
  • Payment, confirmation, and business rules

2. How to Get All Booked Appointments for an Entity

Appointments are schedules attached to the entity.

$appointments = $doctor->schedules()
    ->appointments()
    ->get();
// Get all appointments for a doctor
$appointments = $doctor->schedules()
    ->appointments()           // Scope: ->ofType(ScheduleTypes::APPOINTMENT)
    ->with('periods')          // Include time periods
    ->active()                 // Only active schedules (is_active = true)
    ->get();
 
// For chronological ordering by appointment time (not schedule start_date)
$orderedAppointments = $doctor->schedules()
    ->appointments()
    ->join('schedule_periods', 'schedules.id', '=', 'schedule_periods.schedule_id')
    ->orderBy('schedule_periods.date')
    ->orderBy('schedule_periods.start_time')
    ->get(['schedules.*', 'schedule_periods.date', 'schedule_periods.start_time']);

Notes:

  • This returns Schedule models
  • Load periods if you need time information
$appointments->load('periods');

3. How to Get All Booked Appointments for a User

ZAP does not model users as appointment owners.

The recommended approach is to store user identity in schedule metadata.

use Zap\Models\Schedule;
 
$appointments = Schedule::appointments()
    ->where('metadata->user_id', $user->id)
    ->get();
// Query appointments for a specific user
$userAppointments = Schedule::appointments()
    ->where('metadata->patient_id', $user->id)
    ->with(['schedulable', 'periods'])  // Include entity and time periods
    ->get();
 
// DO NOT use this (doesn't work with JSON paths):
// ❌ return $this->hasMany(Schedule::class, 'metadata->patient_id');
 
// CORRECT: Create a query method in your User model
class User extends Model
{
    public function appointments()
    {
        return Schedule::appointments()
            ->where('metadata->patient_id', $this->id);
    }
}

Why this is intentional:

  • ZAP remains entity-centric
  • Ownership rules vary by domain
  • Metadata keeps scheduling flexible

4. How to Get All Appointments for a Day Across All Entities (Admin)

Dates and times live in schedule_periods, not in schedules.

Correct admin query:

use Zap\Models\SchedulePeriod;
 
$appointments = SchedulePeriod::whereHas('schedule', function ($query) {
        $query->appointments();
    })
    ->forDate('2025-03-15')
    ->get();
use Zap\Models\SchedulePeriod;
 
// Get all appointments for a specific day across all entities
$daysAppointments = SchedulePeriod::whereHas('schedule', function($query) {
        $query->appointments()->active();
    })
    ->whereDate('date', '2025-03-20')
    ->with('schedule.schedulable')  // Include the entity (Doctor, Room, etc.)
    ->orderBy('start_time')
    ->get();
 
// Group by entity type for admin dashboard
$grouped = $daysAppointments->groupBy(function($period) {
    return $period->schedule->schedulable_type;
});

Notes:

  • This returns actual booked time slices
  • Ideal for dashboards, reports, and load analysis
  • You can eager-load the schedulable entity if needed

5. How an Appointment Is Cancelled by the Entity

ZAP has no explicit “cancel” concept.

Cancellation means deactivating or deleting the schedule.

$appointment->update([
    'is_active' => false,
]);
use Zap\Models\Schedule;
 
$appointment = Schedule::appointments()
    ->active()
    ->findOrFail($appointmentId);
 
// Verify the appointment belongs to this entity
if (! $appointment->schedulable->is($doctor)) {
    abort(403, 'Unauthorized cancellation');
}
 
$appointment->update([
    'is_active' => false,
    'metadata' => array_merge($appointment->metadata ?? [], [
        'status' => 'cancelled',
        'cancelled_by' => 'entity',
        'cancelled_at' => now()->toISOString(),
    ]),
]);
 

Why this works:

  • isActiveOn() will return false
  • The appointment no longer blocks time
  • History is preserved

Hard cancellation

$appointment->delete();

This removes the schedule and all its periods.


6. How an Appointment Is Cancelled by the User

ZAP does not distinguish who cancels.

Your application enforces cancellation rules.

Example:

$period = $appointment->periods->first();
 
if (now()->diffInHours($period->start_date_time) < 24) {
    throw new CancellationNotAllowed();
}
 
$appointment->update([
    'is_active' => false,
]);
use Zap\Models\Schedule;
 
$appointment = Schedule::appointments()
    ->active()
    ->findOrFail($appointmentId);
 
// Verify ownership
if (($appointment->metadata['patient_id'] ?? null) !== $user->id) {
    abort(403, 'You are not allowed to cancel this appointment');
}
 
// Optional business rule: prevent late cancellation
$period = $appointment->periods->first();
 
if ($period && now()->greaterThanOrEqualTo($period->start_date_time)) {
    abort(422, 'Appointment has already started');
}
 
// Cancel appointment
$appointment->update([
    'is_active' => false,
    'metadata' => array_merge($appointment->metadata ?? [], [
        'status' => 'cancelled',
        'cancelled_by' => 'patient',
        'cancelled_at' => now()->toISOString(),
    ]),
]);
 

Once cancelled:

  • The appointment stops blocking time
  • Availability is restored automatically

Key Rules to Remember

  • An appointment is a Schedule with type APPOINTMENT
  • Actual time lives in SchedulePeriod
  • ZAP enforces overlap safety, not business policy
  • Ownership and lifecycle are application concerns
  • Deactivating an appointment frees the slot

Mental Summary

  • Availability defines possible time
  • Blocked removes time
  • Appointment consumes time
  • Cancelling deactivates consumption
  • ZAP guarantees temporal correctness

Configuration

Configuration laravel zap