feature: Ward Gateway Service (Local-First Clinical Path)

This commit is contained in:
voltsrage
2026-06-23 16:45:38 +08:00
parent d8e142fffe
commit 1bf8359097
100 changed files with 5474 additions and 4 deletions
@@ -0,0 +1,12 @@
public class BufferedSyncItem
{
public Guid Id { get; set; }
public BufferedSyncItemType ItemType { get; set; }
public string Payload { get; set; } = null!;
public string IdempotencyKey { get; set; } = null!;
public Guid EncounterId { get; set; }
public DateTimeOffset RecordedAt { get; set; }
public bool Synced { get; set; }
public DateTimeOffset? SyncedAt { get; set; }
public DateTimeOffset CreatedAt { get; set; }
}
@@ -0,0 +1,6 @@
public class GatewaySyncState
{
public Guid Id { get; set; }
public DateTimeOffset? LastEncounterSyncAt { get; set; }
public bool InitialSyncCompleted { get; set; }
}
@@ -0,0 +1,19 @@
public class LocalClinicalAlert
{
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 string? ObservationCode { get; set; }
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 Guid ClientAlertId { get; set; }
public ReplicaEncounter Encounter { get; set; } = null!;
}
@@ -0,0 +1,14 @@
public class LocalObservation
{
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 ReplicaEncounter Encounter { get; set; } = null!;
}
@@ -0,0 +1,13 @@
public class ReplicaAlertThreshold
{
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 int? SuppressionWindowMinutes { get; set; }
public DateTimeOffset SyncedAt { get; set; }
}
@@ -0,0 +1,16 @@
public class ReplicaEncounter
{
public Guid Id { get; set; }
public Guid PatientId { get; set; }
public EncounterType EncounterType { get; set; } = EncounterType.Inpatient;
public EncounterStatus Status { get; set; }
public Department Department { get; set; }
public string AttendingPhysician { get; set; } = null!;
public string? RoomBed { get; set; }
public DateTimeOffset AdmittedAt { get; set; }
public DateTimeOffset SyncedAt { get; set; }
public ReplicaPatient Patient { get; set; } = null!;
public ICollection<LocalObservation> Observations { get; set; } = new List<LocalObservation>();
public ICollection<LocalClinicalAlert> Alerts { get; set; } = new List<LocalClinicalAlert>();
}
@@ -0,0 +1,12 @@
public class ReplicaPatient
{
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 DateTimeOffset SyncedAt { get; set; }
public ICollection<ReplicaEncounter> Encounters { get; set; } = new List<ReplicaEncounter>();
}
@@ -0,0 +1,8 @@
public class SyncOutboxEntry
{
public Guid Id { get; set; }
public Guid? BatchId { get; set; }
public SyncOutboxStatus Status { get; set; } = SyncOutboxStatus.Pending;
public DateTimeOffset CreatedAt { get; set; }
public DateTimeOffset? SubmittedAt { 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,225 @@
public enum AlertType
{
[Obsolete("Legacy — replaced by SOFA_SEPSIS in Phase 27. Retained for historical alert queries.")]
SepsisWarning,
CriticalHeartRate,
CriticalTempC,
CriticalPotassiumMeqL,
CriticalSpo2,
CriticalRespRate,
CriticalWbcKUl,
CriticalSystolicBp,
CriticalDiastolicBp,
CriticalLactateMmolL,
CriticalAvpu,
CriticalGlucoseMgDl,
// New — warning-level threshold alerts
WarningHeartRate,
WarningTempC,
WarningPotassiumMeqL,
WarningSpo2,
WarningRespRate,
WarningWbcKUl,
WarningSystolicBp,
WarningDiastolicBp,
WarningLactateMmolL,
WarningGlucoseMgDl,
News2Warning,
News2Emergency,
RapidDeterioration,
[Obsolete("Legacy — replaced by QSOFA_SCREEN in Phase 27. Retained for historical alert queries.")]
QsofaWarning,
QsofaScreen,
GcsCritical,
GcsWarning,
CriticalPao2MmHg,
WarningPao2MmHg,
CriticalPlateletKUl,
WarningPlateletKUl,
CriticalBilirubinMgDl,
WarningBilirubinMgDl,
CriticalCreatinineMgDl,
WarningCreatinineMgDl,
SofaSepsis,
SofaWarning,
}
public static class AlertTypeExtensions
{
#pragma warning disable CS0618 // Legacy alert types retained for historical DB strings
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",
AlertType.CriticalSystolicBp => "CRITICAL_SYSTOLIC_BP",
AlertType.CriticalDiastolicBp => "CRITICAL_DIASTOLIC_BP",
AlertType.CriticalLactateMmolL => "CRITICAL_LACTATE_MMOL_L",
AlertType.CriticalAvpu => "CRITICAL_AVPU",
AlertType.CriticalGlucoseMgDl => "CRITICAL_GLUCOSE_MG_DL",
AlertType.WarningHeartRate => "WARNING_HEART_RATE",
AlertType.WarningTempC => "WARNING_TEMP_C",
AlertType.WarningPotassiumMeqL => "WARNING_POTASSIUM_MEQ_L",
AlertType.WarningSpo2 => "WARNING_SPO2",
AlertType.WarningRespRate => "WARNING_RESP_RATE",
AlertType.WarningWbcKUl => "WARNING_WBC_K_UL",
AlertType.WarningSystolicBp => "WARNING_SYSTOLIC_BP",
AlertType.WarningDiastolicBp => "WARNING_DIASTOLIC_BP",
AlertType.WarningLactateMmolL => "WARNING_LACTATE_MMOL_L",
AlertType.WarningGlucoseMgDl => "WARNING_GLUCOSE_MG_DL",
AlertType.News2Warning => "NEWS2_WARNING",
AlertType.News2Emergency => "NEWS2_EMERGENCY",
AlertType.RapidDeterioration => "RAPID_DETERIORATION",
AlertType.QsofaWarning => "QSOFA_WARNING",
AlertType.GcsCritical => "GCS_CRITICAL",
AlertType.GcsWarning => "GCS_WARNING",
AlertType.CriticalPao2MmHg => "CRITICAL_PAO2_MMHG",
AlertType.WarningPao2MmHg => "WARNING_PAO2_MMHG",
AlertType.CriticalPlateletKUl => "CRITICAL_PLATELET_K_UL",
AlertType.WarningPlateletKUl => "WARNING_PLATELET_K_UL",
AlertType.CriticalBilirubinMgDl => "CRITICAL_BILIRUBIN_MG_DL",
AlertType.WarningBilirubinMgDl => "WARNING_BILIRUBIN_MG_DL",
AlertType.CriticalCreatinineMgDl => "CRITICAL_CREATININE_MG_DL",
AlertType.WarningCreatinineMgDl => "WARNING_CREATININE_MG_DL",
AlertType.SofaSepsis => "SOFA_SEPSIS",
AlertType.SofaWarning => "SOFA_WARNING",
AlertType.QsofaScreen => "QSOFA_SCREEN",
_ => throw new ArgumentOutOfRangeException(nameof(t))
};
#pragma warning restore CS0618
#pragma warning disable CS0618 // Legacy alert types retained for historical DB strings
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,
"CRITICAL_SYSTOLIC_BP" => AlertType.CriticalSystolicBp,
"CRITICAL_DIASTOLIC_BP" => AlertType.CriticalDiastolicBp,
"CRITICAL_LACTATE_MMOL_L" => AlertType.CriticalLactateMmolL,
"CRITICAL_AVPU" => AlertType.CriticalAvpu,
"CRITICAL_GLUCOSE_MG_DL" => AlertType.CriticalGlucoseMgDl,
"WARNING_HEART_RATE" => AlertType.WarningHeartRate,
"WARNING_TEMP_C" => AlertType.WarningTempC,
"WARNING_POTASSIUM_MEQ_L" => AlertType.WarningPotassiumMeqL,
"WARNING_SPO2" => AlertType.WarningSpo2,
"WARNING_RESP_RATE" => AlertType.WarningRespRate,
"WARNING_WBC_K_UL" => AlertType.WarningWbcKUl,
"WARNING_SYSTOLIC_BP" => AlertType.WarningSystolicBp,
"WARNING_DIASTOLIC_BP" => AlertType.WarningDiastolicBp,
"WARNING_LACTATE_MMOL_L" => AlertType.WarningLactateMmolL,
"WARNING_GLUCOSE_MG_DL" => AlertType.WarningGlucoseMgDl,
"NEWS2_WARNING" => AlertType.News2Warning,
"NEWS2_EMERGENCY" => AlertType.News2Emergency,
"RAPID_DETERIORATION" => AlertType.RapidDeterioration,
"QSOFA_WARNING" => AlertType.QsofaWarning,
"QSOFA_SCREEN" => AlertType.QsofaScreen,
"GCS_CRITICAL" => AlertType.GcsCritical,
"GCS_WARNING" => AlertType.GcsWarning,
"CRITICAL_PAO2_MMHG" => AlertType.CriticalPao2MmHg,
"WARNING_PAO2_MMHG" => AlertType.WarningPao2MmHg,
"CRITICAL_PLATELET_K_UL" => AlertType.CriticalPlateletKUl,
"WARNING_PLATELET_K_UL" => AlertType.WarningPlateletKUl,
"CRITICAL_BILIRUBIN_MG_DL" => AlertType.CriticalBilirubinMgDl,
"WARNING_BILIRUBIN_MG_DL" => AlertType.WarningBilirubinMgDl,
"CRITICAL_CREATININE_MG_DL" => AlertType.CriticalCreatinineMgDl,
"WARNING_CREATININE_MG_DL" => AlertType.WarningCreatinineMgDl,
"SOFA_SEPSIS" => AlertType.SofaSepsis,
"SOFA_WARNING" => AlertType.SofaWarning,
_ => throw new ArgumentOutOfRangeException(nameof(v), $"Unknown alert type: '{v}'")
};
#pragma warning restore CS0618
// 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,
"SYSTOLIC_BP" => AlertType.CriticalSystolicBp,
"DIASTOLIC_BP" => AlertType.CriticalDiastolicBp,
"LACTATE_MMOL_L" => AlertType.CriticalLactateMmolL,
"AVPU" => AlertType.CriticalAvpu,
"GLUCOSE_MG_DL" => AlertType.CriticalGlucoseMgDl,
"PAO2_MMHG" => AlertType.CriticalPao2MmHg,
"PLATELET_K_UL" => AlertType.CriticalPlateletKUl,
"BILIRUBIN_MG_DL" => AlertType.CriticalBilirubinMgDl,
"CREATININE_MG_DL" => AlertType.CriticalCreatinineMgDl,
_ => throw new ArgumentOutOfRangeException(
nameof(observationCode), $"No critical alert type for observation code '{observationCode}'")
};
public static AlertType WarningFor(string observationCode) => observationCode switch
{
"HEART_RATE" => AlertType.WarningHeartRate,
"TEMP_C" => AlertType.WarningTempC,
"POTASSIUM_MEQ_L" => AlertType.WarningPotassiumMeqL,
"SPO2" => AlertType.WarningSpo2,
"RESP_RATE" => AlertType.WarningRespRate,
"WBC_K_UL" => AlertType.WarningWbcKUl,
"SYSTOLIC_BP" => AlertType.WarningSystolicBp,
"DIASTOLIC_BP" => AlertType.WarningDiastolicBp,
"LACTATE_MMOL_L" => AlertType.WarningLactateMmolL,
"GLUCOSE_MG_DL" => AlertType.WarningGlucoseMgDl,
"PAO2_MMHG" => AlertType.WarningPao2MmHg,
"PLATELET_K_UL" => AlertType.WarningPlateletKUl,
"BILIRUBIN_MG_DL" => AlertType.WarningBilirubinMgDl,
"CREATININE_MG_DL" => AlertType.WarningCreatinineMgDl,
_ => throw new ArgumentOutOfRangeException(
nameof(observationCode), $"No warning alert type for observation code '{observationCode}'")
};
#pragma warning disable CS0618 // Legacy alert types retained for historical DB strings
public static bool IsSuppressible(this AlertType t) => t switch
{
AlertType.SepsisWarning or AlertType.News2Emergency => false,
AlertType.CriticalHeartRate or AlertType.CriticalTempC or AlertType.CriticalPotassiumMeqL
or AlertType.CriticalSpo2 or AlertType.CriticalRespRate or AlertType.CriticalWbcKUl
or AlertType.CriticalSystolicBp or AlertType.CriticalDiastolicBp
or AlertType.CriticalLactateMmolL or AlertType.CriticalAvpu
or AlertType.CriticalGlucoseMgDl => false,
AlertType.RapidDeterioration => false,
AlertType.GcsCritical => false, // trajectory alerts are never suppressed
AlertType.SofaSepsis => false,
_ => true // all Warning* types, News2Warning, QsofaScreen, SofaWarning, GcsWarning
};
#pragma warning restore CS0618
public static string? ObservationCodeForWarning(this AlertType t) => t switch
{
AlertType.WarningHeartRate => "HEART_RATE",
AlertType.WarningTempC => "TEMP_C",
AlertType.WarningPotassiumMeqL => "POTASSIUM_MEQ_L",
AlertType.WarningSpo2 => "SPO2",
AlertType.WarningRespRate => "RESP_RATE",
AlertType.WarningWbcKUl => "WBC_K_UL",
AlertType.WarningSystolicBp => "SYSTOLIC_BP",
AlertType.WarningDiastolicBp => "DIASTOLIC_BP",
AlertType.WarningLactateMmolL => "LACTATE_MMOL_L",
AlertType.WarningGlucoseMgDl => "GLUCOSE_MG_DL",
AlertType.WarningPao2MmHg => "PAO2_MMHG",
AlertType.WarningPlateletKUl => "PLATELET_K_UL",
AlertType.WarningBilirubinMgDl => "BILIRUBIN_MG_DL",
AlertType.WarningCreatinineMgDl => "CREATININE_MG_DL",
_ => null
};
}
@@ -0,0 +1,22 @@
public enum BufferedSyncItemType { Observation, Alert, Ack, Resolve }
public static class BufferedSyncItemTypeExtensions
{
public static string ToDbString(this BufferedSyncItemType t) => t switch
{
BufferedSyncItemType.Observation => "OBSERVATION",
BufferedSyncItemType.Alert => "ALERT",
BufferedSyncItemType.Ack => "ACK",
BufferedSyncItemType.Resolve => "RESOLVE",
_ => throw new ArgumentOutOfRangeException(nameof(t))
};
public static BufferedSyncItemType FromDbString(string v) => v switch
{
"OBSERVATION" => BufferedSyncItemType.Observation,
"ALERT" => BufferedSyncItemType.Alert,
"ACK" => BufferedSyncItemType.Ack,
"RESOLVE" => BufferedSyncItemType.Resolve,
_ => throw new ArgumentOutOfRangeException(nameof(v), $"Unknown buffered sync item type: '{v}'")
};
}
@@ -0,0 +1,34 @@
public enum Department
{
Icu,
GeneralMedicine,
Emergency,
Cardiology,
Surgery,
Pediatrics
}
public static class DepartmentExtensions
{
public static string ToDbString(this Department d) => d switch
{
Department.Icu => "ICU",
Department.GeneralMedicine => "GENERAL_MEDICINE",
Department.Emergency => "EMERGENCY",
Department.Cardiology => "CARDIOLOGY",
Department.Surgery => "SURGERY",
Department.Pediatrics => "PEDIATRICS",
_ => throw new ArgumentOutOfRangeException(nameof(d))
};
public static Department FromDbString(string v) => v switch
{
"ICU" => Department.Icu,
"GENERAL_MEDICINE" => Department.GeneralMedicine,
"EMERGENCY" => Department.Emergency,
"CARDIOLOGY" => Department.Cardiology,
"SURGERY" => Department.Surgery,
"PEDIATRICS" => Department.Pediatrics,
_ => throw new ArgumentOutOfRangeException(nameof(v), $"Unknown department: '{v}'")
};
}
@@ -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,18 @@
public enum SyncOutboxStatus { Pending, Submitted }
public static class SyncOutboxStatusExtensions
{
public static string ToDbString(this SyncOutboxStatus s) => s switch
{
SyncOutboxStatus.Pending => "PENDING",
SyncOutboxStatus.Submitted => "SUBMITTED",
_ => throw new ArgumentOutOfRangeException(nameof(s))
};
public static SyncOutboxStatus FromDbString(string v) => v switch
{
"PENDING" => SyncOutboxStatus.Pending,
"SUBMITTED" => SyncOutboxStatus.Submitted,
_ => throw new ArgumentOutOfRangeException(nameof(v), $"Unknown sync outbox status: '{v}'")
};
}
@@ -0,0 +1,11 @@
using System.Text.Json;
using System.Text.Json.Serialization;
public sealed class DepartmentJsonConverter : JsonConverter<Department>
{
public override Department Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
=> DepartmentExtensions.FromDbString(reader.GetString()!);
public override void Write(Utf8JsonWriter writer, Department value, JsonSerializerOptions options)
=> writer.WriteStringValue(value.ToDbString());
}