Files
2026-06-26 10:29:23 +02:00

578 lines
30 KiB
C#

using adas_core.Application.Exceptions;
using adas_core.Application.Repositories.Interfaces;
using adas_core.Application.Services.Interfaces;
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.Masters;
using adas_core.Domain.Models.MongoModels;
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 MongoDB.Driver;
namespace adas_core.Application.Services;
public class PointOfCareService(
ILogger<PointOfCareService> logger,
IPointOfCareRepository pointOfCareRepository,
Lazy<IPatientService> patientService,
Lazy<IUnitService> unitService,
ISubscribersService subscribersService,
Lazy<IClientMessageService> clientMessageService,
Lazy<IAdmissionService> admissionService,
IHttpContextAccessor httpContextAccessor,
ILocalAuditService auditService,
ICacheService cacheService,
IOptions<CacheSettings> cacheSettings)
: IPointOfCareService
{
private readonly CacheSettings? _cacheSettings = cacheSettings.Value;
/// <summary>
/// Inserts a new point of care after validating that the referenced unit exists, and records an audit log entry for the operation. Returns null if the associated unit cannot be found or if an exception is raised during the insertion process.
/// </summary>
/// <param name="pointOfCare">The point of care to be inserted; its <c>UnitId</c> is validated against the existing unit and reassigned to the resolved unit's identifier.</param>
/// <returns>A task that resolves to the newly inserted <see cref="PointOfCare"/>, or <c>null</c> when the referenced unit is not found or the operation fails.</returns>
public async Task<PointOfCare?> InsertPointOfCare(PointOfCare pointOfCare)
{
try
{
var unit = await unitService.Value.FindById(pointOfCare.UnitId);
if (unit == null)
{
logger.LogError("Unit id: {unitId} Not Found. PointOfCare not inserted", pointOfCare.UnitId);
return null;
}
pointOfCare.UnitId = unit.Id;
await pointOfCareRepository.InsertOneAsync(pointOfCare);
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, pointOfCare);
return await FindById(pointOfCare.Id);
}
catch (Exception ex)
{
logger.LogError("Exception inserting pointOfCare: {pointOfCare}, Exception: {ex}", pointOfCare, ex);
return null;
}
}
/// <summary>
/// Deletes a Point of Care record by its identifier, but only when the record has no associated admission.
/// The deletion is skipped if the record cannot be found or if it is linked to an admission.
/// </summary>
/// <param name="id">The unique identifier of the Point of Care record to delete.</param>
public async Task Delete(ObjectId id)
{
var poc = await FindById(id);
if (poc is not { AdmissionId: null }) return;
await pointOfCareRepository.Delete(id);
await cacheService.DeleteByPatternAsync(CacheKeys.PointOfCareBase(id));
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, poc, null);
}
/// <summary>
/// Deletes all Points of Care associated with the specified unit identifier and removes the corresponding cached entries.
/// </summary>
/// <param name="unitId">The identifier of the unit whose Points of Care records will be removed.</param>
public async Task DeletePoCsByUnitId(ObjectId unitId)
{
await pointOfCareRepository.DeleteManyByUnitId(unitId);
await cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.PontOfCare));
}
/// <summary>
/// Asynchronously retrieves the set of camera identifiers that are currently in use by delegating to the point of care repository.
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing a <see cref="HashSet{ObjectId}"/> of camera identifiers in use.</returns>
public async Task<HashSet<ObjectId>> FindAllIdCamerasInUse()
{
return await pointOfCareRepository.FindAllIdCamerasInUse();
}
/// <summary>
/// Asynchronously retrieves the set of all ID relay identifiers currently in use by delegating to the point of care repository.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a <see cref="HashSet{T}"/> of <see cref="ObjectId"/> values representing the ID relays that are in use.</returns>
public async Task<HashSet<ObjectId>> FindAllIdRelaysInUse()
{
return await pointOfCareRepository.FindAllIdRelaysInUse();
}
/// <summary>
/// Asynchronously retrieves the set of beacon identifiers that are currently in use from the point of care repository.
/// </summary>
/// <returns>A task that represents the asynchronous operation. The task result contains a <see cref="HashSet{ObjectId}"/> with the identifiers of all beacons in use.</returns>
public async Task<HashSet<ObjectId>> FindAllIdBeaconsInUse()
{
return await pointOfCareRepository.FindAllIdBeaconsInUse();
}
/// <summary>
/// Asynchronously retrieves all points of care associated with the specified unit, including their associated devices.
/// Returns an empty collection when no results are found by the repository.
/// </summary>
/// <param name="unitId">The unique identifier of the unit whose points of care are being queried.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a collection of <see cref="PointOfCare"/> with their devices, or an empty collection if the repository returns no results.</returns>
public async Task<IEnumerable<PointOfCare>?> FindAllByUnitIdWithDevices(ObjectId unitId)
{
var result = await pointOfCareRepository.FindAllByUnitIdWithDevices(unitId);
return result ?? [];
}
/// <summary>
/// Updates an existing point of care record, refreshes the related cache entries, creates an audit log entry for the change, and broadcasts the update.
/// Returns the updated point of care, or <c>null</c> if the updated record cannot be retrieved after the update.
/// </summary>
/// <param name="pointOfCare">The point of care entity containing the updated information to persist.</param>
/// <returns>The updated <see cref="PointOfCare"/>, or <c>null</c> if the record was not found after the update.</returns>
public async Task<PointOfCare?> Update(PointOfCare pointOfCare)
{
var oldPoc = await pointOfCareRepository.FindById(pointOfCare.Id);
await pointOfCareRepository.Update(pointOfCare);
var updatedPoc = await GetInfo(pointOfCare.Id);
await cacheService.DeleteByPatternAsync(CacheKeys.PointOfCareBase(pointOfCare.Id));
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldPoc, updatedPoc);
if (updatedPoc == null) return null;
SendPointOfCareBroadcast(updatedPoc, OperationType.UpdatedPointOfCare);
return updatedPoc;
}
/// <summary>
/// Updates the unit associated with an existing point of care identified by the given id. If no point of care is found, the method returns without changes; otherwise it persists the new unit, invalidates the related cache entries, records an audit log of the change, and broadcasts the update.
/// </summary>
/// <param name="id">The identifier of the point of care to update.</param>
/// <param name="unit">The unit to assign to the point of care.</param>
public async Task UpdateUnit(ObjectId id, Unit unit)
{
var poc = await FindById(id);
if (poc == null)
return;
await pointOfCareRepository.UpdateUnitId(id, unit);
await cacheService.DeleteByPatternAsync(CacheKeys.PointOfCareBase(id));
var newPoc = await FindById(id);
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, poc, newPoc!);
SendPointOfCareBroadcast(poc, OperationType.UpdatedPointOfCare);
}
/// <summary>
/// Asynchronously retrieves all points of care from the repository, returning an empty list when the repository yields a null result.
/// </summary>
/// <returns>A task representing the asynchronous operation, containing the list of points of care, or an empty list if none are available.</returns>
public async Task<List<PointOfCare>> GetAll()
{
var c = await pointOfCareRepository.GetAll();
return c ?? [];
}
/// <summary>
/// Retrieves all point of care configurations from the repository.
/// Returns an empty list if the repository result is null.
/// </summary>
/// <returns>A list of PointOfCare configurations, or an empty list when no configurations are available.</returns>
public async Task<List<PointOfCare>> GetAllConfigs()
{
var c = await pointOfCareRepository.GetAllConfigs();
return c ?? [];
}
/// <summary>
/// Retrieves all point-of-care location information from the repository.
/// Returns an empty list when the repository yields no results, ensuring callers never receive a null collection.
/// </summary>
/// <returns>A task that resolves to a list of <see cref="PointOfCare"/> entries, or an empty list if no locations are found.</returns>
public async Task<List<PointOfCare>> GetAllLocationInfo()
{
var c = await pointOfCareRepository.GetAllLocationInfo();
return c ?? [];
}
/// <summary>
/// Retrieves a paginated list of Points of Care (PoCs) based on the provided pagination filter, along with the total document count.
/// Applies skip and limit operations to return only the items corresponding to the requested page.
/// </summary>
/// <param name="filter">The pagination filter containing the page number and page size used to compute the skip offset and limit the number of returned items.</param>
/// <returns>A <see cref="PaginationResponse{PointOfCare}"/> containing the requested page of points of care, the current page number, page size, and the total document count.</returns>
public async Task<PaginationResponse<PointOfCare>> GetPaginatedPoCs(PaginationFilter filter)
{
var result = pointOfCareRepository.GetPaginatedPoCs(filter);
var count = await result.CountDocumentsAsync();
var data = await result.Skip((filter.PageNumber - 1) * filter.PageSize)
.Limit(filter.PageSize)
.ToCursorAsync();
var dataList = await data.ToListAsync();
return new PaginationResponse<PointOfCare>(dataList, filter.PageNumber, filter.PageSize, count);
}
/// <summary>
/// Updates the Point of Care configuration for the specified identifier, invalidates the associated cache entries, and records an audit log of the change.
/// </summary>
/// <param name="id">The identifier of the Point of Care configuration to update.</param>
/// <param name="configuration">The new configuration values to persist.</param>
/// <returns>A task that represents the asynchronous update operation.</returns>
/// <exception cref="ConflictException">Thrown when no existing Point of Care configuration is found for the specified identifier.</exception>
public async Task UpdateConfiguration(ObjectId id, PointOfCareConfiguration configuration)
{
var old = await pointOfCareRepository.GetPoCConfiguration(id) ??
throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed);
await pointOfCareRepository.UpdateConfiguration(id, configuration);
await cacheService.DeleteByPatternAsync(CacheKeys.PointOfCareBase(id));
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, old.Configuration,
configuration);
}
/// <summary>
/// Retrieves a point of care by its identifier, including all associated configurations.
/// </summary>
/// <param name="id">The unique identifier of the point of care to retrieve.</param>
/// <returns>The matching <see cref="PointOfCare"/> with all configurations, or <c>null</c> if no point of care is found.</returns>
public async Task<PointOfCare?> FindById(ObjectId id)
{
return await pointOfCareRepository.FindByIdAllConfig(id);
}
/// <summary>
/// Retrieves a point of care entity by its identifier, including all associated configuration data.
/// </summary>
/// <param name="id">The unique identifier of the point of care to retrieve.</param>
/// <returns>The matching <see cref="PointOfCare"/> with all configuration when found; otherwise, <c>null</c>.</returns>
public async Task<PointOfCare?> FindByIdAllConfig(ObjectId id)
{
return await pointOfCareRepository.FindByIdAllConfig(id);
}
/// <summary>
/// Retrieves all points of care associated with the specified unit identifier.
/// Returns an empty collection when the underlying repository yields no results, instead of propagating a null reference.
/// </summary>
/// <param name="unit">The unique identifier of the unit whose points of care should be retrieved.</param>
/// <returns>A task that represents the asynchronous operation, containing a collection of points of care for the given unit, or an empty collection if none are found.</returns>
public async Task<IEnumerable<PointOfCare>?> FindAllByUnitId(ObjectId unit)
{
var result = await pointOfCareRepository.FindAllByUnitId(unit);
return result ?? [];
}
/// <summary>
/// Retrieves the points of care associated with the specified unit that match the given status, optionally excluding virtual points of care.
/// </summary>
/// <param name="unitId">The identifier of the unit whose points of care should be retrieved.</param>
/// <param name="poc">The point of care status used to filter the results.</param>
/// <param name="excludeVirtual">When set to <c>true</c>, virtual points of care are excluded from the returned collection.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains the collection of points of care matching the specified unit and status.</returns>
public async Task<IEnumerable<PointOfCare>> FindByUnitAndStatus(ObjectId unitId, StatusEnum.PointOfCare poc,
bool excludeVirtual = false)
{
return await pointOfCareRepository.FindByUnitAndStatus(unitId, poc, excludeVirtual);
}
/// <summary>
/// Check nex admission when patient has exit from poc
/// </summary>
/// <param name="patientLocation">PoC id</param>
public async void CheckNextAdmission(ObjectId? patientLocation)
{
try
{
if (patientLocation == null) return;
var pocToCheck = await GetInfo(patientLocation.Value, null);
if (pocToCheck != null && pocToCheck.Status != StatusEnum.PointOfCare.Locked)
{
if (pocToCheck.AdmissionId != null)
{
pocToCheck.Status = StatusEnum.PointOfCare.Reserved;
}
else
{
// Sabemos que puede ser una lista, pero limitamos directamente a 1 elemento
var next = await admissionService.Value.GetAdmissionByPointOfCareId(pocToCheck.Id);
var adm = next.FirstOrDefault();
if (adm != null)
{
pocToCheck.Admission = adm;
pocToCheck.AdmissionId = adm.Id;
pocToCheck.Status = StatusEnum.PointOfCare.Reserved;
}
else
{
pocToCheck.Status = StatusEnum.PointOfCare.Available;
}
}
await Update(pocToCheck);
}
}
catch (Exception e)
{
logger.LogError("Error checking next admission for PointOfCare {pocId}: {message}", patientLocation,
e.Message);
}
}
/// <summary>
/// Retrieves a <see cref="PointOfCare"/> by bed and unit identifier. Returns <c>null</c> when the unit identifier is <c>null</c> or the bed is <c>null</c> or empty.
/// </summary>
/// <param name="bed">The bed identifier used to locate the point of care.</param>
/// <param name="unitId">The unit identifier; when <c>null</c>, the method short-circuits and returns <c>null</c>.</param>
/// <returns>A <see cref="Task{T}"/> containing the matching <see cref="PointOfCare"/>, or <c>null</c> if no input is valid or no record is found.</returns>
public async Task<PointOfCare?> FindByBedAndUnitId(string? bed, ObjectId? unitId)
{
if (unitId == null || string.IsNullOrEmpty(bed)) return null;
return await pointOfCareRepository.FindByBedAndUnitId(bed, unitId.Value);
}
/// <summary>
/// Updates the relay configuration for the specified Point of Care, refreshing the related cache and recording an audit log entry when a relay ID list is provided.
/// The update, cache invalidation, and audit logging are only performed when <see cref="PointOfCare.Configuration"/> and its <c>RelayIdList</c> are not null.
/// </summary>
/// <param name="poc">The Point of Care whose relay configuration will be updated.</param>
public async Task UpdateRelayConfig(PointOfCare poc)
{
var oldPoc = await pointOfCareRepository.GetPoCConfiguration(poc.Id);
if (poc.Configuration?.RelayIdList != null)
{
await pointOfCareRepository.UpdateRelayConfig(poc.Id, poc.Configuration.RelayIdList);
await cacheService.DeleteByPatternAsync(CacheKeys.PointOfCareBase(poc.Id));
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldPoc, poc);
}
}
/// <summary>
/// Retrieves a collection of points of care associated with the specified room by delegating to the repository.
/// </summary>
/// <param name="room">The room identifier used to look up matching points of care.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains an <see cref="IEnumerable{PointOfCare}"/> of points of care for the given room, or <c>null</c> if no matching points of care are found.</returns>
public async Task<IEnumerable<PointOfCare>?> FindByRoom(string room)
{
return await pointOfCareRepository.FindByRoom(room);
}
/// <summary>
/// Retrieves the collection of points of care associated with the specified bed.
/// </summary>
/// <param name="bed">The bed identifier used to look up the associated points of care.</param>
/// <returns>A task that returns an <see cref="IEnumerable{PointOfCare}"/> of points of care linked to the given bed, or <c>null</c> if no points of care are found.</returns>
public async Task<IEnumerable<PointOfCare>?> FindByBed(string bed)
{
return await pointOfCareRepository.FindByBed(bed);
}
/// <summary>
/// Retrieves all points of care associated with the specified unit identifiers.
/// </summary>
/// <param name="unitIds">The list of unit identifiers used to filter the points of care.</param>
/// <returns>A task that represents the asynchronous operation, containing the list of matching points of care.</returns>
public async Task<List<PointOfCare>> FindAllByUnitIds(List<ObjectId> unitIds)
{
var filter = Builders<PointOfCare>.Filter.In(p => p.UnitId, unitIds);
return await pointOfCareRepository.FindByFilter(filter);
}
/// <summary>
/// Retrieves a <see cref="PointOfCare"/> by its identifier, enriching it with related unit, patient, and admission data.
/// Returns <c>null</c> when no point of care matches the supplied id. When <paramref name="fillPatientData"/> is <c>true</c>, the associated patient and, if present, admission are loaded and attached to the result; the related unit's name is always resolved when available.
/// </summary>
/// <param name="id">The identifier of the point of care to retrieve.</param>
/// <param name="fillPatientData">If <c>true</c>, loads and attaches the related patient and admission (when an admission id is present) to the returned point of care.</param>
/// <returns>A task that yields the <see cref="PointOfCare"/> with resolved related data, or <c>null</c> if no point of care is found for the given id.</returns>
public async Task<PointOfCare?> GetInfo(ObjectId id, bool fillPatientData = true)
{
var poc = await FindById(id);
if (poc == null) return null;
var unit = await unitService.Value.FindById(poc.UnitId);
if (unit is { Name: not null })
poc.UnitName = unit.Name;
if (fillPatientData)
{
var patient = await patientService.Value.GetByPointOfCare(poc);
if (patient != null)
{
poc.Patientid = patient.Id;
poc.Patient = patient;
}
if (poc.AdmissionId != null)
{
var admission = await admissionService.Value.GetAdmissionByIdAsync(poc.AdmissionId.Value);
if (admission != null)
poc.Admission = admission;
}
}
return poc;
}
/// <summary>
/// Retrieves a <see cref="PointOfCare"/> by its identifier, using a cache-aside strategy, and optionally enriches
/// it with related unit, patient, and admission data. When <paramref name="fillPatientData"/> is <c>false</c> or the
/// point of care is not found, the method returns the cached value without further enrichment; the patient lookup
/// is performed using the supplied <paramref name="locale"/> when provided, and admission data is only attached
/// when an <c>AdmissionId</c> exists on the point of care.
/// </summary>
/// <param name="id">The unique identifier of the point of care to retrieve.</param>
/// <param name="locale">Optional locale used to select the appropriate patient translation; when <c>null</c>, a locale-independent patient lookup is used.</param>
/// <param name="fillPatientData">When <c>true</c>, enriches the result with unit, patient, and admission data; when <c>false</c>, returns the point of care as-is.</param>
/// <param name="ct">Token used to cancel the asynchronous operation.</param>
/// <returns>The retrieved and optionally enriched <see cref="PointOfCare"/>, or <c>null</c> if no point of care is found for the given identifier.</returns>
public async Task<PointOfCare?> GetInfo(ObjectId id, LocaleEnum? locale, bool fillPatientData = true,
CancellationToken ct = default)
{
var (key, ttl) = CacheKeys.PointOfCareBaseKeyWithTtl(_cacheSettings, id);
var poc = await cacheService.GetOrSetObjectAsync(key,
() => pointOfCareRepository.FindById(id),
ttl, ct);
if (poc == null || !fillPatientData)
return poc;
var unit = await unitService.Value.FindById(poc.UnitId);
if (unit?.Name != null)
poc.UnitName = unit.Name;
var patient = locale != null
? await patientService.Value.GetByPointOfCareAndLocale(poc, unit, locale)
: await patientService.Value.GetByPointOfCare(poc);
if (patient != null)
{
poc.Patientid = patient.Id;
poc.Patient = patient;
}
if (poc.AdmissionId == null) return poc;
var admission = await admissionService.Value.GetAdmissionByIdAsync(poc.AdmissionId.Value);
if (admission != null)
poc.Admission = admission;
return poc;
}
/// <summary>
/// Retrieves the Point of Care associated with the specified patient by resolving the patient's assigned Point of Care identifier.
/// Returns null if the patient cannot be found or if the patient has no Point of Care assigned.
/// </summary>
/// <param name="patientId">The unique identifier of the patient whose Point of Care is being requested.</param>
/// <returns>The <see cref="PointOfCare"/> associated with the patient if one is assigned; otherwise, <c>null</c>.</returns>
public async Task<PointOfCare?> FindPoCByPatientId(ObjectId patientId)
{
var patient = await patientService.Value.FindById(patientId);
if (patient is { PointOfCareId: not null })
return await FindById(patient.PointOfCareId.Value);
return null;
}
/// <summary>
/// Retrieves the Point of Care associated with the specified patient number.
/// Returns null if no patient is found with the given number, or if the patient exists but has no associated Point of Care.
/// </summary>
/// <param name="patientNumber">The unique patient number used to look up the patient.</param>
/// <returns>A task that represents the asynchronous operation, containing the associated <see cref="PointOfCare"/> if found; otherwise, null.</returns>
public async Task<PointOfCare?> FindPoCByPatientNumber(string patientNumber)
{
var patient = await patientService.Value.FindByPatientNumber(patientNumber);
if (patient is { PointOfCareId: not null })
return await FindById(patient.PointOfCareId.Value);
return null;
}
/// <summary>
/// Asynchronously counts the number of Points of Care (PoCs) associated with the specified unit identifier.
/// </summary>
/// <param name="unitId">The unique identifier of the unit whose PoCs should be counted.</param>
/// <returns>A task representing the asynchronous operation, containing the total number of PoCs linked to the given unit.</returns>
public async Task<long> CountPoCsByUnitId(ObjectId unitId)
{
return await pointOfCareRepository.CountByUnitId(unitId);
}
/// <summary>
/// Asynchronously counts the number of virtual Points of Care (PoCs) associated with the specified unit identifier by delegating to the repository.
/// </summary>
/// <param name="unitId">The unique identifier of the unit whose virtual PoCs are being counted.</param>
/// <returns>A <see cref="Task{long}"/> representing the asynchronous operation, containing the total number of virtual PoCs linked to the given unit.</returns>
public async Task<long> CountVirtualPoCsByUnitId(ObjectId unitId)
{
return await pointOfCareRepository.CountVirtualsByUnitId(unitId);
}
/// <summary>
/// Updates the status of an existing point of care identified by its id, persisting the change,
/// invalidating the related cache entries, recording an audit log of the change, and broadcasting
/// the update. If no point of care is found for the given id, the method returns without making
/// any changes.
/// </summary>
/// <param name="id">The unique identifier of the point of care whose status should be updated.</param>
/// <param name="status">The new point of care status to apply.</param>
public async Task SetPointOfCareStatus(ObjectId id, StatusEnum.PointOfCare status)
{
var pointOfCare = await GetInfo(id, null);
if (pointOfCare == null) return;
pointOfCare.Status = status;
await pointOfCareRepository.Update(pointOfCare);
await cacheService.DeleteByPatternAsync(CacheKeys.PointOfCareBase(id));
var oldPoc = await auditService.DeepCopyAsync(pointOfCare);
if (oldPoc != null)
await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldPoc, pointOfCare);
// await _patientService.Value.UpdatePatientAltable()
SendPointOfCareBroadcast(pointOfCare, OperationType.UpdatedPointOfCare);
}
/// <summary>
/// Asynchronously retrieves a Point of Care associated with the specified patient location by delegating to the repository.
/// Returns a null result if no matching Point of Care is found.
/// </summary>
/// <param name="patientLocation">The patient location used to look up the associated Point of Care.</param>
/// <returns>A task that yields the matching <see cref="PointOfCare"/>, or <c>null</c> if no Point of Care is found for the given location.</returns>
public async Task<PointOfCare?> FindPoCByPatientLocation(PatientLocation patientLocation)
{
return await pointOfCareRepository.FindByPatientLocation(patientLocation);
}
/// <summary>
/// Sends a broadcast notification about a point of care change to all subscribers associated with its location.
/// Any exception thrown while sending the broadcast is caught and logged, so the method never propagates failures to the caller.
/// </summary>
/// <param name="pointOfCare">The point of care whose related subscribers should receive the notification. Its <c>Id</c> is used to match subscriber location identifiers.</param>
/// <param name="operation">The type of operation (e.g., create, update, delete) being broadcast, sent as the message payload.</param>
private void SendPointOfCareBroadcast(PointOfCare pointOfCare, OperationType operation)
{
try
{
var subscribers = subscribersService.GetSubscribers()
.Where(s => s.LocationIds.Any(c => c == pointOfCare.Id)).ToList();
foreach (var subscriber in subscribers)
_ = clientMessageService.Value.SendAsync(subscriber.Id, operation, pointOfCare);
}
catch (Exception ex)
{
logger.LogError("Exception sending PoC broadcast. Operation type: {op}. Exception: {ex}",
operation.ToString(), ex);
}
}
}