ClinicFlow protects clinic capacity through an automated penalty system. When a patient misses an appointment or cancels too late, the system records aDocumentation 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.
PatientPenalty against them. Repeat offenders receive progressively longer booking blocks — all driven by domain logic without any manual staff intervention. Staff retain the ability to apply manual blocks or remove penalties when circumstances warrant.
Penalty Types
ThePenaltyType enum classifies every penalty record:
| Value | Description |
|---|---|
Warning | A non-blocking advisory notice. Does not prevent future bookings on its own. |
TemporaryBlock | A time-limited block. Prevents the patient from booking new appointments until BlockedUntil expires. |
When Penalties Are Triggered
Penalties are applied automatically by domain event handlers that listen to two appointment lifecycle events:AppointmentMarkedAsNoShowEvent— raised whenMarkAsNoShowByStaff()orMarkAsNoShowByDoctor(...)is called on aScheduledappointment.AppointmentLateCancelledEvent— raised whenCancelLate(...)is called, which happens when a patient cancels after the specialty’sCancellationLimitnotice window has passed.
PatientPenaltyService.ApplyPenalty(...).
PatientPenaltyService: Automatic Penalty Logic
Warning. It then decides whether to also create an automatic TemporaryBlock based on two conditions:
history.HasPriorWarnings— there must be at least one existing warning on record (the current warning has not yet been persisted, so “prior” means history at the time of the call).!history.IsCurrentlyBlocked(date)— the patient must not already be under an active block. A patient who is currently blocked does not receive another block for the same event; they only get the warning.
No-show event fires
A staff member or the attending doctor marks the appointment as a no-show. The
AppointmentMarkedAsNoShowEvent domain event is dispatched after the UnitOfWork commit.Event handler invokes PatientPenaltyService
The application-layer event handler calls
PatientPenaltyService.ApplyPenalty(patientId, existingPenalties, appointmentId, PenaltyReasons.NoShow, referenceTime).Warning is always created
PatientPenalty.CreateAutomaticWarning(...) produces a Warning record linked to the appointment ID. BlockedUntil is null.Block escalation decision
PenaltyHistory checks HasPriorWarnings. If this is the patient’s first offence, only the warning is returned — no block. If prior warnings exist and the patient is not already blocked, the block escalation path continues.Block duration determined
PenaltyHistory.DetermineNextBlockDuration() walks the escalation ladder: first block → Minor (5 days), second → Moderate (15 days), third and beyond → Severe (30 days).TemporaryBlock is created
PatientPenalty.CreateAutomaticBlock(...) produces a TemporaryBlock record. BlockedUntil is set to referenceDate + blockDuration (days). This record has no AppointmentId — it is a system-generated aggregate.Block Duration Escalation
Block severity escalates automatically based on the patient’s total historical block count, regardless of whether past blocks have expired or been removed. The escalation ladder is capped atSevere.
| BlockDuration | Days | When Applied |
|---|---|---|
Minor | 5 | First automatic block (patient has 0 prior blocks) |
Moderate | 15 | Second automatic block (patient has 1 prior block) |
Severe | 30 | Third block and every block thereafter |
TotalHistoricalBlocks counts all TemporaryBlock records in the patient’s history, including those that have already expired or been manually removed by staff. A patient cannot reset their escalation level by waiting for blocks to expire.Scheduling Gate: PenaltyHistory.EnsureNotBlocked
At scheduling and rescheduling time (patient-initiated only), the domain checks for active blocks:PatientBlockedException carries the latest BlockedUntil date so callers can communicate exactly when the patient becomes eligible to book again.
PatientPenalty Fields
| Field | Type | Description |
|---|---|---|
PatientId | Guid | The patient this penalty applies to |
AppointmentId | Guid? | Linked appointment for automatic warnings; null for manual or automatic blocks |
Type | PenaltyType | Warning or TemporaryBlock |
Reason | string | Human-readable reason (e.g., "No show", "Late cancellation", "Automatic block due to 3 strikes") |
BlockedUntil | DateOnly? | Expiry date for TemporaryBlock; null for Warning |
IsRemoved | bool | Set to true by staff via Remove(). Excluded from all active-block checks. |
Staff-Initiated Penalties
Staff can issue penalties independently of the automatic system through two Application-layer commands.BlockPatient
Staff call theBlockPatient command to issue a manual TemporaryBlock with an explicit BlockDuration:
BlockDuration value (Minor, Moderate, or Severe) and do not go through the escalation ladder.
RemovePenalty
Staff call theRemovePenalty command to soft-remove any existing penalty:
IsRemoved = true immediately excludes the penalty from EnsureNotBlocked and IsCurrentlyBlocked checks. The record is preserved for audit purposes — it is never physically deleted.
CancellationLimit: Per-Specialty Notice Windows
The trigger point for aLateCancellation (which carries a penalty) is determined per medical specialty by the CancellationLimit value object on MedicalSpecialty.CancellationPolicy:
0 means the specialty has no cancellation restriction, and cancellations are always penalty-free.
Summary: Automatic vs. Manual Penalties
Automatic (System)
Triggered by
NoShow or LateCancellation events. Always starts with a Warning. Escalates to a TemporaryBlock on the second offence and beyond, using the progressive Minor → Moderate → Severe ladder.Manual (Staff)
BlockPatient command creates a TemporaryBlock with any duration chosen by staff. RemovePenalty command soft-removes any penalty. Neither action affects the escalation history counter used for automatic blocks.