Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/0Crazy-0/ClinicFlow/llms.txt

Use this file to discover all available pages before exploring further.

Appointments in ClinicFlow follow a strict, domain-enforced lifecycle. Every state transition is guarded by business rules inside the Appointment entity and its supporting domain services, ensuring that no appointment can skip a step, be cancelled when already cancelled, or be started by the wrong doctor. Understanding these transitions is essential for building any integration or front-end workflow on top of the platform.

Appointment Statuses

The AppointmentStatus enum defines every valid state an appointment can occupy:
ValueIntegerMeaning
Scheduled1Created and waiting for the patient to arrive
CheckedIn8Patient has arrived at the clinic
InProgress3Doctor has started the consultation
Completed4Consultation finished successfully
Cancelled5Cancelled within the allowed window — no penalty
LateCancellation7Cancelled after the window — penalty applied
NoShow6Patient did not arrive
RequiresReassignment9Original doctor suspended; awaiting a new doctor

Normal Appointment Flow

1

Schedule

A patient, doctor, or staff member creates the appointment. The appointment enters the Scheduled state and the AppointmentScheduledEvent domain event fires. RescheduleCount is initialised to 0.
2

Check In

A staff member calls CheckInAppointmentByStaff. The appointment moves to CheckedIn. Optional receptionist notes may be added at this point and stored in ReceptionistNotes. CheckedInAt is recorded.
3

Start

The assigned doctor calls StartAppointmentByDoctor. The domain enforces that the initiating doctor’s Id matches appointment.DoctorId — no other doctor can start the consultation. Status becomes InProgress.
4

Complete

The doctor calls CompleteAppointment. Status moves to Completed. The appointment is now immutable for status purposes.

Scheduling Actors

Three actor roles can create appointments, each with different constraints enforced by AppointmentSchedulingService.

Patient

Uses ScheduleByPatient. The initiator’s phone must be verified, the target patient’s profile must be complete (blood type + emergency contact), and active penalties must not block the requested date. Overbooking is not permitted — the requested time range must fall within the doctor’s active schedule slot for that day.

Doctor

Uses ScheduleByDoctor. The scheduling doctor must be the same doctor as InitiatorDoctor. Doctors can set IsOverbook = true to bypass slot availability and conflict checks. Guardian consent must be verified explicitly rather than derived from the patient’s relationship.

Staff

Uses ScheduleByStaff. Similar to doctor scheduling but the target doctor is specified by DoctorId. Staff can also overbook (IsOverbook = true). The target patient’s profile must be complete. No penalty checks are run for staff-initiated scheduling.

All actors

All scheduling paths go through the same Appointment.Schedule(...) factory. All require a valid SchedulingClearance issued by the regional scheduling service, and all validate patient age eligibility against the appointment type’s policy.

ScheduleByPatientCommand

The patient-facing scheduling command is the most constrained of the three:
public sealed record ScheduleByPatientCommand(
    Guid InitiatorUserId,     // The user account triggering the request
    Guid TargetPatientId,     // The patient the appointment is for (may be a family member)
    Guid DoctorId,            // The doctor to book
    Guid AppointmentTypeId,   // The type of appointment (consultation, procedure, etc.)
    DateOnly ScheduledDate,   // Must be today or in the future
    TimeOnly StartTime,       // Must be before EndTime
    TimeOnly EndTime,         // Must be after StartTime
    string? PatientNotes = null
) : IRequest<Guid>;
InitiatorUserId identifies the user making the request; TargetPatientId identifies the patient being booked (these differ when a parent books for a child). The handler resolves both patient profiles, verifies their relationship via PatientAccessService.EnsureCanActOnBehalfOf, and then delegates to AppointmentSchedulingService.ScheduleByPatient.

ScheduleByDoctorCommand

public sealed record ScheduleByDoctorCommand(
    Guid InitiatorUserId,
    Guid TargetPatientId,
    Guid AppointmentTypeId,
    DateOnly ScheduledDate,
    TimeOnly StartTime,
    TimeOnly EndTime,
    bool IsOverbook,
    bool HasGuardianConsentVerified
) : IRequest<Guid>;
The handler resolves the initiating doctor via GetByUserIdAsync(InitiatorUserId). Setting IsOverbook = true bypasses the slot-availability check entirely.

ScheduleByStaffCommand

