Skip to main content

Appointment Rule Engine - Mobile Implementation Guide

Last Updated: 2026-05-09
Target Platforms: iOS (Swift), Android (Kotlin)
Purpose: Complete logic reference for implementing appointment validation rules in native mobile apps

Table of Contents

  1. Core Concepts
  2. Data Structures
  3. Status State Machine
  4. Rule Categories
  5. Role-Based Permissions
  6. Implementation Checklist

Core Concepts

The appointment rule engine validates three main operations:
  • Creation — Can a new appointment be scheduled?
  • Update — Can an existing appointment be modified?
  • Status Change — Can an appointment move to a new status?
Each operation returns a RuleResult:
  • allowed: Boolean — Whether the operation is permitted
  • severity: String"blocking" (HTTP 409) or "warning" (HTTP 400)
  • code: String — Machine-readable error code (e.g., "DOCTOR_OFF", "CONFLICT")
  • message: String — Human-readable explanation
  • conflicts: Array — Details of detected conflicts (if any)
  • overrideAllowedForRoles: Array<String> — Roles that can override this rule (rarely used)

Data Structures

RuleResult

struct RuleResult {
  allowed: Boolean
  severity: "blocking" | "warning"
  code: String
  message: String
  conflicts: Array<ConflictDetail>?
  overrideAllowedForRoles: Array<String>?
}

struct ConflictDetail {
  type: "doctor" | "room" | "patient_duplicate"
  appointmentId: String
  startTime: String (ISO 8601)
  endTime: String (ISO 8601)
  patientName: String?
}

AppointmentContext

struct AppointmentContext {
  clinicId: String
  doctorId: String?
  patientId: String?
  appointmentDate: String (YYYY-MM-DD)
  appointmentTime: String (HH:MM, 24-hour)
  durationMinutes: Integer
  operatory: String?
  roomId: String?
  requestingRole: String
  existingAppointmentId: String? (for updates; used to exclude from conflict detection)
  bufferMinutes: Integer? (default: from clinic settings)
}

DoctorSchedule

struct DoctorSchedule {
  doctorId: String
  dayOfWeek: String ("monday" | "tuesday" | ... | "sunday")
  startTime: String (HH:MM)
  endTime: String (HH:MM)
  isOff: Boolean (true = doctor off all day)
}

ClinicOperatingHours

struct ClinicOperatingHours {
  dayOfWeek: String ("monday" | "tuesday" | ... | "sunday")
  startTime: String (HH:MM)
  endTime: String (HH:MM)
  isClosed: Boolean (true = clinic closed all day)
}

Status State Machine

Valid Transitions

STATES:
  - requested
  - scheduled
  - confirmed
  - in_progress
  - completed
  - cancelled
  - no_show

TRANSITION RULES:
  from: "requested"   → to: ["scheduled", "confirmed", "cancelled", "completed"]
  from: "scheduled"   → to: ["confirmed", "cancelled", "in_progress", "completed"]
  from: "confirmed"   → to: ["in_progress", "cancelled", "no_show", "scheduled", "completed"]
  from: "in_progress" → to: ["completed", "cancelled", "no_show", "confirmed"]
  from: "completed"   → to: ["in_progress", "confirmed"]
  from: "cancelled"   → to: ["scheduled", "no_show", "confirmed"]
  from: "no_show"     → to: ["scheduled", "cancelled", "confirmed"]

NOTE: Backward transitions are allowed for data correction (e.g., marking a wrong no_show as confirmed)

State Machine Functions

function isValidTransition(currentStatus: String, newStatus: String) → Boolean {
  // Check if newStatus exists in the allowed transitions for currentStatus
}

function isFinalState(status: String) → Boolean {
  // Returns true if status is "completed" or "cancelled"
}

Rule Categories

1. APPOINTMENT CREATION VALIDATION

Applies to: POST /appointments
Required context: All fields in AppointmentContext

Rule 1.1: No Past Appointments

Condition: appointmentDate + appointmentTime < current_datetime
Result: 
  allowed: false
  code: "PAST_DATE"
  message: "Cannot schedule appointments in the past"
  severity: "blocking"

Rule 1.2: Clinic Must Be Open

Condition: 
  - Clinic is marked closed for appointmentDate
  OR
  - appointmentTime is outside clinic's operating hours for that day
Result:
  allowed: false
  code: "CLINIC_CLOSED" | "OUTSIDE_CLINIC_HOURS"
  message: "Clinic is not open at that time"
  severity: "blocking"

