Skip to content

Data Model Reference

The data model lives in the config Django app (config/models.py). All core domain entities -- clinicians, shifts, leave, alerts, audit logs -- are defined there using Django's ORM with Python enum.Enum classes for type-safe choice fields.

Enums

WorkingTermType

Value Description
SALARIED Salaried employee with fixed working pattern
PARTNER GP partner with flexible working pattern
LOCUM Locum doctor, works manually specified dates only
FY_DOCTOR Foundation Year trainee doctor
ST_DOCTOR Specialty Trainee doctor

WorkingTermType.get_resident_types() returns [FY_DOCTOR, ST_DOCTOR].

ShiftType

Value Counts as Worked Counts Toward Minimum Description
STANDARD Yes Yes Regular working shift
DUTY Yes Yes Duty doctor shift
STUDY_LEAVE Yes No Study leave (counts as a worked session since issue #197)
CORONERS Yes No Coroner's court attendance

ShiftDuration

Value Description
FULL Full day shift
HALF Half day shift

ShiftStatus

Value Description
SCHEDULED Shift is scheduled
COMPLETED Shift has been completed
CANCELLED Shift has been cancelled

ChangeType

Value Description
CREATED Shift was created
UPDATED Shift was modified
DELETED Shift was deleted

Used by PendingShiftNotification for batch-mode change tracking.

LeaveType

Value Quota-based Partner-only Description
ANNUAL_LEAVE Yes No Annual leave/vacation
NOT_WORKING No Yes Day not working (partners only)
PLANNED_SICK No No Planned sick leave
STUDY_LEAVE No No Study leave
CORONERS No No Coroner's court attendance

LeaveType.is_quota_based(value) and LeaveType.is_partner_only(value) are class methods.

LeaveStatus

Value Description
REQUESTED Submitted, awaiting processing
APPROVED Approved by admin
DENIED Denied by admin
CANCELLED Cancelled (by admin or clinician)

ManualAdjustmentType

Value Description
ANNUAL_LEAVE_ADJUSTMENT Adjustment to annual leave entitlement
SICK_SHIFT_COMPENSATION Compensation for sick shifts

AlertType

Value Label Description
BELOW_MINIMUM Short-staffed Staffing below minimum required
AT_WARNING_THRESHOLD At Minimum Staffing Staffing at minimum threshold
OVERWORKED_DOCTOR Overworked Doctor Doctor is overworked
CONSTRAINT_VIOLATION Scheduling Concern Rota constraint violation
INSUFFICIENT_DUTY_DOCTORS Not Enough Duty Doctors Insufficient duty doctors on a day

AlertSeverity

Value Description
WARNING Warning level alert
CRITICAL Critical level alert

AlertStatus

Value Auto-sets resolved_at Description
ACTIVE No Alert is currently active
RESOLVED Yes Alert has been resolved
DISMISSED Yes Alert has been dismissed

AuditAction

Value Description
CREATE Entity was created
UPDATE Entity was updated
DELETE Entity was deleted

SwapStatus

Value Description
REQUESTED Swap requested, awaiting target response
ACCEPTED Target accepted, pending admin approval
REJECTED_BY_TARGET Target declined
PENDING_ADMIN Accepted by target, awaiting admin
APPROVED Admin approved, swap executed
REJECTED_BY_ADMIN Admin declined
CANCELLED Requester cancelled

SickLeaveStatus

Value Description
ACTIVE Currently off sick
RETURNED Back to work
CANCELLED Recorded in error

Core Models

SystemConfiguration (singleton)

System-wide configuration. Only one instance should exist (enforced by verbose_name_plural). Access via SystemConfiguration.get_or_create() or SystemConfiguration.get_cached().

Field Type Default Description
target_working_days_per_week DecimalField(4,2) 3.50 FTE working days per week
minimum_doctors JSONField dict Min doctors per weekday: {"Monday": 9, "Tuesday": 7, ...}
duty_doctors_required IntegerField 1 Duty doctors on normal days
duty_doctors_post_bank_holiday IntegerField 2 Duty doctors after bank holidays
post_bank_holiday_minimum CharField(20) "Monday" Which day's minimum to use post-bank-holiday
fixed_period_weeks IntegerField 6 Weeks in the fixed (non-auto-recalc) period
reporting_periods JSONField list Reporting period configs
uk_nation CharField(20) "england" UK nation for bank holiday detection
current_date DateField (nullable) -- Reference date for fixed period boundary
enable_wte_balance BooleanField True Enable WTE balance optimization phase
enable_shift_notifications BooleanField True Enable email notifications for shift changes
shift_notification_batch_mode BooleanField False Queue notifications instead of sending immediately
batch_mode_enabled_at DateTimeField (nullable) -- When batch mode was last enabled
batch_mode_enabled_by CharField(255) "" User who last enabled batch mode
wte_balance_deficit_weight DecimalField(3,2) 0.60 Weight for daily deficit in day selection (0-1)
wte_balance_day_distribution_weight DecimalField(3,2) 0.40 Weight for day-of-week balance in day selection (0-1)
optimization_iterations IntegerField 10 Randomized iterations for optimization
ai_api_enabled BooleanField False Enable AI agent API access
ai_api_approval_mode BooleanField True Require browser approval for AI transactions (disable for autonomous mode)
ai_api_approval_timeout_minutes IntegerField 60 Auto-reject proposed transactions after this many minutes
leave_booking_cutoff DateField (nullable) -- Clinicians cannot book normal leave (annual, not-working, planned sick) beyond this date; study/coroners unrestricted; admins bypass

Notable behaviour: - save() runs full_clean(), creates audit log entries for critical field changes, and invalidates related caches. - uk_nation cannot be changed after shifts exist (prevents historical inconsistencies). - WTE balance weights must sum to 1.0. - Holiday-period blocking (the former "Christmas period") is now driven by the multi-row HolidayPeriod model, not SystemConfiguration fields.

MinimumStaffingRule

Time-limited, conditional override of the daily minimum doctor count (issue #198). A rule fires on a date when it is enabled, the date is inside its window, the weekday matches (or days_of_week is empty), and every clinician in required_clinician_ids is working that date (or the list is empty). A firing rule's minimum_doctors replaces the SystemConfiguration day-of-week base (it may raise or lower it); when several rules fire, the highest wins. Evaluated through config.minimum_staffing.evaluate_minimum by the leave-booking capacity service, StaffingValidator, alerts, deficit ordering, coverage simulation, AI-API floor checks, and reports — each passing its own sense of "working". The CP-SAT rota generator keeps using base minimums. Rules are authored in code or Django admin; the /admin/ Minimum Staffing Rules page exposes only enable/disable and the date window (audited).

Field Type Default Description
name CharField(200) -- Human-readable label
description CharField(500) "" Optional note explaining the rule
days_of_week JSONField (list) [] Weekday names the rule applies to; empty = every weekday
required_clinician_ids JSONField (list) [] Clinician IDs that must ALL be working; empty = no condition
minimum_doctors PositiveIntegerField -- Minimum count used when the rule fires
enabled BooleanField True Master switch; disabled rules are ignored everywhere
start_date DateField (nullable) -- First active date (inclusive); blank = immediately
end_date DateField (nullable) -- Last active date (inclusive); blank = no end

ClinicianGroup

Groups for categorising clinicians (e.g., "FY Doctor", "FY Supervisor"). Separate from Django auth groups.

Field Type Description
name CharField(100, unique) Group name
description TextField (blank) Group purpose

Many-to-many relationship with Clinician via Clinician.groups.

Clinician

A doctor or clinician in the rota system.

Field Type Description
name CharField(255) Full name
email EmailField (nullable) Contact email for notifications
user OneToOneField(auth.User, nullable, SET_NULL) Linked login account; related_name clinician_profile
active BooleanField (default True) Currently active in the rota
groups ManyToManyField(ClinicianGroup) Clinician groups for rota rules
calendar_feed_secret CharField(64, unique, nullable) Secret key for calendar feed
calendar_feed_enabled BooleanField (default False) Whether calendar feed is active

Notable behaviour: - type is a property derived from the active WorkingTerm, not a stored field. Returns SALARIED as default. - is_resident_doctor() returns True if active WorkingTerm is FY_DOCTOR or ST_DOCTOR. - regenerate_calendar_secret() generates a new cryptographically secure token.

Invitation

Invitation for a user to create an account linked to a Clinician.

Field Type Description
clinician FK(Clinician, CASCADE) The clinician to invite
email EmailField Delivery address
token CharField(64, unique) Cryptographic signup token
expires_at DateTimeField Expiry (14 days from creation)
used BooleanField (default False) Whether invitation has been used
used_at DateTimeField (nullable) When it was used

Class method create_invitation(clinician, email, days_valid=14) handles token generation.

UserEmailPreferences

Admin email notification preferences.

Field Type Default Description
user OneToOneField(User, CASCADE) -- Owning user
email_pref_system_failures BooleanField False System failure emails
email_pref_admin_actions BooleanField False Admin action emails
email_pref_operational BooleanField False Operational notification emails
email_pref_ai_mutations BooleanField False AI API mutation emails
email_pref_shift_notifications BooleanField True Shift change notification emails

All preferences are opt-in (default False) except email_pref_shift_notifications, which is opt-out (default True) so admins who never touched their preferences keep receiving shift-change alerts. Admin-email recipient selection is centralised in config.permission_helpers.get_admin_email_recipients_for_type() (#164).

WorkingTerm

A period of employment with a specific working pattern. Clinicians can have multiple non-overlapping terms over time.

Field Type Description
clinician FK(Clinician, CASCADE) The clinician; related_name working_terms
type CharField(20, choices WorkingTermType) Employment type for this term
start_date DateField When the term begins
end_date DateField (nullable) When it ends (null = ongoing)
percentage DecimalField(5,2) FTE percentage, 0-100 (default 100)
fixed_working_days JSONField (list) Days the clinician must work (salaried)
fixed_half_days JSONField (list) Days that must be half days
cannot_work_days JSONField (list) Days the clinician cannot work (partners)
must_work_days JSONField (list) Days the clinician must work (partners)
annual_leave_entitlement JSONField (dict) Single-pool entitlement: {"total": 10.5}
participates_in_duty BooleanField (default True) Whether included in duty rotation
minimum_shifts_per_week IntegerField (nullable) Min shifts/week for partners (null = no minimum)
max_shifts_per_week IntegerField (nullable) Max shifts/week for partners (null = no maximum)

Key class methods:

Method Returns Description
get_active_term_for_date(clinician, date) WorkingTerm or None Term overlapping the given date
get_active_term(clinician, as_of_date=None) WorkingTerm or None Alias, defaults to today
get_term_near_date(clinician, date, months_window=6) WorkingTerm or None Active, upcoming, or recent past term
get_terms_in_period(clinician, start, end) QuerySet All terms overlapping a date range
get_latest_term(clinician) WorkingTerm or None Most recent term by start_date

Properties: total_leave_adjustments sums all linked LeaveAdjustment amounts; get_effective_entitlement() adds adjustments to base entitlement.

SupervisionRelationship

Links a trainee's WorkingTerm to a supervisor Clinician. One supervisor per WorkingTerm (unique constraint).

Field Type Description
supervisor FK(Clinician, CASCADE) The supervising clinician
working_term FK(WorkingTerm, CASCADE) The trainee's working term

Validation: a clinician cannot supervise themselves.

Shift

A scheduled working day for a clinician.

Field Type Description
clinician FK(Clinician, CASCADE) Assigned clinician; related_name shifts
date DateField Shift date (indexed)
type CharField(20, choices ShiftType) Shift type (default STANDARD)
duration CharField(10, choices ShiftDuration) FULL or HALF (default FULL)
status CharField(20, choices ShiftStatus) SCHEDULED, COMPLETED, CANCELLED
is_pinned BooleanField (default False) Immune to automatic rebalancing
is_off_sick BooleanField (default False) Marked as off sick
systm1_synced BooleanField (default False) Synced to Systm1
is_locum_booking BooleanField (default False) Locum booking, bypasses standard validation
created_by CharField(255) User who created
modified_by CharField(255) User who last modified

Database constraint: one SCHEDULED non-locum shift per clinician per day.

Properties: - counts_as_worked -- True for every session type (STANDARD, DUTY, STUDY_LEAVE, CORONERS), including off-sick shifts (issue #197). Leave types (ANNUAL_LEAVE, NOT_WORKING) are not shift types and never appear here. - counts_toward_minimum -- False for STUDY_LEAVE, CORONERS, off-sick shifts, and resident doctor (FY/ST) shifts.

save() accepts send_notification=False kwarg to suppress email notifications (for bulk operations). delete() also accepts this kwarg.

PendingShiftNotification

Queued shift change notification for batch mode. When SystemConfiguration.shift_notification_batch_mode is active, shift changes are recorded here instead of sending emails immediately.

Field Type Description
clinician FK(Clinician, SET_NULL, nullable) Assigned clinician
clinician_name CharField(255) Denormalised name (survives shift deletion)
shift_date DateField Date of the shift
shift_type CharField(20) Shift type
shift_duration CharField(10) FULL or HALF
change_type CharField(10, choices ChangeType) CREATED, UPDATED, or DELETED
change_description TextField Human-readable description
created_by CharField(255) User who made the change

Class method consolidate_for_shift(clinician, date) merges redundant pairs (e.g., CREATED + DELETED = remove both).

LeaveRequest

A request for time off by a clinician.

Field Type Description
clinician FK(Clinician, CASCADE) Requesting clinician; related_name leave_requests
type CharField(20, choices LeaveType) Leave type
start_date DateField First day of leave
end_date DateField Last day of leave
status CharField(20, choices LeaveStatus) REQUESTED, APPROVED, DENIED, CANCELLED
processed_at DateTimeField (nullable) When processed
processed_by CharField(255) User who processed
denial_reason TextField (blank) Required when status is DENIED
affected_shift_count DecimalField(max_digits=10, decimal_places=1) Calculated shifts affected
exceeded_quota BooleanField (default False) Whether approval exceeded remaining quota
note CharField(500, blank) Public note shown on the calendar and ICS feed (issue #200)
originating_round FK(AnnualLeaveRound, nullable, SET_NULL) Set when the request was created by round conversion

Properties: is_quota_based delegates to LeaveType.is_quota_based(). type_display returns human-readable label.

save() accepts skip_validation=True kwarg for status-only changes.

LeaveAdjustment

Manual adjustment to a clinician's leave quota. This is the only mechanism for carry-over (no automatic carry-over exists).

Field Type Description
working_term FK(WorkingTerm, CASCADE) Term this adjustment applies to; related_name leave_adjustments
amount DecimalField(5,2) Positive to add, negative to subtract (cannot be zero)
reason TextField Explanation for the adjustment
created_by CharField(255) Admin who created it

ManualAdjustment

Legacy adjustment model (superseded by LeaveAdjustment for leave quota changes).

Field Type Description
clinician FK(Clinician, CASCADE) Adjusted clinician; related_name manual_adjustments
type CharField(30, choices ManualAdjustmentType) Adjustment type
adjustment_value FloatField Value (can be negative)
reporting_period CharField(50, blank) Reporting period label
reason TextField Reason
created_by CharField(255) Admin who created it

LocumBooking

Scheduled dates for a locum doctor. Locums work manually specified dates only and are not part of automatic shift allocation.

Field Type Description
clinician FK(Clinician, CASCADE) The locum clinician; related_name locum_bookings
dates JSONField (list) Specific dates: ["2024-06-15", ...]
start_date DateField (nullable) Range booking start
end_date DateField (nullable) Range booking end
created_by CharField(255) User who created

SickLeave

Tracks when a clinician is off sick. Created and managed by admins.

Field Type Description
clinician FK(Clinician, CASCADE) Clinician off sick; related_name sick_leaves
start_date DateField First day of sick leave
end_date DateField (nullable) Last day (null = ongoing)
status CharField(20, choices SickLeaveStatus) ACTIVE, RETURNED, CANCELLED
notes TextField (blank) Admin notes (not visible to clinician)
recorded_by CharField(255) Admin who recorded it

Class method get_active_for_date(clinician, date) returns active sick leave for a specific date.

ShiftSwapRequest

Request to swap shifts between two clinicians with approval workflow: target accepts/declines, then admin approves/rejects.

Field Type Description
requester_shift FK(Shift, CASCADE) Shift being offered
target_shift FK(Shift, CASCADE) Shift being requested
requester FK(Clinician, CASCADE) Clinician initiating swap
target_clinician FK(Clinician, CASCADE) Clinician whose shift is requested
status CharField(25, choices SwapStatus) Current status
responded_at DateTimeField (nullable) When target responded
admin_processed_at DateTimeField (nullable) When admin processed
response_by FK(User, SET_NULL, nullable) User who responded on behalf of target
admin_response_by FK(User, SET_NULL, nullable) Admin who approved/rejected
rejection_reason TextField (blank) Reason for rejection

Same-type rule: shifts must match type (duty with duty, standard with standard, locum with locum).

Alert

An alert about a rota issue (staffing, overwork, constraints).

Field Type Description
type CharField(30, choices AlertType) Alert category
severity CharField(10, choices AlertSeverity) WARNING or CRITICAL
date DateField Affected date
message CharField(500) Alert message
details JSONField Additional context
status CharField(20, choices AlertStatus) ACTIVE, RESOLVED, DISMISSED
resolved_at DateTimeField (nullable) Auto-set when status leaves ACTIVE

AuditLog

Tracks all changes to important entities.

Field Type Description
entity_type CharField(50) Entity type name (e.g., "Shift")
entity_id CharField(255) Entity primary key
action CharField(20, choices AuditAction) CREATE, UPDATE, DELETE
user_id CharField(255, nullable) User who made the change
previous_state JSONField Snapshot before change
new_state JSONField Snapshot after change
reason TextField (blank) Reason for change

CustomRule

Defines custom rules for specific rota requirements (e.g., trainer must be present on Mondays).

Field Type Description
name CharField(255) Rule name
role_required CharField(100) Required role (e.g., "trainer")
applicable_days JSONField (list) Day names or specific dates
description TextField (blank) Rule description
active BooleanField (default True) Currently active

HolidayPeriod

A date range during which the practice operates under holiday rules (the successor to the removed SystemConfiguration.christmas_period_* fields). Typically one row per year (e.g. the Christmas/New Year closure), but supports arbitrary disjoint periods. Leave is blocked during holiday periods and the leave-booking calendar renders these days with a distinct grey/hatched style.

Field Type Description
name CharField(100) Human-readable label (e.g. "Christmas 2026")
start_date DateField First day of the period (inclusive)
end_date DateField Last day of the period (inclusive)

Check constraint: end_date >= start_date. Ordered by -start_date. Queried by is_date_in_holiday_period() in calculations/date_utils.py.

LeaveCancellationRequest

A request to cancel already-approved leave that falls inside the Fixed Period (cancellation of in-flight leave requires admin approval). See ADR 0001 and the leave_cancellation_requests[] AI endpoint.

Field Type Description
leave_request FK(LeaveRequest, CASCADE) The approved leave being cancelled
reason TextField Clinician's reason for the cancellation
status CharField(choices CancellationStatus) PENDING (default), APPROVED, or DENIED
denial_reason TextField (blank) Admin's reason when DENIED (required if status is DENIED)
requested_by CharField(255) Clinician who requested the cancellation
processed_by CharField(255) Admin who approved/denied it
created_at DateTimeField (auto) When the request was created
processed_at DateTimeField (nullable) When it was approved/denied

is_within_approval_period(leave_request) classmethod returns whether a leave request overlaps the Fixed Period and therefore requires a cancellation request. Approve/deny are atomic with row locking (select_for_update).

AIApiKey

An API key authenticating an external AI agent against the v3 API. Linked to a Django user and carries the active-transaction context.

Field Type Description
user FK(User) The owning user
key CharField (unique) The secret API key
name CharField Human-readable label
is_active BooleanField Whether the key is enabled
created_at / last_used_at DateTimeField Lifecycle timestamps

AITransaction

A staged-mutation transaction against clinician/working-term/shift data, gating AI writes behind optional browser approval.

Field Type Description
api_key FK(AIApiKey) Key that opened the transaction
status CharField PENDING / PROPOSED / APPROVED / REJECTED / TIMEOUT
created_at / proposed_at / approved_at (+ approved_by) DateTimeField / FK(User) (nullable) Lifecycle timestamps and approver
snapshot JSONField Pre-mutation state captured for rollback

AILock

A date-range lock preventing concurrent AI mutations from racing: one live transaction may hold the lock for a given date range, tied one-to-one to that transaction. While a transaction is PROPOSED its lock has no expiry; approval or rejection releases it.

Field Type Description
transaction OneToOneField(AITransaction) The transaction holding the lock
start_date / end_date DateField The date range being mutated
created_at DateTimeField (auto) When the lock was taken
expires_at DateTimeField (nullable) Auto-release time (NULL while PROPOSED)

ShiftStaging

A shift add/delete staged within a PENDING AITransaction, applied atomically when the transaction is approved (or discarded on rollback). Surfaced via the shifts/staged/ endpoint.

Field Type Description
transaction FK(AITransaction, CASCADE) Owning transaction
clinician FK(Clinician) Assigned clinician
date DateField Shift date
type / duration / status CharField STANDARD/DUTY/etc., FULL/HALF, SCHEDULED/etc.
is_deletion BooleanField True = this stages a delete, False = an add
is_pinned BooleanField Pinned against optimisation
constraint_overrides JSONField Validation constraints to bypass
created_at / modified_at DateTimeField (auto) Timestamps

Annual Leave Round Models (leave_round app)

The staged yearly allocation of next-year annual leave (issues #179–#192). Domain terms are defined in CONTEXT.md ("Annual Leave Round" section); the admin workflow is documented in the Admin Guide.

AnnualLeaveRound

One round per leave year, driven through a lifecycle by RoundService (leave_round/services.py).

Field Type Description
leave_year IntegerField (unique) The year this round allocates leave for
status CharField(choices RoundStatus) DRAFT → OPEN → RESOLUTION → LOCKED
window_opens_at / window_closes_at DateTimeField When clinicians may submit/change selections
round_cap_default IntegerField Default max Week Selections per participant (the cap is min(this, entitlement-in-weeks))
late_year_limit IntegerField Max selections a participant may place in Aug–Dec
opened_at / closed_at / locked_at DateTimeField (nullable) Lifecycle timestamps (cleared by a step-back)
created_by / updated_by CharField Audit usernames

ALLOWED_TRANSITIONS in services.py defines both forward transitions and the one-step undos (OPEN→DRAFT, RESOLUTION→OPEN, LOCKED→RESOLUTION; unlock refused once a newer round exists). Configuration is editable in DRAFT only.

WeekSelection

A participant's ranked claim on one Monday–Friday week.

Field Type Description
round FK(AnnualLeaveRound, CASCADE) Owning round
clinician FK(Clinician, CASCADE) The participant
week_commencing DateField The Monday of the selected week
priority IntegerField 1 = most wanted (advisory, never mechanically enforced)

Unique on (round, clinician, week_commencing). Hard-validated against the participant's round cap and the late-year limit at submission; admins may exceed the cap manually during Resolution.

SelectionGhost

The dotted board record of where a selection originally sat: written when an admin moves or cancels a selection from its creation week, cleared if it returns there.

Field Type Description
round FK(AnnualLeaveRound, CASCADE) Owning round
clinician FK(Clinician, CASCADE) Whose selection it records
week_commencing DateField The original week
priority IntegerField The priority it originally carried

SchoolHolidayPeriod

A named school-holiday date range for one local district (display-only overlay on the selection grid; affects no validation).

Field Type Description
district CharField(100) Which district's pattern (ranges for one district render in one colour)
name CharField(100) Label (e.g. "February half-term")
start_date / end_date DateField Inclusive range

Relationships

erDiagram
    User ||--o| Clinician : "clinician_profile (SET_NULL)"
    Clinician ||--o{ WorkingTerm : "working_terms (CASCADE)"
    Clinician ||--o{ Shift : "shifts (CASCADE)"
    Clinician ||--o{ LeaveRequest : "leave_requests (CASCADE)"
    Clinician ||--o{ ManualAdjustment : "manual_adjustments (CASCADE)"
    Clinician ||--o{ LocumBooking : "locum_bookings (CASCADE)"
    Clinician ||--o{ Invitation : "invitations (CASCADE)"
    Clinician ||--o{ SickLeave : "sick_leaves (CASCADE)"
    Clinician }o--o{ ClinicianGroup : "groups (M2M)"
    Clinician ||--o{ ShiftSwapRequest : "initiated_swap_requests"
    Clinician ||--o{ ShiftSwapRequest : "received_swap_requests"

    WorkingTerm ||--o{ LeaveAdjustment : "leave_adjustments (CASCADE)"
    WorkingTerm ||--o| SupervisionRelationship : "supervision_relationships (CASCADE)"
    Clinician ||--o{ SupervisionRelationship : "supervision_as_supervisor (CASCADE)"

    Shift ||--o{ ShiftSwapRequest : "outgoing_swap_requests"
    Shift ||--o{ ShiftSwapRequest : "incoming_swap_requests"

    Clinician ||--o{ PendingShiftNotification : "pending_notifications (SET_NULL)"

    User ||--o| UserEmailPreferences : "email_prefs (CASCADE)"
    User ||--o{ ShiftSwapRequest : "swap_responses (SET_NULL)"
    User ||--o{ ShiftSwapRequest : "swap_admin_responses (SET_NULL)"

    LeaveRequest ||--o{ LeaveCancellationRequest : "cancellation_requests (CASCADE)"

    User ||--o{ AIApiKey : "ai_api_keys (CASCADE)"
    AIApiKey ||--o{ AITransaction : "transactions (SET_NULL)"
    AITransaction ||--o{ ShiftStaging : "staged_shifts (CASCADE)"
    AIApiKey ||--o{ AILock : "locks (SET_NULL)"

    SystemConfiguration {
        int id
    }

    AuditLog {
        string entity_type
        string entity_id
    }

    Alert {
        string type
        date date
    }

    CustomRule {
        string name
    }

Validation Rules

SystemConfiguration

Rule Field(s)
target_working_days_per_week must be > 0 and <= 7 target_working_days_per_week
minimum_doctors must be a dict with Mon-Fri keys, positive integer values minimum_doctors
duty_doctors_required must be > 0 duty_doctors_required
duty_doctors_post_bank_holiday must be > 0 duty_doctors_post_bank_holiday
post_bank_holiday_minimum must be a weekday name (Mon-Fri; weekend minimums are always 0) post_bank_holiday_minimum
fixed_period_weeks must be > 0 fixed_period_weeks
uk_nation must be one of: england, scotland, wales, northern_ireland uk_nation
uk_nation cannot change after shifts exist uk_nation
WTE balance weights must be between 0 and 1 and sum to 1.0 wte_balance_deficit_weight, wte_balance_day_distribution_weight
optimization_iterations must be >= 1 optimization_iterations
ai_api_approval_timeout_minutes must be positive ai_api_approval_timeout_minutes

WorkingTerm

Rule Field(s)
end_date >= start_date (if both set) start_date, end_date
percentage must be 0-100 percentage
Day fields must contain only valid weekday names fixed_working_days, fixed_half_days, cannot_work_days, must_work_days
No overlapping terms for the same clinician start_date, end_date
annual_leave_entitlement.total must be non-negative numeric annual_leave_entitlement
Partner entitlement capped at 52; salaried at 260 annual_leave_entitlement

Shift

Rule Field(s)
Clinician must have an active WorkingTerm on the shift date date, clinician
Only one SCHEDULED non-locum shift per clinician per day (DB constraint) clinician, date, status, is_locum_booking

LeaveRequest

Rule Field(s)
end_date >= start_date start_date, end_date
denial_reason required when status is DENIED denial_reason, status
NOT_WORKING type only available to partners type, clinician
Partners must request ANNUAL_LEAVE for full calendar weeks (Mon-Fri) start_date, end_date, type

LeaveAdjustment

Rule Field(s)
amount cannot be zero amount

SickLeave

Rule Field(s)
end_date >= start_date (if set) start_date, end_date

SupervisionRelationship

Rule Field(s)
Supervisor cannot be the same clinician as the working term's clinician supervisor, working_term
One supervisor per working term (unique constraint) working_term

Cascade Summary

Parent Child On Delete
Clinician WorkingTerm CASCADE
Clinician Shift CASCADE
Clinician LeaveRequest CASCADE
Clinician ManualAdjustment CASCADE
Clinician LocumBooking CASCADE
Clinician Invitation CASCADE
Clinician SickLeave CASCADE
Clinician SupervisionRelationship (as supervisor) CASCADE
WorkingTerm LeaveAdjustment CASCADE
WorkingTerm SupervisionRelationship CASCADE
Shift ShiftSwapRequest CASCADE
User Clinician (via user FK) SET_NULL
User UserEmailPreferences CASCADE