public sealed record ScheduleByStaffCommand(
    Guid InitiatorUserId,
    Guid TargetPatientId,
    Guid DoctorId,
    Guid AppointmentTypeId,
    DateOnly ScheduledDate,
    TimeOnly StartTime,
    TimeOnly EndTime,
    bool HasGuardianConsentVerified,
    bool IsOverbook
) : IRequest<Guid>;
Unlike the doctor command, the target doctor is specified explicitly by DoctorId rather than derived from the initiator.

Patient Notes vs Receptionist Notes

Appointments carry two separate free-text fields for contextual information.
FieldUpdated byStatus constraint
PatientNotesPatient (via UpdatePatientNotesByPatient) or Staff (via UpdatePatientNotesByStaff)Only when status is Scheduled or RequiresReassignment
ReceptionistNotesStaff only (via UpdateReceptionistNotesByStaff)Only when status is CheckedIn
PatientNotes captures pre-visit information supplied by the patient or entered by a receptionist on their behalf. ReceptionistNotes captures observations made at check-in and is locked to the CheckedIn state to prevent post-visit modifications.

Check In

CheckInAppointmentByStaff is the only command that moves an appointment from Scheduled to CheckedIn:
public sealed record CheckInAppointmentByStaffCommand(
    Guid AppointmentId,
    string? ReceptionistNotes = null
) : IRequest;
The appointment must be in Scheduled status. Attempting to check in an appointment that is CheckedIn, InProgress, or in any terminal state throws a domain validation exception.

No-Show

When a patient fails to appear for a Scheduled appointment, staff or the assigned doctor can mark it as NoShow.
  • MarkAsNoShowByStaff — any staff member can call this; no actor identity check beyond the role.
  • MarkAsNoShowByDoctor — the system verifies that the initiating doctor’s Id matches appointment.DoctorId. A doctor who is not the appointment owner receives an AppointmentNoShowUnauthorizedException.
Both methods delegate to the same private MarkAsNoShow() and raise AppointmentMarkedAsNoShowEvent. The appointment must be in Scheduled status for both paths.
public sealed record MarkAppointmentAsNoShowByStaffCommand(Guid AppointmentId) : IRequest;

public sealed record MarkAppointmentAsNoShowByDoctorCommand(
    Guid AppointmentId,
    Guid InitiatorUserId
) : IRequest;

Cancellation

ClinicFlow distinguishes three cancellation paths, each producing a different domain event and penalty outcome.

Normal Cancellation — Cancel

Triggered when the cancellation is within the specialty’s allowed window. Status becomes Cancelled. AppointmentCancelledEvent fires. No patient penalty is applied.
public sealed record CancelAppointmentByPatientCommand(
    Guid AppointmentId,
    Guid InitiatorUserId,
    string? Reason
) : IRequest;
The handler uses AppointmentCancellationService.CancelByPatient, which checks the specialty’s IsCancellationAllowed rule at runtime. If the window has not passed, it calls appointment.Cancel(...); otherwise it automatically calls appointment.CancelLate(...).

Late Cancellation — CancelLate

When the patient cancels after the specialty-defined cancellation window, CancelLate is invoked automatically. Status becomes LateCancellation and AppointmentLateCancelledEvent fires, which downstream services can use to issue a penalty.
Procedure-type appointments cannot be cancelled by patients. Any attempt throws AppointmentCancellationUnauthorizedException. Emergency appointments can only be cancelled by the patient themselves, or by a parent if the patient is a child under 18.

System Timeout Cancellation — CancelDueToSystemTimeout

System timeout cancellations carry no patient penalty. The appointment entered RequiresReassignment because of a clinic-side event (doctor suspension), and it is the clinic’s responsibility to reassign it in time. If the scheduled date passes without reassignment, CleanExpiredDisplacedAppointments cancels the appointment and fires AppointmentSystemCancelledEvent. The CancellationReason is set to the constant "System timeout: Displaced appointment was not reassigned." and CancelledByUserId is null.
CancelDueToSystemTimeout can only be called on appointments in RequiresReassignment status. Calling it on any other status throws a domain validation exception.

Rescheduling

Any actor (patient, doctor, or staff) can reschedule a Scheduled appointment by providing a new date and time range. AppointmentReschedulingService enforces the same availability and clearance rules as scheduling.
Reschedule limit: 1. The RescheduleCount property tracks how many times an appointment has been rescheduled. Once RescheduleCount >= 1, calling Reschedule() throws AppointmentReschedulingNotAllowedException. This limit applies regardless of the actor — patients, doctors, and staff are all subject to it. If an appointment needs to change a second time it must be cancelled and a new appointment created.
Patient rescheduling also re-validates penalty history against the new date, and the patient’s phone must remain verified. Optional NewPatientNotes may be supplied to update PatientNotes in the same transaction.
public sealed record RescheduleByPatientCommand(
    Guid InitiatorUserId,
    Guid AppointmentId,
    DateOnly NewDate,
    TimeOnly NewStartTime,
    TimeOnly NewEndTime,
    string? NewPatientNotes = null
) : IRequest;