Rule 1.3: Doctor Must Not Be Off

Condition:
  - doctorId provided
  - Doctor's schedule for appointmentDate has isOff = true
Result:
  allowed: false
  code: "DOCTOR_OFF"
  message: "Doctor is not available on {appointmentDate}"
  severity: "blocking"

Rule 1.4: Doctor Working Hours

Condition:
  - doctorId provided
  - appointmentTime falls outside doctor's schedule for appointmentDate
Result:
  allowed: false
  code: "OUTSIDE_DOCTOR_HOURS"
  message: "Doctor is not working at {appointmentTime}"
  severity: "blocking"

LOGIC:
  Get doctor's schedule for day_of_week(appointmentDate)
  If no schedule found: doctor available all clinic hours
  If schedule found:
    Appointment must fall within [startTime, endTime]
    Appointment start + duration must not exceed endTime

Rule 1.5: Patient Duplicate Detection

Condition:
  - Query appointments table for:
    * patientId = context.patientId
    * appointmentDate = context.appointmentDate
    * status != "cancelled"
Result (if found):
  allowed: false
  code: "PATIENT_DUPLICATE"
  message: "Patient already has an appointment on {appointmentDate}"
  severity: "blocking"

Rule 1.6: Doctor/Room Time Conflicts

Condition:
  - Query appointments table for overlapping time slots with buffer
  
LOGIC:
  effectiveStart = appointmentTime - bufferMinutes (in minutes)
  effectiveEnd = appointmentTime + durationMinutes + bufferMinutes
  
  Find appointments where:
    * appointmentDate = context.appointmentDate
    * status != "cancelled"
    * doctorId = context.doctorId (if specified)
    * operatory = context.operatory (if specified)
    * appointment.startTime < effectiveEnd
    * appointment.endTime > effectiveStart
  
  If any appointments found:
    Result:
      allowed: false
      code: "CONFLICT"
      message: "Doctor/Room is already booked at that time"
      severity: "blocking"
      conflicts: [array of ConflictDetail objects]
  
  Include ALL overlapping appointments in conflicts array

Rule 1.7: Final Result

If ANY rule fails → return that rule's result
If ALL rules pass → 
  Result:
    allowed: true
    code: "OK"
    message: "Appointment can be created"

2. APPOINTMENT UPDATE VALIDATION

Applies to: PATCH /appointments/:id
Required context: All fields in AppointmentContext, plus existingAppointmentId
Logic: Same as Creation Validation (Rule 1), except:
  • When checking for conflicts (Rule 1.6), exclude existingAppointmentId from the conflict query
  • This prevents the appointment from conflicting with itself

3. APPOINTMENT CANCELLATION RULES

Applies to: Cancelling an appointment (changing status to “cancelled”)

Rule 3.1: Patient Ownership

Condition:
  - requestingRole = "patient"
  - patientId in request != appointment.patientId
Result:
  allowed: false
  code: "NOT_OWN_APPOINTMENT"
  message: "Patients can only cancel their own appointments"
  severity: "blocking"

Rule 3.2: Cancellation Window (Patient Only)

Condition:
  - requestingRole = "patient"
  - appointmentDateTime < NOW + 48 hours

LOGIC:
  if (appointmentDateTime - NOW) < 48 hours {
    // Cancellation not allowed
  }

Result (if window violated):
  allowed: false
  code: "CANCELLATION_WINDOW"
  message: "Cancellations must be made 48+ hours in advance"
  severity: "blocking"

Rule 3.3: Staff Can Always Cancel

Condition:
  - requestingRole in ["superadmin", "admin", "receptionist", "doctor"]
  - No time restrictions apply
Result:
  allowed: true
  code: "OK"

4. STATUS CHANGE VALIDATION

Applies to: PATCH /appointments/:id/status

Rule 4.1: Valid State Transition

Condition:
  - Use Status State Machine (above) to check if transition is allowed
Result (if invalid):
  allowed: false
  code: "INVALID_TRANSITION"
  message: "Cannot change from {currentStatus} to {newStatus}"
  severity: "blocking"

Rule 4.2: Time-Based Restrictions

Condition:
  - newStatus in ["in_progress", "completed", "no_show"]
  - appointmentDateTime > NOW (in the future)
Result:
  allowed: false
  code: "FUTURE_APPOINTMENT"
  message: "Cannot mark {newStatus} before appointment time"
  severity: "blocking"

LOGIC:
  Cannot transition to these statuses until the appointment time has arrived

