feature: Schema, Migrations, Core CRUD, and Redis Threshold Cache

This commit is contained in:
voltsrage
2026-06-16 17:59:16 +08:00
commit 882d4af3e6
63 changed files with 3923 additions and 0 deletions
@@ -0,0 +1,12 @@
public class AlertThreshold
{
public Guid Id { get; set; }
public string ObservationCode { get; set; } = null!;
public string DisplayName { get; set; } = null!;
public string Unit { get; set; } = null!;
public decimal? CriticalLow { get; set; }
public decimal? WarningLow { get; set; }
public decimal? WarningHigh { get; set; }
public decimal? CriticalHigh { get; set; }
public DateTimeOffset CreatedAt { get; set; }
}
@@ -0,0 +1,17 @@
public class ClinicalAlert
{
public Guid Id { get; set; }
public Guid EncounterId { get; set; }
public Guid PatientId { get; set; }
public Guid? ObservationId { get; set; }
public AlertType AlertType { get; set; }
public AlertSeverity Severity { get; set; }
public string Details { get; set; } = null!;
public AlertStatus Status { get; set; } = AlertStatus.Open;
public DateTimeOffset? AcknowledgedAt { get; set; }
public string? AcknowledgedBy { get; set; }
public DateTimeOffset? ResolvedAt { get; set; }
public DateTimeOffset TriggeredAt { get; set; }
public Encounter Encounter { get; set; } = null!;
}
@@ -0,0 +1,17 @@
public class Encounter
{
public Guid Id { get; set; }
public Guid PatientId { get; set; }
public EncounterType EncounterType { get; set; }
public EncounterStatus Status { get; set; }
public string Department { get; set; } = null!;
public string AttendingPhysician { get; set; } = null!;
public DateTimeOffset AdmittedAt { get; set; }
public DateTimeOffset? DischargedAt { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public Patient Patient { get; set; } = null!;
public ICollection<Observation> Observations { get; set; } = new List<Observation>();
public ICollection<ClinicalAlert> Alerts { get; set; } = new List<ClinicalAlert>();
public ICollection<Order> Orders { get; set; } = new List<Order>();
}
@@ -0,0 +1,14 @@
public class Observation
{
public Guid Id { get; set; }
public Guid EncounterId { get; set; }
public string ObservationCode { get; set; } = null!;
public decimal Value { get; set; }
public string Unit { get; set; } = null!;
public ObservationSource Source { get; set; } = ObservationSource.Manual;
public string? IdempotencyKey { get; set; }
public DateTimeOffset RecordedAt { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public Encounter Encounter { get; set; } = null!;
}
@@ -0,0 +1,13 @@
public class Order
{
public Guid Id { get; set; }
public Guid EncounterId { get; set; }
public OrderType OrderType { get; set; }
public string Description { get; set; } = null!;
public string OrderedBy { get; set; } = null!;
public string Status { get; set; } = "pending";
public DateTimeOffset OrderedAt { get; set; }
public DateTimeOffset? ResultedAt { get; set; }
public Encounter Encounter { get; set; } = null!;
}
@@ -0,0 +1,8 @@
public class OutboxEvent
{
public Guid Id { get; set; }
public string Topic { get; set; } = null!;
public string Payload { get; set; } = null!; // JSON string
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset? ProcessedAt { get; set; }
}
@@ -0,0 +1,13 @@
public class Patient
{
public Guid Id { get; set; }
public string Mrn { get; set; } = null!;
public string FirstName { get; set; } = null!;
public string LastName { get; set; } = null!;
public DateOnly DateOfBirth { get; set; }
public string Gender { get; set; } = null!;
public string Status { get; set; } = "active";
public DateTimeOffset CreatedAt { get; set; }
public ICollection<Encounter> Encounters { get; set; } = new List<Encounter>();
}
@@ -0,0 +1,10 @@
public class ReconciliationAlert
{
public Guid Id { get; set; }
public ReconciliationCheckType CheckType { get; set; }
public Guid? EncounterId { get; set; }
public Guid? PatientId { get; set; }
public string Details { get; set; } = null!;
public DateTimeOffset? ResolvedAt { get; set; }
public DateTimeOffset CreatedAt { get; set; }
}
@@ -0,0 +1,18 @@
public enum AlertSeverity { Warning, Critical }
public static class AlertSeverityExtensions
{
public static string ToDbString(this AlertSeverity s) => s switch
{
AlertSeverity.Warning => "WARNING",
AlertSeverity.Critical => "CRITICAL",
_ => throw new ArgumentOutOfRangeException(nameof(s))
};
public static AlertSeverity FromDbString(string v) => v switch
{
"WARNING" => AlertSeverity.Warning,
"CRITICAL" => AlertSeverity.Critical,
_ => throw new ArgumentOutOfRangeException(nameof(v), $"Unknown alert severity: '{v}'")
};
}
@@ -0,0 +1,22 @@
public enum AlertStatus { Open, Acknowledged, Resolved, Escalated }
public static class AlertStatusExtensions
{
public static string ToDbString(this AlertStatus s) => s switch
{
AlertStatus.Open => "OPEN",
AlertStatus.Acknowledged => "ACKNOWLEDGED",
AlertStatus.Resolved => "RESOLVED",
AlertStatus.Escalated => "ESCALATED",
_ => throw new ArgumentOutOfRangeException(nameof(s))
};
public static AlertStatus FromDbString(string v) => v switch
{
"OPEN" => AlertStatus.Open,
"ACKNOWLEDGED" => AlertStatus.Acknowledged,
"RESOLVED" => AlertStatus.Resolved,
"ESCALATED" => AlertStatus.Escalated,
_ => throw new ArgumentOutOfRangeException(nameof(v), $"Unknown alert status: '{v}'")
};
}
@@ -0,0 +1,50 @@
public enum AlertType
{
SepsisWarning,
CriticalHeartRate,
CriticalTempC,
CriticalPotassiumMeqL,
CriticalSpo2,
CriticalRespRate,
CriticalWbcKUl
}
public static class AlertTypeExtensions
{
public static string ToDbString(this AlertType t) => t switch
{
AlertType.SepsisWarning => "SEPSIS_WARNING",
AlertType.CriticalHeartRate => "CRITICAL_HEART_RATE",
AlertType.CriticalTempC => "CRITICAL_TEMP_C",
AlertType.CriticalPotassiumMeqL => "CRITICAL_POTASSIUM_MEQ_L",
AlertType.CriticalSpo2 => "CRITICAL_SPO2",
AlertType.CriticalRespRate => "CRITICAL_RESP_RATE",
AlertType.CriticalWbcKUl => "CRITICAL_WBC_K_UL",
_ => throw new ArgumentOutOfRangeException(nameof(t))
};
public static AlertType FromDbString(string v) => v switch
{
"SEPSIS_WARNING" => AlertType.SepsisWarning,
"CRITICAL_HEART_RATE" => AlertType.CriticalHeartRate,
"CRITICAL_TEMP_C" => AlertType.CriticalTempC,
"CRITICAL_POTASSIUM_MEQ_L"=> AlertType.CriticalPotassiumMeqL,
"CRITICAL_SPO2" => AlertType.CriticalSpo2,
"CRITICAL_RESP_RATE" => AlertType.CriticalRespRate,
"CRITICAL_WBC_K_UL" => AlertType.CriticalWbcKUl,
_ => throw new ArgumentOutOfRangeException(nameof(v), $"Unknown alert type: '{v}'")
};
// Threshold alerts are derived from observation codes in alert_thresholds — not free-form strings.
public static AlertType CriticalFor(string observationCode) => observationCode switch
{
"HEART_RATE" => AlertType.CriticalHeartRate,
"TEMP_C" => AlertType.CriticalTempC,
"POTASSIUM_MEQ_L" => AlertType.CriticalPotassiumMeqL,
"SPO2" => AlertType.CriticalSpo2,
"RESP_RATE" => AlertType.CriticalRespRate,
"WBC_K_UL" => AlertType.CriticalWbcKUl,
_ => throw new ArgumentOutOfRangeException(
nameof(observationCode), $"No critical alert type for observation code '{observationCode}'")
};
}
@@ -0,0 +1,22 @@
public enum EncounterStatus { Scheduled, Active, Discharged, Cancelled }
public static class EncounterStatusExtensions
{
public static string ToDbString(this EncounterStatus s) => s switch
{
EncounterStatus.Scheduled => "SCHEDULED",
EncounterStatus.Active => "ACTIVE",
EncounterStatus.Discharged => "DISCHARGED",
EncounterStatus.Cancelled => "CANCELLED",
_ => throw new ArgumentOutOfRangeException(nameof(s))
};
public static EncounterStatus FromDbString(string v) => v switch
{
"SCHEDULED" => EncounterStatus.Scheduled,
"ACTIVE" => EncounterStatus.Active,
"DISCHARGED" => EncounterStatus.Discharged,
"CANCELLED" => EncounterStatus.Cancelled,
_ => throw new ArgumentOutOfRangeException(nameof(v), $"Unknown encounter status: '{v}'")
};
}
@@ -0,0 +1,20 @@
public enum EncounterType { Inpatient, Outpatient, Emergency }
public static class EncounterTypeExtensions
{
public static string ToDbString(this EncounterType t) => t switch
{
EncounterType.Inpatient => "INPATIENT",
EncounterType.Outpatient => "OUTPATIENT",
EncounterType.Emergency => "EMERGENCY",
_ => throw new ArgumentOutOfRangeException(nameof(t))
};
public static EncounterType FromDbString(string v) => v switch
{
"INPATIENT" => EncounterType.Inpatient,
"OUTPATIENT" => EncounterType.Outpatient,
"EMERGENCY" => EncounterType.Emergency,
_ => throw new ArgumentOutOfRangeException(nameof(v), $"Unknown encounter type: '{v}'")
};
}
@@ -0,0 +1,20 @@
public enum ObservationSource { Manual, Device, Lab }
public static class ObservationSourceExtensions
{
public static string ToDbString(this ObservationSource s) => s switch
{
ObservationSource.Manual => "MANUAL",
ObservationSource.Device => "DEVICE",
ObservationSource.Lab => "LAB",
_ => throw new ArgumentOutOfRangeException(nameof(s))
};
public static ObservationSource FromDbString(string v) => v switch
{
"MANUAL" => ObservationSource.Manual,
"DEVICE" => ObservationSource.Device,
"LAB" => ObservationSource.Lab,
_ => throw new ArgumentOutOfRangeException(nameof(v), $"Unknown observation source: '{v}'")
};
}
@@ -0,0 +1,22 @@
public enum OrderType { Lab, Imaging, Medication, Procedure }
public static class OrderTypeExtensions
{
public static string ToDbString(this OrderType t) => t switch
{
OrderType.Lab => "LAB",
OrderType.Imaging => "IMAGING",
OrderType.Medication => "MEDICATION",
OrderType.Procedure => "PROCEDURE",
_ => throw new ArgumentOutOfRangeException(nameof(t))
};
public static OrderType FromDbString(string v) => v switch
{
"LAB" => OrderType.Lab,
"IMAGING" => OrderType.Imaging,
"MEDICATION" => OrderType.Medication,
"PROCEDURE" => OrderType.Procedure,
_ => throw new ArgumentOutOfRangeException(nameof(v), $"Unknown order type: '{v}'")
};
}
@@ -0,0 +1,25 @@
public enum ReconciliationCheckType
{
UnacknowledgedCriticalAlert,
PendingOrderNoResult,
ActiveInpatientNoObservation
}
public static class ReconciliationCheckTypeExtensions
{
public static string ToDbString(this ReconciliationCheckType t) => t switch
{
ReconciliationCheckType.UnacknowledgedCriticalAlert => "UNACKNOWLEDGED_CRITICAL_ALERT",
ReconciliationCheckType.PendingOrderNoResult => "PENDING_ORDER_NO_RESULT",
ReconciliationCheckType.ActiveInpatientNoObservation => "ACTIVE_INPATIENT_NO_OBSERVATION",
_ => throw new ArgumentOutOfRangeException(nameof(t))
};
public static ReconciliationCheckType FromDbString(string v) => v switch
{
"UNACKNOWLEDGED_CRITICAL_ALERT" => ReconciliationCheckType.UnacknowledgedCriticalAlert,
"PENDING_ORDER_NO_RESULT" => ReconciliationCheckType.PendingOrderNoResult,
"ACTIVE_INPATIENT_NO_OBSERVATION" => ReconciliationCheckType.ActiveInpatientNoObservation,
_ => throw new ArgumentOutOfRangeException(nameof(v), $"Unknown reconciliation check type: '{v}'")
};
}
@@ -0,0 +1,11 @@
using System.Text.Json;
using System.Text.Json.Serialization;
public sealed class ObservationSourceJsonConverter : JsonConverter<ObservationSource>
{
public override ObservationSource Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> ObservationSourceExtensions.FromDbString(reader.GetString()!);
public override void Write(Utf8JsonWriter writer, ObservationSource value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToDbString());
}