public sealed record RescheduleByDoctorCommand(
    Guid InitiatorUserId,
    Guid AppointmentId,
    DateOnly NewDate,
    TimeOnly NewStartTime,
    TimeOnly NewEndTime,
    bool IsOverbook
) : IRequest;

public sealed record RescheduleByStaffCommand(
    Guid InitiatorUserId,
    Guid AppointmentId,
    DateOnly NewDate,
    TimeOnly NewStartTime,
    TimeOnly NewEndTime,
    bool IsOverbook
) : IRequest;

RequiresReassignment & Doctor Reassignment

When a doctor is suspended, all their future Scheduled appointments are marked RequiresReassignment via appointment.MarkAsRequiresReassignment(). The appointment is placed in a holding queue — the patient is not penalised and the appointment remains active. Staff must resolve each displaced appointment using ReassignAppointment:
public sealed record ReassignAppointmentCommand(
    Guid AppointmentId,
    Guid NewDoctorId,
    DateOnly NewDate,
    TimeOnly NewStartTime,
    TimeOnly NewEndTime
) : IRequest;
AppointmentReassignmentService.Reassign validates that the new doctor’s schedule covers the requested time range, then calls appointment.Reassign(...) which resets DoctorId, ScheduledDate, and TimeRange and sets the status back to Scheduled. AppointmentReassignedEvent fires to notify the patient.

CleanExpiredDisplacedAppointments

CleanExpiredDisplacedAppointmentsCommand is a parameterless command intended to be dispatched by a background job (e.g., a Hangfire recurring task or an IHostedService timer).
public sealed record CleanExpiredDisplacedAppointmentsCommand : IRequest;
The handler calls IAppointmentRepository.GetExpiredDisplacedAppointmentsAsync(now), which returns all appointments in RequiresReassignment status whose ScheduledDate and TimeRange.Start have already passed. Each is cancelled via CancelDueToSystemTimeout in a single SaveChangesAsync call.

AppointmentDto

Queries that return appointment data use AppointmentDto:
public sealed record AppointmentDto(
    Guid Id,
    Guid PatientId,
    Guid DoctorId,
    Guid AppointmentTypeId,
    DateOnly ScheduledDate,
    TimeOnly StartTime,
    TimeOnly EndTime,
    AppointmentStatus Status,
    string PatientNotes,
    string ReceptionistNotes
);
Cancellation metadata (CancelledAt, CancellationReason, CancelledByUserId) and CheckedInAt are intentionally omitted from the DTO — they are available by fetching the full entity through staff-facing endpoints.

Command Reference

CommandActorEffect
ScheduleByPatientCommandPatientCreates appointment in Scheduled
ScheduleByDoctorCommandDoctorCreates appointment in Scheduled; can overbook
ScheduleByStaffCommandStaffCreates appointment in Scheduled; can overbook
CheckInAppointmentByStaffCommandStaffScheduledCheckedIn
StartAppointmentByDoctorCommandAssigned doctorCheckedInInProgress
CompleteAppointmentCommandDoctorInProgressCompleted
MarkAppointmentAsNoShowByStaffCommandStaffScheduledNoShow
MarkAppointmentAsNoShowByDoctorCommandAssigned doctorScheduledNoShow
CancelAppointmentByPatientCommandPatientScheduledCancelled or LateCancellation
CancelAppointmentByDoctorCommandAssigned doctorScheduledCancelled
CancelAppointmentByStaffCommandStaffScheduledCancelled (reason required)
RescheduleByPatientCommandPatientUpdates date/time (max 1 reschedule)
RescheduleByDoctorCommandAssigned doctorUpdates date/time; can overbook
RescheduleByStaffCommandStaffUpdates date/time; can overbook
ReassignAppointmentCommandStaffRequiresReassignmentScheduled with new doctor
CleanExpiredDisplacedAppointmentsCommandSystem/BackgroundRequiresReassignmentCancelled (no penalty)
UpdatePatientNotesByPatientCommandPatientUpdates PatientNotes
UpdatePatientNotesByStaffCommandStaffUpdates PatientNotes
UpdateReceptionistNotesByStaffCommandStaffUpdates ReceptionistNotes (only when CheckedIn)

Build docs developers (and LLMs) love