Rule 4.3: Past Appointment Cancellation Restriction

Condition:
  - currentStatus = "scheduled" | "confirmed" | "in_progress"
  - newStatus = "cancelled"
  - appointmentDateTime < NOW (appointment is in the past)
Result:
  allowed: false
  code: "PAST_APPOINTMENT_CANCEL"
  message: "Cannot cancel past appointments; mark as no_show instead"
  severity: "blocking"

LOGIC:
  For appointments that have already occurred, use "no_show" instead of "cancelled"

Rule 4.4: Role-Based Permission Matrix

Condition:
  - Check if requestingRole has permission to set newStatus

PERMISSION MATRIX:
  superadmin:   ALL statuses ["requested", "scheduled", "confirmed", "in_progress", "completed", "cancelled", "no_show"]
  admin:        ALL statuses ["requested", "scheduled", "confirmed", "in_progress", "completed", "cancelled", "no_show"]
  receptionist: ["scheduled", "confirmed", "cancelled", "no_show", "completed"]
  doctor:       ["confirmed", "in_progress", "completed", "no_show"]
  patient:      ["cancelled"]

Result (if role not permitted):
  allowed: false
  code: "ROLE_PERMISSION"
  message: "{requestingRole} cannot set status to {newStatus}"
  severity: "blocking"

Rule 4.5: Final Result

Check rules in order: 4.1 → 4.2 → 4.3 → 4.4

If ANY rule fails → return that rule's result
If ALL rules pass →
  Result:
    allowed: true
    code: "OK"
    message: "Status can be changed"

5. AVAILABLE SLOTS CALCULATION

Applies to: GET /appointments/available-slots
Parameters: clinicId, date (YYYY-MM-DD), doctorId (optional)
Returns: Array of available time slots (30-minute intervals)

Algorithm

function calculateAvailableSlots(clinicId, date, doctorId?) {
  // Step 1: Get clinic operating hours for day_of_week(date)
  clinicHours = getClinicOperatingHours(clinicId, dayOfWeek)
  if (clinicHours.isClosed) {
    return {
      slots: [],
      clinicClosed: true,
      doctorOff: false,
      clinicPhone: clinic.phone
    }
  }
  
  // Step 2: Resolve time range
  startHour = clinicHours.startTime
  endHour = clinicHours.endTime
  
  // Step 3: If doctorId provided, intersect with doctor's hours
  if (doctorId) {
    doctorSchedule = getDoctorSchedule(doctorId, dayOfWeek)
    if (doctorSchedule.isOff) {
      return {
        slots: [],
        clinicClosed: false,
        doctorOff: true,
        clinicPhone: null
      }
    }
    // Intersect clinic and doctor hours
    startHour = max(clinicHours.startTime, doctorSchedule.startTime)
    endHour = min(clinicHours.endTime, doctorSchedule.endTime)
  }
  
  // Step 4: Generate 30-minute slots
  slots = []
  current = startHour
  while (current + 30min <= endHour) {
    slots.push(current)
    current += 30 minutes
  }
  
  // Step 5: Filter out booked slots
  bookedAppointments = query appointments where:
    * appointmentDate = date
    * status != "cancelled"
    * if doctorId provided: doctorId = context.doctorId
  
  for each bookedAppointment {
    remove from slots: [bookedAppointment.startTime, bookedAppointment.startTime + 30min)
  }
  
  // Step 6: Remove past slots if date = TODAY
  if (date == TODAY) {
    now = current time
    slots = filter(slots where slot time > now)
  }
  
  return {
    slots: slots,
    clinicClosed: false,
    doctorOff: false,
    clinicPhone: null
  }
}

6. APPOINTMENT COMPLETION (BILLING CHECK)

Applies to: Changing status to “completed”
Rule: Cannot mark completed without invoice
Condition:
  - newStatus = "completed"
  - No invoice record exists for this appointment
  - No invoice included in current request
Result:
  allowed: false
  code: "INVOICE_REQUIRED"
  message: "Appointment must have an invoice before completion"
  severity: "blocking"

LOGIC:
  If completing an appointment:
    1. Check if invoice already exists in database
    2. Check if invoice is being created in this request
    If neither: return error

7. EDIT ACCESS CONTROL

Applies to: Any direct update to appointment details (doctor, time, date, etc.)
Condition:
  - requestingRole = "patient"
  - attempting to modify appointment fields (not just status)
