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
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
- Your app computes bookable slots from availability minus blocked minus existing appointments
- User selects a slot
- You create an
APPOINTMENTschedule 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
Schedulemodels - 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.
Soft cancellation (recommended)
$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
Schedulewith typeAPPOINTMENT - 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