98 lines
3.8 KiB
C#
98 lines
3.8 KiB
C#
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
|
|
public class EncounterService : IEncounterService
|
|
{
|
|
// Explicit transition matrix. Every allowed move is listed here.
|
|
// Any transition not in this dictionary is illegal and throws ConflictException.
|
|
private static readonly Dictionary<EncounterStatus, HashSet<EncounterStatus>> _allowedTransitions = new()
|
|
{
|
|
[EncounterStatus.Scheduled] = new() { EncounterStatus.Active, EncounterStatus.Cancelled },
|
|
[EncounterStatus.Active] = new() { EncounterStatus.Discharged, EncounterStatus.Cancelled },
|
|
[EncounterStatus.Discharged] = new(),
|
|
[EncounterStatus.Cancelled] = new(),
|
|
};
|
|
|
|
private readonly AppDbContext _db;
|
|
|
|
public EncounterService(AppDbContext db) => _db = db;
|
|
|
|
public async Task<Encounter> GetByIdAsync(Guid id)
|
|
{
|
|
var encounter = await _db.Encounters
|
|
.Include(e => e.Patient)
|
|
.Include(e => e.Observations.OrderByDescending(o => o.RecordedAt).Take(10))
|
|
.Include(e => e.Alerts.Where(a => a.Status == AlertStatus.Open))
|
|
.FirstOrDefaultAsync(e => e.Id == id);
|
|
|
|
if (encounter is null)
|
|
throw new NotFoundException("Encounter not found.", "ENCOUNTER_NOT_FOUND");
|
|
|
|
return encounter;
|
|
}
|
|
|
|
public async Task<EncounterStatusTransitionResult> TransitionStatusAsync(
|
|
Guid encounterId, EncounterStatus targetStatus)
|
|
{
|
|
var encounter = await _db.Encounters.FindAsync(encounterId);
|
|
if (encounter is null)
|
|
throw new NotFoundException("Encounter not found.", "ENCOUNTER_NOT_FOUND");
|
|
|
|
if (!_allowedTransitions[encounter.Status].Contains(targetStatus))
|
|
throw new ConflictException(
|
|
$"Transition to '{targetStatus}' is not permitted from the current status.",
|
|
"ILLEGAL_STATUS_TRANSITION");
|
|
|
|
var previousStatus = encounter.Status;
|
|
encounter.Status = targetStatus;
|
|
|
|
if (targetStatus == EncounterStatus.Discharged)
|
|
encounter.DischargedAt = DateTimeOffset.UtcNow;
|
|
|
|
_db.OutboxEvents.Add(new OutboxEvent
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Topic = "encounter.status.changed",
|
|
Payload = JsonSerializer.Serialize(new
|
|
{
|
|
encounterId,
|
|
patientId = encounter.PatientId,
|
|
previousStatus = previousStatus.ToDbString(),
|
|
newStatus = targetStatus.ToDbString(),
|
|
changedAt = DateTimeOffset.UtcNow
|
|
}),
|
|
PartitionKey = encounterId.ToString(),
|
|
CreatedAt = DateTimeOffset.UtcNow
|
|
});
|
|
|
|
await _db.SaveChangesAsync();
|
|
return new EncounterStatusTransitionResult(encounterId, targetStatus);
|
|
}
|
|
|
|
public async Task<object> GetTimelineAsync(Guid encounterId)
|
|
{
|
|
var encounter = await _db.Encounters.FindAsync(encounterId);
|
|
if (encounter is null)
|
|
throw new NotFoundException("Encounter not found.", "ENCOUNTER_NOT_FOUND");
|
|
|
|
var observations = await _db.Observations
|
|
.Where(o => o.EncounterId == encounterId)
|
|
.OrderByDescending(o => o.RecordedAt)
|
|
.Select(o => new { type = "observation", timestamp = o.RecordedAt, o.ObservationCode, o.Value, o.Unit })
|
|
.ToListAsync();
|
|
|
|
var alerts = await _db.ClinicalAlerts
|
|
.Where(a => a.EncounterId == encounterId)
|
|
.OrderByDescending(a => a.TriggeredAt)
|
|
.Select(a => new { type = "alert", timestamp = a.TriggeredAt, a.AlertType, a.Severity, a.Status })
|
|
.ToListAsync();
|
|
|
|
var timeline = observations.Cast<object>()
|
|
.Concat(alerts.Cast<object>())
|
|
.OrderByDescending(x => (DateTimeOffset)((dynamic)x).timestamp)
|
|
.ToList();
|
|
|
|
return new { encounterId, events = timeline };
|
|
}
|
|
}
|