Files

610 lines
26 KiB
C#

using adas_core.Application.Exceptions;
using adas_core.Application.Repositories.Interfaces;
using adas_core.Application.Services.Interfaces;
using adas_core.Application.Subscriptions;
using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.Filter;
using adas_core.Domain.Models.MongoModels;
using adas_core.Domain.Models.Pumps;
using adas_core.Domain.Models.Responses;
using adas_core.Domain.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using Patient = adas_core.Domain.Models.MongoModels.Patient;
namespace adas_core.Application.Services
{
/// <summary>
/// Servicio maestro de gestión de bombas
/// - Procesa ApiRequest (HL7/Alaris transformado)
/// - Histórico clínico (pump_observations)
/// - Histórico de alarmas (pump_alarm_events)
/// - Alarmas activas (pump_alarm_state)
/// - Snapshot (pump_state)
/// - Broadcast de snapshots (PumpState + PumpAlarmState)
/// </summary>
public class PumpService(
IPumpObservationRepository pumpObservationRepository,
IPumpStateRepository pumpStateRepo,
IPumpAlarmEventRepository alarmEventRepo,
IPumpAlarmStateRepository alarmStateRepo,
IPumpArchiveRepository pumpArchiveRepo,
IPatientService patientService,
IConfigPumpsService configPumpsService,
IOptions<ApiSettings> apiSettings,
ILogger<PumpService> logger,
ISubscribersService subscribersService,
IClientMessageService clientMessageService,
Lazy<ICalculatedObservationsService> calculatedObservationsService,
IHttpContextAccessor httpContextAccessor,
ILocalAuditService auditService,
IConfigUnitsService configUnitsService)
: IPumpService
{
// Settings
private readonly int _pumpExpiresSeconds = apiSettings.Value.PumpExpiresSeconds;
private readonly bool _sendPumpsZero = apiSettings.Value.SendPumpsZero;
// ======================================================================
// ENTRYPOINT
// ======================================================================
public async Task SaveRequest(ApiRequest req)
{
// Normalizar single vs list (Alaris puede mandar 1 sola)
if (req.PumpObservation != null && (req.PumpObservations == null || req.PumpObservations.Count == 0))
req.PumpObservations = [req.PumpObservation];
if (req.PumpObservations == null || req.PumpObservations.Count == 0)
{
logger.LogWarning("ApiRequest contains 0 PumpObservations");
return;
}
// Busca paciente (lookup/create) a partir de PatientNumber / Patient / PatientId string
var resolvedPatient = await patientService.FindPatientByApiRequest(req);
var foundPatientId = resolvedPatient?.Id;
foreach (var pobs in req.PumpObservations)
{
UpdatePatientFromRequest(req, pobs, foundPatientId);
// Regla Alaris: si el origen es AlarisPump y no hay PatientId -> descartar
if (string.Equals(req.Type, "AlarisPump", StringComparison.OrdinalIgnoreCase) &&
pobs.PatientId == null)
{
logger.LogWarning("AlarisPump: Observation descartada por ausencia de PatientId. DeviceId={device}", pobs.DeviceId);
continue;
}
if (pobs.Time == DateTime.MinValue)
pobs.Time = DateTime.UtcNow;
try
{
// 1) Procesar por tipo HL7 o según ObservationType (Alaris)
switch (req.Type)
{
case "ORU_R01": // PCD-01
case "ORU_R42": // PCD-10
pobs.MessageType = PumpEnum.PumpMessageType.Observation;
await ProcessObservation(pobs);
break;
case "ORU_R40": // PCD-04
pobs.MessageType = PumpEnum.PumpMessageType.Alarm;
await ProcessAlarm(pobs);
break;
case "AlarisPump":
if (pobs.MessageType == PumpEnum.PumpMessageType.Alarm)
await ProcessAlarm(pobs);
else
await ProcessObservation(pobs);
break;
default:
logger.LogWarning("Tipo de request desconocido para PumpService: {type}", req.Type);
break;
}
// 2) Snapshot de bomba
var state = await UpdatePumpState(pobs);
// 3) Alarmas activas del dispositivo
var activeAlarms = await alarmStateRepo.FindAllActiveByDeviceAsync(pobs.DeviceId!);
// 4) Broadcast de snapshots (PumpState + todas las PumpAlarmState)
await SendSnapshotsBroadcast(state, activeAlarms, pobs.PatientId ?? foundPatientId, req);
// 5) Retención (sobre histórico de observaciones) - reglas
await DoRetentionActions(pobs);
}
catch (Exception ex)
{
logger.LogError(ex, "Error procesando observation/alarm DeviceId={deviceId}", pobs.DeviceId);
}
}
}
// ======================================================================
// OBSERVACIONES (PCD-01 / PCD-10)
// ======================================================================
private async Task ProcessObservation(PumpObservation obs)
{
logger.LogDebug("Insertando OBSERVATION DeviceId={dev} Time={time}", obs.DeviceId, obs.Time);
obs.Id = ObjectId.GenerateNewId();
obs.Expires = _pumpExpiresSeconds;
var mapped = await MapPumpObservation(obs);
if (mapped != null)
{
await pumpObservationRepository.InsertAsync(mapped);
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, mapped);
}
}
// ======================================================================
// ALARMAS (PCD-04)
// ======================================================================
private async Task ProcessAlarm(PumpObservation obs)
{
logger.LogDebug("Insertando ALARM DeviceId={dev}, Phase={phase}, Type={type}",
obs.DeviceId, obs.EventPhase, obs.AlarmType);
var alarmEvent = new PumpAlarmEvent
{
Id = ObjectId.GenerateNewId(),
DeviceId = obs.DeviceId,
RackId = obs.RackId,
DeviceTypeMdc = obs.DeviceTypeMdc,
DeviceIp = obs.DeviceIp,
PillarAssembly = obs.PillarAssembly,
PillarRackSlot = obs.PillarRackSlot,
Time = obs.Time,
InfusionId = obs.InfusionId,
AlarmType = obs.AlarmType,
AlarmTypeMdc = obs.AlarmTypeMdc,
AlarmDescription = obs.AlarmDescription,
AlarmPriority = obs.AlarmPriority,
AlarmState = obs.AlarmState,
AlarmInactivationState = obs.AlarmInactivationState,
EventPhase = obs.EventPhase,
AlertSourceMdc = obs.AlertSourceMdc,
PatientId = obs.PatientId
};
await alarmEventRepo.InsertAsync(alarmEvent);
await UpdateAlarmState(obs);
}
private async Task UpdateAlarmState(PumpObservation obs)
{
if (obs.EventPhase == null) return;
var phaseLower = obs.EventPhase.Value.ToString().ToLowerInvariant();
// Cierre
if (phaseLower == "end")
{
await alarmStateRepo.RemoveAsync(obs.DeviceId!, obs.AlarmType, obs.AlarmTypeMdc);
return;
}
// Start/continue → upsert
var state = new PumpAlarmState
{
Id = ObjectId.GenerateNewId(),
DeviceId = obs.DeviceId,
AlarmType = obs.AlarmType,
AlarmCodeMdc = obs.AlarmTypeMdc,
AlarmDescription = obs.AlarmDescription,
AlarmPriority = obs.AlarmPriority,
AlarmState = obs.AlarmState,
LastPhase = obs.EventPhase,
FirstSeen = DateTime.UtcNow,
LastUpdated = DateTime.UtcNow,
AlertSourceMdc = obs.AlertSourceMdc,
InfusionId = obs.InfusionId,
PatientId = obs.PatientId
};
await alarmStateRepo.UpsertActiveAsync(state);
}
// ======================================================================
// SNAPSHOT (devuelve el PumpState actualizado)
// ======================================================================
private async Task<PumpState> UpdatePumpState(PumpObservation obs)
{
var current = await pumpStateRepo.FindByDeviceIdAsync(obs.DeviceId!)
?? new PumpState
{
Id = ObjectId.GenerateNewId(),
DeviceId = obs.DeviceId
};
MergePumpState(current, obs);
current.LastUpdated = DateTime.UtcNow;
await pumpStateRepo.UpsertAsync(current);
return current;
}
private static void MergePumpState(PumpState state, PumpObservation obs)
{
// Identidad / físico
if (!string.IsNullOrWhiteSpace(obs.RackId)) state.RackId = obs.RackId;
if (!string.IsNullOrWhiteSpace(obs.DeviceTypeMdc)) state.DeviceTypeMdc = obs.DeviceTypeMdc;
if (!string.IsNullOrWhiteSpace(obs.DeviceIp)) state.DeviceIp = obs.DeviceIp;
if (!string.IsNullOrWhiteSpace(obs.PillarAssembly)) state.PillarAssembly = obs.PillarAssembly;
if (!string.IsNullOrWhiteSpace(obs.PillarRackSlot)) state.PillarRackSlot = obs.PillarRackSlot;
// Infusión
if (!string.IsNullOrWhiteSpace(obs.InfusionId)) state.InfusionId = obs.InfusionId;
// Estado
if (obs.InfusingStatus != null)
{
state.InfusingStatus = obs.InfusingStatus;
state.IsInfusing = obs.IsInfusing ?? false;
}
if (obs.Status != null) state.Status = obs.Status;
if (obs.PumpMode != null) state.PumpMode = obs.PumpMode;
if (!string.IsNullOrWhiteSpace(obs.ActiveSourceInfo)) state.ActiveSourceInfo = obs.ActiveSourceInfo;
if (!string.IsNullOrWhiteSpace(obs.InfusionModeDetail)) state.InfusionModeDetail = obs.InfusionModeDetail;
if (!string.IsNullOrWhiteSpace(obs.NotDeliveringReason)) state.NotDeliveringReason = obs.NotDeliveringReason;
if (!string.IsNullOrWhiteSpace(obs.Source)) state.Source = obs.Source;
// Métricas
if (HasValue(obs.FlowFluid)) state.FlowFluid = obs.FlowFluid;
if (HasValue(obs.Rate)) state.Rate = obs.Rate;
if (HasValue(obs.VolumeInfused)) state.VolumeInfused = obs.VolumeInfused;
if (HasValue(obs.FluidDelivTotal)) state.FluidDelivTotal = obs.FluidDelivTotal;
if (HasValue(obs.FluidDelivTotalSet)) state.FluidDelivTotalSet = obs.FluidDelivTotalSet;
if (HasValue(obs.VolumeRemaining)) state.VolumeRemaining = obs.VolumeRemaining;
if (HasValue(obs.Vtbi)) state.Vtbi = obs.Vtbi;
if (HasValue(obs.TimeRemaining)) state.TimeRemaining = obs.TimeRemaining;
if (HasValue(obs.TimeProgrammed)) state.TimeProgrammed = obs.TimeProgrammed;
// Medicación
if (!string.IsNullOrWhiteSpace(obs.DrugName)) state.DrugName = obs.DrugName;
if (!string.IsNullOrWhiteSpace(obs.DrugId)) state.DrugId = obs.DrugId;
if (HasValue(obs.Concentration)) state.Concentration = obs.Concentration;
if (HasValue(obs.DoseRate)) state.DoseRate = obs.DoseRate;
if (HasValue(obs.DrugAmount)) state.DrugAmount = obs.DrugAmount;
if (HasValue(obs.DrugDoseDelivered)) state.DrugDoseDelivered = obs.DrugDoseDelivered;
if (HasValue(obs.PatientWeight)) state.PatientWeight = obs.PatientWeight;
if (obs.Syringe != null) state.Syringe = obs.Syringe;
// Eventos / Alarmas resumen
if (obs.Event != null) state.Event = obs.Event;
if (obs.EventPhase != null) state.EventPhase = obs.EventPhase;
if (obs.AlarmType != null) state.AlarmType = obs.AlarmType;
if (!string.IsNullOrWhiteSpace(obs.AlarmDescription)) state.AlarmDescription = obs.AlarmDescription;
if (!string.IsNullOrWhiteSpace(obs.AlarmState)) state.AlarmState = obs.AlarmState;
if (!string.IsNullOrWhiteSpace(obs.AlarmInactivationState)) state.AlarmInactivationState = obs.AlarmInactivationState;
if (!string.IsNullOrWhiteSpace(obs.AlarmPriority)) state.AlarmPriority = obs.AlarmPriority;
if (!string.IsNullOrWhiteSpace(obs.AlarmTypeMdc)) state.AlarmCodeMdc = obs.AlarmTypeMdc;
// Paciente
state.PatientId = obs.PatientId;
}
private static bool HasValue(CommonPumpTypes.PumpValue? v) => v is { Value: not null };
// MAP
public async Task<PumpObservation?> MapPumpObservation(PumpObservation obs)
{
var obs2 = await configPumpsService.Map(obs);
var obs3 = await configUnitsService.Map(obs2);
var obs4 = await calculatedObservationsService.Value.Map(obs3);
if (obs4 == null)
logger.LogDebug("Mapping ignorado para obs");
return obs3;
}
//Métodos
public Task SaveRequestAsync(ApiRequest req) => SaveRequest(req);
// Últimas N observaciones por paciente
public async Task<List<PumpObservation>> FindLastPumpObservations(ObjectId patientId, int num = 1)
{
var list = await pumpObservationRepository.FindByPatientId(patientId);
if (list is List<PumpObservation> pumpObservations)
return pumpObservations is { Count: 0 }
? []
: pumpObservations.OrderByDescending(x => x.Time).Take(num).ToList();
return [];
}
// Última fecha de observación por paciente (para todos los pacientes)
public async Task<Dictionary<ObjectId, DateTime>> FindAllLastPatientObservationTime()
{
return await pumpObservationRepository.FindAllLastPatientObservationTimeAsync();
}
// Borrado completo por paciente (observaciones + alarmas + alarmState)
public async Task DeleteByPatientId(ObjectId id)
{
logger.LogDebug("Delete Pump data by Patient Id {id}", id);
await pumpObservationRepository.DeleteByPatientId(id);
await alarmEventRepo.DeleteByPatientId(id);
await alarmStateRepo.DeleteByPatientId(id);
}
// Archivo → por entidad paciente
public async Task Archive(Patient patient) => await ArchiveByPatientId(patient.Id);
// Archivo → por PatientId (mueve a archive_pumpobservations y elimina del activo)
public async Task ArchiveByPatientId(ObjectId id)
{
var list = await pumpObservationRepository.FindByPatientId(id);
var pumpObservations = list.ToList();
if (pumpObservations.Count != 0)
await pumpArchiveRepo.InsertManyAsync(pumpObservations);
await DeleteByPatientId(id);
logger.LogDebug("Archived Pump observations & deleted active data by Patient Id {id}", id);
}
// Actualización masiva
public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId)
{
if (string.IsNullOrWhiteSpace(nameId))
throw new ArgumentException("nameId no puede ser nulo o vacío.", nameof(nameId));
var updatedObs = await pumpObservationRepository.UpdateManyObjectIdByFieldAsync(nameId, id, oldId);
// actualizar también alarmas activas e históricas
_ = await alarmEventRepo.UpdateManyObjectIdByFiledNameAsync(nameId, id, oldId);
_ = await alarmStateRepo.UpdateManyObjectIdByFieldNameAsync(nameId, id, oldId);
logger.LogInformation(
"UpdateManyObjectId completado. Campo={field}, oldId={oldId}, newId={newId}. Obs actualizadas={obsUpdated}",
nameId, oldId, id, updatedObs);
// Si quieres auditar el cambio:
await auditService.CreateAuditLogAsync(
httpContextAccessor.HttpContext?.User!,
new { Field = nameId, OldId = oldId, NewId = id, Scope = "PumpObservation" },
null);
}
// ConfigPumpsService
public async Task<List<ConfigPumpItem>?> GetItemsById(string id)
=> await configPumpsService.GetConfigItems(id);
public async Task<List<ConfigPumps>?> GetAllPumpConfig()
=> await configPumpsService.GetAllPumpConfigs()
?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
public async Task<ConfigPumps?> GetPumpConfigsById(string id)
=> await configPumpsService.GetPumpConfigById(id)
?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
public async Task<ConfigPumps?> UpdatePumpConfig(ConfigPumps config)
{
var oldConfig = await configPumpsService.GetPumpConfigById(config.Id);
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldConfig, config);
return await configPumpsService.UpdatePumpConfig(config)
?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
}
public async Task<ConfigPumps?> InsertPumpConfig(ConfigPumps config)
{
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, config);
return await configPumpsService.InsertPumpConfig(config)
?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed);
}
public async Task<bool> DeletePumpConfig(ConfigPumps config)
{
var result = await configPumpsService.DeletePumpConfig(config);
if (!result) throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed);
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, config, null);
return result;
}
// Paginación de observaciones
public async Task<PaginationResponse<PumpObservation>?> GetPaginatedPump(PaginationFilter filter)
{
// Implementación compatible sin nuevos métodos en los repos:
// 1) Si llega PatientId, paginamos en memoria desde FindByPatientId.
// 2) Si llega DeviceId, usamos FindByDeviceIdAsync y paginamos en memoria.
// 3) Si no hay filtro, devolvemos vacío para evitar lecturas completas.
var page = filter.PageNumber <= 0 ? 1 : filter.PageNumber;
var size = filter.PageSize <= 0 ? 20 : filter.PageSize;
var fr = filter.FilteredRequest;
if (fr == null) return new PaginationResponse<PumpObservation>([], page, size, 0);
List<PumpObservation> all;
if (!string.IsNullOrWhiteSpace(fr.PatientId))
{
if (!ObjectId.TryParse(fr.PatientId, out var patientId))
return new PaginationResponse<PumpObservation>([], page, size, 0);
var list = await pumpObservationRepository.FindByPatientId(patientId);
all = list.ToList();
if (fr.StartDate.HasValue)
all = all.Where(o => o.Time >= fr.StartDate.Value).ToList();
if (fr.EndDate.HasValue)
all = all.Where(o => o.Time <= fr.EndDate.Value).ToList();
}
else if (!string.IsNullOrWhiteSpace(fr.DeviceId))
{
var found = await pumpObservationRepository.FindByDeviceIdAsync(fr.DeviceId, fr.StartDate, fr.EndDate);
all = found.ToList();
}
else
{
all = [];
}
var count = all.Count;
var pageData = all
.OrderByDescending(o => o.Time)
.Skip((page - 1) * size)
.Take(size)
.ToList();
return new PaginationResponse<PumpObservation>(pageData, page, size, count);
}
// Inserción manual
public async Task InsertPumpObservation(PumpObservation obs)
{
logger.LogDebug("Insert {obs}", obs);
obs.Id = ObjectId.GenerateNewId();
if (obs.Time == DateTime.MinValue) obs.Time = DateTime.UtcNow;
var obsMapped = await MapPumpObservation(obs);
if (obsMapped != null)
{
await pumpObservationRepository.InsertAsync(obsMapped);
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, obsMapped);
if (!_sendPumpsZero && obsMapped.Number == 0) return;
// Tras inserción manual, actualizar y emitir snapshots
var state = await UpdatePumpState(obsMapped);
var activeAlarms = await alarmStateRepo.FindAllActiveByDeviceAsync(obsMapped.DeviceId!);
await SendSnapshotsBroadcast(state, activeAlarms, obsMapped.PatientId, null);
await DoRetentionActions(obsMapped);
}
}
private async Task DoRetentionActions(PumpObservation obs)
{
var result = await configPumpsService.RetentionActions(obs);
if (result is not { RetentionPolicyValue: not null })
return;
switch (result.RetentionPolicy)
{
case RetentionPolicy.DeleteOlderDays:
{
var removed = await pumpObservationRepository.DeleteOlderThanDaysAsync(
result.RetentionPolicyValue.Value);
logger.LogInformation(
"Retention DeleteOlderDays: {removed} deleted (>{days} days)",
removed, result.RetentionPolicyValue);
break;
}
case RetentionPolicy.DeleteOlderNumber:
{
var removed = await pumpObservationRepository.DeleteKeepLastNAsync(
result.RetentionPolicyValue.Value);
logger.LogInformation(
"Retention DeleteOlderNumber: {removed} deleted (keeping {max})",
removed, result.RetentionPolicyValue);
break;
}
case RetentionPolicy.NoDelete:
case RetentionPolicy.DeleteOlderSeconds:
default:
logger.LogWarning("Unknown retention policy: {policy}", result.RetentionPolicy);
break;
}
}
// BROADCAST SNAPSHOTS: PumpState + PumpAlarmState (activos)
private async Task SendSnapshotsBroadcast(
PumpState state,
IEnumerable<PumpAlarmState> activeAlarms,
ObjectId? patientId,
ApiRequest? req)
{
var subscribers = new List<WsSubscriber>();
if (patientId != null)
{
var patient = await patientService.FindById(patientId.Value);
if (patient != null)
{
subscribers = subscribersService.GetSubscribers()
.Where(s => !s.Locations.IsNullOrEmpty()
&& s.Locations.Any(c =>
c.UnitName == patient.Location.UnitName &&
c.Bed == patient.Location.Bed &&
c.Room == patient.Location.Room))
.ToList();
}
}
else if (req?.Location != null)
{
var loc = req.Location;
subscribers = subscribersService.GetSubscribers()
.Where(s => !s.Locations.IsNullOrEmpty()
&& s.Locations.Any(c =>
c.UnitName == loc.UnitName &&
c.Bed == loc.Bed &&
c.Room == loc.Room))
.ToList();
}
if (subscribers.Count == 0) return;
// Enviar PumpState
foreach (var sub in subscribers)
await clientMessageService.SendAsync(sub.Id, OperationType.Pump, state);
// Enviar todas las PumpAlarmState activas
foreach (var alarm in activeAlarms)
{
foreach (var sub in subscribers)
await clientMessageService.SendAsync(sub.Id, OperationType.PumpAlarm, alarm);
}
}
private static void UpdatePatientFromRequest(ApiRequest req, PumpObservation obs, ObjectId? foundPatientId)
{
if (obs.PatientId == null && foundPatientId != null)
obs.PatientId = foundPatientId;
if (obs.PatientId != null || string.IsNullOrWhiteSpace(req.PatientId)) return;
if (ObjectId.TryParse(req.PatientId, out var parsed))
obs.PatientId = parsed;
}
}
}