=== Summary: 675 files | 7 generated | 484 fresh | 4605 untracked | 4268 adopted | 355 marked | 466 validated-ok | 2+0 stale (sig+body) | 0 skipped | 0 failed | elapsed 11:08:11.442 (40091.44s) ===

This commit is contained in:
julian
2026-06-28 02:50:05 -07:00
parent a19fb90902
commit 586f02a2ca
654 changed files with 5260 additions and 0 deletions
@@ -27,6 +27,7 @@ namespace adas_core.Application.Services;
/// Provides a concrete implementation of the <see cref="IPatientService"/> interface,
/// encapsulating patient-related service operations.
/// </summary>
/// <!-- aidoc:v1 sig=10bbe0c -->
public class PatientService : IPatientService
{
private readonly Lazy<IAdmissionService> _admissionService;
@@ -171,6 +172,7 @@ public class PatientService : IPatientService
/// Inserts a new patient into the repository after validating it. If the patient fails validation, the method returns without performing the insertion. After a successful insert, a new patient broadcast notification is dispatched asynchronously.
/// </summary>
/// <param name="patient">The patient entity to be inserted into the repository.</param>
/// <!-- aidoc:v1 sig=d1581db body=77e1954 -->
public async Task Insert(Patient patient)
{
if (!CheckPatient(patient))
@@ -189,6 +191,7 @@ public class PatientService : IPatientService
/// <param name="patientId">The string representation of the patient identifier to parse and look up.</param>
/// <param name="localeEnum">The locale used to translate the patient data via the master list service.</param>
/// <returns>A task containing the translated patient, the untranslated patient if the unit is not found, or null if the patient has no associated unit.</returns>
/// <!-- aidoc:v1 sig=f4d1347 body=c678377 -->
public async Task<Patient?> FindByPatientIdWithLocale(string patientId, LocaleEnum localeEnum)
{
ObjectId.TryParse(patientId, out var id);
@@ -206,6 +209,7 @@ public class PatientService : IPatientService
/// On a successful insert, an audit log is created and a broadcast is dispatched to notify other components.
/// </summary>
/// <param name="patient">The patient entity to be inserted into the system.</param>
/// <!-- aidoc:v1 sig=f6b2b46 body=88c5d39 -->
public async Task InsertAsync(Patient patient)
{
if (!CheckPatient(patient))
@@ -223,6 +227,7 @@ public class PatientService : IPatientService
/// <param name="newPocId">The identifier of the destination point of care.</param>
/// <param name="oldPocId">The identifier of the source point of care that the patient is leaving.</param>
/// <returns>A task that resolves to <c>true</c> when the patient is successfully moved; otherwise, <c>false</c> if the new point of care is null, the old point of care is null, or the new point of care is already in use or locked.</returns>
/// <!-- aidoc:v1 sig=39388b7 body=c03ac2e -->
public async Task<bool> Move(Patient patient, ObjectId newPocId, ObjectId oldPocId)
{
var newPoc = await _pointOfCareService.GetInfo(newPocId);
@@ -289,6 +294,7 @@ public class PatientService : IPatientService
/// <param name="withLocation">When <c>true</c>, enriches the patient with location information such as bed, room, and unit name.</param>
/// <returns>A <see cref="Task{Patient}"/> containing the found patient, or <c>null</c> if no patient matches the identifier or the identifier is empty.</returns>
/// <exception cref="NotFoundException">Thrown when the patient has an associated point of care that cannot be found.</exception>
/// <!-- aidoc:v1 sig=a22429e body=12ee318 -->
public async Task<Patient?> FindById(ObjectId id, bool withLocation = false)
{
if (id == ObjectId.Empty)
@@ -330,6 +336,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="location">The patient location containing the unit name and bed used to locate the patient.</param>
/// <returns>A <see cref="Patient"/> if one is found for the given location; otherwise, <see langword="null"/>.</returns>
/// <!-- aidoc:v1 sig=556acef body=8a73352 -->
public async Task<Patient?> FindByLocation(PatientLocation? location)
{
if (location?.UnitName == null || location.Bed == null)
@@ -347,6 +354,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="pocId">The point of care identifier used to locate the patient.</param>
/// <returns>A <see cref="Patient"/> if one is found with the specified point of care identifier; otherwise, <c>null</c>.</returns>
/// <!-- aidoc:v1 sig=ddcf124 body=0f46364 -->
public async Task<Patient?> FindByPointOfCareId(ObjectId pocId)
{
return await _patientRepository.FindByPointOfCareId(pocId);
@@ -357,6 +365,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="unitId">The <see cref="ObjectId"/> of the unit whose patients should be counted.</param>
/// <returns>A <see cref="Task{TResult}"/> that resolves to the number of patients linked to the given unit.</returns>
/// <!-- aidoc:v1 sig=783969e body=27e7534 -->
public async Task<long> CountPatientsByUnitId(ObjectId unitId)
{
return await _patientRepository.CountByUnitId(unitId);
@@ -366,6 +375,7 @@ public class PatientService : IPatientService
/// Archives a patient according to the configured <see cref="OnArchiveAction"/>: either inserts the patient into the archive repository (with the current UTC time as the archive date) or simply performs no archival step. The patient record is then deleted, an audit log entry is created, associated patient data is archived, and if the patient was assigned to a point of care, a delete broadcast is sent and that point of care is set to Available.
/// </summary>
/// <param name="patient">The patient to archive. If its <c>DisTime</c> is null, it is set to the current UTC time before further processing.</param>
/// <!-- aidoc:v1 sig=f9c9380 body=db59c7e -->
public async Task ArchivePatient(Patient patient)
{
patient.DisTime ??= DateTime.UtcNow;
@@ -402,6 +412,7 @@ public class PatientService : IPatientService
/// Archives or deletes all data associated with a patient based on the configured <c>OnArchiveAction</c>. When the action is set to <c>Archive</c> (the default), patient observations, treatments, diagnoses, appointments, pumps, recording alerts, and care plans are archived; when set to <c>Delete</c>, observations, treatments, diagnoses, appointments, pumps, and recording alerts are deleted (care plan archival is not performed in this case).
/// </summary>
/// <param name="patientid">The <see cref="ObjectId"/> of the patient whose related data should be archived or deleted.</param>
/// <!-- aidoc:v1 sig=734ad23 body=6ea277f -->
public async Task ArchivePatientData(ObjectId patientid)
{
_logger.LogDebug("Archiving patient data action: {onArchiveAction} : {patientid}", _onArchiveAction,
@@ -434,6 +445,7 @@ public class PatientService : IPatientService
/// Updates a patient record in the repository, handling special cases such as resolving duplicated "Sin cama" bed assignments from ICCADB by generating a unique bed identifier, and assigning a patient number when missing. Also creates an audit log comparing the previous and updated patient and asynchronously broadcasts the update.
/// </summary>
/// <param name="patient">The patient entity to update.</param>
/// <!-- aidoc:v1 sig=f9d52dc body=68e4897 -->
public async Task Update(Patient patient)
{
var oldPatient = await _patientRepository.FindById(patient.Id);
@@ -468,6 +480,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="id">The identifier of the patient whose location will be updated.</param>
/// <param name="location">The new patient location, or <c>null</c> to abort the update.</param>
/// <!-- aidoc:v1 sig=9fbc6f9 body=0a33496 -->
public async Task UpdateLocation(ObjectId id, PatientLocation? location)
{
try
@@ -558,6 +571,7 @@ public class PatientService : IPatientService
/// <param name="id">The unique identifier of the patient whose attending doctor is being updated.</param>
/// <param name="doctor">The new attending doctor to assign to the patient.</param>
/// <exception cref="ConflictException">Thrown when the patient cannot be found by id before or after the update operation.</exception>
/// <!-- aidoc:v1 sig=75ae010 body=d0f12e3 -->
public async Task UpdateAttendingDoctor(ObjectId id, Person doctor)
{
var oldPatient = await _patientRepository.FindById(id) ??
@@ -578,6 +592,7 @@ public class PatientService : IPatientService
/// <param name="data">The new <see cref="Person"/> data to apply to the patient.</param>
/// <param name="updatePatientNumber">When <c>true</c>, the patient number is also updated; otherwise only the personal data is changed.</param>
/// <exception cref="ConflictException">Thrown when the patient cannot be found by <paramref name="id"/> either before or after the update operation.</exception>
/// <!-- aidoc:v1 sig=06ac587 body=7aeda33 -->
public async Task UpdatePatientData(ObjectId id, string patientNumber, Person data,
bool updatePatientNumber = true)
{
@@ -600,6 +615,7 @@ public class PatientService : IPatientService
/// <param name="patient">The patient entity containing the updated data.</param>
/// <param name="updatePatientNumber">Indicates whether the patient number should be updated as part of the operation.</param>
/// <exception cref="ConflictException">Thrown when the patient cannot be found before or after the update operation.</exception>
/// <!-- aidoc:v1 sig=0c07015 body=33d6834 -->
public async Task UpdatePatientData(ObjectId id, string patientNumber, Patient patient,
bool updatePatientNumber = true)
{
@@ -624,6 +640,7 @@ public class PatientService : IPatientService
/// <param name="patientId">The unique identifier of the patient to retrieve.</param>
/// <param name="withLocation">When <c>true</c>, additional lookups are performed to populate the patient's bed, room, and unit information; otherwise only the patient record is returned.</param>
/// <returns>The <see cref="Patient"/> matching the specified identifier, or <c>null</c> if no patient is found.</returns>
/// <!-- aidoc:v1 sig=f272074 body=5df30c2 -->
public async Task<Patient?> FindByPatientId(string patientId, bool withLocation = false)
{
@@ -653,6 +670,7 @@ public class PatientService : IPatientService
/// <param name="patientNumber">The unique identifier of the patient to look up.</param>
/// <param name="withLocation">When set to <c>true</c>, populates the returned patient with bed and room information from the point of care and the unit name from the unit service, provided the patient has associated location identifiers.</param>
/// <returns>A <see cref="Task{TResult}"/> containing the matching <see cref="Patient"/> if found; otherwise, <c>null</c>.</returns>
/// <!-- aidoc:v1 sig=890ea54 body=8336edf -->
public async Task<Patient?> FindByPatientNumber(string patientNumber, bool withLocation = false)
{
var patient = await _patientRepository.FindByPatientNumber(patientNumber);
@@ -681,6 +699,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="patientNumber">The unique patient number used to look up the archived patient.</param>
/// <returns>The archived <see cref="Patient"/> if found; otherwise, <c>null</c>.</returns>
/// <!-- aidoc:v1 sig=d3378f0 body=aa9384c -->
public async Task<Patient?> FindByPatientNumberArchived(string patientNumber)
{
return await _patientArchiveRepository.FindByPatientNumber(patientNumber);
@@ -695,6 +714,7 @@ public class PatientService : IPatientService
/// any unexpected failure causes the original exception to be rethrown after logging.
/// </summary>
/// <param name="date">The cutoff date; patients with no observations updated after this date will be archived.</param>
/// <!-- aidoc:v1 sig=549ade5 body=7f270ff -->
public async Task ArchivePatientWithoutObservationsSinceDate(DateTime date)
{
try
@@ -777,6 +797,7 @@ public class PatientService : IPatientService
/// and any error encountered during archiving is logged rather than propagated.
/// </summary>
/// <param name="hoursBeforeArchive">The number of hours that must elapse after a patient's discharge time before they are eligible for archiving.</param>
/// <!-- aidoc:v1 sig=2cc3c13 body=11413f9 -->
public async Task ArchiveDischargedPatients(int hoursBeforeArchive)
{
var disBeforeDate = DateTime.Now.AddHours(-hoursBeforeArchive);
@@ -806,6 +827,7 @@ public class PatientService : IPatientService
/// <param name="location">The patient location used for a location-based lookup; the search only proceeds when both the unit name and the bed are set, and the location is mapped through the POC mapping service before being applied to the resulting patient.</param>
/// <param name="findPatientByLocation">When <c>true</c>, allows a location-based lookup even if the corresponding configuration option is disabled.</param>
/// <returns>The matching <see cref="Patient"/> if found by any of the attempted criteria, or <c>null</c> when no patient can be located.</returns>
/// <!-- aidoc:v1 sig=ef50845 body=2a9514b -->
public async Task<Patient?> FindPatient(string? patientId, string? patientNumber, PatientLocation? location,
bool findPatientByLocation = false)
{
@@ -842,6 +864,8 @@ public class PatientService : IPatientService
/// <param name="observations">Indicates whether related observations should be included in the result.</param>
/// <param name="filterObservations">Optional list of observation identifiers used to filter observations when they are included.</param>
/// <returns>A task that resolves to the matching <see cref="Patient"/>, or null if the lookup fails.</returns>
/// <!-- aidoc-review:v1 severity=high kind=mentions_removed_behavior
/// "The 'observations' and 'filterObservations' parameters are documented as controlling observation inclusion/filtering, but the method body only calls _patientRepository.FindByPointOfCareId(item.Id) and never references either parameter." -->
public async Task<Patient?> GetByPointOfCare(PointOfCare item, bool observations = false,
List<string>? filterObservations = null)
{
@@ -863,6 +887,7 @@ public class PatientService : IPatientService
/// <param name="unit">The unit used to determine the translation; when <c>null</c>, the patient is returned without translation.</param>
/// <param name="localeEnum">The target locale for translation; when <c>null</c>, the patient is returned without translation.</param>
/// <returns>The matching <see cref="Patient"/>, or <c>null</c> if the patient is not found or an error occurs.</returns>
/// <!-- aidoc:v1 sig=fce6f5d body=821f100 -->
public async Task<Patient?> GetByPointOfCareAndLocale(PointOfCare item, Unit? unit, LocaleEnum? localeEnum)
{
try
@@ -885,6 +910,8 @@ public class PatientService : IPatientService
/// <param name="observations">When true, the most recent observations for the patient are loaded and mapped by name; when false, only patient data is returned.</param>
/// <param name="filterObservations">Optional list of observation names used to restrict which observations are retrieved; applies only when <paramref name="observations"/> is true.</param>
/// <returns>A <see cref="Task{Box}"/> containing the populated box. The <c>HasPatient</c> flag is set to false if no patient is found for the point of care, and the box's <c>Observations</c> are populated only when requested.</returns>
/// <!-- aidoc-review:v1 severity=low kind=wrong_returns
/// "The <returns> tag references Task{Box} (non-nullable), but the method's actual return type is Task<Box?>. The method never returns null in practice, so the practical meaning is unchanged." -->
public async Task<Box?> GetBox(PointOfCare poc, bool observations = false,
List<string>? filterObservations = null)
{
@@ -931,6 +958,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="patient">The patient whose arrival should be broadcast to matching subscribers.</param>
/// <returns>A completed task once the broadcast has been dispatched.</returns>
/// <!-- aidoc:v1 sig=fd19240 body=a90f9bf -->
public Task SendNewPatientBroadcast(Patient patient)
{
var subscribers = new List<WsSubscriber>();
@@ -959,6 +987,7 @@ public class PatientService : IPatientService
/// If the patient has no point of care id, the update is skipped and an error is logged.
/// </summary>
/// <param name="patient">The patient whose update will be broadcast; its point of care id is used to filter subscribers and its unit is used to resolve translations.</param>
/// <!-- aidoc:v1 sig=947720c body=71d60b3 -->
public async Task SendPatientUpdateBroadcast(Patient patient)
{
if (!patient.PointOfCareId.HasValue)
@@ -989,6 +1018,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="apiRequest">The API request to be saved.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous save operation.</returns>
/// <!-- aidoc:v1 sig=a3706aa body=c8d77ba -->
public Task SaveRequestAsync(ApiRequest apiRequest)
{
return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); }));
@@ -999,6 +1029,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="apiRequest">The API request containing the ADT type, patient identifiers, locations, and related clinical data to be processed.</param>
/// <exception cref="ApiRequestException">Thrown when both the patient number and location unit name are missing for non-ICCA requests, or when the request type is not a valid patient ADT type.</exception>
/// <!-- aidoc:v1 sig=229d8cd body=bcf1b57 -->
public async Task SaveRequest(ApiRequest apiRequest)
{
if (string.IsNullOrEmpty(apiRequest.PatientNumber) &&
@@ -1399,6 +1430,7 @@ public class PatientService : IPatientService
/// <param name="apiRequest">The API request containing the data used to initialize the new patient.</param>
/// <param name="ignoreLocation">When <c>true</c>, location information from the request is ignored during the patient update.</param>
/// <returns>The newly created patient.</returns>
/// <!-- aidoc:v1 sig=7f3758a body=fdf45d9 -->
public async Task<Patient?> CreatePatientFromRequest(ApiRequest apiRequest, bool ignoreLocation = false)
{
var patient = new Patient
@@ -1417,6 +1449,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="apiRequest">The incoming API request containing the patient identifier, location, facility, request type, and optional person/doctor information used to resolve or create the patient.</param>
/// <returns>A <see cref="Task{Patient}"/> that resolves to the resolved or newly created <see cref="Patient"/>, or <c>null</c> if the point of care cannot be mapped, the request is not handled, no matching patient is found and creation is disabled, or an error occurs while processing the request.</returns>
/// <!-- aidoc:v1 sig=c55d64f body=15f7bac -->
public async Task<Patient?> FindPatientByApiRequest(ApiRequest apiRequest)
{
Patient? patient = null;
@@ -1737,6 +1770,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="withLocation">When <c>true</c>, populates each patient's <see cref="Patient.Bed"/>, <see cref="Patient.Room"/>, and <see cref="Patient.UnitString"/> by looking up the related point of care and unit; when <c>false</c>, returns the patients without performing those lookups.</param>
/// <returns>A task containing the list of patients, with location fields populated when <paramref name="withLocation"/> is <c>true</c> and the corresponding identifiers are present.</returns>
/// <!-- aidoc:v1 sig=9ed8fb2 body=0e56b87 -->
public async Task<List<Patient>> FindAll(bool withLocation = false)
{
var patients = await _patientRepository.FindAll();
@@ -1767,6 +1801,7 @@ public class PatientService : IPatientService
/// Asynchronously retrieves a list of inactive patients of care (PoC) by delegating to the patient repository.
/// </summary>
/// <returns>A task representing the asynchronous operation, containing a list of inactive <see cref="Patient"/> records.</returns>
/// <!-- aidoc:v1 sig=2479e43 body=4a13d43 -->
public async Task<List<Patient>> FindInActivePoC()
{
return await _patientRepository.FindInActivePoC();
@@ -1776,6 +1811,7 @@ public class PatientService : IPatientService
/// Retrieves a list of patients currently in active Point of Care (PoC).
/// </summary>
/// <returns>A task that represents the asynchronous operation, containing the list of patients in active PoC.</returns>
/// <!-- aidoc:v1 sig=1a67064 body=36208e3 -->
public async Task<List<Patient>> FindInInactivePoC()
{
return await _patientRepository.FindInInactivePoC();
@@ -1787,6 +1823,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="filter">The pagination filter containing the page number and page size used to calculate the skip and limit for the query.</param>
/// <returns>A task that represents the asynchronous operation. The task result contains a <see cref="PaginationResponse{Patient}"/> with the patients for the requested page, along with the total count and pagination metadata.</returns>
/// <!-- aidoc:v1 sig=02d2827 body=88e214b -->
public async Task<PaginationResponse<Patient>> GetPaginatedPatients(PaginationFilter filter)
{
var result = _patientRepository.GetPaginatedPatients(filter);
@@ -1810,6 +1847,7 @@ public class PatientService : IPatientService
/// <param name="unit">The identifier of the unit to search by.</param>
/// <param name="pointOfCare">The identifier of the point of care to search by.</param>
/// <returns>A <see cref="Patient"/> if a match is found; otherwise, <see langword="null"/>.</returns>
/// <!-- aidoc:v1 sig=927765b body=f356295 -->
public async Task<Patient?> FindByUnitAndPocId(ObjectId unit, ObjectId pointOfCare)
{
return await _patientRepository.FindByUnitAndPocId(unit, pointOfCare);
@@ -1820,6 +1858,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="pointOfCare">The point of care identifier used to filter patients.</param>
/// <returns>A task that represents the asynchronous operation, containing the list of <see cref="Patient"/> entities matching the specified point of care.</returns>
/// <!-- aidoc:v1 sig=c4523a3 body=4e528f1 -->
public async Task<List<Patient>> FindByPointOfCare(string pointOfCare)
{
return await _patientRepository.FindByPointOfCare(pointOfCare);
@@ -1833,6 +1872,8 @@ public class PatientService : IPatientService
/// <param name="patient">The target patient that will absorb the old patient's data.</param>
/// <param name="oldPatientNumber">The patient number of the patient to be merged into <paramref name="patient"/>.</param>
/// <returns>A task representing the asynchronous merge operation.</returns>
/// <!-- aidoc-review:v1 severity=high kind=wrong_summary
/// "The summary states 'if the old patient has a point of care' as the trigger, but the code checks `oldLocation.HasValue` where `oldLocation` was assigned from `patient.PointOfCareId` (the target/new patient), not from the old patient." -->
public async Task MergePatient(Patient patient, string oldPatientNumber)
{
var oldLocation = patient.PointOfCareId; // sectionService.FindByPatient(patient.id);
@@ -1874,6 +1915,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="sinceDate">The cutoff date used to archive patients who have not had any observations since this time.</param>
/// <param name="hoursBeforeArchive">The number of hours of inactivity used to determine which discharged and inactive point-of-care patients should be archived.</param>
/// <!-- aidoc:v1 sig=54328ba body=397ed49 -->
public async Task DischargeInactivePatients(DateTime sinceDate, int hoursBeforeArchive)
{
try
@@ -1897,6 +1939,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="updatedPatient">The patient object containing the updated information, identified by its Id.</param>
/// <returns>The updated patient, or null if no matching patient was found or the update did not produce a result.</returns>
/// <!-- aidoc:v1 sig=5cf3377 body=975e64a -->
public async Task<Patient?> UpdateOne(Patient updatedPatient)
{
var oldPatient = await _patientRepository.FindById(updatedPatient.Id);
@@ -1912,6 +1955,7 @@ public class PatientService : IPatientService
/// <param name="altable">The new altable option to assign to the patient.</param>
/// <param name="user">The user performing the operation, used to attribute the generated nurse observation.</param>
/// <returns>The updated <see cref="Patient"/>, or <c>null</c> if no patient was found with the specified id.</returns>
/// <!-- aidoc:v1 sig=ba8b79d body=376c710 -->
public async Task<Patient?> UpdatePatientAltable(ObjectId patientId, OptionList altable, User? user)
{
var patient = await _patientRepository.FindByPatientId(patientId);
@@ -1986,6 +2030,7 @@ public class PatientService : IPatientService
/// <param name="id"></param>
/// <param name="archivePatient"></param>
/// <returns></returns>
/// <!-- aidoc:v1 sig=a446242 body=7196f4b -->
public async Task ExitPatientById(ObjectId id, bool archivePatient = true)
{
try
@@ -2025,6 +2070,7 @@ public class PatientService : IPatientService
/// <param name="user">The user performing the update, used for audit purposes and observation generation.</param>
/// <param name="carePlanLog">An optional list of care plan options used when generating care plans for Treatment, Procedure, and Test list types.</param>
/// <returns>The updated <see cref="Patient"/> if the operation succeeds; otherwise, <c>null</c> if the patient is not found or an exception is caught.</returns>
/// <!-- aidoc:v1 sig=99d957a body=ae3ce1f -->
public async Task<Patient?> UpdatePatientMasterList(ObjectId patientId, MasterListType typeName,
List<OptionList> updatedOptions, User? user, List<OptionList>? carePlanLog)
{
@@ -2150,6 +2196,7 @@ public class PatientService : IPatientService
/// <param name="options">The list of options to evaluate against the patient's current items; may be null.</param>
/// <param name="patient">The patient whose care plan is being generated.</param>
/// <param name="user">The user performing the action; may be null.</param>
/// <!-- aidoc:v1 sig=4bf9dea body=9cdb19e -->
public async Task GenerateNurseCarePlanAndInsert(MasterListType carePlanType, List<OptionList>? options,
Patient patient, User? user)
{
@@ -2296,6 +2343,7 @@ public class PatientService : IPatientService
/// <param name="patientId">The unique identifier of the patient to update.</param>
/// <param name="person">The patient object containing the updated incoming data.</param>
/// <exception cref="ConflictException">Thrown when the patient cannot be found by the given identifier, or when the incoming data update operation fails.</exception>
/// <!-- aidoc:v1 sig=a6c56e3 body=de5d2fe -->
public async Task UpdatePatientIncomingData(ObjectId patientId, Patient person)
{
var oldPatient = await _patientRepository.FindByPatientId(patientId) ??
@@ -2315,6 +2363,7 @@ public class PatientService : IPatientService
/// <param name="person">The patient entity containing the updated information to apply.</param>
/// <param name="patientIncomeData">The patient income information to store as a new observation.</param>
/// <param name="user">The user performing the operation, recorded as the author of the observation.</param>
/// <!-- aidoc:v1 sig=35aee7f body=adb3f38 -->
public async Task UpdatePatientIncomingData(ObjectId patientId, Patient person,
PatientIncomeData patientIncomeData, User user)
{
@@ -2337,6 +2386,7 @@ public class PatientService : IPatientService
/// <param name="opt">The master list option update data to apply to the patients.</param>
/// <param name="unitList">The collection of units whose associated patients will have the master list option updated.</param>
/// <param name="typeName">The name of the master list type used to identify which option to update.</param>
/// <!-- aidoc:v1 sig=b539b7b body=162a786 -->
public async Task UpdatePatientMasterListItemChange(UpdateOptionMasterListDto opt, IEnumerable<Unit> unitList,
string typeName)
{
@@ -2359,6 +2409,7 @@ public class PatientService : IPatientService
/// <param name="opt">The master list option to be removed from patient records.</param>
/// <param name="unitList">The collection of units whose associated patients will be updated.</param>
/// <param name="typeName">The name of the master list type from which the option is being deleted.</param>
/// <!-- aidoc:v1 sig=cb62e44 body=7318409 -->
public async Task DeletePatientMasterListItem(OptionList opt, IEnumerable<Unit> unitList, string typeName)
{
var unitIds = unitList.Select(x => x.Id).ToList();
@@ -2388,6 +2439,8 @@ public class PatientService : IPatientService
/// <param name="person">The patient object containing the updated demographic data.</param>
/// <param name="user">The user performing the update, used for audit logging; may be null.</param>
/// <exception cref="ConflictException">Thrown when no patient is found for the specified patient ID.</exception>
/// <!-- aidoc-review:v1 severity=low kind=wrong_param_role
/// "The 'user' parameter is documented as 'used for audit logging', but in the code it is passed to UpdatePatientMasterList calls; the audit log entry instead uses _httpContextAccessor.HttpContext?.User!." -->
public async Task UpdatePatientDemographicData(ObjectId patientId, Patient person, User? user)
{
var oldPatient = await _patientRepository.FindByPatientId(patientId) ??
@@ -2411,6 +2464,7 @@ public class PatientService : IPatientService
/// <param name="patientNumber">The patient number to search for.</param>
/// <param name="unitId">The identifier of the unit used to find a patient that is distinct from it.</param>
/// <returns>A task containing the found <see cref="Patient"/>, or <c>null</c> if no matching patient is found.</returns>
/// <!-- aidoc:v1 sig=c4b3389 body=200f3e6 -->
public Task<Patient?> SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId)
{
return _patientRepository.SearchByPatientNumberAndDistinctUnit(patientNumber, unitId);
@@ -2421,6 +2475,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="archiveProcedureEndDateAfterMinutes">The minimum age, in minutes, of a finished procedure's end date used as the cutoff for including patients in the result.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of <see cref="Patient"/> instances associated with finished procedures matching the specified threshold.</returns>
/// <!-- aidoc:v1 sig=8527a2e body=c772e96 -->
public async Task<List<Patient>> FindAllPatientWithFinishedProcedures(int archiveProcedureEndDateAfterMinutes)
{
return await _patientRepository.FindAllPatientWithFinishedProcedures(archiveProcedureEndDateAfterMinutes);
@@ -2431,6 +2486,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="archiveTestEndDateAfterMinutes">The number of minutes after the archive test end date used to determine which finished tests to include.</param>
/// <returns>A task that represents the asynchronous operation, containing a list of patients with finished tests.</returns>
/// <!-- aidoc:v1 sig=30ee17f body=2964041 -->
public async Task<List<Patient>> FindAllPatientWithFinishedTest(int archiveTestEndDateAfterMinutes)
{
return await _patientRepository.FindAllPatientWithFinishedTests(archiveTestEndDateAfterMinutes);
@@ -2441,6 +2497,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="archiveTreatmentEndDateAfterMinutes">The minimum number of minutes that must have elapsed since the treatment end date for a patient to be included in the result.</param>
/// <returns>A task representing the asynchronous operation, containing a list of patients with finished treatments matching the archive criteria.</returns>
/// <!-- aidoc:v1 sig=6b43cdf body=d57d2f6 -->
public async Task<List<Patient>> FindAllPatientWithFinishedTreatment(int archiveTreatmentEndDateAfterMinutes)
{
return await _patientRepository.FindAllPatientWithFinishedTreatment(archiveTreatmentEndDateAfterMinutes);
@@ -2452,6 +2509,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="patient">The patient instance to validate before insertion.</param>
/// <returns><c>true</c> if the patient passes all validation checks; otherwise, <c>false</c>.</returns>
/// <!-- aidoc:v1 sig=6b334f4 body=b0c1885 -->
private bool CheckPatient(Patient patient)
{
//patient.AdmTime ??= DateTime.UtcNow;
@@ -2491,6 +2549,7 @@ public class PatientService : IPatientService
/// <param name="oldPoc">The optional identifier of the previous point of care; when present, subscribers still associated
/// with it but not with <paramref name="newPoc"/> are removed from the subscription group.</param>
/// <param name="patient">The patient whose grouped observations are generated and dispatched to the subscribers.</param>
/// <!-- aidoc:v1 sig=48b46da body=5e1c89a -->
private async Task SendLastGroupedObsToSubscriberByPoc(ObjectId newPoc, ObjectId? oldPoc,
Patient patient)
{
@@ -2548,6 +2607,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="newPoc">The Point of Care used to look up the list of relevant subscribers.</param>
/// <param name="patient">The patient whose last observations are retrieved and forwarded to the subscribers.</param>
/// <!-- aidoc:v1 sig=12e14ef body=c278c3c -->
private async Task SendLastObsToSubscriberByPoc(PointOfCare newPoc, Patient patient)
{
// Obtén la lista de suscriptores
@@ -2589,6 +2649,7 @@ public class PatientService : IPatientService
/// <param name="id">The identifier of the patient whose location will be updated.</param>
/// <param name="unitId">The identifier of the new unit the patient is being moved to.</param>
/// <param name="pocId">The identifier of the new point-of-care the patient is being moved to.</param>
/// <!-- aidoc:v1 sig=f267994 body=442e412 -->
private async Task UpdateLocation(ObjectId id, ObjectId unitId, ObjectId pocId)
{
try
@@ -2689,6 +2750,8 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="newLocation">New location where patient is getting updated</param>
/// <param name="patientId">Object Id for patient</param>
/// <!-- aidoc-review:v1 severity=high kind=wrong_summary
/// "The summary states the if-branch triggers on 'DELETED or PUSHED', but the code also checks for 'Moved' as a third condition that triggers the same branch (RemoveGroupedObsByPatientId). A reader would incorrectly assume Moved falls into the else branch." -->
private void CheckLocationForWsSubscriber(PatientLocation? newLocation, ObjectId? patientId)
{
if (patientId == null || newLocation == null) return;
@@ -2712,6 +2775,12 @@ public class PatientService : IPatientService
/// <param>hoursBeforeArchive</param>
/// .
/// </summary>
/// <!-- aidoc-review:v1 severity=high kind=wrong_summary
/// "Summary says it archives 'all patients who have not updated since', but the code only archives patients who are NOT in an active PoC (`.Where(p => !p.IsInActivePoC())`); the 'InInactivePoC' filter is the defining behavior of the method and is omitted." -->
/// <!-- aidoc-review:v1 severity=high kind=wrong_summary
/// "References the method name `ArchiveNotUpdatedSince` inside `<c>`, but the actual method is `ArchiveNotUpdatedPatientsInInactivePoCSince`." -->
/// <!-- aidoc-review:v1 severity=high kind=missing_param
/// "No proper `<param name=\"hoursBeforeArchive\">` element is declared; the existing `<param>hoursBeforeArchive</param>` is malformed and nested inside `<summary>` rather than being a child parameter element." -->
public async Task ArchiveNotUpdatedPatientsInInactivePoCSince(int hoursBeforeArchive)
{
_logger.LogDebug(
@@ -2736,6 +2805,8 @@ public class PatientService : IPatientService
/// <param name="patient">The patient associated with the location update.</param>
/// <param name="location">The patient location information to broadcast.</param>
/// <param name="pocs">The list of point-of-care location identifiers used to filter eligible subscribers. If null or empty, no broadcast is performed.</param>
/// <!-- aidoc-review:v1 severity=medium kind=missing_returns
/// "The method returns Task but no <returns> tag is present." -->
public Task SendPatientLocationBroadcast(object patient, PatientLocation location,
List<ObjectId>? pocs)
{
@@ -2764,6 +2835,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="patientid">The unique identifier of the patient whose attending doctor assignment should be broadcast.</param>
/// <param name="attendingDoctor">The <see cref="Person"/> representing the doctor now attending the patient, included in the broadcast payload.</param>
/// <!-- aidoc:v1 sig=d13f6aa body=65b8601 -->
private async Task SendPatientAttendingDoctorBroadcast(ObjectId patientid, Person attendingDoctor)
{
var patient = await FindById(patientid);
@@ -2794,6 +2866,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="patientid">The unique identifier of the patient whose data will be broadcast.</param>
/// <param name="data">The person data payload to be sent to the matching subscribers.</param>
/// <!-- aidoc:v1 sig=06d0d2d body=8761d5c -->
private async Task SendPatientDataBroadcast(ObjectId patientid, Person data)
{
var patient = await FindById(patientid);
@@ -2816,6 +2889,7 @@ public class PatientService : IPatientService
/// Sends an asynchronous patient update broadcast to all subscribers whose location matches the patient's point of care, grouped by their locale. Falls back to the default locale when a group has no locale key, and translates the patient data per locale before dispatching each message as a fire-and-forget send.
/// </summary>
/// <param name="patient">The patient whose information is being broadcast; its <c>PointOfCareId</c> is used to match subscribers and its <c>UnitId</c> is used to resolve the unit for translation.</param>
/// <!-- aidoc:v1 sig=c2a5661 body=d73de90 -->
private async Task SendPatientBroadcast(Patient patient)
{
var subscribersGroup =
@@ -2843,6 +2917,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="patientid">The unique identifier of the patient whose deletion will be broadcast.</param>
/// <param name="pocId">The point of care location identifier used to filter the targeted subscribers.</param>
/// <!-- aidoc:v1 sig=9bf1b1b body=a401d7f -->
private Task SendDeletePatientBroadcast(ObjectId patientid, ObjectId pocId)
{
var subscribers = _subscribersService.GetSubscribers().Where(s => s.LocationIds.Any(x => x == pocId)).ToList();
@@ -2871,6 +2946,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="patient">The patient entity to be updated in place.</param>
/// <param name="apiRequest">The API request providing the source data for the update.</param>
/// <!-- aidoc:v1 sig=4cf567a body=9d597d8 -->
private void UpdatePatientFromRequestToIdList(Patient patient, ApiRequest apiRequest)
{
if (apiRequest.AdmTime.HasValue) patient.AdmTime = apiRequest.AdmTime;
@@ -2910,6 +2986,7 @@ public class PatientService : IPatientService
/// <param name="apiRequest">The API request containing operational data such as the discharge time used to set the patient's discharge timestamp.</param>
/// <param name="patient">The patient to be moved, updated with the new point of care, unit, and location information.</param>
/// <param name="virtualPocEnum">The virtual point of care identifier used to look up the destination bed, room, and unit.</param>
/// <!-- aidoc:v1 sig=3f3e524 body=d3f1b47 -->
private async Task RemovePatientAndSendToVirtualPoc(ApiRequest apiRequest, Patient patient,
VirtualPointOfCare virtualPocEnum)
{
@@ -2951,6 +3028,7 @@ public class PatientService : IPatientService
/// Handles three main cases: creating/updating patients present in ICCA but not in the DB, matching existing patients by number, and moving patients absent from ICCA to a temporal/unknown bed. Also persists patients from ICCA locations that are outside the configured point-of-care boxes, falling back to a default unknown point-of-care when a matching location is not found.
/// </summary>
/// <param name="apiRequest">The API request containing the list of ICCA patients to synchronize with the local database.</param>
/// <!-- aidoc:v1 sig=48ce95c body=52bb041 -->
private async Task ProcessIccaSync(ApiRequest apiRequest)
{
//var sections = await _sectionService.GetAll();
@@ -3077,6 +3155,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="apiRequest">The API request containing the patient number, patient data, and target location information used for the move operation.</param>
/// <param name="patient">The optional patient to be moved; if null, a new patient is created from the request.</param>
/// <!-- aidoc:v1 sig=6ec4557 body=1b97e64 -->
private async Task ProcessAdtMovePatient(ApiRequest apiRequest, Patient? patient)
{
try
@@ -3234,6 +3313,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="apiRequest"></param>
/// <returns>The Inserted patient or null.</returns>
/// <!-- aidoc:v1 sig=5108cc1 body=2ac8fa1 -->
private async Task<Patient?> ProcessAdtPatientAdmit(ApiRequest apiRequest)
{
Patient? patient;
@@ -3374,6 +3454,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="patient">The patient whose properties will be updated with the values from the admission.</param>
/// <param name="patientInAdmission">The admission record providing the source values to be merged into the patient.</param>
/// <!-- aidoc:v1 sig=3ff958f body=1711774 -->
private async Task MergePatientWithAdmission(Patient patient, Admission patientInAdmission)
{
patient.Insulation = patientInAdmission.Insulation;
@@ -3414,6 +3495,8 @@ public class PatientService : IPatientService
/// <param name="patient">The patient entity to mutate with values coming from the request.</param>
/// <param name="apiRequest">The incoming API request whose non-empty fields are applied to the patient.</param>
/// <param name="ignoreLocation">When <c>true</c>, skips unit and point-of-care lookups and any location-based updates.</param>
/// <!-- aidoc-review:v1 severity=low kind=wrong_param_role
/// "ignoreLocation does not skip the unit lookup itself; _unitService.FindByName is invoked unconditionally. It only suppresses the 'unit not found' warning, the main point-of-care lookup, and the location assignment. The 'sin cama' remap block also runs without checking ignoreLocation, so 'any location-based updates' is not fully accurate." -->
private async Task UpdatePatientFromRequest(Patient patient, ApiRequest apiRequest,
bool ignoreLocation = false)
{
@@ -3507,6 +3590,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="patient">The patient whose location will be updated.</param>
/// <param name="apiRequest">The API request containing the new location information.</param>
/// <!-- aidoc:v1 sig=4a3fa9a body=86ff39c -->
private async Task ProcessUpdatePatientLocationWithOru(Patient patient, ApiRequest apiRequest)
{
await UpdateLocation(patient.Id, apiRequest.Location);
@@ -3518,6 +3602,7 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="patient">The incoming patient to be updated with merged data.</param>
/// <param name="oldPatient">The previous patient record whose missing treatments, procedures and tests are preserved during the merge.</param>
/// <!-- aidoc:v1 sig=de7cab7 body=b879387 -->
public async Task UpdatePatientFromMerge(Patient patient, Patient oldPatient)
{
var listObsName = _masterListServiceFactory.StringNurseObs();
@@ -3627,6 +3712,8 @@ public class PatientService : IPatientService
/// <param name="value">The list of option values for the observation, or null to default to an empty list.</param>
/// <param name="param name="patientId">The identifier of the patient associated with the observation.</param>
/// <param name="user">The user creating the observation, whose identifier is assigned to the observation; may be null.</param>
/// <!-- aidoc-review:v1 severity=medium kind=missing_param
/// "The <param> tag for patientId is malformed as `<param name=\"param name=\"patientId\">`, which breaks the XML and effectively omits the patientId parameter documentation." -->
private async Task GenerateNurseObsAndInsert(string name, List<OptionList>? value, ObjectId patientId, User? user)
{
var obs = GenerateObs(
@@ -3645,6 +3732,7 @@ public class PatientService : IPatientService
/// <param name="value">The list of <see cref="OptionList"/> values for the observation, or <c>null</c> to use an empty list.</param>
/// <param name="patientId">The identifier of the patient associated with the observation.</param>
/// <returns>A new <see cref="PatientObservation"/> populated with the provided data, the current date/time, and manual insert mode.</returns>
/// <!-- aidoc:v1 sig=ad36884 body=0603f48 -->
private PatientObservation GenerateObs(string name, List<OptionList>? value, ObjectId patientId)
{
var valueList = value ?? [];
@@ -3664,6 +3752,8 @@ public class PatientService : IPatientService
/// </summary>
/// <param name="masterListNameString">The name of the property in <see cref="ListSettings"/> whose associated manual observation name should be returned. Must match the property name exactly.</param>
/// <returns>The <c>ManualObservationName</c> of the resolved <see cref="ListSettingItem"/>, or a descriptive error string if the property is not found or the property value is not a <see cref="ListSettingItem"/>.</returns>
/// <!-- aidoc-review:v1 severity=low kind=wrong_summary
/// "The summary describes the error messages as 'localized', but the code returns hardcoded Spanish string literals rather than localized resources." -->
public string GetManualObservationName(string masterListNameString)
{
// 1. Obtener el tipo de la clase ListSettings.