Result:
  allowed: false
  code: "PATIENT_WRITE"
  message: "Patients cannot modify appointment details; use cancellation to reschedule"
  severity: "blocking"

NOTE: Patients CAN:
  - Cancel their own appointments
  - View their own appointments
  
Patients CANNOT:
  - Change appointment time/date/doctor/operatory
  - Create appointments for others
  - Delete appointment records

Role-Based Permissions

Status Change Permissions

RoleCan Set To
superadminrequested, scheduled, confirmed, in_progress, completed, cancelled, no_show
adminrequested, scheduled, confirmed, in_progress, completed, cancelled, no_show
receptionistscheduled, confirmed, cancelled, no_show, completed
doctorconfirmed, in_progress, completed, no_show
patientcancelled (only own appointments, 48h window)

Appointment Operations Permissions

RoleCreateUpdateCancelComplete
superadmin✅ (no restrictions)✅ (with invoice)
admin✅ (no restrictions)✅ (with invoice)
receptionist✅ (no restrictions)✅ (with invoice)
doctor✅ (no restrictions)✅ (with invoice)
patient✅ (own, 48h)

Implementation Checklist

Phase 1: Core Data Structures

  • Define RuleResult struct/class
  • Define ConflictDetail struct/class
  • Define AppointmentContext struct/class
  • Define DoctorSchedule struct/class
  • Define ClinicOperatingHours struct/class

Phase 2: State Machine

  • Implement isValidTransition(from, to) function
  • Implement isFinalState(status) function
  • Add comprehensive unit tests for all 7 states

Phase 3: Creation Rules

  • Rule 1.1: Past date blocking
  • Rule 1.2: Clinic hours validation
  • Rule 1.3: Doctor off detection
  • Rule 1.4: Doctor working hours
  • Rule 1.5: Patient duplicate detection
  • Rule 1.6: Time conflict detection (with buffer)
  • Combine all into canCreateAppointment() function
  • Unit tests: 20+ test cases

Phase 4: Update & Cancellation

  • Rule 2: Update validation (wraps creation with exclusion)
  • Rule 3.1-3.3: Cancellation rules
  • Implement canCancelAppointment() function
  • Unit tests: 15+ test cases

Phase 5: Status Change

  • Rule 4.1: State transition validation
  • Rule 4.2: Time-based restrictions
  • Rule 4.3: Past appointment cancellation
  • Rule 4.4: Role-based permissions
  • Implement canChangeStatus() function
  • Unit tests: 25+ test cases

Phase 6: Supporting Features

  • Rule 5: Available slots calculation
  • Rule 6: Completion invoice requirement
  • Rule 7: Patient edit access control
  • Unit tests: 30+ test cases

Phase 7: Integration

  • Wire rules into appointment creation flow
  • Wire rules into appointment update flow
  • Wire rules into appointment status change flow
  • Wire rules into available slots endpoint
  • Integration tests with sample data

Phase 8: Testing & Documentation

  • Minimum 80% code coverage
  • Document any platform-specific deviations (iOS/Android)
  • Add inline code comments for complex logic
  • Create example usage in both Swift and Kotlin

Common Implementation Pitfalls

  1. Buffer Time Confusion
    • Buffer is added BEFORE and AFTER appointment time
    • Example: 30-min appointment at 14:00 with 15-min buffer = occupies 13:45-14:45
  2. Daylight Saving Time
    • Always use date-only (YYYY-MM-DD) for schedules
    • Never use absolute timestamps for day-of-week checks
    • Store all times in clinic’s local timezone
  3. Conflict Detection with Self
    • When updating, exclude the existing appointment from conflict check
    • This allows moving an appointment to a different time without conflicting with itself
  4. Patient Cancellation Window
    • 48 hours must be calculated in the clinic’s timezone
    • Check NOW + 48 hours > appointmentDateTime (not less than)
  5. Status Transition Order
    • Always check transition validity BEFORE checking time restrictions
    • Always check time restrictions BEFORE checking role permissions
  6. Role Hierarchy
    • superadmin/admin have same permissions (both unrestricted)
    • Receptionist < Doctor (doctor cannot create/update)
    • Patient has minimal access (cancel own, view own)

Version History

DateChanges
2026-05-09Initial documentation for mobile implementation

Questions or Feedback?

For clarifications on rule logic or iOS/Android implementation details, refer to:
  • Server source: /server/src/lib/rules/
  • Type definitions: /server/src/lib/rules/types.ts
  • Full implementation: /server/src/lib/rules/scheduling/