commit 319fd3dfb0fc8becb3795a29db132ccfd5b15f11 Author: jrojas Date: Fri Jun 26 09:52:35 2026 +0200 Creado apartir del commit b888de6c92056de294456f581a56e25e734d4ab7 de develop diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..3729ff0c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,25 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..f0231b83 --- /dev/null +++ b/.gitignore @@ -0,0 +1,381 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +bin/ +obj/ +Debug/ +Release/ +*.tlog +packages +*.log +TestResult.xml +*.VisualState.xml +Version.cs +Version.h +Version.cpp +.vs +/adas-core/Logs + + + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd +.idea diff --git a/.teralevel-ci.yml b/.teralevel-ci.yml new file mode 100644 index 00000000..747d1d1f --- /dev/null +++ b/.teralevel-ci.yml @@ -0,0 +1,70 @@ +steps: + test: + image: mcr.microsoft.com/dotnet/sdk:8.0 + when: + event: + - push + - commit + commands: + - (dotnet nuget add source $${ADAS_NUGET_URL} -n "ADAS Nugget Server") | 2>/dev/null + - dotnet build --configuration Debug /p:Platform=x64 /nowarn:1701,1702,IL2121,CS1591,IL2121,1701,1702,CS1587,CA1416 + "adas-core/adas-core.csproj" + - dotnet test --configuration Debug /p:Platform=x64 /nowarn:1701,1702,IL2121,CS1591,IL2121,1701,1702,CS1587,CA1416 + "./adas-core.Test/adas-core.Test.csproj" + environment: + ADAS_NUGET_URL: + from_secret: ADAS_NUGET_URL + upload: + image: docker:dind + volumes: + - /var/run/docker.sock:/var/run/docker.sock + when: + event: + - push + - commit + branch: + - main + commands: + - docker login -u $${CI_REGISTRY_USER} -p $${CI_REGISTRY_PASSWORD} $${CI_REGISTRY} + - docker build -t $${CI_REGISTRY}/adas/adas-core/adas-core:latest . + - docker push $${CI_REGISTRY}/adas/adas-core/adas-core:latest + environment: + CI_REGISTRY_USER: + from_secret: CI_REGISTRY_USER + CI_REGISTRY_PASSWORD: + from_secret: CI_REGISTRY_PASSWORD + CI_REGISTRY: + from_secret: CI_REGISTRY + ADAS_HYDE_SSH_KEY: + from_secret: ADAS_HYDE_SSH_KEY + SSH_SERVER_HOSTKEYS: + from_secret: SSH_SERVER_HOSTKEYS + restart-docker-service: + image: alpine + when: + branch: + - main + commands: + - command -v ssh-agent >/dev/null || ( apk add --update openssh ) + - eval $(ssh-agent -s) + - echo "$${ADAS_HYDE_SSH_KEY}" | tr -d '\r' | ssh-add - + - mkdir -p ~/.ssh + - chmod 700 ~/.ssh + - echo "$${SSH_SERVER_HOSTKEYS}" >> ~/.ssh/known_hosts + - chmod 644 ~/.ssh/known_hosts + - ssh -o "StrictHostKeyChecking no" gitlabci@10.0.20.67 -p 22 "cd /opt/smartuci;docker-compose + stop adas-core;docker-compose rm -f adas-core;docker-compose pull adas-core;docker-compose + up -d adas-core" || ssh -o "StrictHostKeyChecking no" gitlabci@red1.epigram.es + -p 1522 "cd /opt/adascore;docker-compose stop adas-core;docker-compose rm + -f adas-core;docker-compose pull adas-core;docker-compose up -d adas-core" + environment: + CI_REGISTRY_USER: + from_secret: CI_REGISTRY_USER + CI_REGISTRY_PASSWORD: + from_secret: CI_REGISTRY_PASSWORD + CI_REGISTRY: + from_secret: CI_REGISTRY + ADAS_HYDE_SSH_KEY: + from_secret: ADAS_HYDE_SSH_KEY + SSH_SERVER_HOSTKEYS: + from_secret: SSH_SERVER_HOSTKEYS diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..26556846 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,57 @@ +# -------------------------------------------------------------------------------------- +# 1. ETAPA DE CONSTRUCCIÓN (BUILD STAGE) +# Utiliza la imagen SDK para compilar la aplicación. +# -------------------------------------------------------------------------------------- +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src + +COPY NuGet.Config . +# Copia los archivos de proyecto (.csproj) de cada subdirectorio y restáuralos. +# Esto aprovecha el caché de Docker si solo cambian los paquetes, no el código. +COPY ["adas-core/adas-core.csproj", "adas-core/"] +COPY ["adas-core.Application/adas-core.Application.csproj", "adas-core.Application/"] +COPY ["adas-core.Authentication/adas-core.Authentication.csproj", "adas-core.Authentication/"] +COPY ["adas-core.Domain/adas-core.Domain.csproj", "adas-core.Domain/"] +COPY ["adas-core.Infrastructure/adas-core.Infrastructure.csproj", "adas-core.Infrastructure/"] +COPY ["adas-core.LdapLogin/adas-core.LdapLogin.csproj", "adas-core.LdapLogin/"] +COPY ["adas-core.LocalLogin/adas-core.LocalLogin.csproj", "adas-core.LocalLogin/"] +COPY ["adas-core.module.LightBeacons/adas-core.module.LightBeacons.csproj", "adas-core.module.LightBeacons/"] +COPY ["adas-core.module.ProxyDevices/adas-core.module.ProxyDevices.csproj", "adas-core.module.ProxyDevices/"] +COPY ["adas-core.module.Relays/adas-core.module.Relays.csproj", "adas-core.module.Relays/"] +COPY ["adas-core.Test/adas-core.Test.csproj", "adas-core.Test/"] +# ... (Añade COPY para el resto de proyectos .csproj: LdapLogin, LocalLogin, modules, Test si son dependencias) + +# Copia el archivo de solución (.sln) si existe, si no, usa el siguiente comando con los .csproj +# En este caso, asumiremos que se restaura y compila por proyecto principal. +# Si tienes un archivo de solución, es mejor copiarlo aquí. + +# Restaura las dependencias (NuGet) +RUN dotnet restore "adas-core/adas-core.csproj" + +# Copia el resto de los archivos fuente (todo el código) +COPY . . + +# Cambia al directorio del proyecto principal +WORKDIR /src/adas-core + +# Publica la aplicación final en modo Release +# El argumento -o /app/publish indica dónde se guardará el resultado de la publicación. +RUN dotnet publish "adas-core.csproj" -c Release -o /app/publish /p:UseAppHost=false + + +# -------------------------------------------------------------------------------------- +# 2. ETAPA FINAL (RUNTIME STAGE) +# Utiliza la imagen de ejecución, que es más pequeña y no contiene herramientas de compilación. +# -------------------------------------------------------------------------------------- +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS final +WORKDIR /app + +# Copia los archivos publicados desde la etapa 'build' +COPY --from=build /app/publish . + +# Define el punto de entrada de la aplicación +# Este es el nombre del ensamblado DLL principal +ENTRYPOINT ["dotnet", "adas-core.dll"] +# El puerto de escucha por defecto de ASP.NET Core en Docker es el 8080 (o 80 en versiones anteriores) +ENV ASPNETCORE_URLS=http://+:80 +EXPOSE 80 \ No newline at end of file diff --git a/Documentation/IEC 62304/Capítulo 10/FT-SW-VER-001_Plantilla_casos_de_prueba_VER_C10.docx b/Documentation/IEC 62304/Capítulo 10/FT-SW-VER-001_Plantilla_casos_de_prueba_VER_C10.docx new file mode 100644 index 00000000..3e7afb46 Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 10/FT-SW-VER-001_Plantilla_casos_de_prueba_VER_C10.docx differ diff --git a/Documentation/IEC 62304/Capítulo 11/ANX-AD-05-05_Plan_V&V_ADAS_C11.docx b/Documentation/IEC 62304/Capítulo 11/ANX-AD-05-05_Plan_V&V_ADAS_C11.docx new file mode 100644 index 00000000..2a27adb9 Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 11/ANX-AD-05-05_Plan_V&V_ADAS_C11.docx differ diff --git a/Documentation/IEC 62304/Capítulo 11/FT-SW-VAL-001_Plantilla_evidencia_de_validacion_C11.docx b/Documentation/IEC 62304/Capítulo 11/FT-SW-VAL-001_Plantilla_evidencia_de_validacion_C11.docx new file mode 100644 index 00000000..8b79a46c Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 11/FT-SW-VAL-001_Plantilla_evidencia_de_validacion_C11.docx differ diff --git a/Documentation/IEC 62304/Capítulo 12/FT-SW-REL-001_Plantilla_Release_Audit_Package_C12.docx b/Documentation/IEC 62304/Capítulo 12/FT-SW-REL-001_Plantilla_Release_Audit_Package_C12.docx new file mode 100644 index 00000000..e5e66747 Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 12/FT-SW-REL-001_Plantilla_Release_Audit_Package_C12.docx differ diff --git a/Documentation/IEC 62304/Capítulo 12/FT-SW-REL-002_Plantilla_Notas_de_Version_C12.docx b/Documentation/IEC 62304/Capítulo 12/FT-SW-REL-002_Plantilla_Notas_de_Version_C12.docx new file mode 100644 index 00000000..18ee9fac Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 12/FT-SW-REL-002_Plantilla_Notas_de_Version_C12.docx differ diff --git a/Documentation/IEC 62304/Capítulo 12/FT-SW-REL-003_Plantilla_Issues_Abiertos_y_Aceptabilidad_C12.docx b/Documentation/IEC 62304/Capítulo 12/FT-SW-REL-003_Plantilla_Issues_Abiertos_y_Aceptabilidad_C12.docx new file mode 100644 index 00000000..0f1eac29 Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 12/FT-SW-REL-003_Plantilla_Issues_Abiertos_y_Aceptabilidad_C12.docx differ diff --git a/Documentation/IEC 62304/Capítulo 12/FT-SW-REL-004_Plantilla_Aprobacion_Formal_de_Release_C12.docx b/Documentation/IEC 62304/Capítulo 12/FT-SW-REL-004_Plantilla_Aprobacion_Formal_de_Release_C12.docx new file mode 100644 index 00000000..b61ee11b Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 12/FT-SW-REL-004_Plantilla_Aprobacion_Formal_de_Release_C12.docx differ diff --git a/Documentation/IEC 62304/Capítulo 13/FT-SW-CR-001-C13 - Registro de solicitud de cambio CR_C13.docx b/Documentation/IEC 62304/Capítulo 13/FT-SW-CR-001-C13 - Registro de solicitud de cambio CR_C13.docx new file mode 100644 index 00000000..4f10471b Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 13/FT-SW-CR-001-C13 - Registro de solicitud de cambio CR_C13.docx differ diff --git a/Documentation/IEC 62304/Capítulo 14/RMF-SW-ADAS_marco_gestion_riesgos_C14.docx b/Documentation/IEC 62304/Capítulo 14/RMF-SW-ADAS_marco_gestion_riesgos_C14.docx new file mode 100644 index 00000000..f05e629c Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 14/RMF-SW-ADAS_marco_gestion_riesgos_C14.docx differ diff --git a/Documentation/IEC 62304/Capítulo 15/FT-SW-SEC-005-C15_hardening_con_evidencias_VF.docx b/Documentation/IEC 62304/Capítulo 15/FT-SW-SEC-005-C15_hardening_con_evidencias_VF.docx new file mode 100644 index 00000000..438a10b8 Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 15/FT-SW-SEC-005-C15_hardening_con_evidencias_VF.docx differ diff --git a/Documentation/IEC 62304/Capítulo 16/adas_core_pipeline_v2.yml b/Documentation/IEC 62304/Capítulo 16/adas_core_pipeline_v2.yml new file mode 100644 index 00000000..3256e0f8 --- /dev/null +++ b/Documentation/IEC 62304/Capítulo 16/adas_core_pipeline_v2.yml @@ -0,0 +1,343 @@ +steps: + collect_inputs: + image: alpine:3.20 + # Se ejecuta en todas las ramas: features, env/adas/dev, env/adas/pre + commands: + - set -eu + - apk add --no-cache jq coreutils git findutils + - mkdir -p ci-evidence/test-results + - SHORT_SHA="$(printf "%s" "${DRONE_COMMIT_SHA}" | cut -c1-12)" + - BUILD_TIME="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + - ENV_NAME="$(printf "%s" "${DRONE_BRANCH}" | sed 's|env/adas/||')" + - RELEASE_ID="${RELEASE_ID:-REL-${ENV_NAME}-${DRONE_BUILD_NUMBER}-${SHORT_SHA}}" + - echo "$BUILD_TIME" > ci-evidence/build-time.txt + - echo "${DRONE_COMMIT_SHA}" > ci-evidence/commit-sha.txt + - echo "${SHORT_SHA}" > ci-evidence/commit-short.txt + - echo "${DRONE_BRANCH}" > ci-evidence/branch.txt + - echo "${ENV_NAME}" > ci-evidence/env-name.txt + - echo "${DRONE_BUILD_NUMBER}" > ci-evidence/pipeline-id.txt + - echo "${RELEASE_ID}" > ci-evidence/release-id.txt + - echo "Teralevel CI" > ci-evidence/ci-tool.txt + - git rev-parse --short HEAD > ci-evidence/git-short-head.txt + - git rev-parse HEAD > ci-evidence/git-full-head.txt + - git status --short > ci-evidence/git-status.txt || true + - | + find . \ + -maxdepth 4 \ + -type f \ + \( -name 'Dockerfile' -o -name '.teralevel-ci.yml' -o -name '*.sln' -o -name '*.csproj' -o -name 'nuget.config' \) \ + ! -path '*/bin/*' \ + ! -path '*/obj/*' \ + -exec sha256sum {} \; > ci-evidence/input-checksums.txt || true + - | + jq -n \ + --arg release_id "$RELEASE_ID" \ + --arg ur_prd_version "${UR_PRD_VERSION:-PENDIENTE}" \ + --arg srs_version "${SRS_VERSION:-PENDIENTE}" \ + --arg rmf_version "${RMF_VERSION:-PENDIENTE}" \ + --arg trace_version "${TRACE_MATRIX_VERSION:-PENDIENTE}" \ + --arg sbom_ref "ci-evidence/sbom-cyclonedx.json" \ + '{ + release_id: $release_id, + ur_prd_version: $ur_prd_version, + srs_version: $srs_version, + rmf_version: $rmf_version, + traceability_matrix_version: $trace_version, + sbom_reference: $sbom_ref + }' > ci-evidence/release-baseline-ref.json + + test_backend: + image: mcr.microsoft.com/dotnet/sdk:8.0 + # Se ejecuta en todas las ramas: features, env/adas/dev, env/adas/pre + environment: + ADAS_NUGET_URL: + from_secret: ADAS_NUGET_URL + commands: + - set -eu + - mkdir -p ci-evidence/test-results/dotnet + - dotnet --info > ci-evidence/dotnet-info.txt + - (dotnet nuget add source "${ADAS_NUGET_URL}" -n "ADAS Nugget Server") 2>/dev/null || true + - dotnet restore "adas-core/adas-core.csproj" + - dotnet restore "./adas-core.Test/adas-core.Test.csproj" + - set +e + - dotnet build --configuration Debug /p:Platform=x64 /nowarn:1701,1702,IL2121,CS1591,CS1587,CA1416 "adas-core/adas-core.csproj" + - BUILD_EXIT=$? + - dotnet test --configuration Debug /p:Platform=x64 /nowarn:1701,1702,IL2121,CS1591,CS1587,CA1416 "./adas-core.Test/adas-core.Test.csproj" --results-directory ci-evidence/test-results/dotnet --logger "trx;LogFileName=adas-core-tests.trx" + - TEST_EXIT=$? + - set -e + - | + cat > ci-evidence/test-results/dotnet-summary.json < ci-evidence/image-ref-immutable.txt + - echo "$IMAGE_REF_MUTABLE" > ci-evidence/image-ref-mutable.txt + - docker version > ci-evidence/docker-version.txt + - | + docker build \ + --label org.opencontainers.image.revision="${DRONE_COMMIT_SHA}" \ + --label org.opencontainers.image.version="${IMAGE_TAG_IMMUTABLE}" \ + --label org.opencontainers.image.created="$(cat ci-evidence/build-time.txt)" \ + -t "${IMAGE_REF_IMMUTABLE}" \ + -t "${IMAGE_REF_MUTABLE}" \ + -f Dockerfile \ + . > ci-evidence/docker-build.log 2>&1 || { cat ci-evidence/docker-build.log; exit 1; } + - docker push "${IMAGE_REF_IMMUTABLE}" 2>&1 | tee -a ci-evidence/docker-build.log + - docker push "${IMAGE_REF_MUTABLE}" 2>&1 | tee -a ci-evidence/docker-build.log + - | + docker image inspect "${IMAGE_REF_IMMUTABLE}" \ + --format '{{index .RepoDigests 0}}' > ci-evidence/image-digest.txt 2>/dev/null \ + || docker image inspect "${IMAGE_REF_IMMUTABLE}" \ + --format '{{.Id}}' > ci-evidence/image-digest.txt + - cat ci-evidence/docker-build.log + + generate_sbom: + image: anchore/syft:v1.18.1 + when: + branch: + - env/adas/dev + - env/adas/pre + environment: + CI_REGISTRY_USER: + from_secret: CI_REGISTRY_USER + CI_REGISTRY_PASSWORD: + from_secret: CI_REGISTRY_PASSWORD + CI_REGISTRY: + from_secret: CI_REGISTRY + commands: + - set -eu + - mkdir -p /root/.docker ci-evidence + - | + cat > /root/.docker/config.json < ci-evidence/syft-version.txt + - syft "${IMAGE_REF_IMMUTABLE}" -o cyclonedx-json=ci-evidence/sbom-cyclonedx.json + - syft "${IMAGE_REF_IMMUTABLE}" -o spdx-json=ci-evidence/sbom-spdx.json + + assemble_ci_evidence_predeploy: + image: alpine:3.20 + when: + branch: + - env/adas/dev + - env/adas/pre + commands: + - set -eu + - apk add --no-cache jq coreutils + - BUILD_TIME="$(cat ci-evidence/build-time.txt)" + - COMMIT_SHA="$(cat ci-evidence/commit-sha.txt)" + - SHORT_SHA="$(cat ci-evidence/commit-short.txt)" + - BRANCH="$(cat ci-evidence/branch.txt)" + - ENV_NAME="$(cat ci-evidence/env-name.txt)" + - PIPELINE_ID="$(cat ci-evidence/pipeline-id.txt)" + - RELEASE_ID="$(cat ci-evidence/release-id.txt)" + - IMAGE_REF_IMMUTABLE="$(cat ci-evidence/image-ref-immutable.txt)" + - IMAGE_REF_MUTABLE="$(cat ci-evidence/image-ref-mutable.txt)" + - IMAGE_DIGEST="$(cat ci-evidence/image-digest.txt)" + - DOTNET_SUMMARY="$(cat ci-evidence/test-results/dotnet-summary.json)" + - | + jq -n \ + --arg project "ADAS" \ + --arg component "adas-core" \ + --arg environment "$ENV_NAME" \ + --arg release_id "$RELEASE_ID" \ + --arg build_time_utc "$BUILD_TIME" \ + --arg commit_sha "$COMMIT_SHA" \ + --arg commit_short "$SHORT_SHA" \ + --arg branch "$BRANCH" \ + --arg pipeline_id "$PIPELINE_ID" \ + --arg ci_tool "Teralevel CI" \ + --arg image_ref_immutable "$IMAGE_REF_IMMUTABLE" \ + --arg image_ref_mutable "$IMAGE_REF_MUTABLE" \ + --arg image_digest "$IMAGE_DIGEST" \ + --argjson backend_tests "$DOTNET_SUMMARY" \ + '{ + project: $project, + component: $component, + environment: $environment, + release_id: $release_id, + build_time_utc: $build_time_utc, + commit_sha: $commit_sha, + commit_short: $commit_short, + branch: $branch, + pipeline_id: $pipeline_id, + ci_tool: $ci_tool, + image_ref_immutable: $image_ref_immutable, + image_ref_mutable: $image_ref_mutable, + image_digest: $image_digest, + tests: { + backend: $backend_tests + } + }' > ci-evidence/build-metadata.json + - sha256sum ci-evidence/* > ci-evidence/checksums-predeploy.txt || true + + deploy_adas_dev: + image: alpine:3.20 + when: + branch: + - env/adas/dev + environment: + ADAS_SSH_KEY: + from_secret: ADAS_SSH_KEY + SSH_SERVER_HOSTKEYS: + from_secret: SSH_SERVER_HOSTKEYS + DEPLOY_HOST_DEV: + from_secret: DEPLOY_HOST_DEV + DEPLOY_USER: + from_secret: DEPLOY_USER + commands: + - set -eu + - apk add --no-cache openssh-client jq + - mkdir -p ~/.ssh + - echo "$ADAS_SSH_KEY" > ~/.ssh/id_ed25519 + - chmod 600 ~/.ssh/id_ed25519 + - echo "$SSH_SERVER_HOSTKEYS" >> ~/.ssh/known_hosts + - chmod 644 ~/.ssh/known_hosts + - IMAGE_REF_MUTABLE="$(cat ci-evidence/image-ref-mutable.txt)" + - | + ssh -o "StrictHostKeyChecking yes" "${DEPLOY_USER}@${DEPLOY_HOST_DEV}" \ + "cd /opt/adas-core-dev && \ + docker compose stop adas-core && \ + docker compose rm -f adas-core && \ + docker compose pull adas-core && \ + docker compose up -d adas-core" + - | + REMOTE_DIGEST="$(ssh -o "StrictHostKeyChecking yes" "${DEPLOY_USER}@${DEPLOY_HOST_DEV}" \ + "docker image inspect ${IMAGE_REF_MUTABLE} --format='{{index .RepoDigests 0}}'")" + - | + jq -n \ + --arg environment "dev" \ + --arg host "${DEPLOY_HOST_DEV}" \ + --arg deployed_ref "$IMAGE_REF_MUTABLE" \ + --arg deployed_digest "$REMOTE_DIGEST" \ + '{ + environment: $environment, + host: $host, + deployed_ref: $deployed_ref, + deployed_digest: $deployed_digest + }' > ci-evidence/deploy-info.json + + deploy_adas_pre: + image: alpine:3.20 + when: + branch: + - env/adas/pre + environment: + ADAS_SSH_KEY: + from_secret: ADAS_SSH_KEY + SSH_SERVER_HOSTKEYS: + from_secret: SSH_SERVER_HOSTKEYS + DEPLOY_HOST_PRE: + from_secret: DEPLOY_HOST_PRE + DEPLOY_USER: + from_secret: DEPLOY_USER + commands: + - set -eu + - apk add --no-cache openssh-client jq + - mkdir -p ~/.ssh + - echo "$ADAS_SSH_KEY" > ~/.ssh/id_ed25519 + - chmod 600 ~/.ssh/id_ed25519 + - echo "$SSH_SERVER_HOSTKEYS" >> ~/.ssh/known_hosts + - chmod 644 ~/.ssh/known_hosts + - IMAGE_REF_MUTABLE="$(cat ci-evidence/image-ref-mutable.txt)" + - | + ssh -o "StrictHostKeyChecking yes" "${DEPLOY_USER}@${DEPLOY_HOST_PRE}" \ + "cd /opt/adas-core-pre && \ + docker compose stop adas-core && \ + docker compose rm -f adas-core && \ + docker compose pull adas-core && \ + docker compose up -d adas-core" + - | + REMOTE_DIGEST="$(ssh -o "StrictHostKeyChecking yes" "${DEPLOY_USER}@${DEPLOY_HOST_PRE}" \ + "docker image inspect ${IMAGE_REF_MUTABLE} --format='{{index .RepoDigests 0}}'")" + - | + jq -n \ + --arg environment "pre" \ + --arg host "${DEPLOY_HOST_PRE}" \ + --arg deployed_ref "$IMAGE_REF_MUTABLE" \ + --arg deployed_digest "$REMOTE_DIGEST" \ + '{ + environment: $environment, + host: $host, + deployed_ref: $deployed_ref, + deployed_digest: $deployed_digest + }' > ci-evidence/deploy-info.json + + finalize_and_publish_ci_evidence: + image: alpine:3.20 + when: + branch: + - env/adas/dev + - env/adas/pre + environment: + ADAS_SSH_KEY: + from_secret: ADAS_SSH_KEY + SSH_SERVER_HOSTKEYS: + from_secret: SSH_SERVER_HOSTKEYS + DEPLOY_HOST_DEV: + from_secret: DEPLOY_HOST_DEV + DEPLOY_HOST_PRE: + from_secret: DEPLOY_HOST_PRE + DEPLOY_USER: + from_secret: DEPLOY_USER + commands: + - set -eu + - apk add --no-cache openssh-client jq coreutils tar gzip + - mkdir -p ~/.ssh + - echo "$ADAS_SSH_KEY" > ~/.ssh/id_ed25519 + - chmod 600 ~/.ssh/id_ed25519 + - echo "$SSH_SERVER_HOSTKEYS" >> ~/.ssh/known_hosts + - chmod 644 ~/.ssh/known_hosts + - RELEASE_ID="$(cat ci-evidence/release-id.txt)" + - ENV_NAME="$(cat ci-evidence/env-name.txt)" + - sha256sum ci-evidence/* > ci-evidence/checksums-final.txt || true + - tar -czf "ci-evidence-${RELEASE_ID}.tar.gz" ci-evidence + - | + if [ "${ENV_NAME}" = "dev" ]; then + EVIDENCE_HOST="${DEPLOY_HOST_DEV}" + else + EVIDENCE_HOST="${DEPLOY_HOST_PRE}" + fi + - ssh -o "StrictHostKeyChecking yes" "${DEPLOY_USER}@${EVIDENCE_HOST}" "mkdir -p /opt/adas-ci-evidence/core/${ENV_NAME}" + - scp -o "StrictHostKeyChecking yes" "ci-evidence-${RELEASE_ID}.tar.gz" "${DEPLOY_USER}@${EVIDENCE_HOST}:/opt/adas-ci-evidence/core/${ENV_NAME}/" diff --git a/Documentation/IEC 62304/Capítulo 16/adas_core_pipeline_v2_C16.docx b/Documentation/IEC 62304/Capítulo 16/adas_core_pipeline_v2_C16.docx new file mode 100644 index 00000000..fe7c807e Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 16/adas_core_pipeline_v2_C16.docx differ diff --git a/Documentation/IEC 62304/Capítulo 16/adas_front_pipeline_v2.yaml b/Documentation/IEC 62304/Capítulo 16/adas_front_pipeline_v2.yaml new file mode 100644 index 00000000..31b57321 --- /dev/null +++ b/Documentation/IEC 62304/Capítulo 16/adas_front_pipeline_v2.yaml @@ -0,0 +1,413 @@ +steps: + collect_inputs: + image: alpine:3.20 + # Se ejecuta en todas las ramas: features, env/adas/dev, env/adas/pre + commands: + - set -eu + - apk add --no-cache jq coreutils git findutils + - mkdir -p ci-evidence/test-results + - SHORT_SHA="$(printf "%s" "${DRONE_COMMIT_SHA}" | cut -c1-12)" + - BUILD_TIME="$(date -u +"%Y-%m-%dT%H:%M:%SZ")" + - ENV_NAME="$(printf "%s" "${DRONE_BRANCH}" | sed 's|env/adas/||')" + - RELEASE_ID="${RELEASE_ID:-REL-${ENV_NAME}-${DRONE_BUILD_NUMBER}-${SHORT_SHA}}" + - echo "$BUILD_TIME" > ci-evidence/build-time.txt + - echo "${DRONE_COMMIT_SHA}" > ci-evidence/commit-sha.txt + - echo "${SHORT_SHA}" > ci-evidence/commit-short.txt + - echo "${DRONE_BRANCH}" > ci-evidence/branch.txt + - echo "${ENV_NAME}" > ci-evidence/env-name.txt + - echo "${DRONE_BUILD_NUMBER}" > ci-evidence/pipeline-id.txt + - echo "${RELEASE_ID}" > ci-evidence/release-id.txt + - echo "Teralevel CI" > ci-evidence/ci-tool.txt + - git rev-parse --short HEAD > ci-evidence/git-short-head.txt + - git rev-parse HEAD > ci-evidence/git-full-head.txt + - git status --short > ci-evidence/git-status.txt || true + - | + find . \ + -maxdepth 4 \ + -type f \ + \( -name 'Dockerfile' -o -name '.teralevel-ci.yml' -o -name 'package-lock.json' -o -name 'angular.json' -o -name '*.sln' -o -name '*.csproj' -o -name 'nuget.config' \) \ + ! -path '*/node_modules/*' \ + ! -path '*/bin/*' \ + ! -path '*/obj/*' \ + -exec sha256sum {} \; > ci-evidence/input-checksums.txt || true + - | + jq -n \ + --arg release_id "$RELEASE_ID" \ + --arg ur_prd_version "${UR_PRD_VERSION:-PENDIENTE}" \ + --arg srs_version "${SRS_VERSION:-PENDIENTE}" \ + --arg rmf_version "${RMF_VERSION:-PENDIENTE}" \ + --arg trace_version "${TRACE_MATRIX_VERSION:-PENDIENTE}" \ + --arg sbom_ref "ci-evidence/sbom-cyclonedx.json" \ + '{ + release_id: $release_id, + ur_prd_version: $ur_prd_version, + srs_version: $srs_version, + rmf_version: $rmf_version, + traceability_matrix_version: $trace_version, + sbom_reference: $sbom_ref + }' > ci-evidence/release-baseline-ref.json + + test_backend: + image: mcr.microsoft.com/dotnet/sdk:8.0 + # Se ejecuta en todas las ramas: features, env/adas/dev, env/adas/pre + commands: + - set -eu + - mkdir -p ci-evidence/test-results/dotnet + - dotnet --info > ci-evidence/dotnet-info.txt + - SOLUTION="$(find . -maxdepth 4 -type f -name '*.sln' ! -path '*/obj/*' ! -path '*/bin/*' | head -n 1 || true)" + - | + if [ -z "$SOLUTION" ]; then + TEST_PROJECTS="$(find . -maxdepth 5 -type f \( -name '*Test*.csproj' -o -name '*Tests*.csproj' \) ! -path '*/obj/*' ! -path '*/bin/*' | tr '\n' ' ' || true)" + else + TEST_PROJECTS="" + fi + - | + if [ -n "$SOLUTION" ]; then + dotnet restore "$SOLUTION" + set +e + dotnet test "$SOLUTION" \ + --configuration Release \ + --results-directory ci-evidence/test-results/dotnet \ + --logger "trx;LogFileName=dotnet-tests.trx" + TEST_EXIT=$? + set -e + elif [ -n "$TEST_PROJECTS" ]; then + TEST_EXIT=0 + for proj in $TEST_PROJECTS; do + dotnet restore "$proj" + set +e + dotnet test "$proj" \ + --configuration Release \ + --results-directory ci-evidence/test-results/dotnet \ + --logger "trx;LogFileName=$(basename "$proj" .csproj)-tests.trx" + CUR_EXIT=$? + set -e + if [ "$CUR_EXIT" -ne 0 ]; then TEST_EXIT=$CUR_EXIT; fi + done + else + TEST_EXIT=0 + fi + - | + if [ -n "$SOLUTION" ] || [ -n "${TEST_PROJECTS:-}" ]; then + STATUS="executed" + else + STATUS="skipped" + fi + - | + cat > ci-evidence/test-results/dotnet-summary.json < ci-evidence/node-version.txt + - npm --version > ci-evidence/npm-version.txt + - FRONTEND_DIR="$(find . -maxdepth 4 -type f -name 'angular.json' ! -path '*/node_modules/*' -exec dirname {} \; | head -n 1 || true)" + - | + if [ -z "$FRONTEND_DIR" ]; then + FRONTEND_DIR="$(find . -maxdepth 4 -type f -name 'package.json' ! -path '*/node_modules/*' -exec dirname {} \; | head -n 1 || true)" + fi + - | + if [ -n "$FRONTEND_DIR" ]; then + cd "$FRONTEND_DIR" + export CI=true + export CHROME_BIN=/usr/bin/chromium + npm ci + set +e + npm test -- --watch=false --browsers=ChromeHeadless --progress=false + TEST_EXIT=$? + set -e + cd - + STATUS="executed" + else + TEST_EXIT=0 + STATUS="skipped" + fi + - | + cat > ci-evidence/test-results/frontend-summary.json < ci-evidence/image-ref-immutable.txt + - echo "$IMAGE_REF_MUTABLE" > ci-evidence/image-ref-mutable.txt + - docker version > ci-evidence/docker-version.txt + - | + docker build \ + --label org.opencontainers.image.revision="${DRONE_COMMIT_SHA}" \ + --label org.opencontainers.image.version="${IMAGE_TAG_IMMUTABLE}" \ + --label org.opencontainers.image.created="$(cat ci-evidence/build-time.txt)" \ + -t "${IMAGE_REF_IMMUTABLE}" \ + -t "${IMAGE_REF_MUTABLE}" \ + -f Dockerfile \ + . > ci-evidence/docker-build.log 2>&1 || { cat ci-evidence/docker-build.log; exit 1; } + - docker push "${IMAGE_REF_IMMUTABLE}" 2>&1 | tee -a ci-evidence/docker-build.log + - docker push "${IMAGE_REF_MUTABLE}" 2>&1 | tee -a ci-evidence/docker-build.log + - | + docker image inspect "${IMAGE_REF_IMMUTABLE}" \ + --format '{{index .RepoDigests 0}}' > ci-evidence/image-digest.txt 2>/dev/null \ + || docker image inspect "${IMAGE_REF_IMMUTABLE}" \ + --format '{{.Id}}' > ci-evidence/image-digest.txt + - cat ci-evidence/docker-build.log + + generate_sbom: + image: anchore/syft:v1.18.1 + when: + branch: + - env/adas/dev + - env/adas/pre + environment: + REGISTRY_USER: + from_secret: REGISTRY_USER + REGISTRY_PASS: + from_secret: REGISTRY_PASS + commands: + - set -eu + - mkdir -p /root/.docker ci-evidence + - | + cat > /root/.docker/config.json < ci-evidence/syft-version.txt + - syft "${IMAGE_REF_IMMUTABLE}" -o cyclonedx-json=ci-evidence/sbom-cyclonedx.json + - syft "${IMAGE_REF_IMMUTABLE}" -o spdx-json=ci-evidence/sbom-spdx.json + + assemble_ci_evidence_predeploy: + image: alpine:3.20 + when: + branch: + - env/adas/dev + - env/adas/pre + commands: + - set -eu + - apk add --no-cache jq coreutils + - BUILD_TIME="$(cat ci-evidence/build-time.txt)" + - COMMIT_SHA="$(cat ci-evidence/commit-sha.txt)" + - SHORT_SHA="$(cat ci-evidence/commit-short.txt)" + - BRANCH="$(cat ci-evidence/branch.txt)" + - ENV_NAME="$(cat ci-evidence/env-name.txt)" + - PIPELINE_ID="$(cat ci-evidence/pipeline-id.txt)" + - RELEASE_ID="$(cat ci-evidence/release-id.txt)" + - IMAGE_REF_IMMUTABLE="$(cat ci-evidence/image-ref-immutable.txt)" + - IMAGE_REF_MUTABLE="$(cat ci-evidence/image-ref-mutable.txt)" + - IMAGE_DIGEST="$(cat ci-evidence/image-digest.txt)" + - DOTNET_SUMMARY="$(cat ci-evidence/test-results/dotnet-summary.json)" + - FRONTEND_SUMMARY="$(cat ci-evidence/test-results/frontend-summary.json)" + - | + jq -n \ + --arg project "ADAS" \ + --arg component "adas-webdisplay" \ + --arg environment "$ENV_NAME" \ + --arg release_id "$RELEASE_ID" \ + --arg build_time_utc "$BUILD_TIME" \ + --arg commit_sha "$COMMIT_SHA" \ + --arg commit_short "$SHORT_SHA" \ + --arg branch "$BRANCH" \ + --arg pipeline_id "$PIPELINE_ID" \ + --arg ci_tool "Teralevel CI" \ + --arg image_ref_immutable "$IMAGE_REF_IMMUTABLE" \ + --arg image_ref_mutable "$IMAGE_REF_MUTABLE" \ + --arg image_digest "$IMAGE_DIGEST" \ + --argjson dotnet_tests "$DOTNET_SUMMARY" \ + --argjson frontend_tests "$FRONTEND_SUMMARY" \ + '{ + project: $project, + component: $component, + environment: $environment, + release_id: $release_id, + build_time_utc: $build_time_utc, + commit_sha: $commit_sha, + commit_short: $commit_short, + branch: $branch, + pipeline_id: $pipeline_id, + ci_tool: $ci_tool, + image_ref_immutable: $image_ref_immutable, + image_ref_mutable: $image_ref_mutable, + image_digest: $image_digest, + tests: { + backend: $dotnet_tests, + frontend: $frontend_tests + } + }' > ci-evidence/build-metadata.json + - sha256sum ci-evidence/* > ci-evidence/checksums-predeploy.txt || true + + deploy_webdisplay_dev: + image: alpine:3.20 + when: + branch: + - env/adas/dev + environment: + ADAS_SSH_KEY: + from_secret: ADAS_SSH_KEY + SSH_SERVER_HOSTKEYS: + from_secret: SSH_SERVER_HOSTKEYS + DEPLOY_HOST_DEV: + from_secret: DEPLOY_HOST_DEV + DEPLOY_USER: + from_secret: DEPLOY_USER + commands: + - set -eu + - apk add --no-cache openssh-client jq + - mkdir -p ~/.ssh + - echo "$ADAS_SSH_KEY" > ~/.ssh/id_ed25519 + - chmod 600 ~/.ssh/id_ed25519 + - echo "$SSH_SERVER_HOSTKEYS" >> ~/.ssh/known_hosts + - chmod 644 ~/.ssh/known_hosts + - IMAGE_REF_MUTABLE="$(cat ci-evidence/image-ref-mutable.txt)" + - | + ssh -o "StrictHostKeyChecking yes" "${DEPLOY_USER}@${DEPLOY_HOST_DEV}" \ + "cd /opt/adas-icu-dev && \ + docker compose stop webdisplay-dev && \ + docker compose rm -f webdisplay-dev && \ + docker compose pull webdisplay-dev && \ + docker compose up -d webdisplay-dev" + - | + REMOTE_DIGEST="$(ssh -o "StrictHostKeyChecking yes" "${DEPLOY_USER}@${DEPLOY_HOST_DEV}" \ + "docker image inspect ${IMAGE_REF_MUTABLE} --format='{{index .RepoDigests 0}}'")" + - | + jq -n \ + --arg environment "dev" \ + --arg host "${DEPLOY_HOST_DEV}" \ + --arg deployed_ref "$IMAGE_REF_MUTABLE" \ + --arg deployed_digest "$REMOTE_DIGEST" \ + '{ + environment: $environment, + host: $host, + deployed_ref: $deployed_ref, + deployed_digest: $deployed_digest + }' > ci-evidence/deploy-info.json + + deploy_webdisplay_pre: + image: alpine:3.20 + when: + branch: + - env/adas/pre + environment: + ADAS_SSH_KEY: + from_secret: ADAS_SSH_KEY + SSH_SERVER_HOSTKEYS: + from_secret: SSH_SERVER_HOSTKEYS + DEPLOY_HOST_PRE: + from_secret: DEPLOY_HOST_PRE + DEPLOY_USER: + from_secret: DEPLOY_USER + commands: + - set -eu + - apk add --no-cache openssh-client jq + - mkdir -p ~/.ssh + - echo "$ADAS_SSH_KEY" > ~/.ssh/id_ed25519 + - chmod 600 ~/.ssh/id_ed25519 + - echo "$SSH_SERVER_HOSTKEYS" >> ~/.ssh/known_hosts + - chmod 644 ~/.ssh/known_hosts + - IMAGE_REF_MUTABLE="$(cat ci-evidence/image-ref-mutable.txt)" + - | + ssh -o "StrictHostKeyChecking yes" "${DEPLOY_USER}@${DEPLOY_HOST_PRE}" \ + "cd /opt/adas-icu-pre && \ + docker compose stop webdisplay-pre && \ + docker compose rm -f webdisplay-pre && \ + docker compose pull webdisplay-pre && \ + docker compose up -d webdisplay-pre" + - | + REMOTE_DIGEST="$(ssh -o "StrictHostKeyChecking yes" "${DEPLOY_USER}@${DEPLOY_HOST_PRE}" \ + "docker image inspect ${IMAGE_REF_MUTABLE} --format='{{index .RepoDigests 0}}'")" + - | + jq -n \ + --arg environment "pre" \ + --arg host "${DEPLOY_HOST_PRE}" \ + --arg deployed_ref "$IMAGE_REF_MUTABLE" \ + --arg deployed_digest "$REMOTE_DIGEST" \ + '{ + environment: $environment, + host: $host, + deployed_ref: $deployed_ref, + deployed_digest: $deployed_digest + }' > ci-evidence/deploy-info.json + + finalize_and_publish_ci_evidence: + image: alpine:3.20 + when: + branch: + - env/adas/dev + - env/adas/pre + environment: + ADAS_SSH_KEY: + from_secret: ADAS_SSH_KEY + SSH_SERVER_HOSTKEYS: + from_secret: SSH_SERVER_HOSTKEYS + DEPLOY_HOST_DEV: + from_secret: DEPLOY_HOST_DEV + DEPLOY_HOST_PRE: + from_secret: DEPLOY_HOST_PRE + DEPLOY_USER: + from_secret: DEPLOY_USER + commands: + - set -eu + - apk add --no-cache openssh-client jq coreutils tar gzip + - mkdir -p ~/.ssh + - echo "$ADAS_SSH_KEY" > ~/.ssh/id_ed25519 + - chmod 600 ~/.ssh/id_ed25519 + - echo "$SSH_SERVER_HOSTKEYS" >> ~/.ssh/known_hosts + - chmod 644 ~/.ssh/known_hosts + - RELEASE_ID="$(cat ci-evidence/release-id.txt)" + - ENV_NAME="$(cat ci-evidence/env-name.txt)" + - sha256sum ci-evidence/* > ci-evidence/checksums-final.txt || true + - tar -czf "ci-evidence-${RELEASE_ID}.tar.gz" ci-evidence + - | + if [ "${ENV_NAME}" = "dev" ]; then + EVIDENCE_HOST="${DEPLOY_HOST_DEV}" + else + EVIDENCE_HOST="${DEPLOY_HOST_PRE}" + fi + - ssh -o "StrictHostKeyChecking yes" "${DEPLOY_USER}@${EVIDENCE_HOST}" "mkdir -p /opt/adas-ci-evidence/webdisplay/${ENV_NAME}" + - scp -o "StrictHostKeyChecking yes" "ci-evidence-${RELEASE_ID}.tar.gz" "${DEPLOY_USER}@${EVIDENCE_HOST}:/opt/adas-ci-evidence/webdisplay/${ENV_NAME}/" diff --git a/Documentation/IEC 62304/Capítulo 16/adas_front_pipeline_v2_C16.docx b/Documentation/IEC 62304/Capítulo 16/adas_front_pipeline_v2_C16.docx new file mode 100644 index 00000000..cc810898 Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 16/adas_front_pipeline_v2_C16.docx differ diff --git a/Documentation/IEC 62304/Capítulo 5/FT-SW-001_Matriz_roles_y_asignaciones_C5.docx b/Documentation/IEC 62304/Capítulo 5/FT-SW-001_Matriz_roles_y_asignaciones_C5.docx new file mode 100644 index 00000000..1c4c36f6 Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 5/FT-SW-001_Matriz_roles_y_asignaciones_C5.docx differ diff --git a/Documentation/IEC 62304/Capítulo 5/FT-SW-002_Perfil_de_rol_y_funciones_C5.docx b/Documentation/IEC 62304/Capítulo 5/FT-SW-002_Perfil_de_rol_y_funciones_C5.docx new file mode 100644 index 00000000..b595f018 Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 5/FT-SW-002_Perfil_de_rol_y_funciones_C5.docx differ diff --git a/Documentation/IEC 62304/Capítulo 5/FT-SW-003_Plan_y_registro_de_formacion_C5.docx b/Documentation/IEC 62304/Capítulo 5/FT-SW-003_Plan_y_registro_de_formacion_C5.docx new file mode 100644 index 00000000..6b30bb21 Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 5/FT-SW-003_Plan_y_registro_de_formacion_C5.docx differ diff --git a/Documentation/IEC 62304/Capítulo 5/FT-SW-004_Matriz_proveedores_SOUP_y_software_de_terceros_C5.docx b/Documentation/IEC 62304/Capítulo 5/FT-SW-004_Matriz_proveedores_SOUP_y_software_de_terceros_C5.docx new file mode 100644 index 00000000..4b62ea82 Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 5/FT-SW-004_Matriz_proveedores_SOUP_y_software_de_terceros_C5.docx differ diff --git a/Documentation/IEC 62304/Capítulo 5/FT-SW-005_Registro_revisiones_y_aprobaciones_C5.docx b/Documentation/IEC 62304/Capítulo 5/FT-SW-005_Registro_revisiones_y_aprobaciones_C5.docx new file mode 100644 index 00000000..132b0823 Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 5/FT-SW-005_Registro_revisiones_y_aprobaciones_C5.docx differ diff --git a/Documentation/IEC 62304/Capítulo 5/HERR-INV-001_Inventario_Herramientas_ADAS_C5.docx b/Documentation/IEC 62304/Capítulo 5/HERR-INV-001_Inventario_Herramientas_ADAS_C5.docx new file mode 100644 index 00000000..f3d4a6f5 Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 5/HERR-INV-001_Inventario_Herramientas_ADAS_C5.docx differ diff --git a/Documentation/IEC 62304/Capítulo 6/2025_Documento de Integración_C6.xlsx b/Documentation/IEC 62304/Capítulo 6/2025_Documento de Integración_C6.xlsx new file mode 100644 index 00000000..f323ae3c Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 6/2025_Documento de Integración_C6.xlsx differ diff --git a/Documentation/IEC 62304/Capítulo 6/Anexo_A_Matriz_trazabilidad_UR_SRS_RISK_VER_Release_C6.docx b/Documentation/IEC 62304/Capítulo 6/Anexo_A_Matriz_trazabilidad_UR_SRS_RISK_VER_Release_C6.docx new file mode 100644 index 00000000..fd372818 Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 6/Anexo_A_Matriz_trazabilidad_UR_SRS_RISK_VER_Release_C6.docx differ diff --git a/Documentation/IEC 62304/Capítulo 6/FT_SW_Especificacion_SRS_C6.docx b/Documentation/IEC 62304/Capítulo 6/FT_SW_Especificacion_SRS_C6.docx new file mode 100644 index 00000000..9db13784 Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 6/FT_SW_Especificacion_SRS_C6.docx differ diff --git a/Documentation/IEC 62304/Capítulo 6/FT_SW_RR_AD_Registro_revision_y_decisiones_requisitos_C6.docx b/Documentation/IEC 62304/Capítulo 6/FT_SW_RR_AD_Registro_revision_y_decisiones_requisitos_C6.docx new file mode 100644 index 00000000..d992406d Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 6/FT_SW_RR_AD_Registro_revision_y_decisiones_requisitos_C6.docx differ diff --git a/Documentation/IEC 62304/Capítulo 6/FT_SW_Registro_requisitos_ciberseguridad_C6.docx b/Documentation/IEC 62304/Capítulo 6/FT_SW_Registro_requisitos_ciberseguridad_C6.docx new file mode 100644 index 00000000..119d9182 Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 6/FT_SW_Registro_requisitos_ciberseguridad_C6.docx differ diff --git a/Documentation/IEC 62304/Capítulo 6/FT_SW_Registro_requisitos_no_funcionales_C6.docx b/Documentation/IEC 62304/Capítulo 6/FT_SW_Registro_requisitos_no_funcionales_C6.docx new file mode 100644 index 00000000..28f75027 Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 6/FT_SW_Registro_requisitos_no_funcionales_C6.docx differ diff --git a/Documentation/IEC 62304/Capítulo 6/FT_SW_UR_PRD_Registro_C6.docx b/Documentation/IEC 62304/Capítulo 6/FT_SW_UR_PRD_Registro_C6.docx new file mode 100644 index 00000000..b0b192ee Binary files /dev/null and b/Documentation/IEC 62304/Capítulo 6/FT_SW_UR_PRD_Registro_C6.docx differ diff --git a/Documentation/IEC 62304/SDP.docx b/Documentation/IEC 62304/SDP.docx new file mode 100644 index 00000000..b1c8f009 Binary files /dev/null and b/Documentation/IEC 62304/SDP.docx differ diff --git a/Documentation/IEC 62304/Tablas_IMQ/TablaMedical_60601-1vs62304_ES_cumplimentada_ADAS_claseA_V1.docx b/Documentation/IEC 62304/Tablas_IMQ/TablaMedical_60601-1vs62304_ES_cumplimentada_ADAS_claseA_V1.docx new file mode 100644 index 00000000..57d05616 Binary files /dev/null and b/Documentation/IEC 62304/Tablas_IMQ/TablaMedical_60601-1vs62304_ES_cumplimentada_ADAS_claseA_V1.docx differ diff --git a/Documentation/IEC 62304/Tablas_IMQ/TablaMedical_62304_ES.docx b/Documentation/IEC 62304/Tablas_IMQ/TablaMedical_62304_ES.docx new file mode 100644 index 00000000..ed3d27cf Binary files /dev/null and b/Documentation/IEC 62304/Tablas_IMQ/TablaMedical_62304_ES.docx differ diff --git a/Documentation/Manuals/Manual-Usuario-ADAS-Display_ES_rev.200426.pdf b/Documentation/Manuals/Manual-Usuario-ADAS-Display_ES_rev.200426.pdf new file mode 100644 index 00000000..0aac83b6 Binary files /dev/null and b/Documentation/Manuals/Manual-Usuario-ADAS-Display_ES_rev.200426.pdf differ diff --git a/Documentation/Manuals/Manual_Técnico-Software_Epigram_ADAS_v.130226.pdf b/Documentation/Manuals/Manual_Técnico-Software_Epigram_ADAS_v.130226.pdf new file mode 100644 index 00000000..f684c65f Binary files /dev/null and b/Documentation/Manuals/Manual_Técnico-Software_Epigram_ADAS_v.130226.pdf differ diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 00000000..949b11a1 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/README.md b/README.md new file mode 100644 index 00000000..0483b040 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +ADAS CORE API diff --git a/adas-core.Application/Customizations/BD/CalculatedObservations.cs b/adas-core.Application/Customizations/BD/CalculatedObservations.cs new file mode 100644 index 00000000..26ffacef --- /dev/null +++ b/adas-core.Application/Customizations/BD/CalculatedObservations.cs @@ -0,0 +1,104 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.Pumps; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; + +namespace adas_core.Application.Customizations.BD; + +public class CalculatedObservations(IServiceProvider serviceProvider) : ICalculatedObservations +{ + private readonly ILogger _logger = + serviceProvider.GetRequiredService>(); + + private readonly Lazy _observationService = + serviceProvider.GetRequiredService>(); + + public Task Map(T obs, bool onlyByName = false) where T : BasePatientObservation + { + return Task.FromResult(obs)!; + } + + public Task Map(PatientTreatment treatment) + { + return Task.FromResult(treatment); + } + + public Task Map(PatientDiagnosis diagnosis) + { + return Task.FromResult(diagnosis); + } + + public async Task Map(PumpObservation pumpObservation) + { + if (pumpObservation.Code == "IHE PCD-04") + await CreatePumpAlarmObservation(pumpObservation); + return pumpObservation; + } + + public Task CalculateMedicineObservation(List activeMedicines, ObjectId patientId) + { + return Task.CompletedTask; + } + + public Task CalculateActiveBolus(ObjectId patientId) + { + return Task.CompletedTask; + } + + public Task> GetActiveTreatmentsByPatient(ObjectId id) + { + throw new NotImplementedException(); + } + + public Task FixTimeInconsistencyWithLast(PatientObservation newObservation) + { + throw new NotImplementedException(); + } + + public Task> PreMapList(List listToInsert) + { + return Task.FromResult(listToInsert); + } + + public Task MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert) + { + throw new NotImplementedException(); + } + + public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code) + { + throw new NotImplementedException(); + } + + private Task CreatePumpAlarmObservation(PumpObservation pumpObservation) + { + PatientObservation patientObservationAlarm = new() + { + CodingSystem = "ADAS_ALARM", + Code = pumpObservation.AlarmState, + Name = $"Alarm_Pump_{pumpObservation.AlarmType}", + Value = pumpObservation.DeviceId != null + ? $"Device Id: {pumpObservation.DeviceId}" + : $"Infusion Id {pumpObservation.InfusionId}", + Time = pumpObservation.Time + }; + + if (pumpObservation.PatientId.HasValue) patientObservationAlarm.PatientId = pumpObservation.PatientId.Value; + + + //CheckAlarmConfig(patientObservationAlarm); + try + { + _ = _observationService.Value.InsertObservation(patientObservationAlarm, mapObs: false); + } + catch (Exception e) + { + _logger.LogError("Error inserting Pump Observation Alarm. Exception: {e}", e); + } + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/adas-core.Application/Customizations/CHUO/CalculatedObservations.cs b/adas-core.Application/Customizations/CHUO/CalculatedObservations.cs new file mode 100644 index 00000000..c632448a --- /dev/null +++ b/adas-core.Application/Customizations/CHUO/CalculatedObservations.cs @@ -0,0 +1,80 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.Pumps; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; + +namespace adas_core.Application.Customizations.CHUO; + +public class CalculatedObservations(IServiceProvider serviceProvider) : ICalculatedObservations +{ + private readonly ILogger _logger = + serviceProvider.GetRequiredService>(); + + public Task CalculateActiveBolus(ObjectId patientId) + { + return Task.CompletedTask; + } + + public async Task Map(PumpObservation pumpObservation) + { + return await Task.FromResult(pumpObservation); + } + + public Task CalculateMedicineObservation(List activeMedicines, ObjectId patientId) + { + return Task.CompletedTask; + + } + + public Task FixTimeInconsistencyWithLast(PatientObservation newObservation) + { + throw new NotImplementedException(); + } + + public Task> GetActiveTreatmentsByPatient(ObjectId id) + { + throw new NotImplementedException(); + } + + public Task Map(T obs, bool onlyByName = false) where T : BasePatientObservation + { + _logger.LogTrace("Mapping {name} observation {obs}", obs.Name, obs); + + return Task.FromResult(obs); + } + + + public Task Map(PatientTreatment treatment) + { + throw new NotImplementedException(); + } + + public Task Map(PatientDiagnosis diagnosis) + { + throw new NotImplementedException(); + } + + public Task> PreMapList(List listToInsert) + { + return Task.FromResult(listToInsert); + } + + public Task MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert) + { + return Task.FromResult(obs); + } + + public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code) + { + return Task.CompletedTask; + + } + + //public Task> MapList(List obsToInsert) + //{ + // throw new NotImplementedException(); + //} +} \ No newline at end of file diff --git a/adas-core.Application/Customizations/H12O/UCIN/CalculatedObservations.cs b/adas-core.Application/Customizations/H12O/UCIN/CalculatedObservations.cs new file mode 100644 index 00000000..b316cedb --- /dev/null +++ b/adas-core.Application/Customizations/H12O/UCIN/CalculatedObservations.cs @@ -0,0 +1,1547 @@ +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.Pumps; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; + +namespace adas_core.Application.Customizations.H12O.UCIN; + +public class CalculatedObservations : ICalculatedObservations +{ + private readonly List _complexityObservations = + ["Respiratory", "IntravenousLines", "Medication", "Surgery", "Weight_Newborn", "Weight_Current", "Monitor"]; + + private readonly List _ecmo = []; + private readonly List _electroencephalogram = []; + + private readonly List _highFrequencyVentilation = []; + //private readonly List _shift = []; + + + private readonly Tuple>[] _intraVenousLinesTypeValue = + [ + Tuple.Create(IntraVenousLineTypes.Artery, 5, + new List { "Catéter arterial", "Catéter UMBILICAL arteria" }), + Tuple.Create(IntraVenousLineTypes.CentralVein, 4, + new List { "Catéter venoso CENTRAL", "Catéter UMBILICAL vena" }), + Tuple.Create(IntraVenousLineTypes.Picc, 3, new List { "Catéter PICC" }), + Tuple.Create(IntraVenousLineTypes.MiddleLine, 2, + new List { "Catéter línea media", "Catéter EPICUTÁNEO PERIFÉRICO" }), + Tuple.Create(IntraVenousLineTypes.Peripheral, 1, new List { "Catéter Venoso PERIFÉRICO" }) + ]; + + private readonly List _invasiveVentilation = []; + private readonly ILogger _logger; + + + private readonly List _medicationBolus = []; + + //private readonly Lazy medicineService; + private readonly IMedicineService _medicineService; + + private readonly List _nonInvasiveVentilation = []; + + //private readonly List Oni = new List(); + private readonly List _notesIndicatingMedication = []; + + + private readonly Lazy _observationService; + private readonly List _oniCodes = []; + private readonly List _regionalBrainSaturation = []; + private readonly List _respiratory = []; + + + private readonly Tuple>[] _respiratoryTypeValue = + [ +// Tuple.Create(RespiratoryTypes.INO, 10, new List{ ""}), + Tuple.Create(RespiratoryTypes.Vafo, 5, new List { "V.A.F.O." }), + Tuple.Create(RespiratoryTypes.Vmc, 3, new List { "V.M.C." }), + Tuple.Create(RespiratoryTypes.Vmni, 2, new List { "V.N.I. Ciclada", "CPAP" }), + Tuple.Create(RespiratoryTypes.NasalCannulas, 1, new List { "Alto Flujo", "Bajo Flujo" }), + Tuple.Create(RespiratoryTypes.None, 0, new List { "Sin assistance respiratoria" }) + ]; + + private readonly List _rxaStatus = ["Concluido"]; + private readonly List _surgery = []; + private readonly List _surgeryText = []; + private readonly List _transcutanous = []; + private readonly Lazy _treatmentService; + + + public CalculatedObservations(IServiceProvider serviceProvider) + { + _observationService = serviceProvider.GetRequiredService>(); //observationService; + _medicineService = serviceProvider.GetRequiredService(); //medicineService; + _treatmentService = serviceProvider.GetRequiredService>(); //treatmentService; + + var apiSettings = serviceProvider.GetRequiredService>(); //apiSettings; + + _logger = serviceProvider.GetRequiredService>(); + + var electroencephalogram = apiSettings.Value.Electroencephalogram ?? null; + electroencephalogram?.ForEach(x => _electroencephalogram.Add(x.Trim())); + + var medicationBolus = apiSettings.Value.MedicationBolus ?? null; + medicationBolus?.ForEach(x => _medicationBolus.Add(x.Trim())); + + var transcutaneous = apiSettings.Value.Transcutaneous ?? null; + transcutaneous?.ForEach(x => _transcutanous.Add(x.Trim())); + + var regionalBrainSaturation = apiSettings.Value.RegionalBrainSaturation ?? null; + regionalBrainSaturation?.ForEach(x => _regionalBrainSaturation.Add(x.Trim())); + + var surgery = apiSettings.Value.Surgery ?? null; + surgery?.ForEach(x => _surgery.Add(x.Trim())); + + var surgeryText = apiSettings.Value.SurgeryText ?? null; + surgeryText?.ForEach(x => _surgeryText.Add(x.Trim())); + + var respiratory = apiSettings.Value.Respiratory ?? null; + respiratory?.ForEach(x => _respiratory.Add(x.Trim())); + //inoCode = Respiratory.LastOrDefault(); + + var oni = apiSettings.Value.Oni ?? null; + oni?.ForEach(x => _oniCodes.Add(x.Trim())); + + var highFrequencyVentilation = apiSettings.Value.HighFrequencyVentilation ?? null; + highFrequencyVentilation?.ForEach(x => _highFrequencyVentilation.Add(x.Trim())); + + var invasiveVentilation = apiSettings.Value.InvasiveVentilation ?? null; + invasiveVentilation?.ForEach(x => _invasiveVentilation.Add(x.Trim())); + + var nonInvasiveVentilation = apiSettings.Value.NonInvasiveVentilation ?? null; + nonInvasiveVentilation?.ForEach(x => _nonInvasiveVentilation.Add(x.Trim())); + + //var shift = apiSettings.Value.Shift ?? null; + //shift?.ForEach(x => _shift.Add(x.Trim())); + + var notesIndicatingMedication = + apiSettings.Value.NotesIndicatingMedication ?? null; + notesIndicatingMedication?.ForEach(x => _notesIndicatingMedication.Add(x.Trim())); + + var ecmo = apiSettings.Value.Ecmo ?? null; + ecmo?.ForEach(x => _ecmo.Add(x.Trim())); + } + + + public async Task Map(T obs, bool onlyByName) where T : BasePatientObservation + { + if (string.IsNullOrEmpty(obs.Name)) return obs; + //Llega Vías o algo en Vías dictionary + //Solo tenemos en cuenta las observaciones de ICA + if (!string.IsNullOrEmpty(obs.Code) && _respiratory.Contains(obs.Code) && obs.CodingSystem == "SNM") + { + _logger.LogDebug("Mapping observation Respiratorio {obs}", obs); + await CalculateAsistResp(obs); + } + + if (obs.Name is "AirPressure_Mean" or "FiO2" or "PaO2") await CalculateOxygenationIndex(obs); + + //Solo tenemos en cuenta las observaciones de Central + if (obs.Name == "Resp_Mode") + { + _logger.LogDebug("Mapping Resp mode observation {obs}", obs); + await CalculateVentilationMode(obs); + } + + + if (obs.Name is "TAm" or "Age_Gestational_Fixed" or "Age_Gestational") await CalculateTAmAlert(obs); + + if (obs.Name is "Weight_Newborn" or "Weight_Current") + if (obs is PatientObservation { Expired: false }) + obs = (T)await ParseWeight(obs); + + if (obs is { Name: "Temp_Incubator" } or { Name: "Temp_Patient" }) await CalculateTempGradient(obs); + + + if (obs is { Name: "IntravenousLinesObs" }) + { + var intraObs = await CalculateIntravenousLineObservation(obs); + + if (intraObs != null) + obs = (T)intraObs; + } + + if (obs.Name is "pCO2tc" or "Transcutaneous_O2") + if (obs.Code != null && _regionalBrainSaturation.Contains(obs.Code)) + await CalculateMonitor(obs); + + if (obs.Name == "Surgery") await CheckSurgeryExpired(obs); + + + if (obs.Name != null && _complexityObservations.Contains(obs.Name)) + { + _logger.LogDebug("Mapping observation Complexity {obs}", obs); + await CalculateComplexity(obs); + } + + if (obs.Name != null && (obs.Name.Equals("Diuresis_Hour") || + obs.Name.Equals("Drainages_Hour") || + obs.Name.Equals("Hydric_Balance") || + obs.Name.Equals("IV_Entries_Hour") || + obs.Name.Equals("Enteral_Entries_Hour"))) + obs = (T)await CheckObsExistsAndIncrementTime(obs); + + if (obs.Name != null && (obs.Name.Equals("Temp_Patient") || + obs.Name.Equals("Temp_Incubator") + )) + obs = (T)await CheckObsWithSameTimeExistsAndIncrementTime(obs); + return obs; + } + + public async Task Map(PatientTreatment treatment) + { + if (treatment.SingleDose) + treatment = CalculateSingleDoseEndDate(treatment); + + var tempTreatment = treatment; + + await CheckTreatmentMedicines(tempTreatment); + + + if (tempTreatment.RequestedGiveCodes.Any()) + { + if (tempTreatment.RequestedGiveCodes.Any(t => + _medicationBolus.Contains(t.Identifier))) //remove && t.codingSystem == "FTPCS" not match always + { + _logger.LogDebug("Medication Bolus detected. Creating calculated observation"); + await CalculateBolus(tempTreatment); + } + + if (tempTreatment.RequestedGiveCodes.Any(t => _electroencephalogram.Contains(t.Identifier))) + await CalculateMonitor(tempTreatment); + + if (tempTreatment.RequestedGiveCodes.Any(t => + _surgery.Contains(t.Identifier) && _surgeryText.Contains(t.Text))) + await CalculateSurgery(tempTreatment); + + + if (treatment.RequestedGiveCodes.Any(t => _ecmo.Contains(t.Identifier))) + //If ecmo treatment is NW recalculate complexity to show ECMO if is DC recalculate complexity + await CalculateComplexityOnEcmo(treatment); + + if (tempTreatment.RequestedGiveCodes.Any(t => _oniCodes.Contains(t.Identifier))) + { + if (tempTreatment.OrderControl == OrderControlType.Nw) + { + var oniObservation = new PatientObservation + { + PatientId = treatment.PatientId, + Code = "84481000140102", + Name = "ONi", + CodingSystem = "MDC", + Value = tempTreatment.RequestedGiveAmountMinimum ?? '-', + Time = tempTreatment.OrderTime ?? DateTime.Now, + Units = "ppm" + }; + await _observationService.Value.InsertObservation(oniObservation, mapObs: false); + } + + if (treatment.OrderControl == OrderControlType.Dc) + { + var lastOnis = await _observationService.Value.FindLastObservations(treatment.PatientId, 1, + ["ONi"]); + var lastOni = lastOnis.FirstOrDefault(); + if (lastOni == null) return treatment; + lastOni.Expired = true; + await _observationService.Value.UpdateObservation(lastOni); + await _observationService.Value.InsertObservation(lastOni, false); + } + } + } + + + return treatment; + } + + + public async Task Map(PumpObservation pumpObservation) + { + return await Task.FromResult(pumpObservation); + + } + + public async Task CalculateMedicineObservation(List activeMedicines, ObjectId patientId) + { + await CalculateMedicineObservation(activeMedicines, [], new PatientTreatment { PatientId = patientId }); + } + + public async Task CalculateActiveBolus(ObjectId patientId) + { + // SIN RXA + /* + * var activeBolus = treatmentService.Value.GetActiveTreatmentsByPatient(patientId) + .FindAll(p => p.requestedGiveCodes.Any(r => MedicationBolus.Contains(r.identifier)) && p.orderTime > DateTime.Now.AddHours(-12) && p.boloPom); + + var bolusObs = new PatientObservation() + { + patientid = patientId, + time = DateTime.Now, + //code = "MedicationBolus", //treatment.requestedGiveCode.identifier, + //codingSystem = "ADAS", //treatment.requestedGiveCode.codingSystem, + name = "OpiateBoluses", + codingSystem = "ADAS", + value = activeBolus.Count + }; + + var lastBolus = observationService.Value.FindLastObservations(patientId, 1, new List { "OpiateBoluses" }).FirstOrDefault(); + if (lastBolus != null && int.Parse(lastBolus.value.ToString()) == int.Parse(bolusObs.value.ToString())) + { + return; + } + observationService.Value.InsertObservation(bolusObs); + */ + //Por RXA + + var activeBolus = await GetActiveBolus(patientId, null); + var bolusObs = new PatientObservation + { + PatientId = patientId, + Time = DateTime.Now, + //code = "MedicationBolus", //treatment.requestedGiveCode.identifier, + //codingSystem = "ADAS", //treatment.requestedGiveCode.codingSystem, + Name = "OpiateBoluses", + CodingSystem = "ADAS", + Value = activeBolus.Count + }; + + + var lastsBolus = await _observationService.Value.FindLastObservations(patientId, 1, ["OpiateBoluses"]); + var lastBolus = lastsBolus.FirstOrDefault(); + if (lastBolus != null && int.TryParse(lastBolus.Value.ToString(), out var lastbolus) && + int.TryParse(bolusObs.Value.ToString(), out var bolusobs) && lastbolus == bolusobs) return; + await _observationService.Value.InsertObservation(bolusObs, mapObs: false); + } + + public async Task> GetActiveTreatmentsByPatient(ObjectId id) + { + var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(id); + + return activeTreatments; + } + + public Task Map(PatientDiagnosis diagnosis) + { + return Task.FromResult(diagnosis); + } + + /* + * Designed to fix observations like intravenous with the possibility to receive multiple intravenous observations in the same hl7 message. + * Sometimes ADAS calculates many in the same second. causing inconsistencies when retrieving last observation. + */ + public async Task FixTimeInconsistencyWithLast(PatientObservation newObservation) + { + if (string.IsNullOrEmpty(newObservation.Name)) + { + _logger.LogError("Observation name is null or empty: {newObservation}", newObservation); + return null; + } + + var lastObservations = + await _observationService.Value.FindLastObservations(newObservation.PatientId, 1, [newObservation.Name]); + + var lastObservation = lastObservations.FirstOrDefault(); + + if (lastObservation != null && DateTime.Compare( + new DateTime(lastObservation.Time.Year, lastObservation.Time.Month, + lastObservation.Time.Day, lastObservation.Time.Hour, + lastObservation.Time.Minute, lastObservation.Time.Second), + new DateTime(newObservation.Time.Year, newObservation.Time.Month, + newObservation.Time.Day, newObservation.Time.Hour, + newObservation.Time.Minute, newObservation.Time.Second) + ) >= 0) + newObservation.Time = lastObservation.Time.AddSeconds(1); + + newObservation.Id = ObjectId.GenerateNewId(); + + return newObservation; + } + + public Task> PreMapList(List listToInsert) + { + return Task.FromResult(listToInsert); + } + + public Task MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert) + { + return Task.FromResult(obs); + } + + public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code) + { + throw new NotImplementedException(); + } + + public async Task CheckSurgeryExpired(BasePatientObservation obs) + { + try + { + var pobs = (PatientObservation)obs; + if (!pobs.Expired) return; + var surgeryObs = new PatientObservation + { + PatientId = obs.PatientId, + Name = "Surgery", + Min = 0, + Max = 5, + CodingSystem = "ADAS", + Time = obs.Time.AddSeconds(1), + Value = 0, + Expires = pobs.Expires + }; + _logger.LogDebug("CheckSurgeryExpired. Inserting: {obs}", surgeryObs.ToString()); + + await _observationService.Value.InsertObservation(surgeryObs, mapObs: false); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + /* + *Used for observations that can come with the same time, the last which enter is the newest. So increment time so that the system can identify which is the last. + */ + public async Task CheckObsExistsAndIncrementTime(BasePatientObservation obs) + { + var pobs = (PatientObservation)obs; + + var lastObsFromHour = + await _observationService.Value.FindLastBeforeDate(pobs.PatientId, pobs.Time.AddMinutes(59), obs.Name); + + if (lastObsFromHour != null && + DateTime.Compare(new DateTime(lastObsFromHour.Time.Year, lastObsFromHour.Time.Month, + lastObsFromHour.Time.Day, lastObsFromHour.Time.Hour, 0, 0) + , new DateTime(pobs.Time.Year, pobs.Time.Month, pobs.Time.Day, pobs.Time.Hour, 0, 0)) == 0) + pobs.Time = lastObsFromHour.Time.AddSeconds(1); + + return pobs; + } + + + public async Task CheckObsWithSameTimeExistsAndIncrementTime(BasePatientObservation obs) + { + var pobs = (PatientObservation)obs; + var existObsWithSameTime = + await _observationService.Value.FindAnyWithSameDate(pobs.PatientId, pobs.Time, pobs.Name); + + if (existObsWithSameTime != null && existObsWithSameTime.Any()) pobs.Time = pobs.Time.AddSeconds(1); + + return pobs; + } + + + /// + /// Oxygenation index is calculated => PMAP x FiO2 x 100 / PaO2 + /// the method is called when it receives pmap or fio or pao2 and tries to take the other values to perform the + /// calculation if they are not + /// in database then does nothing. + /// + public async Task CalculateOxygenationIndex(BasePatientObservation obs) + { + var toSearchList = new List(); + PatientObservation? airPressMeanObs = null; + PatientObservation? fiO2Obs = null; + PatientObservation? paO2Obs = null; + + try + { + toSearchList.AddRange(new List { "AirPressure_Mean", "FiO2", "PaO2" }); + + var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList); + + switch (obs.Name) + { + case "AirPressure_Mean": + airPressMeanObs = (PatientObservation)obs; + fiO2Obs = values.FirstOrDefault(o => o.Name == "FiO2"); + paO2Obs = values.FirstOrDefault(o => o.Name == "PaO2"); + break; + + case "FiO2": + fiO2Obs = (PatientObservation)obs; + airPressMeanObs = values.FirstOrDefault(o => o.Name == "AirPressure_Mean"); + paO2Obs = values.FirstOrDefault(o => o.Name == "PaO2"); + break; + + case "PaO2": + paO2Obs = (PatientObservation)obs; + fiO2Obs = values.FirstOrDefault(p => p.Name == "FiO2"); + airPressMeanObs = values.FirstOrDefault(o => o.Name == "AirPressure_Mean"); + break; + } + + + //Controlar que fio2 no haya expirada al recogerla + if (fiO2Obs != null && paO2Obs != null && airPressMeanObs != null) + if (int.TryParse(airPressMeanObs.Value.ToString(), out var airPressureMean) && + int.TryParse(fiO2Obs.Value.ToString(), out var fio2) && + int.TryParse(paO2Obs.Value.ToString(), out var pao2)) + { + if (pao2 == 0) + { + _logger.LogWarning( + "Calculate Oxygenation Index observation. PaO2: {pao2}, observation: {obsName}", pao2, + obs.Name); + return; + } + + var oxygenationIndexValue = airPressureMean * fio2 * 100 / pao2; + + var oxygenationIndexObs = new PatientObservation + { + PatientId = obs.PatientId, + Name = "Oxygenation_Index", + CodingSystem = "ADAS", + Value = oxygenationIndexValue, + Time = DateTime.Now, + Expires = 10 //Expires in 10 seg because its FiO2 expire time. + }; + + await _observationService.Value.InsertObservation(oxygenationIndexObs, mapObs: false); + } + } + catch (Exception ex) + { + _logger.LogError("Error calculate Oxygenation Index observation. Exception: {exMessage}", ex.Message); + } + } + + + private static PatientTreatment CalculateSingleDoseEndDate(PatientTreatment treatment) + { + /* Deprecated calc finish treatment date on shift end. Now every treatment single dose is last 8 Hours + DateTime now = DateTime.UtcNow; + //Turno actual, tenemos que poner como fecha fin la fecha de inicio del turno siguiente + var shiftNow = Shift.Where( + i => DateTime.ParseExact(i, "HH:mm", CultureInfo.InvariantCulture).Ticks < DateTime.Now.Ticks + ).LastOrDefault(); + + var index = Shift.IndexOf(shiftNow); + + string endDateShift; + + if (Shift.Count <= index + 1) endDateShift = Shift[0]; + else endDateShift = Shift[index + 1]; + + var HourMinutes = endDateShift.Split(':'); + + if (now.Hour.CompareTo(int.Parse(HourMinutes[0])) > 0) + now = now.AddDays(1); + + DateTime finisTreatmentDate = new DateTime(now.Year, now.Month, now.Day, int.Parse(HourMinutes[0]), int.Parse(HourMinutes[1]), 0); + */ + treatment.EndTime = treatment.OrderTime?.AddHours(8) ?? DateTime.Now.AddHours(8); + return treatment; + } + + public async Task CheckTreatmentMedicines(PatientTreatment treatment) + { + //sI TIENE UNA NOTA CON NPT SON DE TIPO NUTRICIÓN PARENTERAL Y SUMAN 1 + var listNotesCode = treatment.Notes.Select(note => note.Comment).ToList(); + + if (treatment.RequestedGiveCodes.Any()) + listNotesCode.AddRange(treatment.RequestedGiveCodes.Select(treatmentCodes => treatmentCodes.Identifier)); + + //var medicines = medicineService.Value.GetByCodeOrNote(listNotesCode); + var medicines = await _medicineService.GetByCodeOrNote(listNotesCode); + var parentalNutritionMedicine = await CalculateParentalNutritionMedicine(treatment); + medicines.Add(parentalNutritionMedicine); + + //If not detect any medicine in medicine table, check if contáis NTE with, if it's true, it's a medicine. + /* + NTE|||Medicación|^OrderDefinition\r + + NTE|||Perfusiones|^OrderDefinition\r + + NTE|||SUEROS Y HEMODER|^OrderDefinition\r + */ + if (medicines is { Count: 0 }) + { + var isMedicine = treatment.Notes.Any(n => _notesIndicatingMedication.Contains(n.Comment)); + + if (isMedicine && treatment.RequestedGiveCodes is { Count: > 0 }) + treatment.RequestedGiveCodes.ForEach(c => + { + if (!string.IsNullOrEmpty(c.Identifier)) + medicines.Add(new Medicine + { + Codes = [c.Identifier], + Name = c.Text + }); + }); + } + + if (medicines.Any()) + { + if (!string.IsNullOrEmpty(treatment.RequestedGiveTreatment)) + try + { + medicines = + [ + new Medicine + { + Name = treatment.RequestedGiveTreatment, + Type = medicines.FindAll(t => t.Type.Any()) + .Select(m => m.Type.Aggregate((x, y) => x + "," + y)).Distinct().ToList(), + Codes = medicines.FindAll(t => t.Codes.Any()) + .Select(m => m.Codes.Aggregate((x, y) => x + "," + y)).Distinct().ToList(), + Group = medicines.FindAll(t => t.Group.Any()) + .Select(m => m.Group.Aggregate((x, y) => x + "," + y)).Distinct().ToList(), + Notes = medicines.FindAll(t => t.Notes.Any()) + .Select(m => m.Notes.Aggregate((x, y) => x + "," + y)).Distinct().ToList() + } + ]; + } + catch (Exception ex) + { + _logger.LogError("Error Checking Treatment Medicines. {medicines} . Exception {ex}", + string.Join(",", medicines), ex); + } + + var _ = await GetActiveTreatmentsByPatient(treatment.PatientId); + var activeTreatments = _.ToList(); + //var activeMedicines = medicineService.Value.GetMedicinesOfTreatments(activeTreatments); + var __ = await _medicineService.GetMedicinesOfTreatments(activeTreatments); + var activeMedicines = __.ToList(); + + var activeNptTreatments = + activeTreatments.FindAll(t => t?.Notes.FirstOrDefault(n => n.Comment == "NPT") != null); + if (activeNptTreatments.Count > 0) + { + //activeMedicines.AddRange((IEnumerable)activeNPTTreatments.Select(async s => await CalculateParentalNutritionMedicine(s)).Where(parentalMedicine => parentalMedicine != null)); + //activeMedicines.AddRange(await Task.WhenAll(activeNPTTreatments.Select(async s => await CalculateParentalNutritionMedicine(s))).Where(parentalMedicine => parentalMedicine != null)); + + var tasks = activeNptTreatments.Select(async s => await CalculateParentalNutritionMedicine(s)); + var taskResults = await Task.WhenAll(tasks); + var filteredMedicines = taskResults.ToList(); + if (filteredMedicines.Any()) filteredMedicines.ForEach(m => { activeMedicines.Add(m); }); + } + + await CalculateMedicineObservation(activeMedicines, medicines, treatment); + } + } + + private static Task CalculateParentalNutritionMedicine(PatientTreatment? treatment) + { + List medicineType = []; + Note? commentType = null; + if (treatment?.Notes.FirstOrDefault(n => n.Comment == "NPT") != null) + { + commentType = treatment.Notes.FirstOrDefault(n => n.CommentType == "formularybaseformulation"); + + + if (treatment.Notes.FirstOrDefault(n => + n.Comment is "LÍPIDOS NEONATALES AL 20%" or "LÍPIDOS NEONATALES AL 20% CON...") != null) + medicineType = [nameof(MedicineEnum.Types.ParenteralNutritionLipids)]; + else + medicineType = [nameof(MedicineEnum.Types.ParenteralNutrition)]; + } + + var medicine = new Medicine + { + Type = medicineType, + Name = commentType != null ? commentType.Comment : "UnNamed" + }; + return Task.FromResult(medicine); + } + + /* + * Cada vez que entra un tratamiento recalcular medicación y riesgo de medicación: recuperamos todos los tratamientos activos del paciente, + * siendo activos: todos aquellos que no han sido cancelados DC Y SIENDO NW NUEVO, + * Medicación: Si treatment.orderControl = NW entra nuevo sumamos 1 si no hay ninguno de ese tipo ya activo + * si entra treatment.orderControl = DC restamos 1 si no hay ninguno de ese tipo medición.type + * y + */ + public async Task CalculateMedicineObservation(List activeMedicines, List medicines, + PatientTreatment treatment) + { + var medicinesWithType = activeMedicines.FindAll(m => m.Type.Any()); + + foreach (var medicine in medicines) + if (treatment.OrderControl is OrderControlType.Nw or OrderControlType.Xo) + { + if (treatment.StartTime != null && DateTime.UtcNow.CompareTo(treatment.StartTime) < 0) return; + + if (treatment.OrderControl == OrderControlType.Nw) + activeMedicines.Add(medicine); + + if (treatment.OrderControl == OrderControlType.Xo + && activeMedicines.All(m => m.Name != medicine.Name)) activeMedicines.Add(medicine); + + if (medicinesWithType != null && + medicinesWithType.All(m => m.Type != medicine.Type)) + medicinesWithType.Add(medicine); + } + else if (treatment.OrderControl == OrderControlType.Dc) + { + if (medicinesWithType != null) + { + medicinesWithType.Remove(medicine); + activeMedicines.RemoveAll(m => m.Name == medicine.Name); + } + } + + var medicineTypes = medicinesWithType!.SelectMany(m => m.Type.Select(t => new { type = t })) + .GroupBy(x => x.type).Count(); + + var medicineObs = new PatientObservation + { + PatientId = treatment.PatientId, + Name = "Medication", + CodingSystem = "ADAS", + Value = medicineTypes, + Time = DateTime.Now + }; + + var lastMedicationsObs = await _observationService.Value.FindLastObservations(treatment.PatientId, 1, + ["Medication"]); + var lastMedicationObs = lastMedicationsObs.FirstOrDefault(); + if (lastMedicationObs == null || !lastMedicationObs.Value.Equals(medicineObs.Value)) + await _observationService.Value.InsertObservation(medicineObs, mapObs: false); + + await CalculateErMedication(activeMedicines, treatment); + } + + + private async Task CalculateErMedication(List activeMedicines, PatientTreatment treatment) + { + //var activeTreatments = treatmentService.Value.GetActiveTreatmentsByPatient(treatment.patientid); + var result = await CalculateErMedicineLevels(activeMedicines); + + + var erMedicationObs = new PatientObservation + { + Name = "ERMedication", + CodingSystem = "ADAS", + PatientId = treatment.PatientId, + Time = DateTime.Now, + Min = 0, + Max = 5, + Value = result + }; + + var lastErMedicationsObsList = await _observationService.Value.FindLastObservations(treatment.PatientId, 1, + ["ERMedication"]); + var lastErMedicationObs = lastErMedicationsObsList.FirstOrDefault(); + if (lastErMedicationObs == null || !lastErMedicationObs.Value.Equals(erMedicationObs.Value)) + await _observationService.Value.InsertObservation(erMedicationObs, mapObs: false); + } + + + private static Task CalculateErMedicineLevels(List activeMedicines) + { + var ironVitaminDCodes = new List { "374424002", "175041000140104" }; + + //Aquí a cambiar que la nutrición parenteral siempre sume 4 no 5 quitar de medicinas de 5 solo suma 5 en caso de ser + //Los lípidos son dos órdenes de NP con el segmento RXO6.2 (NPT: 1 ml LÍPIDOS NEONATALES AL 20% CON MEDICAMENTOS 1 o + //NPT: 1 ml LÍPIDOS NEONATALES AL 20% 1) + + var medicines5Points = activeMedicines.Where(m => + m.Type.Contains(nameof(MedicineEnum.Types.Pge1)) || + m.Type.Contains(nameof(MedicineEnum.Types.Insulin)) || + m.Group.Contains(nameof(MedicineEnum.Group.DoubleSignature)) || + m.Group.Contains(nameof(MedicineEnum.Group.Vasoactive)) || + m.Type.Contains(nameof(MedicineEnum.Types.ParenteralNutritionLipids)) + ) + .ToList(); + + if (medicines5Points.Count > 2 || + activeMedicines.FirstOrDefault(m => + m.Type.Contains(nameof(MedicineEnum.Types.ParenteralNutritionLipids))) != + null) return Task.FromResult(5); + + if (medicines5Points.Count is > 0 and <= 2 || + activeMedicines.FirstOrDefault(m => m.Type.Contains(nameof(MedicineEnum.Types.ParenteralNutrition))) != + null + ) return Task.FromResult(4); + + var medicines2Points = activeMedicines.Where(m => + m.Group.Contains(nameof(MedicineEnum.Group.Metabolic)) + ).ToList(); + + if (medicines2Points.Count > 0) return Task.FromResult(2); + + //All medicines without type/group and not vitamin D or Iron + + var medicines1Point = activeMedicines.FindAll(m => !m.Codes.Any(c => ironVitaminDCodes.Contains(c))); + + if (medicines1Point.Count > 0) return Task.FromResult(1); + + + var ironVitaminDMedicines = activeMedicines.Where(m => + m.Codes.Any(c => ironVitaminDCodes.Contains(c)) + ).ToList(); + + if (ironVitaminDMedicines.Count == activeMedicines.Count || activeMedicines.Count == 0) + return Task.FromResult(0); + + return Task.FromResult(1); + } + + + //TODO revisar con los valores reales cuando se sepan + public async Task CalculateVentilationMode(BasePatientObservation obs) + { + var pobs = (PatientObservation)obs; + var respTypeObs = new PatientObservation + { + Name = "Resp_Type", + CodingSystem = "ADAS", + PatientId = obs.PatientId, + Time = obs.Time + }; + + var strValue = pobs.Value.ToString(); + + + respTypeObs.Value = _highFrequencyVentilation.Contains(strValue ?? string.Empty) + ? nameof(RespirationType.HighFrequencyVentilation) + : _invasiveVentilation.Contains(strValue ?? string.Empty) + ? respTypeObs.Value = nameof(RespirationType.Invasive) + : _nonInvasiveVentilation.Contains(strValue ?? string.Empty) + ? respTypeObs.Value = nameof(RespirationType.NonInvasive) + : nameof(RespirationType.None); + + await _observationService.Value.InsertIfChanged("Resp_Type", respTypeObs); + } + + public async Task CalculateAsistResp(BasePatientObservation obs) + { + //Comprobar si hay un resp_type más nuevo y si no lo hay ignorar + + var pobs = (PatientObservation)obs; + var lastsAsistResp = await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["Resp_Mode"]); + var lastAsistResp = lastsAsistResp.FirstOrDefault(); + if (lastAsistResp != null && lastAsistResp.Time.CompareTo(obs.Time) > 0 && + lastAsistResp.Name != "ONi") return obs; + + var calculatedRespiratory = new PatientObservation + { + PatientId = obs.PatientId, + Time = DateTime.Now, + CodingSystem = "ADAS", + Name = "Respiratory", + Value = 0 + }; + + if (obs.Name == "ONi") + { + //find last assist resp obs + if (lastAsistResp != null) + { + var respiratoryValue = _respiratoryTypeValue.FirstOrDefault(r => r.Item3.Contains(lastAsistResp.Value)); + if (respiratoryValue != null) calculatedRespiratory.Value = respiratoryValue.Item2; + } + } + else + { + var respiratoryValue = _respiratoryTypeValue.FirstOrDefault(r => r.Item3.Contains(pobs.Value)); + + if (respiratoryValue != null) calculatedRespiratory.Value = respiratoryValue.Item2; + } + + + var isIno = !string.IsNullOrEmpty(obs.Code) && _oniCodes.Contains(obs.Code); + + if (isIno && !pobs.Expired) calculatedRespiratory.Value = 10; + + + //Check if last ino exist and is not expired + var lastsIno = await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["ONi"]); + var lastIno = lastsIno.FirstOrDefault(); + if (lastIno is { Expired: false }) calculatedRespiratory.Value = 10; + + await _observationService.Value.InsertObservation(calculatedRespiratory, mapObs: false); + + return obs; + } + + + private async Task CalculateComplexityOnEcmo(PatientTreatment treatment) + { + var ecmoObs = new PatientObservation + { + PatientId = treatment.PatientId, + Name = "ECMO" + }; + + switch (treatment.OrderControl) + { + case OrderControlType.Nw: + ecmoObs.Value = OrderControlType.Nw; + + break; + case OrderControlType.Dc: + var haveEcmo = await _treatmentService.Value.GetActiveTreatmentsByPatient(treatment.PatientId); + + + ecmoObs.Value = haveEcmo.Any(t => t != null && t.RequestedGiveCodes.Any(r => + _ecmo.Contains(r.Identifier) && + t.PlacerOrder?.EntityIdentifier != treatment.PlacerOrder?.EntityIdentifier)) + ? OrderControlType.Xo + : OrderControlType.Dc; + + break; + + default: + return; + } + + await CalculateComplexity(ecmoObs); + } + + + /** + * Calculamos la complejidad basándonos en el valor de PESO/VÍAS/ASSIST.RESP/MONIT./CIRUGÍA/MEDICACIÓN + * cuando entra una nueva observación que afecte a complejidad, se recalcula de 0 para no tener que estar pendiente de qué puntos corresponden a qué observación + * La complejidad va de 0 a 5. Guardamos la observación que nos llega y recalculamos complejidad. + */ + public async Task CalculateComplexity(BasePatientObservation obs, bool ignoreObs = false) + { + try + { + var logUid = Guid.NewGuid(); + var complexity = new PatientObservation + { + PatientId = obs.PatientId, Time = DateTime.UtcNow, CodingSystem = "ADAS", Name = "Complexity", Value = 0 + }; + var complexityValue = 0; + var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(obs.PatientId); + var haveEcmo = activeTreatments.Any(t => + t != null && t.RequestedGiveCodes.Any(r => _ecmo.Contains(r.Identifier))); + var pobs = obs as PatientObservation; + + var complexityObservations = + await _observationService.Value.FindLastObservations(obs.PatientId, 1, _complexityObservations); + + //Nos acaba de entrar, la usamos por delante de la última de base de datos + //21-02-22 No es correcto, solo remover si el tiempo de esa observación que entra es más nuevo que la de bd + var actualObsInBd = complexityObservations.FirstOrDefault(o => o.Name == obs.Name); + if (actualObsInBd != null && obs.Time > actualObsInBd.Time) + complexityObservations.RemoveAll(it => + it.Name == obs.Name); + + if (!ignoreObs && pobs is { Expired: false }) complexityObservations.Add(pobs); + + foreach (var observation in complexityObservations) + switch (observation.Name) + { + case "Weight_Current": + // if(observation.expired) break; A este no le importa que esté expirado, lo tiene que hacer siempre. + var weightCurrentValue = await CalculateWeight(observation); + + _logger.LogDebug( + "complexity for patient {patientid} id log{logUID} => Weight_Current plus complexity: {WeightCurrentValue}", + observation.PatientId, logUid, weightCurrentValue); + complexityValue += weightCurrentValue; + break; + + case "Weight_Newborn": //Only count newborn weight when weight current don't exist. + if (complexityObservations.FirstOrDefault(o => o.Name == "Weight_Current") == null) + { + var weightNewbornValue = await CalculateWeight(observation); + _logger.LogDebug( + "complexity for patient {patientid} id log{logUID} => Weight_Newborn plus complexity: {Weight_NewbornValue}", + observation.PatientId, logUid, weightNewbornValue); + complexityValue += weightNewbornValue; + } + + break; + + case "Medication": + if (observation.Expired) break; + var value = observation.Value.ToString(); + + if (!int.TryParse(value, out var valueParsed)) + valueParsed = 0; + + _logger.LogDebug( + "complexity for patient {patientid} id log{logUID} => Medication plus complexity: {valueParsed}", + observation.PatientId, logUid, valueParsed); + complexityValue += valueParsed; + break; + + case "Surgery": + if (observation.Expired) break; + var surgeryValue = Convert.ToInt32(observation.Value); + _logger.LogDebug( + "complexity for patient {patientid} id log{logUID} => Surgery plus complexity: {surgeryValue}", + observation.PatientId, logUid, surgeryValue); + complexityValue += surgeryValue; + break; + + case "IntravenousLines": + if (observation.Expired) break; + var intravenousLinesValue = Convert.ToInt32(observation.Value); + _logger.LogDebug( + "complexity for patient {patientid} id log{logUID} => IntravenousLines plus complexity: {IntravenousLinesValue}", + observation.PatientId, logUid, intravenousLinesValue); + complexityValue += intravenousLinesValue; + break; + + case "Monitor": + if (observation.Expired) continue; + var monitorValue = Convert.ToInt32(observation.Value); + _logger.LogDebug( + "complexity for patient {patientid} id log{logUID} => monitor plus complexity: {monitorValue}", + observation.PatientId, logUid, monitorValue); + complexityValue += monitorValue; + break; + + case "Respiratory": + if (observation.Expired) break; + var respiratoryValue = Convert.ToInt32(observation.Value); + _logger.LogDebug( + "complexity for patient {patientid} id log{logUID} => respiratory plus complexity: {respiratoryValue}", + observation.PatientId, logUid, respiratoryValue); + complexityValue += respiratoryValue; + break; + } + + complexity.Value = complexityValue; + + if (haveEcmo && obs.Name != "ECMO") + //complexity.value = $"ECMO ({complexityValue})"; + complexity.Value = $"{complexityValue} ECMO"; + + if (complexityObservations.Any(o => o.Name == "ECMO")) + { + var ecmoObs = complexityObservations.FirstOrDefault(o => o.Name == "ECMO"); + + if (Enum.TryParse(typeof(OrderControlType), ecmoObs?.Value.ToString(), out var treatmentControl)) + switch (treatmentControl) + { + case OrderControlType.Dc: + break; + //case OrderControlType.XO: + //case OrderControlType.NW: + default: + //complexity.value = $"ECMO ({complexityValue})"; + complexity.Value = $"{complexityValue} ECMO"; + //var pObs = await FixTimeInconsistencyWithLast(complexity); + if (pobs != null) + await _observationService.Value.InsertObservation(pobs); + break; + } + } + + var lastsObsComplexity = + await _observationService.Value.FindLastObservations(obs.PatientId, 1, [complexity.Name]); + var lastObsComplexity = lastsObsComplexity.FirstOrDefault(); + if (lastObsComplexity is not null) + { + var lastObsParsed = int.TryParse(lastObsComplexity.Value.ToString(), out var lastObsValue); + + if (lastObsParsed && int.TryParse(complexity.Value.ToString(), out var intValue) && + lastObsValue == intValue) return obs; + } + + var fixedTimeComplexity = await FixTimeInconsistencyWithLast(complexity); + _logger.LogDebug( + "complexity for patient {patientid} id log{logUID} => inserting complexity value: {complexityValue} for patient: {complexityPatientid} at time {fixedTimeComplexityTime}", + complexity.PatientId, logUid, complexity.Value, complexity.PatientId, fixedTimeComplexity?.Time); + + if (fixedTimeComplexity != null) + await _observationService.Value.InsertObservation(fixedTimeComplexity, mapObs: false); + + return obs; + } + catch (Exception ex) + { + _logger.LogError("Error calculating complexity. Exception: {ex}", ex); + return obs; + } + } + + + /// + /// Calculate intravenous line when new intravenous observation enters. Get all intravenous obs and filtering actives. + /// Si existe ya un catéter del mismo tipo, apuntando a la misma localización con una fecha que ya existe y no la ha + /// sido retirada y vuelve a venir + /// una inserción, no hacemos caso porque es una actualización. + /// + /// + /// + public async Task CalculateIntravenousLineObservation(BasePatientObservation obs) + { + try + { + var pobs = (PatientObservation)obs; + var intravenousLineValue = (PatientIntravenousLinesValue)pobs.Value; + + //Check for doctor miss click on ICCA. sometimes they put insert with remove time + //when it has a remove time always is remove, never insert + if (intravenousLineValue is { Action: "Insertado", RemoveTime: not null }) + intravenousLineValue.Action = "Retirado"; + + + //GetByCodeSysAndCode active Intravenous observations + //var intraVenousLineObservations = observationService.Value.FindLastObservations(obs.patientid, 1, new List { obs.name }); + List activeIntraVenousLineObservations = []; + try + { + activeIntraVenousLineObservations = + await _observationService.Value.FindLastIntravenousLinesObservationByLocation(obs.PatientId); + } + catch (Exception ex) + { + _logger.LogWarning( + "Cannot transform to PatientObservation value to PatientIntravenousLinesValue {exMessage}", + ex.Message); + } + + if (activeIntraVenousLineObservations.Any()) + activeIntraVenousLineObservations = activeIntraVenousLineObservations.FindAll(v => + v != null && ((PatientIntravenousLinesValue)v.Value).Action == "Insertado"); + /* + var activeIntravenousLineObservations = intraVenousLineObservations.GroupBy(obs => (((PatientIntravenousLinesValue)obs.value).type, + ((PatientIntravenousLinesValue)obs.value).insertTime, + ((PatientIntravenousLinesValue)obs.value).location)) + .Where(grp => grp.All(o => ((PatientIntravenousLinesValue)o.value).RemoveTime == null)) + .SelectMany(group => group).ToList(); + */ + //If intravenous obs have remove time, remove it from activeIntravenousLines before calculating. + + if (intravenousLineValue.RemoveTime != null) + { + activeIntraVenousLineObservations.RemoveAll(o => + o != null && ((PatientIntravenousLinesValue)o.Value).Location == intravenousLineValue.Location); + } + else + { + //If insert come with same data than other insert and same insertTime is update. Don't need to register it again as insert.is updated + var intravenousLineBeforeUpdate = activeIntraVenousLineObservations.FirstOrDefault(o => o != null && + ((PatientIntravenousLinesValue)o.Value).Location == intravenousLineValue.Location && + Equals(((PatientIntravenousLinesValue)o.Value).InsertTime, intravenousLineValue.InsertTime) && + ((PatientIntravenousLinesValue)o.Value).Type == intravenousLineValue.Type && + intravenousLineValue.RemoveTime == null); + + if (intravenousLineBeforeUpdate != null) + { + //If duration changed Update register + + if (((PatientIntravenousLinesValue)intravenousLineBeforeUpdate.Value).Duration == + intravenousLineValue.Duration) + return null; + + ((PatientIntravenousLinesValue)intravenousLineBeforeUpdate.Value).Duration = + intravenousLineValue.Duration ?? string.Empty; + + await _observationService.Value.UpdateObservation(intravenousLineBeforeUpdate); + + return null; + } + + + if (!activeIntraVenousLineObservations.Any(o => + o != null && + ((PatientIntravenousLinesValue)o.Value).Location == intravenousLineValue.Location && + ((PatientIntravenousLinesValue)o.Value).Type == intravenousLineValue.Type)) + activeIntraVenousLineObservations.Add(pobs); + } + + //Generate calculad intravenousLine obs + + //var intraVenousLineScore2 = activeIntraVenousLineObservations.Select(activeObservation => IntraVenousLinesTypeValue.Where(i => i.Item3.Contains(((PatientIntravenousLinesValue)activeObservation.value).type))).Select(typeValue => typeValue.FirstOrDefault()?.Item2??0).Sum(); + + // Step 1: Select active observations and cast to PatientIntravenousLinesValue + var activeObservations = activeIntraVenousLineObservations.Select(activeObservation => + (PatientIntravenousLinesValue?)activeObservation?.Value); + + // Step 2: Match active observations with type-value mappings + // We use .Replace("\u00A0", " ") on type to avoid special codification for spaces between characters + var matchedValues = activeObservations.Select(activeObservation => _intraVenousLinesTypeValue.Where(i => + activeObservation is { Type: not null } && + i.Item3.Contains(activeObservation.Type.Replace("\u00A0", " ")))) + .ToList(); + + // Step 3: Extract the values from matched tuples + var values = 0; + matchedValues.ForEach(typeValue => values += typeValue.FirstOrDefault()?.Item2 ?? 0); + + // Step 4: Calculate the sum + var intraVenousLineScore = values; + + if (intraVenousLineScore > 10) intraVenousLineScore = 10; + + + var intravenousObs = new PatientObservation + { + PatientId = obs.PatientId, + Name = "IntravenousLines", + Min = 0, + CodingSystem = "ADAS", + Max = 10, + Time = DateTime.UtcNow, + Value = intraVenousLineScore + }; + + var ob = await FixTimeInconsistencyWithLast(intravenousObs); + if (ob != null) + await _observationService.Value.InsertObservation(ob, mapObs: false); + } + catch (InvalidCastException ex) + { + _logger.LogError("CalculateIntravenousLineObservation {obs}: {exMessage}", obs, ex.Message); + } + + return obs; + } + + + private async Task CalculateWeight(BasePatientObservation obs) + { + var complexityValueOfWeight = 0; + if (obs.Units == "kg") obs = await ParseWeight(obs); + + var pobs = (PatientObservation)obs; + + + if (!double.TryParse(pobs.Value.ToString(), out var valueParsed)) + return complexityValueOfWeight; + + complexityValueOfWeight = valueParsed switch + { + < 750 => 7, + >= 750 and <= 999 => 5, + >= 1000 and <= 1249 => 2, + >= 1250 and <= 1999 => 1, + >= 2000 => 0, + _ => complexityValueOfWeight + }; + + return complexityValueOfWeight; + } + + + public Task ParseWeight(BasePatientObservation obs) + { + if (obs is not PatientObservation pobs || !double.TryParse(pobs.Value.ToString(), out var dValue)) + { + _logger.LogError("Error casting weight Observation {obs}:", obs); + return Task.FromResult(obs); + } + + pobs.Units = "gr"; + pobs.Value = dValue * 1000; + return Task.FromResult(obs); + } + + + //Monitor observation can be a observation or treatment + //TODO como saber si tiene fecha de fin o frecuencia + public async Task CalculateMonitor(object monitorObservation) + { + var monitorValue = 0; + BasePatientObservation? pobs = null; + ObjectId patientId = new(); + PatientTreatment? tobs = null; + + if (monitorObservation.GetType() == typeof(PatientObservation)) + { + pobs = (BasePatientObservation)monitorObservation; + patientId = pobs.PatientId; + } + else if (monitorObservation.GetType() == typeof(PatientTreatment)) + { + tobs = (PatientTreatment)monitorObservation; + patientId = tobs.PatientId; + } + + + //Observations in last 30 min are active + var activeMonitorObservationsList = await _observationService.Value.FindLastObservations(patientId, 1, + ["Saturation_RegBrain", "Saturation_RegSomatic", "Transcutaneous_O2"]); + if (!activeMonitorObservationsList.Any()) + { + _logger.LogWarning("Active Monitor Observation List is null or empty for patientId: {patientId}", + patientId); + return; + } + + var activeMonitorObservations = activeMonitorObservationsList.FindAll(o => !o.Expired); + + + if (pobs != null && !((PatientObservation)pobs).Expired) + activeMonitorObservations.Add((PatientObservation)pobs); + + if (activeMonitorObservations.Any(o => o.Code != null && _transcutanous.Contains(o.Code))) monitorValue += 1; + if (activeMonitorObservations.Any(o => o.Code != null && _regionalBrainSaturation.Contains(o.Code))) + monitorValue += 1; + + + //Patient treatment + var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(patientId); + + activeTreatments = activeTreatments.ToList().FindAll(p => + p != null && p.RequestedGiveCodes.Any(r => + _electroencephalogram.Contains(r.Identifier) && r.Text.Contains("Monitor EEGa"))); + + if (tobs != null && tobs.OrderControl != OrderControlType.Dc && activeTreatments.Any()) monitorValue += 1; + else if (tobs is { OrderControl: OrderControlType.Nw }) monitorValue += 1; + + + //Generate calculad monitor obs + var monitorObs = new PatientObservation + { + PatientId = patientId, + Name = "Monitor", + CodingSystem = "ADAS", + Min = 0, + Max = 3, + Time = DateTime.Now, + Value = monitorValue + }; + + await _observationService.Value.InsertObservation(monitorObs, mapObs: false); + } + + + private async Task CalculateSurgery(PatientTreatment treatment) + { + var surgeryScore = 0; + var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(treatment.PatientId); + //Filter only surgery treatments + var activeSurgeryTreatments = activeTreatments.ToList().FindAll(t => + t != null && t.RequestedGiveCodes.Any(code => + _surgery.Contains(code.Identifier) && _surgeryText.Contains(code.Text))); + + switch (treatment.OrderControl) + { + //Refactor solo con los tratamientos activos de cirugía, un DC cancela a su entityIdentifier correspondiente. + + case OrderControlType.Nw: + activeSurgeryTreatments.Add(treatment); + break; + case OrderControlType.Dc: + activeSurgeryTreatments.RemoveAll(t => + t != null && t.PlacerOrder?.EntityIdentifier == treatment.PlacerOrder?.EntityIdentifier); + break; + } + + + if (activeSurgeryTreatments.Count > 0) surgeryScore = 5; + + var surgeryObs = new PatientObservation + { + PatientId = treatment.PatientId, + Name = "Surgery", + Min = 0, + Max = 5, + CodingSystem = "ADAS", + Time = DateTime.Now, + Value = surgeryScore + }; + + await _observationService.Value.InsertObservation(surgeryObs, mapObs: false); + } + + + private async Task> GetActiveBolus(ObjectId patientId, PatientTreatment? newTreatment) + { + var treatmentsProcessed = new List(); + var activeTreatments = await _treatmentService.Value.GetBolusTreatments(patientId); + + + if (newTreatment != null) activeTreatments.Add(newTreatment); + + //Agrupar por namespaceID y quedarme con el último message time que será el último que han reecho. + //Si ese último es concluido suma 1. + + var treatmentsGroupedByNamespaceId = activeTreatments + .GroupBy(t => t.PlacerOrder?.NamespaceId) + //.Select(group => group.OrderByDescending(t => t.messageTime)).ToList(); + .Select(grp => grp.ToList()) + .ToList(); + //.Select(g => g.FirstOrDefault()).ToList(); + + foreach (var treatmentGroup in treatmentsGroupedByNamespaceId) + { + var modificatedBolus = treatmentGroup.FindAll(t => + t.RequestedGiveCodesStatus.Any(r => r.EndAdministrationTime != DateTime.MinValue)); + //Eliminamos todos aquellos que han sido modificados con posterioridad, esto nos lo indica cuando tiene endAdministrationTime != null + treatmentGroup.RemoveAll(t => + modificatedBolus.Any(m => m.RequestedGiveCodesStatus.FirstOrDefault()?.EndAdministrationTime + == t.RequestedGiveCodesStatus.FirstOrDefault()?.AdministrationTime)); + /* treatmentGroup.RemoveAll(t => t.requestedGiveCodesStatus + .Any(r => r.endAdministrationTime == null && modificatedBolus.FirstOrDefault(m => + m.requestedGiveCodesStatus.Any(code => code.endAdministrationTime == r.administrationTime)) + != null)); + */ + //modificatedBolus.Any(b => + // b.requestedGiveCodesStatus.Any(code => code.endAdministrationTime == r.administrationTime)))); + + + //Opción B a veces se cancelan sin endTime. Simplemente, agrupar por administrationTime y quedarme con el último por time del mensaje. + treatmentGroup.RemoveAll(t => !t.RequestedGiveCodesStatus.Any()); + + var groupByAdmTime = treatmentGroup.GroupBy(t => + t.RequestedGiveCodesStatus.FirstOrDefault()!.AdministrationTime) + .Select(grp => grp.OrderByDescending(t => t.MessageTime).First()); + + + treatmentsProcessed.AddRange(groupByAdmTime); + //todos los que tengan startOfAdministration y no tenga esa fecha ninguno de modificatedBolus en endOfTreatment + } + + + return treatmentsProcessed + .FindAll(p => + p.RequestedGiveCodes.Any(r => _medicationBolus.Contains(r.Identifier)) && + p.RequestedGiveCodesStatus.FirstOrDefault() != null && + p.RequestedGiveCodesStatus.FirstOrDefault()!.AdministrationTime?.ToUniversalTime() > + DateTime.UtcNow.AddHours(-12) && + p.RequestedGiveCodesStatus.FirstOrDefault()!.AdministrationTime?.ToUniversalTime() <= DateTime.UtcNow && + p.RequestedGiveCodesStatus.Any(rxa => rxa.Status != null && _rxaStatus.Contains(rxa.Status))); + } + + + private async Task CalculateBolus(PatientTreatment treatment) + { + var activeBolus = await GetActiveBolus(treatment.PatientId, treatment); + + var bolusObs = new PatientObservation + { + PatientId = treatment.PatientId, + Time = DateTime.Now, + Name = "OpiateBoluses", + CodingSystem = "ADAS", + Value = activeBolus.Count + }; + await _observationService.Value.InsertObservation(bolusObs); + } + + /* + * En rojo si TAm < Edad Gestacional en semanas en la primera semana y luego edad corregida + */ + public async Task CalculateTAmAlert(BasePatientObservation obs) + { + var pobs = (PatientObservation)obs; + var tamStatus = StatusEnum.Type.Ok; + var groupedObservations = new List { pobs }; + + var gestationalWeeksToCheck = 0; + + if (obs.Name == "Age_Gestational") + groupedObservations = await _observationService.Value.FindLastObservations(obs.PatientId, 1, + ["Age_Gestational_Fixed", "TAm"]); + if (obs.Name == "Age_Gestational_Fixed") + groupedObservations = await _observationService.Value.FindLastObservations(obs.PatientId, 1, + ["Age_Gestational", "TAm"]); + + if (obs.Name == "TAm") + groupedObservations = await _observationService.Value.FindLastObservations(obs.PatientId, 1, + ["Age_Gestational", "Age_Gestational_Fixed"]); + + var ageGestational = groupedObservations.FirstOrDefault(o => o.Name == "Age_Gestational"); + if (ageGestational != null && int.TryParse(ageGestational.Value.ToString(), out var ageParsed) && ageParsed < 2) + { + gestationalWeeksToCheck = Convert.ToInt32(ageGestational.Value); + } + else + { + var ageGestationalFixed = groupedObservations.FirstOrDefault(o => o.Name == "Age_Gestational_Fixed"); + if (ageGestationalFixed != null) gestationalWeeksToCheck = Convert.ToInt32(ageGestationalFixed.Value); + //var gestationalWeeks = int.Parse(ageGestationalFixed.value.ToString()); + //if (Convert.ToInt32(pobs.value) < gestationalWeeks) TAmStatus = ObservationStatus.Alert; + } + + var tamObs = groupedObservations.FirstOrDefault(o => o.Name == "TAm"); + if (tamObs != null) + if (Convert.ToInt32(tamObs.Value) < gestationalWeeksToCheck) + tamStatus = StatusEnum.Type.Alert; + + if (pobs.Name == "TAm") + { + pobs.Status = tamStatus; + } + else + { + if (tamObs != null) + { + tamObs.Id = ObjectId.GenerateNewId(); + tamObs.Time = DateTime.Now; + tamObs.Status = tamStatus; + await _observationService.Value.InsertObservation(tamObs, mapObs: false); + } + } + } + + /*Entra temp axilar o incubadora. Restamos la axilar a la incubadora. Solo se calcula si están los dos valores. */ + public async Task CalculateTempGradient(BasePatientObservation obs) + { + List tempRetrievedesFromBd; + PatientObservation? tempRetrievedFromBd = null; + var pobs = (PatientObservation)obs; + if ("Temp_Patient".Equals(pobs.Name)) + { + tempRetrievedesFromBd = + await _observationService.Value.FindLastObservations(pobs.PatientId, 1, ["Temp_Incubator"]); + tempRetrievedFromBd = tempRetrievedesFromBd.FirstOrDefault(); + } + + if ("Temp_Incubator".Equals(pobs.Name)) + { + tempRetrievedesFromBd = + await _observationService.Value.FindLastObservations(pobs.PatientId, 1, ["Temp_Patient"]); + tempRetrievedFromBd = tempRetrievedesFromBd.FirstOrDefault(); + } + + + //Now Temp Gradient is only calculated when temp_Patient and Temp_Incubator come in last 10 min. + //if (tempRetrievedFromBd != null && DateTime.UtcNow.CompareTo(tempRetrievedFromBd.time.ToUniversalTime().AddMinutes(10)) <= 0) + if (tempRetrievedFromBd == null) return; + var difFechas = pobs.Time.ToUniversalTime() - tempRetrievedFromBd.Time.ToUniversalTime(); + //if (tempRetrievedFromBd != null && pobs.time.ToUniversalTime().CompareTo(tempRetrievedFromBd.time.ToUniversalTime().AddMinutes(10)) <= 0) + if (Math.Abs(difFechas.TotalMinutes) <= 10 && + double.TryParse(tempRetrievedFromBd.Value.ToString(), out var tempRetrievedFromBdParsed) && + double.TryParse(pobs.Value.ToString(), out var pobsParsed)) + { + double tempGradientValue; + if ("Temp_Patient".Equals(pobs.Name)) + tempGradientValue = tempRetrievedFromBdParsed - pobsParsed; + else + tempGradientValue = pobsParsed - tempRetrievedFromBdParsed; + var tempGradientObs = new PatientObservation + { + PatientId = obs.PatientId, + Name = "Temp_Gradient", + CodingSystem = "ADAS", + Time = obs.Time, + Value = tempGradientValue + }; + + var obsToInsert = (PatientObservation)await CheckObsWithSameTimeExistsAndIncrementTime(tempGradientObs); + + obsToInsert.Id = ObjectId.GenerateNewId(); + + await _observationService.Value.InsertObservation(obsToInsert, mapObs: false); + } + } + + private enum IntraVenousLineTypes + { + Artery, + CentralVein, + Picc, + MiddleLine, + Peripheral + } + + // ReSharper disable once UnusedMember.Local + private enum RespiratoryTypes + { + Ino, + Vafo, + Vmc, + Vmni, + NasalCannulas, + None + } +} \ No newline at end of file diff --git a/adas-core.Application/Customizations/HGM/CalculatedObservations.cs b/adas-core.Application/Customizations/HGM/CalculatedObservations.cs new file mode 100644 index 00000000..9877a0c0 --- /dev/null +++ b/adas-core.Application/Customizations/HGM/CalculatedObservations.cs @@ -0,0 +1,133 @@ +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.Pumps; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; + +namespace adas_core.Application.Customizations.HGM; + +//Custom for Hospital Gregorio Marañón +public class CalculatedObservations : ICalculatedObservations +{ + private readonly List _highFrequencyVentilation = []; + private readonly List _invasiveVentilation = []; + private readonly ILogger _logger; + private readonly List _nonInvasiveVentilation = []; + private readonly Lazy _observationService; + + public CalculatedObservations(IServiceProvider serviceProvider) + { + var apiSettings = serviceProvider.GetRequiredService>().Value; + _logger = serviceProvider.GetRequiredService>(); + _observationService = serviceProvider.GetRequiredService>(); + + var highFrequencyVentilation = apiSettings.HighFrequencyVentilation ?? null; + highFrequencyVentilation?.ForEach(x => _highFrequencyVentilation.Add(x.Trim())); + + var invasiveVentilation = apiSettings.InvasiveVentilation ?? null; + invasiveVentilation?.ForEach(x => _invasiveVentilation.Add(x.Trim())); + + var nonInvasiveVentilation = apiSettings.NonInvasiveVentilation ?? null; + nonInvasiveVentilation?.ForEach(x => _nonInvasiveVentilation.Add(x.Trim())); + } + + public async Task Map(T obs, bool onlyByName = false) where T : BasePatientObservation + { + if (string.IsNullOrEmpty(obs.Name)) return obs; + + switch (obs.Name) + { + case "Resp_Mode": + await CalculateVentilationMode(obs); + break; + } + + return obs; + } + + + public Task Map(PatientTreatment treatment) + { + throw new NotImplementedException(); + } + + public Task Map(PatientDiagnosis diagnosis) + { + throw new NotImplementedException(); + } + + public async Task Map(PumpObservation pumpObservation) + { + return await Task.FromResult(pumpObservation); + } + + + public Task CalculateActiveBolus(ObjectId patientId) + { + throw new NotImplementedException(); + } + + public Task CalculateMedicineObservation(List activeMedicines, ObjectId patientId) + { + throw new NotImplementedException(); + } + + public Task FixTimeInconsistencyWithLast(PatientObservation newObservation) + { + throw new NotImplementedException(); + } + + public Task> GetActiveTreatmentsByPatient(ObjectId id) + { + throw new NotImplementedException(); + } + + public Task> PreMapList(List listToInsert) + { + return Task.FromResult(listToInsert); + } + + public Task MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert) + { + return Task.FromResult(obs); + } + + public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code) + { + throw new NotImplementedException(); + } + + private async Task CalculateVentilationMode(BasePatientObservation obs) + { + var pobs = (PatientObservation)obs; + var respTypeObs = new PatientObservation + { + Name = "Resp_Type", + CodingSystem = "ADAS", + PatientId = obs.PatientId, + Time = obs.Time + }; + + var strValue = pobs.Value.ToString(); + if (string.IsNullOrEmpty(strValue)) + respTypeObs.Value = nameof(RespirationType.None); + else + respTypeObs.Value = _highFrequencyVentilation.Contains(strValue) + ? nameof(RespirationType.HighFrequencyVentilation) + : _invasiveVentilation.Contains(strValue) + ? respTypeObs.Value = nameof(RespirationType.Invasive) + : _nonInvasiveVentilation.Contains(strValue) + ? respTypeObs.Value = nameof(RespirationType.NonInvasive) + : nameof(RespirationType.None); + + + var wasInserted = await _observationService.Value.InsertIfChanged("Resp_Type", respTypeObs); + if (wasInserted) + _logger.LogDebug("Calculated Resp_Type for patientid: {respTypeObsPatientid} value: {respType}", + respTypeObs.PatientId, respTypeObs.Value); + } +} \ No newline at end of file diff --git a/adas-core.Application/Customizations/HPAZ/CalculatedObservations.cs b/adas-core.Application/Customizations/HPAZ/CalculatedObservations.cs new file mode 100644 index 00000000..11a7590e --- /dev/null +++ b/adas-core.Application/Customizations/HPAZ/CalculatedObservations.cs @@ -0,0 +1,641 @@ +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.Pumps; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Serilog; + +namespace adas_core.Application.Customizations.HPAZ; + +public class CalculatedObservations(IServiceProvider serviceProvider) : ICalculatedObservations +{ + private readonly Lazy _alarmService = serviceProvider.GetRequiredService>(); + + + private readonly ApiSettings + _apiSettings = serviceProvider.GetRequiredService>().Value; //apiSettings; + + private readonly IConfigObservationService _configObservationService = + serviceProvider.GetRequiredService(); //configObservationService; + + private readonly ILogger _logger = + serviceProvider.GetRequiredService>(); + + //Use GroupedObservation to avoid circular dependency with _observationService + private readonly Lazy _observationService = + serviceProvider.GetRequiredService>(); //observationService; + + private readonly List _pressBloodArteryMean = ["TAm"]; + + + public Task CalculateActiveBolus(ObjectId patientId) + { + return Task.CompletedTask; + } + + public Task CalculateMedicineObservation(List activeMedicines, ObjectId patientId) + { + return Task.CompletedTask; + } + + public Task> GetActiveTreatmentsByPatient(ObjectId id) + { + return Task.FromResult>(new List()); + } + + /// + /// Maps the obs to an event or alarm depending on its type and inserts it if needed + /// + /// Type + /// Observation + /// True if the observation needs to generate an alert + /// Mapped observation + public async Task Map(T source, bool onlyByName) where T : BasePatientObservation + { + _logger.LogDebug("Mapping {obsName} mode observation {obs}", source.Name, source); + + if (source is not BasePatientObservationValue obs) return source; + + var name = obs is PatientObservationAlarm obsAlarm ? obsAlarm.Event : obs.Name; + name ??= string.Empty; + + + //No generamos alertas si viene onlyByName + if (name == "Resp_Mode") + obs = CalculateVentilationMode(obs); + + if (name is "Sattc" or "FiO2") await CalculateSf(obs, name); + + if (name is "FiO2" or "P_VAM" or "PaO2_Tidal") await CalculateOxygenationIndex(obs, name); + + if (name is "FiO2" or "PaO2_Tidal") await CalculatePf(obs, name); + + if (name == "TEST_ALARMA") await _alarmService.Value.CalculateAlarmTest(obs, name); + + obs = await CheckObsWithSameTimeExistsAndIncrementTime(obs); + + if (obs is PatientObservation pobs) + { + pobs = await CheckAlarmConfig(pobs); + return pobs as T; + } + + return obs as T; + } + + + public async Task Map(PumpObservation pumpObservation) + { + try + { + if (pumpObservation.Status is PumpEnum.Status.Alarm or PumpEnum.Status.Warning) + { + PatientObservation? patientObservation = null; + string? name = null; + + //La oclusión es para todos los tipos + var volumetricAirOcclusionAlarm = _apiSettings.VolumetricAirOcclusionAlarm ?? null; + + if (volumetricAirOcclusionAlarm != null && pumpObservation.AlarmType != null && + volumetricAirOcclusionAlarm.Contains(pumpObservation.AlarmType.ToString() ?? string.Empty)) + { + name = "VolumetricAirOcclusion"; + //alerta oclusión aire para bombas volumétricas + patientObservation = await CreatePumpAlarmObservation(name, pumpObservation); + } + + + var listInotropicMedicines = _apiSettings.InotropicMedicines ?? null; + + if (listInotropicMedicines is { Count: > 0 } && pumpObservation.DrugName != null && + listInotropicMedicines.Contains(pumpObservation.DrugName)) //INOTRÓPICOS + { + var inotropicEndInfusionAlarm = _apiSettings.InotropicEndInfusionAlarm ?? null; + + if (inotropicEndInfusionAlarm != null && pumpObservation.AlarmType != null && + inotropicEndInfusionAlarm.Contains(pumpObservation.AlarmType.ToString() ?? string.Empty)) + { + //Inotrópicos próxima a fin de infusión + // Y + //proximo fin de infusión, oclusión, y presencia aire…. + //Solo inotrópicos + //Está en la línea 19 del excel y abarca la que hay en la línea 9 + // TODO: revisar en los logs que nombre de type (Buscar por crud xmlns) + name = "InotropicEndInfusion"; + patientObservation = await CreatePumpAlarmObservation(name, pumpObservation); + } + } + else + { + var pumPressureAlarm = _apiSettings.PumPressureAlarm ?? null; + + if (pumPressureAlarm != null && pumpObservation.AlarmType != null && + pumPressureAlarm.Contains(pumpObservation.AlarmType.ToString() ?? string.Empty)) + { + name = "Pressure"; + //alerta de presión(la alerta de la bomba se programa + -30mmHg por encima de la presión de la línea). + patientObservation = await CreatePumpAlarmObservation(name, pumpObservation); + } + + var endContinuousInfusionAlarm = _apiSettings.EndContinuousInfusionAlarm ?? null; + + if (endContinuousInfusionAlarm != null && pumpObservation.AlarmType != null && + endContinuousInfusionAlarm.Contains(pumpObservation.AlarmType.ToString() ?? string.Empty)) + { + name = "EndContinuousInfusion"; + //próximo a fin de infusión /fin de infusión + //información de bombas solo perfusión continua + patientObservation = await CreatePumpAlarmObservation(name, pumpObservation); + } + + var volumetricAirInLineAlarm = _apiSettings.VolumetricAirInLineAlarm ?? null; + + if (volumetricAirInLineAlarm != null && pumpObservation.AlarmType != null && + volumetricAirInLineAlarm.Contains(pumpObservation.AlarmType.ToString() ?? string.Empty)) + { + name = "VolumetricAirInLine"; + //alerta aire para bombas volumétricas (burbujas de aire para la bomba) + patientObservation = await CreatePumpAlarmObservation(name, pumpObservation); + } + } + + + if (patientObservation != null && name != null) + { + _ = _observationService.Value.InsertObservation(patientObservation); + _ = _alarmService.Value.SendAlarm(patientObservation, name, AlarmEnum.Name.Pump, + AlarmEnum.Severity.None, AlarmEnum.Type.Auto); + } + } + } + catch (Exception ex) + { + _logger.LogError("Error mapping alarm pump observation. Exception: {exMessage}", ex.Message); + } + + return pumpObservation; + } + + + public Task Map(PatientTreatment treatment) + { + throw new NotImplementedException(); + } + + public Task Map(PatientDiagnosis diagnosis) + { + throw new NotImplementedException(); + } + + + public async Task FixTimeInconsistencyWithLast(PatientObservation newObservation) + { + if (string.IsNullOrEmpty(newObservation.Name)) + { + _logger.LogError( + "Error FixTimeInconsistencyWithLast new observation name is null or empty: {newObservation}.", + newObservation); + return newObservation; + } + + var lasObservations = await _observationService.Value.FindLastObservations(newObservation.PatientId, 1, + [newObservation.Name]); + var lastObservation = lasObservations.FirstOrDefault(); + + if (lastObservation != null && DateTime.Compare( + new DateTime(lastObservation.Time.Year, lastObservation.Time.Month, + lastObservation.Time.Day, lastObservation.Time.Hour, + lastObservation.Time.Minute, lastObservation.Time.Second), + new DateTime(newObservation.Time.Year, newObservation.Time.Month, + newObservation.Time.Day, newObservation.Time.Hour, + newObservation.Time.Minute, newObservation.Time.Second) + ) >= 0) + newObservation.Time = lastObservation.Time.AddSeconds(1); + + return newObservation; + } + + public Task> PreMapList(List listToMap) + { + var mappedObsList = listToMap + .Select(obs => obs.DeepCopy()) + .Select(obs => _configObservationService.Map(obs).Result) + .Where(obs => obs != null) + .ToList(); + + // mappedObsList contains observation mapped + // Check blue code + + _ = CalculateBlueCodeList(mappedObsList); + + + return Task.FromResult(listToMap); + } + + public Task MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert) + { + if (!string.IsNullOrEmpty(alarmToInsert.EventId)) + obs.Code = alarmToInsert.EventId; + + if (!string.IsNullOrEmpty(alarmToInsert.Event)) + obs.Name = alarmToInsert.Event; + + obs.Value = alarmToInsert.Value; + + return Task.FromResult(obs); + } + + Task ICalculatedObservations.SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code) + { + throw new NotImplementedException(); + } + + + private async Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code, AlarmEnum.Type type) + { + await _alarmService.Value.SendAlarm(obs, name, code, AlarmEnum.Severity.None, type); + } + + + /// + /// Initial check to ignore old observations generating blue codes + /// + /// Observation + /// True if it needs to be ignored + private bool CheckIfObservationIsOlderAndShouldBeIgnored(PatientObservation obs) + { + if (obs.Time.ToUniversalTime().AddMinutes(_apiSettings.IgnoreCalcObservationsOlderInMinutesThan) < + DateTime.UtcNow) + { + Log.Debug( + "ignoring calc old observation: name:{obsName} obs_time:{obsTime} system_time {DateTimeNow} id:{obsId}", + obs.Name ?? "null", obs.Time, DateTime.Now, obs.Id); + return true; + } + + return false; + } + + + private async Task CheckAlarmConfig(PatientObservation pobs) + { + var configObs = await _configObservationService.Get(new PatientObservation + { + Name = pobs.Name, + PatientId = pobs.PatientId + }); + + pobs.Alarm = configObs?.Alarm ?? null; + + return pobs; + } + + private static PatientObservation CreateAlarmObservation(string name, PatientObservation pobs) + { + return new PatientObservation + { + CodingSystem = "ADAS_ALARM", + Code = name, + Name = $"Alarm_{name}", + Value = pobs.Value, + PatientId = pobs.PatientId, + Time = pobs.Time, + Alarm = pobs.Alarm + }; + } + + /// + /// Checks if a blue code observation Lists needs to be generated and inserts it. + /// + /// + public async Task CalculateBlueCodeList(List? obsList) + { + //Las 3 observaciones vienen siempre juntas en el mismo mensaje + //Solo se comprueba si se cumplen 2 de las 3 condiciones + + var condition = 0; + var patientId = obsList?.FirstOrDefault()?.PatientId; + if (patientId == null) return; + + var fcObs = obsList?.FirstOrDefault(o => o is { Name: "FC" }); + var sattcObs = obsList?.FirstOrDefault(o => o is { Name: "Sattc" }); + var tamObs = obsList?.FirstOrDefault(o => o is { Name: "TAm" }); + + // FC < 60 + if (fcObs != null && !CheckIfObservationIsOlderAndShouldBeIgnored(fcObs) && + double.TryParse(fcObs.Value.ToString(), out var cardiacBeatRate) && cardiacBeatRate < 60) + { + Log.Debug("BlueCode condition: FC {fc} for patient: {obsPatientid} fc increase limit", cardiacBeatRate, + fcObs.PatientId); + condition++; + } + + // SpO2 < 80 (Sattc) + if (sattcObs != null && !CheckIfObservationIsOlderAndShouldBeIgnored(sattcObs) && + double.TryParse(sattcObs.Value.ToString(), out var spO2Value) && spO2Value < 80) + { + Log.Debug("Blue code condition: SpO2 {sp} for patient: {obsPatientid} Sattc increase limit", spO2Value, + sattcObs.PatientId); + condition++; + } + + if (condition == 0) return; + + // Early exit if 2 conditions are met + if (condition == 2) + { + _ = SendBlueCode(fcObs); + return; + } + + + // TAm actual es un 50% por debajo de la medición de 30 seg antes + if (tamObs != null && !CheckIfObservationIsOlderAndShouldBeIgnored(tamObs) && + double.TryParse(tamObs.Value.ToString(), out var tamObsValue)) + { + // Buscamos el anterior + var lastPressBloodObsList = + await _observationService.Value.FindLastObservations(tamObs.PatientId, 1, _pressBloodArteryMean); + var lastPressBloodObs = lastPressBloodObsList.FirstOrDefault(); + + // Diferencia entre la última observación es < 30 s + if (lastPressBloodObs != null && + double.TryParse(lastPressBloodObs.Value.ToString(), out var lastPressBloodValue) && + (tamObs.Time - lastPressBloodObs.Time).TotalSeconds <= 35 && + tamObsValue <= lastPressBloodValue / 2) + { + Log.Debug("Blue code condition: TAm decrease limit over 30s for patient: {obsPatientid}", + tamObs.PatientId); + condition++; + } + } + + if (condition >= 2) _ = SendBlueCode(tamObs); + } + + + private async Task SendBlueCode(PatientObservation? pobs) + { + if (pobs == null) + return; + + Log.Debug("Blue code Alarm for patient: {obsPatientid}", pobs.PatientId); + + var blueCodeObservation = CreateAlarmObservation("BlueCode", pobs); + + blueCodeObservation = await CheckAlarmConfig(blueCodeObservation); + + _ = _observationService.Value.InsertObservation(blueCodeObservation); + + _ = SendAlarm(blueCodeObservation, "BlueCode", AlarmEnum.Name.Blue, AlarmEnum.Type.Auto); + } + + + //PRVC : donde aparezca cambiar y mostrar en su lugar: VCRP + //FLUJO ALTO: poner OAF en su lugar + private BasePatientObservationValue CalculateVentilationMode(BasePatientObservationValue obs) + { + var value = obs is PatientObservationAlarm oAlarm ? oAlarm.Value + : obs is PatientObservation o ? o.Value : null; + + + if (value != null && value.ToString()!.Contains("PRVC")) + obs.Value = value.ToString()!.Replace("PRVC", "VCRP"); + else if (value != null && value.ToString()!.Contains("FLUJ.ALTO")) obs.Value = "OAF"; + + _logger.LogDebug("Calculating patient id: {id} ventilation mode: {obsName}, value : {obsValue}", obs.PatientId, + obs.Name, obs.Value); + + return obs; + } + + //TODO duplicado con el calculated del 12o, sacar a un utils + private async Task CheckObsWithSameTimeExistsAndIncrementTime( + BasePatientObservationValue obs) + { + if (obs is not PatientObservation pobs) return obs; + + var existObsWithSameTime = + await _observationService.Value.FindAnyWithSameDate(pobs.PatientId, pobs.Time, pobs.Name); + + if (existObsWithSameTime != null && existObsWithSameTime.Any()) pobs.Time = pobs.Time.AddSeconds(1); + + return pobs; + } + + //S/F Calc ( craneal saturation / FiO2 + private async Task CalculateSf(BasePatientObservationValue obs, string name) + { + try + { + PatientObservation? saturation = null; + PatientObservation? fio2 = null; + + var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["FiO2", "Sattc"]); + + switch (name) + { + case "FiO2": + fio2 = (PatientObservation)obs; + saturation = values.FirstOrDefault(o => o.Name == "Sattc"); + break; + + case "Sattc": + saturation = (PatientObservation)obs; + fio2 = values.FirstOrDefault(o => o.Name == "FiO2"); + break; + } + + if (fio2 != null && saturation != null && !fio2.Expired && + double.TryParse(fio2.Value.ToString(), out var fio) && !saturation.Expired && + double.TryParse(saturation.Value.ToString(), out var sat) && sat <= 97) + { + if (fio == 0) + { + _logger.LogError("Error calculating SF. FiO2: {fio}", fio); + return; + } + + var sfValue = sat / fio; + + sfValue = Math.Round(sfValue, 4); + + var sfObs = new PatientObservation + { + PatientId = obs.PatientId, + Name = "Sattc_FiO2", + CodingSystem = "ADAS", + Value = sfValue, + Time = obs.Time + }; + + await _observationService.Value.InsertObservation(sfObs); + } + } + catch (Exception ex) + { + _logger.LogError("Error calculate S/F observation. Exception: {exMessage}", ex.Message); + } + } + + + /// + /// Oxygenation index is calculated => P_VAM x FiO2 x 100 / PaO2_Tidal + /// the method is called when it receives P_VAM or FiO2 or PaO2_Tidal and tries to take the other values to perform the + /// calculation if they are not + /// in database then does nothing. + /// + public async Task CalculateOxygenationIndex(BasePatientObservationValue obs, string name) + { + var toSearchList = new List(); + PatientObservation? pVam = null; + PatientObservation? fiO2 = null; + PatientObservation? paO2 = null; + + toSearchList.AddRange(new List { "P_VAM", "FiO2", "PaO2_Tidal" }); + + var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList); + + try + { + switch (name) + { + case "P_VAM": + pVam = (PatientObservation)obs; + fiO2 = values.FirstOrDefault(o => o.Name == "FiO2"); + paO2 = values.FirstOrDefault(o => o.Name == "PaO2_Tidal"); + break; + + case "FiO2": + fiO2 = (PatientObservation)obs; + pVam = values.FirstOrDefault(o => o.Name == "P_VAM"); + paO2 = values.FirstOrDefault(o => o.Name == "PaO2_Tidal"); + break; + + case "PaO2_Tidal": + paO2 = (PatientObservation)obs; + fiO2 = values.FirstOrDefault(o => o.Name == "FiO2"); + pVam = values.FirstOrDefault(o => o.Name == "P_VAM"); + break; + } + + //Controlar que FiO2 no haya expirada al recogerla + if (fiO2 != null && paO2 != null && pVam != null && + int.TryParse(pVam.Value.ToString(), out var pvam) && + int.TryParse(fiO2.Value.ToString(), out var fio2) && + int.TryParse(paO2.Value.ToString(), out var pao2)) + { + if (pao2 == 0) + { + _logger.LogError( + "Error calculate Oxygenation Index observation. PaO2: {pao2}, observation: {obsName}", pao2, + name); + return; + } + + var oxygenationIndexValue = pvam * fio2 * 100 / pao2; + + var oxygenationIndexObs = new PatientObservation + { + PatientId = obs.PatientId, + Name = "Oxygenation_Index", + CodingSystem = "ADAS", + Value = oxygenationIndexValue, + Time = obs.Time + }; + + await _observationService.Value.InsertObservation(oxygenationIndexObs); + } + } + catch (Exception ex) + { + _logger.LogError("Error calculate Oxygenation Index observation. Exception: {exMessage}", ex.Message); + } + } + + //P/F Calc ( PaO2_Tidal / FiO2 + private async Task CalculatePf(BasePatientObservationValue obs, string name) + { + try + { + PatientObservation? paO2 = null; + PatientObservation? fio2 = null; + + var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, + ["FiO2", "PaO2_Tidal"]); + + switch (name) + { + case "FiO2": + fio2 = (PatientObservation)obs; + paO2 = values.FirstOrDefault(o => o.Name == "PaO2_Tidal"); + break; + + case "PaO2_Tidal": + paO2 = (PatientObservation)obs; + fio2 = values.FirstOrDefault(o => o.Name == "FiO2"); + break; + } + + if (fio2 != null && paO2 != null && !fio2.Expired && !paO2.Expired && + double.TryParse(paO2.Value.ToString(), out var pa) && + double.TryParse(fio2.Value.ToString(), out var fi)) + { + if (fi == 0) + { + _logger.LogError("Error Calculating PF. FiO2 value is 0. Observation:{obs}", obs); + return; + } + + var sfValue = pa / fi; + + sfValue = Math.Round(sfValue, 4); + + var sfObs = new PatientObservation + { + PatientId = obs.PatientId, + Name = "PaO2_FiO2", + CodingSystem = "ADAS", + Value = sfValue, + Time = obs.Time + }; + + await _observationService.Value.InsertObservation(sfObs); + } + } + catch (Exception ex) + { + _logger.LogError("Error calculate P/F observation. Exception: {exMessage}", ex.Message); + } + } + + + private async Task CreatePumpAlarmObservation(string name, + PumpObservation pumpObservation) + { + var positionPump = pumpObservation.IsAux != null && !pumpObservation.IsAux.Value + ? $"Rack {pumpObservation.GatewayNumber} Bomba {pumpObservation.Number}" + : $"Rack Aux {pumpObservation.GatewayNumber} Bomba {pumpObservation.Number}"; + + + + PatientObservation patientObservation = new() + { + CodingSystem = "ADAS_ALARM", + Code = $"Pump_{name}", + Name = $"Alarm_Pump_{name}", + Value = pumpObservation.DrugName ?? positionPump, + Time = pumpObservation.Time + }; + + if (pumpObservation.PatientId != null) + patientObservation.PatientId = pumpObservation.PatientId.Value; + + return await CheckAlarmConfig(patientObservation); + + } +} \ No newline at end of file diff --git a/adas-core.Application/Customizations/HRYC/CalculatedObservations.cs b/adas-core.Application/Customizations/HRYC/CalculatedObservations.cs new file mode 100644 index 00000000..d5909d93 --- /dev/null +++ b/adas-core.Application/Customizations/HRYC/CalculatedObservations.cs @@ -0,0 +1,883 @@ +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.MongoModels; +using adas_core.Domain.Models.Pumps; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; + +namespace adas_core.Application.Customizations.HRYC; + +public class CalculatedObservations : ICalculatedObservations +{ + private readonly IOptions _apiSettings; + private readonly List _codesForInvasiveVentilation = []; + + private readonly IConfigObservationService _configObservationService; + + //private readonly Lazy _boxService; + private readonly List _highFrequencyVentilation = []; + private readonly Lazy _lightBeaconService; + private readonly ILogger _logger; + private readonly List _nonInvasiveVentilation = []; + private readonly Lazy _observationService; + private readonly Lazy _patientService; + + + public CalculatedObservations(IServiceProvider serviceProvider) + { + _observationService = serviceProvider.GetRequiredService>(); //observationService; + _patientService = serviceProvider.GetRequiredService>(); + _apiSettings = serviceProvider.GetRequiredService>(); //apiSettings; + //_boxService = serviceProvider.GetRequiredService>(); + _logger = serviceProvider.GetRequiredService>(); + _lightBeaconService = serviceProvider.GetRequiredService>(); + _configObservationService = + serviceProvider.GetRequiredService(); //configObservationService; + + + var highFrenquencyVentilation = _apiSettings.Value.HighFrequencyVentilation ?? null; + highFrenquencyVentilation?.ForEach(x => _highFrequencyVentilation.Add(x.Trim())); + + + var nonInvasiveVentilation = _apiSettings.Value.NonInvasiveVentilation ?? null; + nonInvasiveVentilation?.ForEach(x => _nonInvasiveVentilation.Add(x.Trim())); + + var codesForInvasiveVentilation = //TODO comprobar si se refiere a InvasiveVentilation + _apiSettings.Value.InvasiveVentilation ?? null; + codesForInvasiveVentilation?.ForEach(x => _codesForInvasiveVentilation.Add(x.Trim())); + } + + + public async Task Map(T obs, bool onlyByName) where T : BasePatientObservation + { + _logger.LogTrace("Mapping {name} observation {obs}", obs.Name, obs); + + switch (obs.Name) + { + case "Resp_Mode": + _logger.LogTrace("Mapping Resp mode observation {obs}", obs); + await CalculateVentilationMode(obs); + break; + case "Diuresis": + case "Weight_Current": + if (obs.Name == "Weight_Current") await CalculateWeight_DiffObservation(obs); + await CalculateDiureis_WeightObservation(obs); + break; + case "AllergiesObs": + await CalculateAllergiesObservation(obs); + break; + case "DrainagesObs": + await CalculateDrainagesObservation(obs); + break; + case "PEEP": + case "Pleateu_Pressure": + await CalculateDelta_PressureObservation(obs); + break; + case "Daily_Balance": + await CalculateDaily_BalanceObservation(obs); + break; + case "Hydric_Balance": + await CalculateHydricBalanceCalculated(obs); + obs = (T)await CalculateHydricBalance(obs); + break; + case "Hour_Balance": + obs = (T)await CalculateHourBalance(obs); + break; + case "FR": + await CalculateRespRate(obs); + await CalculateIrox(obs); + break; + case "Vent_Rate": + await CalculateRespRate(obs); + break; + case "SpO2": + case "FiO2": + await CalculateIrox(obs); + break; + case "NEWS": + await CalculateNews(obs); + break; + } + + return obs; + } + + + public Task Map(PatientTreatment treatment) + { + throw new NotImplementedException(); + } + + + public async Task Map(PumpObservation pumpObservation) + { + return await Task.FromResult(pumpObservation); + + } + + public Task CalculateMedicineObservation(List activeMedicines, ObjectId patientId) + { + return Task.CompletedTask; + } + + public Task CalculateActiveBolus(ObjectId patientId) + { + return Task.CompletedTask; + } + + public Task> GetActiveTreatmentsByPatient(ObjectId id) + { + return Task.FromResult(new List().AsEnumerable()); + } + + public Task Map(PatientDiagnosis diagnosis) + { + return Task.FromResult(diagnosis); + } + + + + public async Task FixTimeInconsistencyWithLast(PatientObservation newObservation) + { + if (string.IsNullOrEmpty(newObservation.Name)) + { + _logger.LogError( + "Error FixTimeInconsistencyWithLast newObservation name ia null or empty. Observation: {newObservation}", + newObservation); + return newObservation; + } + + var lastObservations = await _observationService.Value.FindLastObservations(newObservation.PatientId, 1, + [newObservation.Name]); + var lastObservation = lastObservations.FirstOrDefault(); + + if (lastObservation != null && DateTime.Compare( + new DateTime(lastObservation.Time.Year, lastObservation.Time.Month, + lastObservation.Time.Day, lastObservation.Time.Hour, + lastObservation.Time.Minute, lastObservation.Time.Second), + new DateTime(newObservation.Time.Year, newObservation.Time.Month, + newObservation.Time.Day, newObservation.Time.Hour, + newObservation.Time.Minute, newObservation.Time.Second) + ) >= 0) + newObservation.Time = lastObservation.Time.AddSeconds(1); + + return newObservation; + } + + public async Task> PreMapList(List listToInsert) + { + var ventRate = listToInsert.FirstOrDefault(obs => obs.Name == "MDC_VENT_RESP_RATE"); + var respRate = listToInsert.FirstOrDefault(obs => obs.Name == "MDC_RESP_RATE"); + + // Check if obs for Vent_Rate and FR is on the list to insert + if (ventRate == null || respRate == null) return listToInsert; + + var ventRateValue = double.TryParse(ventRate.Value.ToString(), out var v1) ? v1 : 0; + // ventRate is on the list to insert but its value is 0 + if (ventRateValue == 0) + { + _logger.LogDebug("Find ventRate on PreMap request not processed because its value is 0"); + return listToInsert; + } + + var respRateValue = double.TryParse(ventRate.Value.ToString(), out var v2) ? v2 : 0; + // respRate is on the list to insert but its value is 0 + if (respRateValue == 0) + { + _logger.LogDebug("Find respRate on PreMap request not processed because its value is 0"); + return listToInsert; + } + + + // Exists both obs and have value != 0 + // In that case we need to calculate Resp_Rate_Calculated just with the value of Vent_Rate + try + { + _logger.LogDebug( + "Find on PreMap Vent_Rate and FR in the same request process Vent_Rate first to calculate Resp_Rate_Calculated"); + await _observationService.Value.InsertObservation(ventRate); + listToInsert.Remove(ventRate); + } + catch (Exception e) + { + _logger.LogError("Error while PreMap on insert obs: {ventRate} exception: {e}", ventRate, e); + } + + return listToInsert; + } + + public Task MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert) + { + return Task.FromResult(obs); + } + + public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code) + { + throw new NotImplementedException(); + } + + /// + /// IF news greater or equal than 5 and less than 7 is warning beacon and if greater or equal than 7 alert + /// + /// + private async Task CalculateNews(BasePatientObservation obs) + { + var pobs = (PatientObservation)obs; + var parsed = int.TryParse(pobs.Value.ToString(), out var valueParsed); + var patient = await _patientService.Value.FindById(obs.PatientId); + if (patient == null) return; + //var beaconConfig = await _boxService.Value.GetBeaconConfig(new PatientLocation(patient.PointOfCare, patient.Bed)); + + //power off beacon + + //recuperar la beacon del paciente y encenderla apagarla si el valor es > 5&6 o 7 alert + + if ((parsed && valueParsed < 5) || !parsed) + //await _balizaService.Value.PowerOffLed(beaconCnf); + //await _observationService.Value.SendObsBroadcast(obs); + await SendAlarm(obs, AlarmEnum.Name.NewsOff); + else + switch (valueParsed) + { + case >= 5 and < 7: + _logger.LogDebug("beacon yellow alert by NEWS for patient: {patientid}", obs.PatientId); + await SendAlarm(obs, AlarmEnum.Name.NewsWarning); + //await _balizaService.Value.SendBeaconColor(patient, LightBeaconColor.YELLOW); + break; + case >= 7: + _logger.LogDebug("beacon red alert by NEWS for patient: {patientid}", obs.PatientId); + await SendAlarm(obs, AlarmEnum.Name.NewsAlert); + //await _balizaService.Value.SendBeaconColor(patient, LightBeaconColor.RED); + break; + } + + //if (obsAlarm == null) return; + } + + private async Task SendAlarm(BasePatientObservation obs, AlarmEnum.Name name) + { + var pobs = (PatientObservation)obs; + PatientObservation nObs = new() + { + CodingSystem = "ADAS_ALARM", + Code = name.ToString(), + Name = $"Alarm_{name}", + PatientId = pobs.PatientId, + Time = pobs.Time + }; + try + { + //ConfigObservations + var configObs = await _configObservationService.Get(new PatientObservation + { + Name = nObs.Name, + PatientId = obs.PatientId + } + ); + if (configObs == null) + { + _logger.LogError("Config observation is null on send alarm NEWS for {nObs}", nObs); + return; + } + + if (configObs is { Alarm.Enabled: true }) + { + nObs.Alarm = configObs.Alarm; + + if (configObs.Alarm.Beacon is { Enabled: true }) + { + var patient = await _patientService.Value.FindById(obs.PatientId); + if (patient != null) + { + _logger.LogDebug( + "PatientId: {nObsPatientId}. Send Beacon alarmName {configObsAlarmBeaconBeaconColorValue}", + nObs.PatientId, configObs.Alarm.Beacon.BeaconColor); + nObs.Value = configObs.Alarm.Beacon.BeaconColor.ToString(); + SendBeaconCode(configObs.Alarm.Beacon.BeaconColor, patient); + } + } + } + + _logger.LogDebug( + "Insert obs alarm for news at: {DateTimeNow} patientId: {nObsPatientId} color value: {nObsValue}", + DateTime.Now, nObs.PatientId, nObs.Value); + await _observationService.Value.InsertObservation(nObs); + } + catch (Exception ex) + { + _logger.LogError("Exception sending alarm: {exMessage}", ex.Message); + } + } + + private void SendBeaconCode(AlarmEnum.BeaconColor color, Patient patient) + { + if (!patient.PointOfCareId.HasValue) + { + _logger.LogError("Error sending beacon color on calculateObservations poc id on patient is null {Patient}", + patient.ToString()); + return; + } + + switch (color) + { + case AlarmEnum.BeaconColor.Blue: + _lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Blue); + break; + case AlarmEnum.BeaconColor.Yellow: + _lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Yellow); + break; + case AlarmEnum.BeaconColor.Red: + _lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Red); + break; + case AlarmEnum.BeaconColor.None: + _lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Off); + break; + } + } + + //(SpO2/FiO2)/FR IROX formula. Only calculate when all observations are in last 10 minutes. + private async Task CalculateIrox(BasePatientObservation obs) + { + try + { + var pob = (PatientObservation)obs; + var obsToCalc = new List { pob }; + switch (pob.Name) + { + case "SpO2": + + var fio2Frvalues = await _observationService.Value.FindLastObservations(obs.PatientId, 1, + ["FiO2", "FR"]); + if (fio2Frvalues.Count > 0) obsToCalc.AddRange(fio2Frvalues); + + break; + case "FiO2": + var spo2Frvalues = await _observationService.Value.FindLastObservations(obs.PatientId, 1, + ["SpO2", "FR"]); + if (spo2Frvalues.Count > 0) obsToCalc.AddRange(spo2Frvalues); + + break; + case "FR": + var fio2Spo2Values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, + ["FiO2", "SpO2"]); + + if (fio2Spo2Values.Count > 0) obsToCalc.AddRange(fio2Spo2Values); + + + break; + } + + if (!obsToCalc.All(o => o.Time.CompareTo(DateTime.UtcNow.AddMinutes(10)) <= 0)) return; + + var fio2 = obsToCalc.FirstOrDefault(o => o.Name == "FiO2"); + var fr = obsToCalc.FirstOrDefault(o => o.Name == "FR"); + var spo2 = obsToCalc.FirstOrDefault(o => o.Name == "SpO2"); + if (fio2 == null || fr == null || spo2 == null) return; + + + if (!double.TryParse(spo2.Value.ToString(), out var nSpo2) || + !double.TryParse(fio2.Value.ToString(), out var nFio2) || + !double.TryParse(fr.Value.ToString(), out var nFr)) + { + _logger.LogWarning("Error parsing values. fio2: {fio2}, fr: {fr}, spo2: {spo2}", fio2, fr, spo2); + return; + } + + var spo2Value = nSpo2; + var fio2Value = nFio2; + var frValue = nFr; + + if (spo2Value == 0 || fio2Value == 0 || frValue == 0) return; + + + var value = nSpo2 / nFio2 / nFr; + + var irox = new PatientObservation + { + Name = "IROX", + CodingSystem = "ADAS", + PatientId = obs.PatientId, + Time = obs.Time, + Value = value + }; + await _observationService.Value.InsertObservation(irox); + } + catch (Exception e) + { + _logger.LogError("Exception calculating IROX, error: {eMessage}", e.Message); + } + } + + /* + * + RespRate solo se tiene que poner cuando no hay ningún ventRate en los últimos min y veinte segundos y el último no es un 0. + + */ + + //static readonly SemaphoreSlim semaphoreCalculateRespRate = new(1, 1); + private async Task CalculateRespRate(BasePatientObservation obs) + { + var pobs = (PatientObservation)obs; + _logger.LogDebug("CalculateRespRate {obsPatientid} wait {obsName} {obsTime} {pobsValue}", obs.PatientId, + obs.Name, obs.Time, pobs.Value); + if (Convert.ToDouble(pobs.Value) == 0) return; + + if (Convert.ToDouble(pobs.Value) == 0) return; + + var insert = true; + if (obs.Name == "FR") + { + // Insertamos si no existe una obs Vent_Rate o si es existe tiene más de X" y su valor es > 0 + var lastVentRate = + (await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["Vent_Rate"])) + .FirstOrDefault(); + if (lastVentRate == null || (double.TryParse(lastVentRate.Value.ToString(), out var v) ? v : 0) == 0) + { + _logger.LogDebug("CalculateRespRate {id} lastVentRate: null", obs.PatientId); + } + else + { + var diff = DateTime.UtcNow.Subtract(lastVentRate.Time.ToUniversalTime()); + insert = + lastVentRate.Time.ToUniversalTime().AddMinutes(_apiSettings.Value.CalculateRespRateVentExpires) < + DateTime.UtcNow; + _logger.LogDebug( + "CalculateRespRate {patientid} lastVentRate: {value} - {time} passed {diff} {insert}", + obs.PatientId, lastVentRate.Value, lastVentRate.Time, diff, insert ? "Inserted" : "Not inserted"); + } + } + + if (insert) + { + _logger.LogDebug( + "CalculateRespRate {patientid} Insert Resp_Rate_Calculated with {name} - {time} value {value}", + obs.PatientId, obs.Name, obs.Time, pobs.Value); + var calculatedRespRate = new PatientObservation + { + Id = new ObjectId(), + Name = "Resp_Rate_Calculated", + CodingSystem = "ADAS", + PatientId = obs.PatientId, + Time = obs.Name == "FR" ? obs.Time : obs.Time.AddSeconds(1), + Value = pobs.Value, + ParentData = new ParentDataClass { Name = obs.Name } + }; + await _observationService.Value.InsertObservation(calculatedRespRate); + } + else + { + _logger.LogDebug( + "CalculateRespRate {patientid} Insert Resp_Rate_Calculated with {name} - {time} value {value}", + obs.PatientId, obs.Name, obs.Time, pobs.Value); + } + + _logger.LogDebug("CalculateRespRate {patientid} release {name} {time} {value}", obs.PatientId, + obs.Name, obs.Time, pobs.Value); + } + + private async Task CalculateHydricBalance(BasePatientObservation obs) + { + var pobs = (PatientObservation)obs; + + var lastHydricBalanceFromHour = + await _observationService.Value.FindLastBeforeDate(pobs.PatientId, pobs.Time.AddMinutes(59), + "Hydric_Balance"); + + if (lastHydricBalanceFromHour != null && + DateTime.Compare(new DateTime(lastHydricBalanceFromHour.Time.Year, lastHydricBalanceFromHour.Time.Month, + lastHydricBalanceFromHour.Time.Day, lastHydricBalanceFromHour.Time.Hour, 0, 0) + , new DateTime(pobs.Time.Year, pobs.Time.Month, pobs.Time.Day, pobs.Time.Hour, 0, 0)) == 0) + pobs.Time = lastHydricBalanceFromHour.Time.AddSeconds(1); + + return obs; + } + + private async Task CalculateHourBalance(BasePatientObservation obs) + { + var pobs = (PatientObservation)obs; + + var lastHourBalanceFromHour = + await _observationService.Value.FindLastBeforeDate(pobs.PatientId, pobs.Time.AddMinutes(59), + "Hour_Balance"); + + if (lastHourBalanceFromHour != null && + DateTime.Compare(new DateTime(lastHourBalanceFromHour.Time.Year, lastHourBalanceFromHour.Time.Month, + lastHourBalanceFromHour.Time.Day, lastHourBalanceFromHour.Time.Hour, 0, 0) + , new DateTime(pobs.Time.Year, pobs.Time.Month, pobs.Time.Day, pobs.Time.Hour, 0, 0)) == 0) + pobs.Time = lastHourBalanceFromHour.Time.AddSeconds(1); + + return obs; + } + + private async Task CalculateHydricBalanceCalculated(BasePatientObservation obs) + { + //Hydric_Balance_Calculated + //No guardamos como calculadas nunca horas futuras. En el obx siempre vienen con la hora y los minutos a 00 + + + var pobs = (PatientObservation)obs; + var nowUniversalTime = DateTime.Now.ToUniversalTime(); + //La hora de la observación es mayor que la actual. No generamos la calculada + if (DateTime.Compare(new DateTime(obs.Time.Year, obs.Time.Month, obs.Time.Day, obs.Time.Hour, 0, 0), + new DateTime(nowUniversalTime.Year, nowUniversalTime.Month, nowUniversalTime.Day, nowUniversalTime.Hour, + 0, 0)) > 0 + ) + { + _logger.LogDebug( + "HYDRIC BALANCE: La hora de la observación es mayor que la actual. No generamos la calculada {obs}", + obs); + return; + } + + //Para la calculada. Si la última que tenemos es de la hora actual y lo que viene es de una hora anterior no entra como calculada. + var lastHydricBalanceList = await _observationService.Value.FindLastObservations(obs.PatientId, 1, + ["Hydric_Balance_Calculated"]); + var lastHydricBalance = lastHydricBalanceList.FirstOrDefault(); + + if (lastHydricBalance != null && lastHydricBalance.Time.Hour == DateTime.UtcNow.Hour && + obs.Time.Hour != DateTime.UtcNow.Hour) + { + _logger.LogDebug( + "HYDRIC BALANCE: ultima que tenemos es de la hora actual y lo que viene es de una hora anterior no entra como calculada {obs}", + obs); + return; + } + + //Si el último hydric balance es de una hora posterior a la observación que acaba de llegar no entra como calculada. + if (lastHydricBalance != null && + DateTime.Compare(new DateTime(lastHydricBalance.Time.Year, lastHydricBalance.Time.Month, + lastHydricBalance.Time.Day, lastHydricBalance.Time.Hour, 0, 0) + , new DateTime(pobs.Time.Year, pobs.Time.Month, pobs.Time.Day, pobs.Time.Hour, 0, 0)) == 1 + ) + { + _logger.LogDebug( + "HYDRIC BALANCE: Si el ultimo hydric balance es de una hora posterior a la observación que acaba de llegar no entra como calculada. {obs}", + obs); + return; + } + + var calculatedHydricBalance = new PatientObservation + { + Name = "Hydric_Balance_Calculated", + CodingSystem = "ADAS", + PatientId = obs.PatientId, + Time = obs.Time.AddSeconds(1), //To discriminate updates. + Value = pobs.Value, + Units = obs.Units + }; + + + if (lastHydricBalance != null && DateTime.Compare( + new DateTime(lastHydricBalance.Time.Year, lastHydricBalance.Time.Month, lastHydricBalance.Time.Day, + lastHydricBalance.Time.Hour, 0, 0) + , new DateTime(pobs.Time.Year, pobs.Time.Month, pobs.Time.Day, pobs.Time.Hour, 0, 0)) == 0) + calculatedHydricBalance.Time = lastHydricBalance.Time.AddSeconds(1); + + await _observationService.Value.InsertObservation(calculatedHydricBalance); + } + + private async Task CalculateVentilationMode(BasePatientObservation obs) + { + _logger.LogDebug("CalculateVentilationMode {obs}", obs); + var pobs = (PatientObservation)obs; + + var respTypeObs = new PatientObservation + { + Name = "Resp_Type", + CodingSystem = "ADAS", + PatientId = obs.PatientId, + Time = obs.Time + }; + //https://epigram.teamwork.com/#/tasks/34765750 + if (obs.Code != null && _codesForInvasiveVentilation.Contains(obs.Code)) + { + //añadir que sea solo cuando el valor es invasiva + respTypeObs.Value = nameof(RespirationType.Invasive); + respTypeObs.Time = obs.Time.AddSeconds(1); + await _observationService.Value.InsertIfChanged("Resp_Type", respTypeObs); + } + else + { + var strValue = pobs.Value.ToString(); + if (strValue == null) + return; + + //si es en espera tendrá que insertar que es en espera + if (!"EnESPERA".Equals(strValue)) + { + respTypeObs.Value = _highFrequencyVentilation.Contains(strValue) + ? nameof(RespirationType.HighFrequencyVentilation) + : _nonInvasiveVentilation.Contains(strValue) + ? respTypeObs.Value = nameof(RespirationType.NonInvasive) + : nameof(RespirationType.Invasive); + + await _observationService.Value.InsertIfChanged("Resp_Type", respTypeObs); + } + } + } + + private async Task CalculateWeight_DiffObservation(BasePatientObservation obs) + { + var toSearchList = new List(); + + toSearchList.AddRange(new List { "Weight_Current" }); + + var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList); + + var pWeight = values.FirstOrDefault(o => o.Name == "Weight_Current"); + + var newWeightObs = (PatientObservation)obs; + + if (pWeight?.Value != null && double.TryParse(pWeight.Value.ToString(), out var weight) && + double.TryParse(newWeightObs.Value.ToString(), out var newWeight)) + { + var weightDiffValue = Math.Round(newWeight - weight, 2); + + var weightDiff = new PatientObservation + { + Name = "Weight_Diff", + CodingSystem = "ADAS", + PatientId = obs.PatientId, + Time = obs.Time, + Value = weightDiffValue, + Units = obs.Units + }; + + await _observationService.Value.InsertObservation(weightDiff); + } + } + + private async Task CalculateDiureis_WeightObservation(BasePatientObservation obs) + { + var toSearchList = new List(); + + PatientObservation? diuresisObs; + PatientObservation? weightObs; + + toSearchList.AddRange(new List { "Diuresis", "Weight_Current" }); + + var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList); + + if (obs.Name == "Diuresis") + { + diuresisObs = (PatientObservation)obs; + weightObs = values.FirstOrDefault(o => o.Name == "Weight_Current"); + } + else + { + weightObs = (PatientObservation)obs; + diuresisObs = values.FirstOrDefault(o => o.Name == "Diuresis"); + + if (diuresisObs == null || CheckExpired(diuresisObs)) return; + } + + if (weightObs?.Value != null) + if (double.TryParse(weightObs.Value.ToString(), out var nWeight) && nWeight != 0 && + double.TryParse(diuresisObs.Value.ToString(), out var diuresis)) + { + var diruesisWeightValue = Math.Round(diuresis / nWeight, 2); + + var diruesisWeight = new PatientObservation + { + Name = "Diuresis_Weight", + CodingSystem = "ADAS", + PatientId = obs.PatientId, + Time = obs.Time, + Value = diruesisWeightValue, + Units = "ml/kg" + }; + + await _observationService.Value.InsertObservation(diruesisWeight); + } + } + + private async Task CalculateAllergiesObservation(BasePatientObservation obs) + { + try + { + var pobs = (PatientObservation)obs; + + var allergiesObs = new PatientObservation + { + Name = "Allergies", + CodingSystem = "ADAS", + PatientId = obs.PatientId, + Time = obs.Time, + MessageTime = pobs.MessageTime + }; + + var patientAllergiesValues = + new List((IEnumerable)pobs.Value); + + var allergiesValues = patientAllergiesValues.GroupBy(o => o.Type) + .Select(x => + new PatientAllergiesValue + { + Type = x.Key, + Value = string.Join(", ", x.Select(v => v.Value)), + Notes = string.Join(", ", x.Select(n => n.Notes)) + }).ToList(); + + + var farmacosType = false; + var farmacosValues = new List(); + var values = new List(); + + foreach (var allergies in allergiesValues) + { + if (allergies.Value == null) + continue; + + var type = allergies.Type?.Replace("Alergia a ", "").Replace("Alergia ", "").ToUpper(); + + if (type != null) + switch (type) + { + case "FÁRMACOS": + farmacosType = true; + farmacosValues.Add(allergies.Value.ToUpper()); + break; + + default: + values.Add(type); + break; + } + } + + if (farmacosType) + { + values.Add($"FÁRMACOS ({string.Join(", ", farmacosValues)})"); + allergiesObs.Status = StatusEnum.Type.Alert; + } + + allergiesObs.Value = string.Join(", ", values); + + await _observationService.Value.InsertObservation(allergiesObs); + } + catch (InvalidCastException) + { + _logger.LogError("CalculateAllergiesObservation {obs}", obs); + } + } + + private async Task CalculateDrainagesObservation(BasePatientObservation obs) + { + var pobs = (PatientObservation)obs; + + var patientDraingesValue = (PatientDrainagesValue)pobs.Value; + + if (patientDraingesValue.Type == "Drenaje ventricular") + { + var dve = new PatientObservation + { + Name = "DVE", + CodingSystem = "ADAS", + PatientId = obs.PatientId, + Time = obs.Time, + Units = obs.Units + }; + + var drainageHeight = new PatientObservation + { + Name = "Drainage_Height", + CodingSystem = "ADAS", + PatientId = obs.PatientId, + Time = obs.Time + }; + + if (patientDraingesValue.Volume != null) + { + dve.Value = patientDraingesValue.Volume; + await _observationService.Value.InsertObservation(dve); + } + + if (patientDraingesValue.Height != null) + { + drainageHeight.Value = patientDraingesValue.Height; + await _observationService.Value.InsertObservation(drainageHeight); + } + } + } + + private async Task CalculateDelta_PressureObservation(BasePatientObservation obs) + { + var toSearchList = new List(); + + PatientObservation? peep; + PatientObservation? pleateuPressure; + + toSearchList.AddRange(new List { "PEEP", "Pleateu_Pressure" }); + + var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList); + var time = obs.Time; + double pleateuPressureValue = 0; + + if (obs.Name == "PEEP") + { + peep = (PatientObservation)obs; + pleateuPressure = values.FirstOrDefault(o => o.Name == "Pleateu_Pressure"); + + if (pleateuPressure?.Time != null && time.CompareTo(pleateuPressure.Time) > 0) time = pleateuPressure.Time; + } + else + { + pleateuPressure = (PatientObservation)obs; + peep = values.FirstOrDefault(o => o.Name == "PEEP"); + + if (peep?.Time != null && time.CompareTo(peep.Time) > 0) time = peep.Time; + } + + if (peep?.Value != null && pleateuPressure?.Value != null) + { + var peepSuccess = double.TryParse(peep.Value.ToString(), out var peepValue); + if (!peepSuccess) peepValue = 0; + + if (!peepSuccess) pleateuPressureValue = 0; + + var deltaPressure = Math.Round(pleateuPressureValue - peepValue, 2); + + + var deltaPressureObs = new PatientObservation + { + Name = "Delta_Pressure", + CodingSystem = "ADAS", + PatientId = obs.PatientId, + Time = time, + Value = deltaPressure + }; + + await _observationService.Value.InsertObservation(deltaPressureObs); + } + } + + private async Task CalculateDaily_BalanceObservation(BasePatientObservation obs) + { + if (obs.Time.ToLocalTime().Hour == 8) + { + var dailyBalanceCalculated = new PatientObservation + { + Name = "Daily_Balance_Calculated", + CodingSystem = "ADAS", + PatientId = obs.PatientId, + Time = DateTime.Now, + Units = obs.Units, + Value = ((PatientObservation)obs).Value + }; + + await _observationService.Value.InsertObservation(dailyBalanceCalculated); + } + } + + private static bool CheckExpired(PatientObservation obs) + { + if (obs.Expires == null) return false; + + var timeExpire = obs.Time.ToLocalTime().AddSeconds(Convert.ToDouble(obs.Expires)); + + return DateTime.Now.CompareTo(timeExpire) > 0; + } +} \ No newline at end of file diff --git a/adas-core.Application/Customizations/HUVH/UCIA/CalculatedObservations.cs b/adas-core.Application/Customizations/HUVH/UCIA/CalculatedObservations.cs new file mode 100644 index 00000000..e274c821 --- /dev/null +++ b/adas-core.Application/Customizations/HUVH/UCIA/CalculatedObservations.cs @@ -0,0 +1,695 @@ +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.Pumps; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Customizations.HUVH.UCIA; + +public class CalculatedObservations : ICalculatedObservations +{ + private const string CodingSystem = "ADAS"; + private readonly List? _antibioticList; + private readonly List? _antidepressantsList; + private readonly List? _antihypertensivesList; + private readonly List? _antipsicoticList; + private readonly List? _anxiolyticsList; + private readonly List? _inotropicMedicines; + private readonly ILogger _logger; + private readonly Lazy _medicineService; + private readonly List? _neuroMedicationList; + private readonly Lazy _observationService; + + private readonly List? _sedationList; + private readonly List? _serumColloidList; + private readonly List? _serumCrystalloidList; + + private readonly Lazy _treatmentService; + + public CalculatedObservations(IServiceProvider serviceProvider) + { + _treatmentService = serviceProvider.GetRequiredService>(); + _medicineService = serviceProvider.GetRequiredService>(); + _observationService = serviceProvider.GetRequiredService>(); + _logger = serviceProvider.GetRequiredService>(); + + var apiSettings = serviceProvider.GetRequiredService>(); //apiSettings; + + _sedationList = apiSettings.Value.Sedation; + _inotropicMedicines = apiSettings.Value.InotropicMedicines; + _antibioticList = apiSettings.Value.AntibioticList; + _anxiolyticsList = apiSettings.Value.AnxiolyticList; + _antipsicoticList = apiSettings.Value.AntipsicoticList; + _antidepressantsList = apiSettings.Value.AntidepressantsList; + _neuroMedicationList = apiSettings.Value.NeuroMedicationList; + _serumCrystalloidList = apiSettings.Value.SerumCrystalloidList; + _serumColloidList = apiSettings.Value.SerumColloidList; + _antihypertensivesList = apiSettings.Value.AntihypertensivesList; + } + + + public Task CalculateActiveBolus(ObjectId patientId) + { + throw new NotImplementedException(); + } + + + public async Task Map(T obs, bool onlyByName = false) where T : BasePatientObservation + { + if (obs.Name is "PSI" or "RASS" or "TS") _ = CalculateOverSedation(obs); + + if (obs.Name is "Inspiratory_Pressure" or "Compliancia" or "Air_Flow") _ = CalculateVentilationMode(obs); + + if (obs.Name is "ECMO_Location" or "Location_Drainage_Cannula" or "Location_Return_Cannula") + obs = (T)await CalculateEcmoLocation(obs); + + if (obs.Name is "EVN" or "ANI" or "ESCID") _ = CalculateOverAnalgesia(obs); + + if (obs.Name is "Pmeset" or "PEEP") _ = CalculateDrivingPressure(obs); + + if (obs.Name is "SpO2_FiO2_Ratio" or "FR") _ = CalculateRoxIndex(obs); + + if (obs.Name is "Diuresis") _ = CalculateDiuresis(obs); + + if (obs.Name is "Rehabilitation") obs = (T)await CalculateRehabilitationAlarm(obs); + + return obs; + } + + public async Task Map(PatientTreatment treatment) + { + var order = treatment.PlacerOrder?.EntityIdentifier; //aquí almacenamos el número de orden + if (string.IsNullOrEmpty(order)) return treatment; + + //comprobamos el estado de los tratamientos anteriores + var oldTreatments = await CheckExpiredPatientTreatments(treatment); + + treatment.OrderControl = oldTreatments.Any() ? OrderControlType.Xo : OrderControlType.Nw; + + if (treatment.EndTime != null) + //el tratamiento ha expirado + treatment.OrderControl = OrderControlType.Dc; + + await CheckTreatmentMedicines(treatment); + + return treatment; + } + + public async Task Map(PumpObservation pumpObservation) + { + return await Task.FromResult(pumpObservation); + + } + + public Task CalculateMedicineObservation(List activeMedicines, ObjectId patientId) + { + //Esto viene del schedulerService para chequear los medicamentos activos + CheckMedicationCategories(patientId, activeMedicines); + + return Task.CompletedTask; + } + + public async Task> GetActiveTreatmentsByPatient(ObjectId id) + { + var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(id); + return activeTreatments; + } + + + public Task Map(PatientDiagnosis diagnosis) + { + throw new NotImplementedException(); + } + + + + public async Task FixTimeInconsistencyWithLast(PatientObservation newObservation) + { + if (string.IsNullOrEmpty(newObservation.Name)) + { + _logger.LogError( + "Error FixTimeInconsistencyWithLast. Observation name is null or empty. Observation: {newObservation}", + newObservation); + return newObservation; + } + + var lastObservations = await _observationService.Value.FindLastObservations(newObservation.PatientId, 1, + [newObservation.Name]); + var lastObservation = lastObservations.FirstOrDefault(); + + if (lastObservation != null && DateTime.Compare( + new DateTime(lastObservation.Time.Year, lastObservation.Time.Month, + lastObservation.Time.Day, lastObservation.Time.Hour, + lastObservation.Time.Minute, lastObservation.Time.Second), + new DateTime(newObservation.Time.Year, newObservation.Time.Month, + newObservation.Time.Day, newObservation.Time.Hour, + newObservation.Time.Minute, newObservation.Time.Second) + ) >= 0) + newObservation.Time = lastObservation.Time.AddSeconds(1); + + return newObservation; + } + + public Task> PreMapList(List listToInsert) + { + return Task.FromResult(listToInsert); + } + + public Task MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert) + { + return Task.FromResult(obs); + } + + public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code) + { + throw new NotImplementedException(); + } + + private async Task CalculateRehabilitationAlarm(BasePatientObservation obs) + { + //En rojo si el grado es de 0 a 2 incluidos, por más de 7 días + + if (obs is not PatientObservation pobs || !int.TryParse(pobs.Value.ToString(), out var pobsValue) || + pobsValue > 2) return obs; + + var result = await _observationService.Value.FindLastObservations(obs.PatientId, 6, ["Rehabilitation"]); + + var obsInAlert = result.Count(o => + int.TryParse(o.Value.ToString(), out var oValue) && oValue is >= 0 and <= 2); + if (obsInAlert == 6) pobs.Status = StatusEnum.Type.Alert; + + return obs; + } + + private async Task CalculateDiuresis(BasePatientObservation obs) + { + // Calculado como el sumatorio de la Diuresis de las últimas 6h + // entre el último peso medido del paciente y entre 6. Medido en ml/kg/h + // Valor = sum(Diuresis últimas 6 horas) / Weight / 6 + + if (obs is not PatientObservation pobs) + return; + + double weightValue = 0; + var numValues = pobs.Name switch + { + "Diuresis" => 2, // Si la observación entrante es "Diuresis", necesitamos las 2 últimas para completar 6h. + "Weight" => 3, // Si la observación entrante es "Weight", necesitamos las 3 últimas de "Diuresis". + _ => 0 + }; + + if (numValues == 0) + return; // Si no es ni Diuresis ni Weight, no aplicamos cálculo. + + // Obtener las observaciones de diuresis necesarias + var diuresisObservations = + await _observationService.Value.FindLastObservations(pobs.PatientId, numValues, ["Diuresis"]); + var diuresisValues = diuresisObservations + .Select(o => double.TryParse(o.Value.ToString(), out var value) ? value : 0) + .ToList(); + + if (pobs.Name == "Diuresis") + { + // Si la observación entrante es Diuresis, la agregamos + if (double.TryParse(pobs.Value.ToString(), out var currentDiuresis)) + diuresisValues.Insert(0, currentDiuresis); + + // Buscar el último peso registrado + var weightObservations = + await _observationService.Value.FindLastObservations(pobs.PatientId, 1, ["Weight"]); + var weightObs = weightObservations.FirstOrDefault(); + if (weightObs != null && double.TryParse(weightObs.Value.ToString(), out var wObsValue)) + weightValue = wObsValue; + } + else if (pobs.Name == "Weight" && double.TryParse(pobs.Value.ToString(), out var wValue)) + { + // Si la observación entrante es Weight, asignamos su valor directamente. + weightValue = wValue; + } + + // Validaciones antes de calcular + if (diuresisValues.Count < 3) + { + _logger.LogWarning("Calculated Diuresis lacks sufficient values. Required: 3, Found: {count}", + diuresisValues.Count); + return; + } + + if (weightValue <= 0) + { + _logger.LogWarning("Calculated Diuresis aborted due to invalid weight value: {weight}", weightValue); + return; + } + + // Cálculo del índice de diuresis + var diuresisIndex = diuresisValues.Sum() / weightValue / 6; + + var diuresisObs = new PatientObservation + { + PatientId = pobs.PatientId, + Time = DateTime.UtcNow, + CodingSystem = CodingSystem, + Name = "Calculated_Diuresis", + Value = diuresisIndex + }; + + await _observationService.Value.InsertObservation(diuresisObs, mapObs: false); + } + + + private async Task CalculateRoxIndex(BasePatientObservation obs) + { + // Cálculo dividiendo el ratio SpO2/FiO2 entre la FR + // Valor = (SpO2_FiO2_Ratio) / FR + + if (obs is not PatientObservation pobs) + return; + + List? requiredObservations = pobs.Name switch + { + "SpO2_FiO2_Ratio" => ["FR"], // Si la observación actual es SpO2_FiO2_Ratio, necesitamos FR + "FR" => ["SpO2_FiO2_Ratio"], // Si la observación actual es FR, necesitamos SpO2_FiO2_Ratio + _ => null + }; + + if (requiredObservations is null) + return; + + var result = await _observationService.Value.FindLastObservations(pobs.PatientId, 1, requiredObservations); + var targetObs = result.FirstOrDefault(); + + if (targetObs == null || !double.TryParse(targetObs.Value.ToString(), out var targetValue)) + return; + + // Determinar los valores necesarios para el cálculo + var spo2FiO2Ratio = pobs.Name == "SpO2_FiO2_Ratio" + ? double.TryParse(pobs.Value.ToString(), out var ratio) ? ratio : 0 + : targetValue; + + var fr = pobs.Name == "FR" + ? double.TryParse(pobs.Value.ToString(), out var frValue) ? frValue : 0 + : targetValue; + + if (fr == 0) // Evita división por cero + return; + + var roxIndexObs = new PatientObservation + { + PatientId = pobs.PatientId, + Time = DateTime.UtcNow, + CodingSystem = CodingSystem, + Name = "Rox_Index", + Value = spo2FiO2Ratio / fr + }; + + await _observationService.Value.InsertObservation(roxIndexObs, mapObs: false); + } + + private async Task CalculateDrivingPressure(BasePatientObservation obs) + { + //Diferencia entre la Pmeset y la PEEP + //Calculo Valor = Pmeset - PEEP + + try + { + if (obs is not PatientObservation pobs) + return; + + var targetObservation = pobs.Name switch + { + "Pmeset" => "PEEP", + "PEEP" => "Pmeset", + _ => null + }; + + if (targetObservation is null) + return; + + var resultList = + await _observationService.Value.FindLastObservations(obs.PatientId, 1, [targetObservation]); + var targetObs = resultList.FirstOrDefault(); + if (targetObs == null || !int.TryParse(targetObs.Value.ToString(), out var targetValue)) + return; + + var peepValue = pobs.Name == "PEEP" + ? int.TryParse(pobs.Value.ToString(), out var peep) ? peep : 0 + : targetValue; + var pmesetValue = pobs.Name == "Pmeset" + ? int.TryParse(pobs.Value.ToString(), out var pmeset) ? pmeset : 0 + : targetValue; + + var drivingPressureObs = new PatientObservation + { + PatientId = obs.PatientId, + Time = DateTime.UtcNow, + CodingSystem = CodingSystem, + Name = "Driving_Pressure", + Value = pmesetValue - peepValue + }; + + await _observationService.Value.InsertObservation(drivingPressureObs, mapObs: false); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + private async Task CalculateOverAnalgesia(BasePatientObservation? obs, ObjectId? patientId = null) + { + // Mandar observación 'SOBREANALGESIA' + // 0 <= EVN <= 3 o ESCID = 3 o ANI > 70 + // y + // Perfusiones de morfina, remifentanilo y fentanilo sostenidas > 24h. + + try + { + List requiredObservations = ["EVN", "ESCID", "ANI"]; + + // Si patientId es null, la información proviene de una observación. + // Si tiene valor, proviene de un tratamiento. + if (!patientId.HasValue) + { + if (obs is not PatientObservation pobs || !int.TryParse(pobs.Value.ToString(), out _)) + return; + + patientId = pobs.PatientId; + } + + var observations = + await _observationService.Value.FindLastObservations(patientId.Value, 1, requiredObservations); + + // Verificar si alguna observación cumple la condición + var condition1 = observations.Any(observation => + int.TryParse(observation.Value.ToString(), out var obsValue) && observation.Name switch + { + "EVN" when obsValue is >= 0 and <= 3 => true, + "ESCID" when obsValue == 3 => true, + "ANI" when obsValue > 70 => true, + _ => false + }); + + if (!condition1) + return; + + // Verificar tratamientos activos y umbral de analgesia + var treatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(patientId.Value); + var patientTreatments = treatments.ToList(); + if (!patientTreatments.Any()) return; + + var morfina = + patientTreatments.FirstOrDefault(t => t != null && t.RequestedGiveCodes.Any(c => c.Text is "Morfina")); + var remifentanilo = patientTreatments.FirstOrDefault(t => + t != null && t.RequestedGiveCodes.Any(c => c.Text is "Remifentanilo")); + var fentanilo = + patientTreatments.FirstOrDefault(t => + t != null && t.RequestedGiveCodes.Any(c => c.Text is "Fentanilo")); + + if (morfina != null && !ExceedsAnalgesiaThreshold(morfina) && + remifentanilo != null && !ExceedsAnalgesiaThreshold(remifentanilo) && + fentanilo != null && !ExceedsAnalgesiaThreshold(fentanilo)) + return; + + + var overAnalgesiaObs = new PatientObservation + { + PatientId = patientId.Value, + Time = DateTime.UtcNow, + CodingSystem = CodingSystem, + Name = "Over_Analgesia", + Value = bool.TrueString + }; + + await _observationService.Value.InsertObservation(overAnalgesiaObs, mapObs: false); + } + catch (Exception ex) + { + _logger.LogError("Error calculating Over_Analgesia. Exception: {ex}", ex); + throw; + } + } + + private Task CalculateEcmoLocation(BasePatientObservation obs) + { + //Las opciones pueden ser VV, VA o ECMO. + //Cuando llegue VV se muestra VV. + //Cuando llegue VA se muestra VA. + //Cuando llegue los siguientes parámetros, se debe mostrar ECMO: ECCO2r, VVDL, VA+V, VVDL+V, VV+V, VVA + if (obs is not PatientObservation pobs) + return Task.FromResult(obs); + + var ecmoValues = new List { "ECCO2r", "VVDL", "VA+V", "VVDL+V", "VV+V", "VVA" }; + + var obsValue = pobs.Value.ToString(); + + if (obsValue != null && ecmoValues.Contains(obsValue)) pobs.Value = "ECMO"; + + return Task.FromResult(obs); + } + + private async Task CalculateVentilationMode(BasePatientObservation obs) + { + //- Si llega valor de PI pero NO COMPL, el tipo de ventilación es VMNI + //- Si llega valor de PS pero no llega COMPL, el tipo de ventilación es VMNI + //- Si llega COMPL, el tipo de ventilación es VMI + //- Si llega Flujo, leer el tipo de ventilación de CCC(CNAF o CTAF) + + var mode = string.Empty; + + switch (obs.Name) + { + case "Inspiratory_Pressure": + var complObs = await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["Compliancia"]); + if (!complObs.Any()) + mode = "VMNI"; + + break; + case "Compliancia": + mode = "VMI"; + break; + case "Air_Flow": + var cccVentMode = + await _observationService.Value.FindByPatientIdAndCodingSystemAsync(obs.PatientId, "CCC", + "Ventilation_Mode"); + + if (!await cccVentMode.AnyAsync()) + return; + mode = cccVentMode.First().Value.ToString() ?? string.Empty; + break; + default: + return; + } + + if (string.IsNullOrEmpty(mode)) return; + + + var ventilationModeObservation = new PatientObservation + { + PatientId = obs.PatientId, + Time = DateTime.UtcNow, + CodingSystem = CodingSystem, + Name = "Ventilation_Mode", + Value = mode + }; + + _ = _observationService.Value.InsertObservation(ventilationModeObservation, mapObs: false); + } + + private async Task CalculateOverSedation(BasePatientObservation? obs, ObjectId? patientId = null) + { + //Mandar observación 'SOBRESEDACIÓN' + //cuando: + //(RASS =-4 o RASS =-5) y + //PSI < 25 y + //TS > 5 y + //(Propofol > 3 o Midazolam > 0,05 ó Isoflorano > 10) + + try + { + var pobsName = string.Empty; + + //si patientId viene null viene la observación + //Si tiene valor viene de un tratamiento + + if (!patientId.HasValue) + { + if (obs is not PatientObservation pobs || !int.TryParse(pobs.Value.ToString(), out var pobsValue)) + return; + + patientId = pobs.PatientId; + pobsName = pobs.Name; + + switch (pobsName) + { + case "RASS" when pobsValue is not (-4 or -5): + case "PSI" when pobsValue >= 25: + case "TS" when pobsValue <= 5: + return; + } + } + + + var requiredObservations = pobsName switch + { + "RASS" => ["PSI", "TS"], + "PSI" => ["RASS", "TS"], + "TS" => ["RASS", "PSI"], + _ => new List { "RASS", "PSI", "TS" } + }; + + var result = await _observationService.Value.FindLastObservations(patientId.Value, 1, requiredObservations); + var observations = result.ToDictionary(r => r.Name?.ToString() ?? "null"); + + if ((pobsName != "RASS" && (!observations.TryGetValue("RASS", out var rassObs) || + !int.TryParse(rassObs.Value.ToString(), out var rassValue) || + rassValue is not (-4 or -5))) || + (pobsName != "PSI" && (!observations.TryGetValue("PSI", out var psiObs) || + !int.TryParse(psiObs.Value.ToString(), out var psiValue) || psiValue >= 25)) || + (pobsName != "TS" && (!observations.TryGetValue("TS", out var tsObs) || + !int.TryParse(tsObs.Value.ToString(), out var tsValue) || tsValue <= 5))) + return; + + var treatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(patientId.Value); + if (!treatments.Any(ExceedsSedationThreshold)) return; + + var overSedationObs = new PatientObservation + { + PatientId = patientId.Value, + Time = DateTime.UtcNow, + CodingSystem = CodingSystem, + Name = "Over_Sedation", + Value = bool.TrueString + }; + + await _observationService.Value.InsertObservation(overSedationObs, mapObs: false); + } + catch (Exception ex) + { + _logger.LogError("Error calculating Over_Sedation. Exception: {ex}", ex); + throw; + } + } + + private static bool ExceedsAnalgesiaThreshold(PatientTreatment? treatment) + { + // Perfusiones de morfina, remifentanilo o fentanilo sostenidas > 24 h + + return treatment is { StartTime: not null } && + treatment.StartTime.Value.AddSeconds(86400) < DateTime.UtcNow; + } + + private static bool ExceedsSedationThreshold(PatientTreatment? treatment) + { + return (treatment != null && + treatment.RequestedGiveCodes.Any(c => + c.Text == "Propofol" && treatment.RequestedGiveAmountMinimum > 3)) || + (treatment != null && treatment.RequestedGiveCodes.Any(c => + c.Text == "Midazolam" && treatment.RequestedGiveAmountMinimum > 0.05)) || + (treatment != null && treatment.RequestedGiveCodes.Any(c => + c.Text == "Isoflorano" && treatment.RequestedGiveAmountMinimum > 10)); + } + + private async Task> CheckExpiredPatientTreatments(PatientTreatment treatment) + { + //si el tratamiento ha expirado actualizamos el OrderControl a DC y devolvemos las que siguen activas + var result = new List(); + var oldTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(treatment.PatientId); + + foreach (var t in oldTreatments.Where(t => t != null)) + if (t is { EndTime: not null } && t.EndTime > DateTime.UtcNow) + { + t.OrderControl = OrderControlType.Dc; + _ = _treatmentService.Value.UpdateTreatment(t); + } + else if (t != null) + { + result.Add(t); + } + + + return result; + } + + private async Task CheckTreatmentMedicines(PatientTreatment treatment) + { + var newMedicines = (await Task.WhenAll(treatment.RequestedGiveCodes + .Select(async code => await _medicineService.Value.GetByCode(code.Identifier)))) + .Where(m => m != null) + .ToList(); + + if (newMedicines.Count == 0) return; + + var activeTreatments = (await GetActiveTreatmentsByPatient(treatment.PatientId)).ToList(); + var activeMedicines = (await _medicineService.Value.GetMedicinesOfTreatments(activeTreatments)).ToList(); + + // Combina ambas listas y selecciona solo elementos únicos con el mismo nombre + var uniqueMedicines = newMedicines + .Concat(activeMedicines) + .GroupBy(m => m?.Name) + .Select(g => g.First()) + .ToList(); + + // uniqueMedicines contiene la lista de medicamentos activos de un paciente + CheckMedicationCategories(treatment.PatientId, uniqueMedicines as List); + } + + private void CheckMedicationCategories(ObjectId patientId, List uniqueMedicines) + { + var medicationCategories = new Dictionary?> + { + { "Sedation_Medication_Multivalue", _sedationList }, + { "Inotropic_Medication_Multivalue", _inotropicMedicines }, + { "Antibiotic_Medication_Multivalue", _antibioticList }, + { "Anxiolytic_Medication_Multivalue", _anxiolyticsList }, + { "Antipsychotic_Medication_Multivalue", _antipsicoticList }, + { "Antidepressants_Medication_Multivalue", _antidepressantsList }, + { "Neuro_Medication_Multivalue", _neuroMedicationList }, + { "Crystalloid_Serum_Medication_Multivalue", _serumCrystalloidList }, + { "Colloid_Serum_Medication_Multivalue", _serumColloidList }, + { "Antihypertensives_Medication_Multivalue", _antihypertensivesList } + }; + + foreach (var category in medicationCategories) + { + var observations = uniqueMedicines? + .Where(m => category.Value != null && !string.IsNullOrEmpty(m.Name) && m.Name != null && + category.Value.Contains(m.Name)) + .ToList(); + + if (observations != null && observations.Any()) + _ = CreateMedicationMultivalueObservation(observations, patientId, category.Key); + } + } + + + private async Task CreateMedicationMultivalueObservation(IEnumerable medications, ObjectId patientId, + string obsName) + { + var newObs = new PatientObservation + { + PatientId = patientId, + Name = obsName, + CodingSystem = CodingSystem, + Value = medications.Select(m => m?.Name).ToArray(), + Time = DateTime.Now + }; + + var lastObsList = await _observationService.Value.FindLastObservations(patientId, 1, [obsName]); + var lastObs = lastObsList.FirstOrDefault(); + if (lastObs is { Expired: false }) + { + //expiramos la anterior + lastObs.Expired = true; + _ = _observationService.Value.UpdateObservation(lastObs); + } + + await _observationService.Value.InsertObservation(newObs, + mapObs: false); //mapObs:no volvemos a mapear la observación + } +} \ No newline at end of file diff --git a/adas-core.Application/Customizations/HUVH/UCIN/CalculatedObservations.cs b/adas-core.Application/Customizations/HUVH/UCIN/CalculatedObservations.cs new file mode 100644 index 00000000..986aaf1d --- /dev/null +++ b/adas-core.Application/Customizations/HUVH/UCIN/CalculatedObservations.cs @@ -0,0 +1,711 @@ +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.Pumps; +using adas_core.Domain.Utils; +using adas_core.Domain.Utils.Interfaces; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; + +namespace adas_core.Application.Customizations.HUVH.UCIN; + +public class CalculatedObservations : ICalculatedObservations +{ + private const string Spo2PreObservationName = "SpO2"; + private const string Spo2PostObservationName = "SpO2_Post"; + + private const string Spo2PrePostObservationName = "SpO2_Pre_Post"; + private const string CodingSystem = "ADAS"; + + private readonly List _complexityObservations = + [ + "Respiratory_Device_Multivalue", "Intravenous_Routes_Multivalue", "Medication_Multivalue", + "System_Multivalue", "Newborn_Weight", "Surgery_Multivalue" + ]; + + private readonly ILogger _logger; + private readonly IMappingUtils _mappingUtils; + private readonly Lazy _medicineService; + private readonly Lazy _observationService; + + private readonly Lazy _treatmentService; + + + public CalculatedObservations(IServiceProvider serviceProvider) + { + _treatmentService = serviceProvider.GetRequiredService>(); + _medicineService = serviceProvider.GetRequiredService>(); + _observationService = serviceProvider.GetRequiredService>(); + _logger = serviceProvider.GetRequiredService>(); + + var apiSettings = serviceProvider.GetRequiredService>(); //apiSettings; + _mappingUtils = new MappingUtils(apiSettings); + } + + + public Task CalculateActiveBolus(ObjectId patientId) + { + throw new NotImplementedException(); + } + + + public async Task Map(T obs, bool onlyByName = false) where T : BasePatientObservation + { + if (obs.Name is "Intervention_In" or "Intervention_Out") await CalculateInterventionMultiValueObservation(obs); + if (obs.Name is "Surgery") await CalculateMultiValueObservation(obs); + if (obs.Name != null && _complexityObservations.Contains(obs.Name)) + { + _logger.LogDebug("Mapping observation Complexity {obs}", obs); + //TODO ver si hay un máximo de puntuación por grupo de observaciones + _ = CalculateComplexity(obs); + } + + if (obs.Name is Spo2PreObservationName or Spo2PostObservationName) + { + _logger.LogDebug("Pre-post saturation observation {obs}", obs); + _ = CalculateSaturation_DiffObservation(obs); + } + + if (obs.Name is "ALPS" or "NPASS_Sedation" or "NPASS_Analgesia") _ = CalculatePainScale(obs); + + + if (obs.Name is "Last_Defecation") obs = (T)CalculateLastDefecationValue(obs); + + return obs; + } + + + public async Task Map(PatientTreatment treatment) + { + var order = treatment.PlacerOrder?.EntityIdentifier; //aquí almacenamos el número de orden + if (string.IsNullOrEmpty(order)) return treatment; + + //comprobamos el estado de los tratamientos anteriores + var oldTreatments = await CheckExpiredPatientTreatments(treatment); + + treatment.OrderControl = oldTreatments.Any() ? OrderControlType.Xo : OrderControlType.Nw; + + if (treatment.EndTime != null) + //el tratamiento ha expirado + treatment.OrderControl = OrderControlType.Dc; + + await CheckTreatmentMedicines(treatment); + + return treatment; + } + + public async Task Map(PumpObservation pumpObservation) + { + return await Task.FromResult(pumpObservation); + + } + + public async Task CalculateMedicineObservation(List activeMedicines, ObjectId patientId) + { + var medicineObs = new PatientObservation + { + PatientId = patientId, + Name = "Medication_Multivalue", + CodingSystem = CodingSystem, + Value = activeMedicines.Select(m => m.Name ?? string.Empty).Distinct().ToArray(), + Time = DateTime.Now + }; + + var lastMedicationsObs = + await _observationService.Value.FindLastObservations(patientId, 1, ["Medication_Multivalue"]); + var lastMedicationObs = lastMedicationsObs.FirstOrDefault(); + if (lastMedicationObs != null) + { + //expiramos la anterior + lastMedicationObs.Expired = true; + _ = _observationService.Value.UpdateObservation(lastMedicationObs); + } + + await _observationService.Value + .InsertObservation( + medicineObs); //mapObs:volvemos a mapear la observación para que pase por el cálculo de la complejidad + } + + public async Task> GetActiveTreatmentsByPatient(ObjectId id) + { + var activeTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(id); + return activeTreatments; + } + + + public Task Map(PatientDiagnosis diagnosis) + { + throw new NotImplementedException(); + } + + + public async Task FixTimeInconsistencyWithLast(PatientObservation newObservation) + { + if (string.IsNullOrEmpty(newObservation.Name)) + { + _logger.LogError( + "Error FixTimeInconsistencyWithLast. Observation name is null or empty. Observation: {newObservation}", + newObservation); + return newObservation; + } + + var lastObservations = await _observationService.Value.FindLastObservations(newObservation.PatientId, 1, + [newObservation.Name]); + var lastObservation = lastObservations.FirstOrDefault(); + + if (lastObservation != null && DateTime.Compare( + new DateTime(lastObservation.Time.Year, lastObservation.Time.Month, + lastObservation.Time.Day, lastObservation.Time.Hour, + lastObservation.Time.Minute, lastObservation.Time.Second), + new DateTime(newObservation.Time.Year, newObservation.Time.Month, + newObservation.Time.Day, newObservation.Time.Hour, + newObservation.Time.Minute, newObservation.Time.Second) + ) >= 0) + newObservation.Time = lastObservation.Time.AddSeconds(1); + + return newObservation; + } + + public Task> PreMapList(List listToInsert) + { + return Task.FromResult(listToInsert); + } + + public Task MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert) + { + return Task.FromResult(obs); + } + + public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code) + { + throw new NotImplementedException(); + } + + private async Task CalculatePainScale(BasePatientObservation obs) + { + if (obs is not PatientObservation pobs) return; + + + object valueObs; + + switch (pobs.Name) + { + case "ALPS": + valueObs = pobs.Value; + break; + case "NPASS_Sedation": + { + //Buscamos la complementaria "NPASS Analgesia" + var analgesiaObs = + await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["NPASS_Analgesia"]); + var analgesiaValue = analgesiaObs.FirstOrDefault()?.Value.ToString() ?? "0"; + + valueObs = $"{pobs.Value}/{analgesiaValue}"; + } + break; + case "NPASS_Analgesia": + { + //Buscamos la complementaria "NPASS Sedation" + var sedationObs = + await _observationService.Value.FindLastObservations(obs.PatientId, 1, ["NPASS_Sedation"]); + var sedationValue = sedationObs.FirstOrDefault()?.Value.ToString() ?? "0"; + + valueObs = $"{sedationValue}/{pobs.Value}"; + } + break; + default: + return; + } + + var painScaleObs = new PatientObservation + { + PatientId = obs.PatientId, + Time = DateTime.UtcNow, + CodingSystem = "ADAS", + Name = "Pain_Scale", + Value = valueObs + }; + + _ = _observationService.Value.InsertObservation(painScaleObs, mapObs: false); + } + + private static BasePatientObservation CalculateLastDefecationValue(BasePatientObservation obs) + { + if (obs is not PatientObservation pobs) return obs; + //Assign obs time to value + //Convert the UTC DateTime to local time. + var localTime = pobs.Time.ToLocalTime(); + + //Format the local time into a short date/time string. + pobs.Value = localTime.ToString("dd/MM/yyyy HH:mm"); + + return pobs; + } + + + private async Task CalculateMultiValueObservation(BasePatientObservation obs) + { + //El valor de la observación es un array de strings + if (obs is not PatientObservation pobs || string.IsNullOrEmpty(obs.Name)) return; + + var newObsName = $"{obs.Name}_Multivalue"; + var actualObsInBd = + await _observationService.Value.FindLastObservations(obs.PatientId, 1, [newObsName]); + + + var newValue = new List(); + + if (pobs.Value is not string stringValueName) return; + + + if (actualObsInBd.Count > 0) + if (actualObsInBd.First().Value is string[] oldValue) + { + oldValue = oldValue.Where(x => x != stringValueName).ToArray(); + newValue.AddRange(oldValue); + } + + newValue.Add(stringValueName); + + if (newValue.Count == 0) return; + + //Expiramos la anterior + var actualObs = actualObsInBd.FirstOrDefault(); + if (actualObs != null) + { + actualObs.Expired = true; + + await _observationService.Value.UpdateObservation(actualObs); + } + + var newObservation = new PatientObservation + { + PatientId = obs.PatientId, + Time = DateTime.UtcNow, + CodingSystem = CodingSystem, + Name = newObsName, + Value = newValue.ToArray() + }; + + _logger.LogDebug( + "Calculate {name}_Multivalue {outpatient} Insert Calculated {time} value {value}", + newObsName, newObservation.PatientId, newObservation.Time, + newObservation.Value.ToJson()); + + await _observationService.Value.InsertObservation(newObservation); + } + + private async Task CalculateInterventionMultiValueObservation(BasePatientObservation obs) + { + if (obs is not PatientObservation pobs || string.IsNullOrEmpty(obs.Name) || + pobs.Value is not string valueObs) return; + + if (!GetInterventionValue(valueObs, out var r) || r == null) return; + + var obsType = r.Value.type; //"Intravenous_Routes"; + var obsGroup = r.Value.Name; // "Vía arterial"; + var newObsName = $"{obsType}_Multivalue"; + + // Obtener la última observación multivalue + var result = await _observationService.Value.FindLastObservations(obs.PatientId, 1, [newObsName]); + var actualMultiValueObsInDb = result.FirstOrDefault(o => !o.Expired); + + var oldValue = actualMultiValueObsInDb?.Value as string[] ?? []; + var newValue = new List(oldValue); + + if (obs.Name.Contains("_In")) + { + //Por seguridad solo tomamos el último valor que nos llega + //Puede ser que no se acuerden de cancelar el anterior antes de añadir un nuevo dispositivo y no puede haber dos dispositivos del mismo tipo activos al mismo tiempo + if (obsType == "Respiratory_Device") + newValue.Clear(); + + // Inserción dispositivo + if (!oldValue.Contains(obsGroup)) + newValue.Add(obsGroup); + } + else if (obs.Name.Contains("_Out")) + { + // Retirada del dispositivo + //Buscamos la inserción + var name = $"{obs.Name.Split('_')[0]}_In"; + var inObs = await _observationService.Value.FindLastNotExpiredObservatonsByPatient(obs.PatientId, name); + + var sameValueObs = inObs.Where(o => o.Value == pobs.Value).ToList(); + + if (sameValueObs.Any()) + // Si hay más de una observación del mismo tipo, no eliminar del multivalue + if (sameValueObs.Count > 1) + return; + + // Actualizar valor eliminando el grupo + newValue.Remove(obsGroup); + } + else + { + return; + } + + //Expiramos la anterior + if (actualMultiValueObsInDb is { Expired: false }) + { + actualMultiValueObsInDb.Expired = true; + await _observationService.Value.UpdateObservation(actualMultiValueObsInDb); + } + + // Si no hay valores nuevos, no se crea una nueva observación + if (!newValue.Any()) + return; + + + // Crear y registrar nueva observación + var newObservation = new PatientObservation + { + PatientId = obs.PatientId, + Time = DateTime.UtcNow, + CodingSystem = CodingSystem, + Name = newObsName, + Value = newValue.ToArray() + }; + + _logger.LogDebug( + "Calculate {name}_Multivalue {patientid} Insert Calculated {time} value {value}", + newObsName, newObservation.PatientId, newObservation.Time, + newObservation.Value.ToJson() + ); + + await _observationService.Value.InsertObservation(newObservation); + } + + private bool GetInterventionValue(string valueObs, out (string type, string Name, string Group)? r) + { + var code = valueObs.Split(" ")[0]; // + if (code.EndsWith(".")) // Verifica si termina con un punto + code = code.Substring(0, code.Length - 1).Replace('.', ','); // Remueve el último carácter + + if (!double.TryParse(code, out var codeParsed)) + { + r = null; + return false; + } + + + r = _mappingUtils.SearchByCode(codeParsed, "Intervention"); //TODO validar este parámetro con Lola + + return r != null; + } + + + /** + * Calculamos la complejidad basándonos en el valor de Respiratorio/Vías/Medicación/Sistemas/Peso nacimiento/cirugía + * cuando entra una nueva observación que afecte a complejidad, se recalcula de 0 para no tener que estar pendiente de qué puntos corresponden a qué observación + * Valor máximo 46 Guardamos la observación que nos llega y recalculamos complejidad. + */ + private async Task CalculateComplexity(BasePatientObservation obs) + { + var pobs = obs as PatientObservation; + if (pobs == null) return; + + var complexity = new PatientObservation + { + PatientId = obs.PatientId, + Time = DateTime.UtcNow, + CodingSystem = CodingSystem, + Name = "Complexity", + Value = 0 + }; + + var complexityValue = 0; + + // Diccionario para manejar las operaciones basadas en el nombre de la observación + var operations = new Dictionary>> + { + { + "Respiratory_Device_Multivalue", + (patientId, observation) => + CalculateMultivalueObservationComplexityValue(patientId, "Respiratory_Device_Multivalue", + observation) + }, + { + "Intravenous_Routes_Multivalue", + (patientId, observation) => + CalculateMultivalueObservationComplexityValue(patientId, "Intravenous_Routes_Multivalue", + observation) + }, + { + "Medication_Multivalue", + (patientId, observation) => + CalculateMultivalueObservationComplexityValue(patientId, "Medication_Multivalue", observation) + }, + { + "System_Multivalue", + (patientId, observation) => + CalculateMultivalueObservationComplexityValue(patientId, "System_Multivalue", observation) + }, + { "Newborn_Weight", CalculateWeightNewBornValue }, + { + "Surgery_Multivalue", + (patientId, observation) => + CalculateMultivalueObservationComplexityValue(patientId, "Surgery_Multivalue", observation) + } + }; + + // Realizar cálculos iterando sobre las operaciones + foreach (var operation in operations) + complexityValue += await operation.Value(pobs.PatientId, operation.Key.Equals(pobs.Name) ? pobs : null); + + complexity.Value = Math.Min(complexityValue, 46); // Limitar el valor máximo a 46 + + _logger.LogDebug( + "complexity for patient {patientid} => inserting complexity value: {complexityValue} for patient: {complexityPatientid} at time {fixedTimeComplexityTime}", + complexity.PatientId, complexity.Value, complexity.PatientId, complexity.Time); + + await _observationService.Value.InsertObservation(complexity, mapObs: false); + } + + private async Task CalculateMultivalueObservationComplexityValue(ObjectId patientId, string observationName, + PatientObservation? observation = null) + { + var result = + await _observationService.Value.FindLastObservations(patientId, 1, [observationName]); + var actualObsInBd = result.FirstOrDefault(); + + var valueObj = observation == null ? actualObsInBd?.Value : observation.Value; + var valueList = valueObj as string[]; + + var valueToReturn = valueList?.Sum(v => _mappingUtils.GetComplexityValue(v)) ?? 0; + + _logger.LogDebug( + "complexity for patient {patientid} => observation: {observationName} plus complexity: {Value}", patientId, + observationName, valueToReturn); + + return valueToReturn; + } + + + private async Task CalculateWeightNewBornValue(ObjectId patientId, PatientObservation? pobs = null) + { + var result = + await _observationService.Value.FindLastObservations(patientId, 1, ["Newborn_Weight"]); + var actualObsInBd = result.FirstOrDefault(); + + string? strValue; + string? obsName; + if (pobs == null) + { + if (actualObsInBd?.Value == null) return 0; + + strValue = actualObsInBd.Value.ToString(); + obsName = actualObsInBd.Name; + } + else + { + if (pobs.Units == "kg") pobs = ParseWeight(pobs); + + strValue = pobs.Value.ToString(); + obsName = pobs.Name; + } + + if (!double.TryParse(strValue, out var valueParsed) || obsName == null) + return 0; + + var weightNewBornValue = _mappingUtils.GetComplexityValue(obsName, valueParsed); + + + _logger.LogDebug( + "complexity for patient {patientid} => Weight_Newborn plus complexity: {Weight_NewbornValue}", + patientId, weightNewBornValue); + + return weightNewBornValue; + } + + private PatientObservation ParseWeight(PatientObservation obs) + { + if (!double.TryParse(obs.Value.ToString(), out var dValue)) + { + _logger.LogError("Error casting weight Observation {obs}:", obs); + return obs; + } + + obs.Units = "gr"; + obs.Value = dValue * 1000; + return obs; + } + + + private async Task CalculateSaturation_DiffObservation(BasePatientObservation obs) + { + var toSearchList = new List(); + PatientObservation? pre; + PatientObservation? post; + + toSearchList.AddRange(new List { Spo2PreObservationName, Spo2PostObservationName }); + + var values = await _observationService.Value.FindLastObservations(obs.PatientId, 1, toSearchList); + var time = obs.Time; + + if (obs.Name == Spo2PreObservationName) + { + pre = (PatientObservation)obs; + post = values.FirstOrDefault(o => o.Name == Spo2PostObservationName); + + if (post?.Time != null && time.CompareTo(post.Time) > 0) time = post.Time; + } + else + { + post = (PatientObservation)obs; + pre = values.FirstOrDefault(o => o.Name == Spo2PreObservationName); + + if (pre?.Time != null && time.CompareTo(pre.Time) > 0) time = pre.Time; + } + + if (pre?.Value != null && post?.Value != null) + { + var preSuccess = double.TryParse(pre.Value.ToString(), out var preValue); + if (!preSuccess) preValue = 0; + var postSuccess = double.TryParse(post.Value.ToString(), out var postValue); + if (!postSuccess) postValue = 0; + + var saturationDiff = Math.Round(preValue - postValue, 2); + + var saturationDiffObs = new PatientObservation + { + Name = "SpO2_Diff", + CodingSystem = CodingSystem, + PatientId = obs.PatientId, + Time = time, + Value = saturationDiff + }; + var saturationPrePost = new PatientObservation + { + Name = Spo2PrePostObservationName, + CodingSystem = CodingSystem, + PatientId = obs.PatientId, + Time = time, + Value = pre.Value + "/" + post.Value + }; + + await _observationService.Value.InsertObservation(saturationDiffObs); + await _observationService.Value.InsertObservation(saturationPrePost); + } + } + + private async Task> CheckExpiredPatientTreatments(PatientTreatment treatment) + { + //si el tratamiento ha expirado actualizamos el OrderControl a DC y devolvemos las que siguen activas + var result = new List(); + var oldTreatments = await _treatmentService.Value.GetActiveTreatmentsByPatient(treatment.PatientId); + + foreach (var t in oldTreatments) + { + if (t == null) continue; + + if (t.PlacerOrder?.EntityIdentifier == treatment.PlacerOrder?.EntityIdentifier) + { + //Es el mismo tratamiento actualizamos + + if (t.EndTime != null && t.EndTime > DateTime.UtcNow) treatment.OrderControl = OrderControlType.Dc; + + treatment.Id = t.Id; //Asignamos el ID del tratamiento existente para actualizarlo + _ = _treatmentService.Value.UpdateTreatment(treatment); + } + else + { + result.Add(t); + } + } + + + return result; + } + + private async Task CheckTreatmentMedicines(PatientTreatment treatment) + { + var newMedicines = new List(); + var hasVasoactives = false; + foreach (var code in treatment.RequestedGiveCodes) + { + var m = await _medicineService.Value.GetByCode(code.Identifier); + if (m == null) continue; + + //Si el código es de tipo nutrición entonces creamos una observación de ese tipo que tendrá el valor mapeado con el nombre correspondiente + //CreateNutritionObservation y NO agregamos a medicamentos + var r = _mappingUtils.SearchByCode(m.Codes.First(), "Treatment"); + if (r is { type: "Nutrition" }) + { + await CreateNutritionObservation(r.Value.group, treatment.PatientId); + continue; + } + + if (r is { type: "Medication", group: "Drogas vasoactivas" }) + hasVasoactives = true; + + newMedicines.Add(m); + } + + if (hasVasoactives) + { + var vObs = new PatientObservation + { + Name = "HasVasoactive", + Value = true, + Time = DateTime.UtcNow, + CodingSystem = CodingSystem + }; + + await _observationService.Value.InsertObservation(vObs, mapObs: false); + } + + if (newMedicines.Count == 0) + return; + + var _ = await GetActiveTreatmentsByPatient(treatment.PatientId); + + var activeTreatments = _.ToList(); + + var __ = await _medicineService.Value.GetMedicinesOfTreatments(activeTreatments); //Todo revisar + var activeMedicines = __.ToList(); + + // Combina ambas listas y selecciona solo elementos únicos con el mismo nombre + var uniqueMedicines = newMedicines + .Concat(activeMedicines) + .GroupBy(m => m.Name) + .Select(g => g.First()) + .ToList(); + + await CalculateMedicineObservation(uniqueMedicines, treatment.PatientId); + } + + private async Task CreateNutritionObservation(string value, ObjectId patientId) + { + var nutritionObs = new PatientObservation + { + PatientId = patientId, + Name = "Feeding_Type", + CodingSystem = CodingSystem, + Value = value, + Time = DateTime.Now + }; + + if (value.ToLower().Contains("parenteral")) + { + nutritionObs.Name = "Parenteral"; + nutritionObs.Value = "SI"; + } + + var lastNutritionObsList = + await _observationService.Value.FindLastObservations(patientId, 1, [nutritionObs.Name]); + var lastNutritionObs = lastNutritionObsList.FirstOrDefault(); + if (lastNutritionObs is { Expired: false }) + { + //expiramos la anterior + lastNutritionObs.Expired = true; + _ = _observationService.Value.UpdateObservation(lastNutritionObs); + } + + await _observationService.Value.InsertObservation(nutritionObs); + } +} \ No newline at end of file diff --git a/adas-core.Application/Customizations/NursePlan/CalculatedObservations.cs b/adas-core.Application/Customizations/NursePlan/CalculatedObservations.cs new file mode 100644 index 00000000..8294d19e --- /dev/null +++ b/adas-core.Application/Customizations/NursePlan/CalculatedObservations.cs @@ -0,0 +1,71 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.Pumps; +using Microsoft.Extensions.DependencyInjection; +using MongoDB.Bson; + +namespace adas_core.Application.Customizations.NursePlan; + +public class CalculatedObservations(IServiceProvider serviceProvider) : ICalculatedObservations +{ + private readonly Lazy _observationService = + new(serviceProvider.GetRequiredService); + + public Task CalculateActiveBolus(ObjectId patientId) + { + throw new NotImplementedException(); + } + + public async Task Map(PumpObservation pumpObservation) + { + return await Task.FromResult(pumpObservation); + + } + + public Task CalculateMedicineObservation(List activeMedicines, ObjectId patientId) + { + throw new NotImplementedException(); + } + + public Task FixTimeInconsistencyWithLast(PatientObservation newObservation) + { + throw new NotImplementedException(); + } + + public Task> GetActiveTreatmentsByPatient(ObjectId id) + { + throw new NotImplementedException(); + } + + public Task Map(T obs, bool onlyByName = false) where T : BasePatientObservation + { + return Task.FromResult(obs)!; + } + + public Task Map(PatientTreatment treatment) + { + return Task.FromResult(treatment); + } + + public Task Map(PatientDiagnosis diagnosis) + { + return Task.FromResult(diagnosis); + } + + + public Task MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert) + { + return Task.FromResult(obs); + } + + public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code) + { + return Task.CompletedTask; + } + + public Task> PreMapList(List listToInsert) + { + return Task.FromResult(listToInsert); + } +} \ No newline at end of file diff --git a/adas-core.Application/Exceptions/APIRequestException.cs b/adas-core.Application/Exceptions/APIRequestException.cs new file mode 100644 index 00000000..38ac651e --- /dev/null +++ b/adas-core.Application/Exceptions/APIRequestException.cs @@ -0,0 +1,3 @@ +namespace adas_core.Application.Exceptions; + +public class ApiRequestException(string message) : Exception($"Api request exception: {message}"); \ No newline at end of file diff --git a/adas-core.Application/Exceptions/BadRequestException.cs b/adas-core.Application/Exceptions/BadRequestException.cs new file mode 100644 index 00000000..38e4e40c --- /dev/null +++ b/adas-core.Application/Exceptions/BadRequestException.cs @@ -0,0 +1,14 @@ +using adas_core.Domain.Enums; + +namespace adas_core.Application.Exceptions; + +public class BadRequestException : Exception +{ + public BadRequestException(HttpEnum.ErrorMessage message) : base(((int)message).ToString()) + { + } + + public BadRequestException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/adas-core.Application/Exceptions/ConflictException.cs b/adas-core.Application/Exceptions/ConflictException.cs new file mode 100644 index 00000000..863567fe --- /dev/null +++ b/adas-core.Application/Exceptions/ConflictException.cs @@ -0,0 +1,14 @@ +using adas_core.Domain.Enums; + +namespace adas_core.Application.Exceptions; + +public class ConflictException : Exception +{ + public ConflictException(HttpEnum.ErrorMessage message) : base(((int)message).ToString()) + { + } + + public ConflictException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/adas-core.Application/Exceptions/CustomArgumentException.cs b/adas-core.Application/Exceptions/CustomArgumentException.cs new file mode 100644 index 00000000..68de19fd --- /dev/null +++ b/adas-core.Application/Exceptions/CustomArgumentException.cs @@ -0,0 +1,14 @@ +using adas_core.Domain.Enums; + +namespace adas_core.Application.Exceptions; + +public class CustomArgumentException : Exception +{ + public CustomArgumentException(HttpEnum.ErrorMessage message) : base(((int)message).ToString()) + { + } + + public CustomArgumentException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/adas-core.Application/Exceptions/ForbbidenException.cs b/adas-core.Application/Exceptions/ForbbidenException.cs new file mode 100644 index 00000000..dabfc05f --- /dev/null +++ b/adas-core.Application/Exceptions/ForbbidenException.cs @@ -0,0 +1,14 @@ +using adas_core.Domain.Enums; + +namespace adas_core.Application.Exceptions; + +public class ForbbidenException : Exception +{ + public ForbbidenException(HttpEnum.ErrorMessage message) : base(((int)message).ToString()) + { + } + + public ForbbidenException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/adas-core.Application/Exceptions/InvalidFormatException.cs b/adas-core.Application/Exceptions/InvalidFormatException.cs new file mode 100644 index 00000000..ccd51944 --- /dev/null +++ b/adas-core.Application/Exceptions/InvalidFormatException.cs @@ -0,0 +1,14 @@ +using adas_core.Domain.Enums; + +namespace adas_core.Application.Exceptions; + +public class InvalidFormatException : Exception +{ + public InvalidFormatException(HttpEnum.ErrorMessage message) : base(((int)message).ToString()) + { + } + + public InvalidFormatException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/adas-core.Application/Exceptions/NotFoundException.cs b/adas-core.Application/Exceptions/NotFoundException.cs new file mode 100644 index 00000000..23e88715 --- /dev/null +++ b/adas-core.Application/Exceptions/NotFoundException.cs @@ -0,0 +1,18 @@ +using adas_core.Domain.Enums; + +namespace adas_core.Application.Exceptions; + +public class NotFoundException : Exception +{ + public NotFoundException() + { + } + + public NotFoundException(HttpEnum.ErrorMessage message) : base(((int)message).ToString()) + { + } + + public NotFoundException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/adas-core.Application/Exceptions/TokenException.cs b/adas-core.Application/Exceptions/TokenException.cs new file mode 100644 index 00000000..3bc7116c --- /dev/null +++ b/adas-core.Application/Exceptions/TokenException.cs @@ -0,0 +1,3 @@ +namespace adas_core.Application.Exceptions; + +public class TokenException(string message) : Exception(message); \ No newline at end of file diff --git a/adas-core.Application/Exceptions/UnauthorizedException.cs b/adas-core.Application/Exceptions/UnauthorizedException.cs new file mode 100644 index 00000000..6ddb7204 --- /dev/null +++ b/adas-core.Application/Exceptions/UnauthorizedException.cs @@ -0,0 +1,14 @@ +using adas_core.Domain.Enums; + +namespace adas_core.Application.Exceptions; + +public class UnauthorizedException : Exception +{ + public UnauthorizedException(HttpEnum.ErrorMessage message) : base(((int)message).ToString()) + { + } + + public UnauthorizedException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/adas-core.Application/Exceptions/UnprocessableEntityException.cs b/adas-core.Application/Exceptions/UnprocessableEntityException.cs new file mode 100644 index 00000000..5408501a --- /dev/null +++ b/adas-core.Application/Exceptions/UnprocessableEntityException.cs @@ -0,0 +1,14 @@ +using adas_core.Domain.Enums; + +namespace adas_core.Application.Exceptions; + +public class UnprocessableEntityException : Exception +{ + public UnprocessableEntityException(HttpEnum.ErrorMessage message) : base(((int)message).ToString()) + { + } + + public UnprocessableEntityException(string message) : base(message) + { + } +} \ No newline at end of file diff --git a/adas-core.Application/Providers/AdasProvider.cs b/adas-core.Application/Providers/AdasProvider.cs new file mode 100644 index 00000000..f2b8a207 --- /dev/null +++ b/adas-core.Application/Providers/AdasProvider.cs @@ -0,0 +1,150 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Providers; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Serilog; + +namespace adas_core.Application.Providers; + +public class AdasProvider(IOptions providerSettings, IHttpClientFactory httpClientFactory) + : BaseProvider(providerSettings, httpClientFactory) +{ + /* + * Get observations of: + * Predict of UCI stay duration + * Predict of medications for patients + */ + public override async Task> GetObservations(Patient patient) + { + var calculatedObservations = new List(); + + //get data from two endpoints one from UCI stay another for medication + var predictOfStay = await GetPredictOfStay(patient.PatientNumber); + + if (predictOfStay == null || predictOfStay.Result.ToLower().Contains("error")) + Log.Error("Error retrieving predict of stay in ADAS result, message: {predictOfStay}", predictOfStay); + else + { + var durationPredicted1 = predictOfStay.Payload.MaxBy(i => i.PercentageMin)!.Duration; + var durationPredicted2 = predictOfStay.Payload.MinBy(i => i.PercentageMin)!.Duration; + + calculatedObservations.Add( + new PatientObservation + { + PatientId = patient.Id, + CodingSystem = "ADAS", + Name = "Stay_Predict_Days_Most_Confident", + //get the most confident interval to show the most probable scenario + Value = durationPredicted1, + Time = DateTime.UtcNow + }); + + calculatedObservations.Add(new PatientObservation + { + PatientId = patient.Id, + CodingSystem = "ADAS", + Name = "Stay_Predict_Days_Less_Confident", + //get the less confident interval to show the less probable scenario + Value = durationPredicted2, + Time = DateTime.UtcNow + }); + } + + + var predictMedication = await GetPredictMedications(patient.PatientNumber); + + + if (predictMedication != null && predictMedication.Any()) + calculatedObservations.Add(new PatientObservation + { + PatientId = patient.Id, + CodingSystem = "ADAS", + Name = "Medication_Predict", + Value = ParseAdasMedicationsToMedicationObservation(predictMedication), + Time = DateTime.UtcNow + }); + + + return calculatedObservations; + } + + private static string ParseAdasMedicationsToMedicationObservation( + List medicationPredict) + { + return string.Join("^", medicationPredict.Take(5)); + } + + private async Task GetPredictOfStay(string? patientNumber) + { + if (patientNumber == null) + { + Log.Error("Error retrieving ADAS predict of stay patient number is null"); + return null; + } + + try + { + var client = GetClient(); + var response = await client.GetAsync($"predictStay?patientNumber={patientNumber}"); + + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject(content); + } + + Log.Error( + "Error retrieving calculated ADAS observation length of stay for patient: {patientNumber}, status code: {response.StatusCode} ", + patientNumber, response.StatusCode); + return null; + } + + + catch (Exception e) + { + Log.Error("Exception retrieving ADAS predict of stay for patient: {patientNumber} exception: {e}", + patientNumber, e.Message); + return null; + } + } + + private async Task?> GetPredictMedications(string? patientNumber) + { + if (patientNumber == null) + { + Log.Error("Error retrieving ADAS predict of stay for patient number is null"); + return null; + } + + try + { + var client = GetClient(); + + var response = await client.GetAsync($"predictMedicationNeedsPharmacy?patientNumber={patientNumber}"); + if (response.IsSuccessStatusCode) + { + var content = await response.Content.ReadAsStringAsync(); + return JsonConvert.DeserializeObject>(content); + } + + Log.Error( + "Error retrieving calculated ADAS medicines for patient: {patientNumber}, status code: {response.StatusCode}", + patientNumber, response.StatusCode); + return null; + } + catch (Exception e) + { + Log.Error("Exception retrieving ADAS predict medicines for patient: {patientNumber} exception: {e}", + patientNumber, e.Message); + return null; + } + } + + private HttpClient GetClient() + { + var client = HttpClientFactory.CreateClient(); + client.BaseAddress = new Uri(Url); + return client; + } +} \ No newline at end of file diff --git a/adas-core.Application/Providers/BaseProvider.cs b/adas-core.Application/Providers/BaseProvider.cs new file mode 100644 index 00000000..7ee4a8ab --- /dev/null +++ b/adas-core.Application/Providers/BaseProvider.cs @@ -0,0 +1,16 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Providers; +using Microsoft.Extensions.Options; + +namespace adas_core.Application.Providers; + +public abstract class BaseProvider( + IOptions providerSettings, + IHttpClientFactory httpClientFactory) +{ + protected IHttpClientFactory HttpClientFactory = httpClientFactory; + protected string Url { get; set; } = providerSettings.Value.Url; + + public abstract Task> GetObservations(Patient patient); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IAdmissionRepository.cs b/adas-core.Application/Repositories/Interfaces/IAdmissionRepository.cs new file mode 100644 index 00000000..fabb092e --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IAdmissionRepository.cs @@ -0,0 +1,42 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IAdmissionRepository : IMongoRepository +{ + Task Delete(ObjectId id); + + Task Update(Admission admission); + + Task UpdateLocation(ObjectId id, ObjectId newLocation); + + Task UpdatePatient(ObjectId id, Person patient); + + Task> FindAll(); + + Task FindById(ObjectId id); + + Task FindByNhc(string nhc); + + //Task?> FindByUnit(string unit); + + Task> FindByLocation(PatientLocation location); + + Task?> FindByOrigin(string origin); + Task InsertOneAsyncAndReturn(Admission origin); + Task SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId); + Task> GetAdmissionByUnitIdWithOutPoC(ObjectId unitId); + Task> FindByPointOfCareId(ObjectId pocId); + Task> FindByUnitIds(List unitIds); + Task CountByUnitId(ObjectId unitId); + + Task> UpdateMasterListOption(List unitIds, UpdateOptionMasterListDto opt, + string typeName); + + Task> DeleteMasterListOption(List unitIds, OptionList opt, string typeName); + Task DeleteAdmissionsByUnitId(ObjectId unitId); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IAlarmRepository.cs b/adas-core.Application/Repositories/Interfaces/IAlarmRepository.cs new file mode 100644 index 00000000..5cd2b6df --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IAlarmRepository.cs @@ -0,0 +1,15 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IAlarmRepository : IMongoRepository +{ + Task> AggregatedPatientLastObservationsByField(ObjectId patientId, + List? filterObservations = null); + + Task> AggregatedPatientNotExpiredObservationsByField(ObjectId patientId, + List? filterObservations, List configAlarm); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IAppointmentArchiveRepository.cs b/adas-core.Application/Repositories/Interfaces/IAppointmentArchiveRepository.cs new file mode 100644 index 00000000..165ee8fe --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IAppointmentArchiveRepository.cs @@ -0,0 +1,10 @@ +using adas_core.Domain.Models; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IAppointmentArchiveRepository +{ + Task InsertOneAsync(PatientAppointment patientAppointment); + Task DeleteBeforeDate(DateTime date); + Task InsertBatch(IEnumerable appointment); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IAppointmentRepository.cs b/adas-core.Application/Repositories/Interfaces/IAppointmentRepository.cs new file mode 100644 index 00000000..e9a0a480 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IAppointmentRepository.cs @@ -0,0 +1,22 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IAppointmentRepository : IMongoRepository +{ + Task> GetByPatient(ObjectId patientId); + + new Task InsertOneAsync(PatientAppointment appointment); + Task Update(PatientAppointment appointment); + Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId); + new Task DeleteAsync(ObjectId id); + Task> FindByPatientIdAsync(ObjectId patientId); + Task DeleteByPatientId(ObjectId patientId); + Task FindByPatientAndVisitNumber(ObjectId patientId, string visitNumber); + Task FindByPatientAndReason(ObjectId patientId, string? appointmentReason); + Task> FindByLocation(PatientLocation location); + Task> FindByPoC(PointOfCare poc); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IArchivePatientCarePlanRepository.cs b/adas-core.Application/Repositories/Interfaces/IArchivePatientCarePlanRepository.cs new file mode 100644 index 00000000..3c737b78 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IArchivePatientCarePlanRepository.cs @@ -0,0 +1,17 @@ +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IArchivePatientCarePlanRepository : IMongoRepository +{ + Task?> FindByPatientId(ObjectId patientId); + Task?> FindByPatientId(string patientId); + Task?> FindByPatientNumber(string patientId); + Task> FindAll(); + + Task?> FindByIds(ObjectId oldPatientId, string? oldPatientPatientId, + string? oldPatientPatientNumber); + + Task InsertManyAsync(List patientCarePla); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IAuthorityRepository.cs b/adas-core.Application/Repositories/Interfaces/IAuthorityRepository.cs new file mode 100644 index 00000000..a2565e66 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IAuthorityRepository.cs @@ -0,0 +1,16 @@ +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IAuthorityRepository : IMongoRepository +{ + public Task GetById(ObjectId authId); + void CreateNewAuthority(string roleName, ObjectId userId); + public Task> GetUserAuthorities(ObjectId userId); + public Task> GetAllAuthorities(); + public Task DeleteAllAuthoritiesByUser(ObjectId userId); + Task DeleteAllAuthoritiesByUnit(ObjectId unitId); + Task DeleteAllAuthoritiesByDisplay(ObjectId displayId); + Task> GetByUnitId(ObjectId unitId); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/ICameraRepository.cs b/adas-core.Application/Repositories/Interfaces/ICameraRepository.cs new file mode 100644 index 00000000..3d2f2335 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/ICameraRepository.cs @@ -0,0 +1,17 @@ +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface ICameraRepository : IMongoRepository +{ + Task GetById(ObjectId cameraId); + Task GetByName(string name); + List GetCameraInList(List configurationRelayList); + IFindFluent GetPaginatedCameras(PaginationFilter request); + Task InsertOneCamera(Camera camera); + Task UpdateCameraAsync(ObjectId objectId, Camera camera); + Task> GetSearchByNameCameras(string textToSearch); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IConfigObservationRepository.cs b/adas-core.Application/Repositories/Interfaces/IConfigObservationRepository.cs new file mode 100644 index 00000000..a108161d --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IConfigObservationRepository.cs @@ -0,0 +1,30 @@ +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IConfigObservationRepository : IMongoRepository +{ + Task FindById(ObjectId id); + + Task Update(ConfigObservation configObservation); + + Task Delete(ObjectId id); + + Task> FindAllIds(); + + Task> FindAll(); + + Task> GetConfigNames(string id); + Task> GetConfigNames(); + Task Count(); + Task> GetPaginatedItems(PaginationFilter filter); + Task FindByName(string name); + Task GetByCodeSysAndCode(string? codingSystem, string? code); + Task InsertOneAsyncAndReturn(ConfigObservation configObservationItem); + Task> FindAllByName(string name); + + Task GetSingleConfigObservationItem(string? code, string? codingSystem, string? name, + string? originalName); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IConfigPumpsRepository.cs b/adas-core.Application/Repositories/Interfaces/IConfigPumpsRepository.cs new file mode 100644 index 00000000..751f984e --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IConfigPumpsRepository.cs @@ -0,0 +1,12 @@ +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IConfigPumpsRepository : IMongoRepository +{ + Task FindById(string id); + Task?> GetAllConfigs(); + + Task UpdateConfig(ConfigPumps config); + Task DeleteConfig(ConfigPumps config); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IConfigUnitsRepository.cs b/adas-core.Application/Repositories/Interfaces/IConfigUnitsRepository.cs new file mode 100644 index 00000000..a478de45 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IConfigUnitsRepository.cs @@ -0,0 +1,8 @@ +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IConfigUnitsRepository : IMongoRepository +{ + Task FindById(string id); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IDeviceRepository.cs b/adas-core.Application/Repositories/Interfaces/IDeviceRepository.cs new file mode 100644 index 00000000..8ce6da58 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IDeviceRepository.cs @@ -0,0 +1,14 @@ +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IDeviceRepository : IMongoRepository +{ + Task FindByMacAddr(string deviceDtoMacAddr); + Task FindBySerialNumber(string deviceDtoSerialNumber); + Task FindByUuid(string deviceDtoUuid); + Task FindByKey(string deviceDtoKey); + Task UpdateDeviceStats(ObjectId id,DeviceDto deviceExist); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IDiagnosisArchiveRepository.cs b/adas-core.Application/Repositories/Interfaces/IDiagnosisArchiveRepository.cs new file mode 100644 index 00000000..ff53e8ab --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IDiagnosisArchiveRepository.cs @@ -0,0 +1,10 @@ +using adas_core.Domain.Models; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IDiagnosisArchiveRepository +{ + Task InsertOneAsync(PatientDiagnosis patientDiagnosis); + Task DeleteBeforeDate(DateTime date); + Task InsertBatch(IEnumerable diagnosis); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IDiagnosisRepository.cs b/adas-core.Application/Repositories/Interfaces/IDiagnosisRepository.cs new file mode 100644 index 00000000..ac53db0b --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IDiagnosisRepository.cs @@ -0,0 +1,17 @@ +using adas_core.Domain.Models; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IDiagnosisRepository : IMongoRepository +{ + Task> GetByPatient(ObjectId patientId); + + new Task InsertOneAsync(PatientDiagnosis diagnosis); + new Task DeleteAsync(ObjectId id); + Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId); + Task> FindByPatientIdAsync(ObjectId patientId); + Task DeleteByPatientId(ObjectId patientId); + Task FindByPatientIdAndCode(ObjectId patientId, string? diagnosisCode, string? codingSystem); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IDischargeRepository.cs b/adas-core.Application/Repositories/Interfaces/IDischargeRepository.cs new file mode 100644 index 00000000..f57329fa --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IDischargeRepository.cs @@ -0,0 +1,44 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IDischargeRepository : IMongoRepository +{ + Task Delete(ObjectId id); + + Task Update(Discharge discharge); + + Task UpdateUnit(ObjectId id, string unit); + + Task UpdatePatient(ObjectId id, Patient patient); + + Task> FindAll(); + + Task FindById(ObjectId id); + + Task?> FindByUnit(string unit); + + Task CountByUnitId(ObjectId unitId); + + Task?> FindByDestination(string destination); + + Task?> FindByService(string service); + Task?> GetDischargesByUnitIds(IEnumerable? unitIds); + + + Task GetDischargeByLocation(PatientLocation location); + Task GetByPatientId(ObjectId patientId); + + Task?> FindByPoCId(ObjectId pocId); + Task GetDischargeByPointOfCareId(ObjectId poc); + + Task> UpdateMasterListOption(List unitIds, UpdateOptionMasterListDto opt, + string typeName); + + Task> DeleteMasterListOption(List unitIds, OptionList opt, string typeName); + Task DeleteByUnitId(ObjectId unitId); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IDisplayCardConfigRepository.cs b/adas-core.Application/Repositories/Interfaces/IDisplayCardConfigRepository.cs new file mode 100644 index 00000000..4ef9b776 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IDisplayCardConfigRepository.cs @@ -0,0 +1,14 @@ +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IDisplayCardConfigRepository : IMongoRepository +{ + Task> GetAll(); + Task GetById(ObjectId configId); + Task InsertOneAsyncAndReturn(CardConfig config); + Task> UpdateOne(CardConfig? config); + Task DeleteOne(ObjectId configId); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IDisplayChartConfigRepository.cs b/adas-core.Application/Repositories/Interfaces/IDisplayChartConfigRepository.cs new file mode 100644 index 00000000..0a1975b0 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IDisplayChartConfigRepository.cs @@ -0,0 +1,14 @@ +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IDisplayChartConfigRepository : IMongoRepository +{ + Task> GetAll(); + Task GetById(ObjectId configId); + Task InsertOneAsyncAndReturn(ChartConfig config); + Task> UpdateOne(ChartConfig? config); + Task DeleteOne(ObjectId configId); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IDisplayConfigRepository.cs b/adas-core.Application/Repositories/Interfaces/IDisplayConfigRepository.cs new file mode 100644 index 00000000..f139f11a --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IDisplayConfigRepository.cs @@ -0,0 +1,42 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.DTO.Display; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IDisplayConfigRepository : IMongoRepository +{ + Task> GetAll(); + IFindFluent GetAllPaginated(PaginationFilter request); + Task> GetByType(DisplayConfigEnums.DisplayType type); + Task GetById(ObjectId id); + Task GetDefault(DisplayConfigEnums.DisplayType type); + Task InsertOneAsyncAndReturn(DisplayConfig config); + Task UpdateSmartDisplay(ObjectId displayConfigId, SmartDisplay? newDisplayConfig); + + Task UpdateDisplayNurse(ObjectId displayConfigId, DisplayNurseDto? newDisplayConfig, + List nurseObs); + + Task GetDefaultByUnitIdAndType(ObjectId unitId, DisplayConfigEnums.DisplayType displayType); + Task UpdateConfigColor(ObjectId objectIdConfigDisplay, ColorConfig colorConfig); + Task UpdateSetHomeBanner(ObjectId objectIdConfigDisplay, List bannerItems); + Task UpdateBaseConfig(ObjectId objectIdConfigDisplay, DisplayConfig baseConfig); + Task UpdateHeaderConfig(ObjectId objectIdConfigDisplay, HeaderConfig headerConfig); + Task UpdateDisplayConfigHospitalName(ObjectId objectIdConfigDisplay, string name); + Task UpdateFieldList(ObjectId objectIdConfigDisplay, List fields); + Task DeleteDisplayConfig(ObjectId objectIdConfigDisplay); + Task> GetAllCompact(); + Task> GetAllByCardConfigId(ObjectId cardConfigId); + Task> GetAllByCardConfigIdAndRotating(ObjectId cardConfigId); + Task UpdateCardConfigId(ObjectId? displayConfigId, ObjectId? resultId); + Task AddChartId(ObjectId? displayConfigId, ObjectId newChartIdToAdd); + Task UpdateDeletedChartConfig(ObjectId deletedId); + Task GetChartConfig(ObjectId objectIdConfigChart); + Task UpdateDetailConfigId(ObjectId? displayConfigId, ObjectId? resultId); + Task> GetAllByCardDetailConfigId(ObjectId baseConfigId); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IDisplayDetailConfigRepository.cs b/adas-core.Application/Repositories/Interfaces/IDisplayDetailConfigRepository.cs new file mode 100644 index 00000000..62853b73 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IDisplayDetailConfigRepository.cs @@ -0,0 +1,14 @@ +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IDisplayDetailConfigRepository : IMongoRepository +{ + Task> GetAll(); + Task GetById(ObjectId configId); + Task InsertOneAsyncAndReturn(CardDetailsConfig config); + Task> UpdateOne(CardDetailsConfig? config); + Task DeleteOne(ObjectId configId); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IDisplayRepository.cs b/adas-core.Application/Repositories/Interfaces/IDisplayRepository.cs new file mode 100644 index 00000000..e5beb6bf --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IDisplayRepository.cs @@ -0,0 +1,35 @@ +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IDisplayRepository : IMongoRepository +{ + Task> GetAll(); + + // FilterByAuthorities / FindByUser + // Delete + // FindByUnit + // UpdateConfig -> algo en especfico? Toda la coleccin? + Task> GetByPointOfCare(PointOfCare pointOfCare); + + // Task> GetByUser(); + Task GetByName(string name); + Task GetById(ObjectId id); + Task GetByIdWithConfigDisplay(ObjectId id); + Task> GetByUnitId(ObjectId id); + Task> GetByConfigId(ObjectId id); + + Task UpdatePointOfCareList(ObjectId objectId, List listPocObId); + Task UpdateConfig(ObjectId oldDisplayId, DisplayConfig newDisplayConfigCast); + Task UpdateConfigPreset(ObjectId objectIdDisplay, ObjectId objectIdConfigDisplay); + Task UpdateName(Display display, string name); + IFindFluent GetPaginatedDisplays(PaginationFilter filter); + Task IsDisplayConfigInUse(ObjectId displayConfigId); + Task UpdateConfigId(ObjectId objectId, ObjectId displayConfigId); + Task CountByUnitId(ObjectId unitId); + Task DeleteManyByUnitId(ObjectId unitId); + Task> GetByCardConfigId(ObjectId configId); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IHistoricalConfigChangesRepository.cs b/adas-core.Application/Repositories/Interfaces/IHistoricalConfigChangesRepository.cs new file mode 100644 index 00000000..9487db72 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IHistoricalConfigChangesRepository.cs @@ -0,0 +1,25 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IHistoricalConfigChangesRepository : IMongoRepository +{ + Task FindById(ObjectId id); + + Task Update(HistoricalConfigChanges historicalConfigChange); + + Task Delete(ObjectId id); + + Task> FindAllIds(); + + Task> FindAll(); + new Task InsertOneAsync(HistoricalConfigChanges patientObservation); + + Task> FindLastHistoricalConfigChangesByType( + DisplayConfigEnums.ConfigTypes cfgType, int num = 10); + + Task> FindLastHistoricalConfigChangesByUser(string user, + DisplayConfigEnums.ConfigTypes? cfgType = null, int num = 10); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/ILightBeaconRepository.cs b/adas-core.Application/Repositories/Interfaces/ILightBeaconRepository.cs new file mode 100644 index 00000000..1ef7b02a --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/ILightBeaconRepository.cs @@ -0,0 +1,16 @@ +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface ILightBeaconRepository : IMongoRepository +{ + List GetLightBeaconInList(List configurationRelayList); + Task GetById(ObjectId relayId); + Task GetByName(string name); + Task InsertOneAsyncAndReturn(LightBeacon beacon); + IFindFluent GetPaginatedRelays(PaginationFilter filter); + Task> GetSearchByName(string textToSearch); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IMasterListRepository.cs b/adas-core.Application/Repositories/Interfaces/IMasterListRepository.cs new file mode 100644 index 00000000..254df1c4 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IMasterListRepository.cs @@ -0,0 +1,34 @@ +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.Masters; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IMasterListRepository : IMongoRepository where T : MasterList +{ + Task Delete(ObjectId id); + Task Update(T list); + Task FindById(ObjectId id, LocaleEnum? locale); + Task FindById(ObjectId id); + Task FindOptionItemById(ObjectId masterId, ObjectId optionId, LocaleEnum locale); + Task FindOptionItemById(ObjectId masterId, ObjectId optionId); + Task> GetAll(); + Task> GetAllWithoutOptions(); + Task FindByName(string name); + Task> GetMasterListByIdAndTextSearchContaining(ObjectId id, string? textSearch); + Task AddOptionToMasterList(ObjectId id, FilterOptionListElement opt); + Task UpdateMasterListOption(ObjectId id, OptionList newOpt, LocaleEnum locale); + Task UpdateFullMasterListOption(ObjectId id, OptionList newOpt); + Task UpdateMasterListOption(ObjectId id, OptionList newOpt); + Task DeleteMasterListOption(ObjectId id, ObjectId deleteOptId); + Task UpdateOptionDetailsToMasterList(ObjectId id, UpdateMasterListDetailsDto opt); + Task UpdateMasterListName(ObjectId id, string name); + Task UpdateMasterListDescription(ObjectId id, string description); + Task RemoveMasterListOption(ObjectId id, OptionList oldOpt); + IFindFluent GetPaginatedMasterList(PaginationFilter filter); + Task> GetPaginatedOptions(PaginationFilter filter, ObjectId listId); + Task Count(); + Task> GetMasterListByIdAndSearchOptions(ObjectId id, FilterOptionListElement? filterOption); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IMedicineRepository.cs b/adas-core.Application/Repositories/Interfaces/IMedicineRepository.cs new file mode 100644 index 00000000..8d763b61 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IMedicineRepository.cs @@ -0,0 +1,22 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.Filter; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IMedicineRepository : IMongoRepository +{ + Task GetMedicine(string code); + + Task> GetMedicineByCodeOrNote(List codeNotes); + Task GetMedicineByName(string name); + Task> GetAll(); + Task GetMedicineById(ObjectId medicineId); + Task PostMedicine(Medicine medicine); + Task UpdateMedicine(Medicine medicine); + Task DeleteMedicineById(ObjectId medicineId); + IAggregateFluent GetDistinctFieldDataQuery(string field); + + IFindFluent GetPaginatedMedicines(PaginationFilter filter); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IMongoRepository.cs b/adas-core.Application/Repositories/Interfaces/IMongoRepository.cs new file mode 100644 index 00000000..9ad0f562 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IMongoRepository.cs @@ -0,0 +1,14 @@ +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IMongoRepository +{ + IMongoCollection Collection { get; } + + string GetCollectionName(); + Task InsertOneAsync(T obj); + Task DeleteAsync(ObjectId id); + Task UpdateOneAsync(ObjectId id, T obj); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/INoticeRepository.cs b/adas-core.Application/Repositories/Interfaces/INoticeRepository.cs new file mode 100644 index 00000000..35ecd4bc --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/INoticeRepository.cs @@ -0,0 +1,15 @@ +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface INoticeRepository : IMongoRepository +{ + Task Delete(ObjectId id); + Task Update(Notice notice); + Task> FindAll(); + Task FindById(ObjectId id); + Task?> FindByDate(DateTime date); + Task?> FindByType(string type); + Task?> FindByDisplayId(ObjectId displayId); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IObservationArchiveRepository.cs b/adas-core.Application/Repositories/Interfaces/IObservationArchiveRepository.cs new file mode 100644 index 00000000..5ad99cd2 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IObservationArchiveRepository.cs @@ -0,0 +1,16 @@ +using adas_core.Domain.Models; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IObservationArchiveRepository : IMongoRepository +{ + new Task InsertOneAsync(PatientObservation patientObservation); + Task DeleteBeforeDate(DateTime date); + Task InsertBatch(IEnumerable observations); + + Task> AggregatedPatientLastObservations(ObjectId patientId, int num, DateTime lastDate, + List filterObservations); + + Task> FindAllFromPatient(ObjectId patientId); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IObservationRepository.cs b/adas-core.Application/Repositories/Interfaces/IObservationRepository.cs new file mode 100644 index 00000000..02a2e801 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IObservationRepository.cs @@ -0,0 +1,74 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IObservationRepository : IMongoRepository +{ + Task> AggregatedPatientGroupedObservations(ObjectId patientId, GroupedField groupedField); + + Task> AggregatedPatientLastObservations(ObjectId patientId, int num, + List? filterObservations = null); + + Task> AggregatedPatientLastObservationsByLastDate(ObjectId patientId, int num, + DateTime lastDate, List? filterObservations = null); + + Task> AggregatedPatientLastObservationsByField(ObjectId patientId, + List? filterObservations = null); + + Task> FindLastObservations(ObjectId patientId, string codingSystem, string code, + int num = 2); + + Task> FindLastObservationsByCodingSystem(ObjectId patientId, string codingSystem, + int num = 10); + + Task FindLastObservationBeforeDate(ObjectId patientId, string? name, DateTime date); + + Task UpdateExpiredObservations(List expiredObservations); + + Task?> FindAnyWithSameDate(ObjectId patientId, string? name, DateTime date); + Task> FindAnyBeforeDate(ObjectId patientId, DateTime date); + + Task> FindLatestUniqueValuesByName(ObjectId patientId, string name, int? expires); + + new Task InsertOneAsync(PatientObservation patientObservation); + Task DeleteByPatientId(ObjectId id); + Task> FindByPatientIdAsync(ObjectId patientId); + + Task> FindByPatientIdAndCodingSystemAsync(ObjectId patientId, string codingSystem, + string name); + + new Task DeleteAsync(ObjectId id); + Task> DeleteOlderDaysAsync(string name, int retentionPolicyValue); + Task> DeleteOlderNumberAsync(string name, int retentionPolicyValue); + Task ExistBySystemId(ObjectId patientid, string systemId); + Task> DeleteOlderSecondsAsync(string name, int value); + Task> AggregatedPatientActiveIntravenousLinesObservations(ObjectId patientId); + Task> FindAllLastPatientObservationTime(); + + Task FindById(ObjectId id); + + Task?> FindByPatientId(ObjectId id); + Task Update(PatientObservation observation); + Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId); + + Task> FindNotExpired(List? filterObservations); + + Task> FindByName(string name, DateTime? date); + + Task UpdateMany(IEnumerable patientObservations, UpdateDefinition update); + + Task> FindAll(); + + Task ExpireExpiredObservations(List configObservationsToExpire); + //Task> GetPaginatedObservations(PaginationFilter filter); + + IFindFluent GetPaginatedObservations(PaginationFilter filter); + + Task> FindLastNotExpiredObservatonsByPatient(ObjectId patientId, string name, + int? endAfter = null, int? num = null); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IPatientArchiveRepository.cs b/adas-core.Application/Repositories/Interfaces/IPatientArchiveRepository.cs new file mode 100644 index 00000000..ef85d139 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IPatientArchiveRepository.cs @@ -0,0 +1,12 @@ +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IPatientArchiveRepository : IMongoRepository +{ + Task FindByPatientNumber(string patientNumber); + + Task> FindAll(); + Task SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IPatientCarePlanRepository.cs b/adas-core.Application/Repositories/Interfaces/IPatientCarePlanRepository.cs new file mode 100644 index 00000000..74b4547c --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IPatientCarePlanRepository.cs @@ -0,0 +1,12 @@ +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IPatientCarePlanRepository : IMongoRepository +{ + Task> FindByPatientId(ObjectId patientId); + Task> FindByUserId(ObjectId userId); + Task> FindAll(); + Task UpdateManyObjectId(string patientid, ObjectId patientId, ObjectId oldId); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IPatientRepository.cs b/adas-core.Application/Repositories/Interfaces/IPatientRepository.cs new file mode 100644 index 00000000..b67971ac --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IPatientRepository.cs @@ -0,0 +1,45 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IPatientRepository : IMongoRepository +{ + Task Delete(ObjectId id); + Task FindByLocation(PatientLocation location); + Task FindById(ObjectId id); + Task UpdateLocation(ObjectId id, PatientLocation location); + Task UpdateLocation(ObjectId id, ObjectId location); + Task UpdateAttendingDoctor(ObjectId id, Person attendingDoctor); + Task UpdatePatientData(ObjectId id, string patientNumber, Person data, bool updatePatientNumber = true); + Task Update(Patient patient); + Task FindByPatientNumber(string patientNumber); + Task FindByPatientId(string patientId); + Task> FindAll(); + Task> FindByPointOfCare(string pointOfCare); + Task> FindByPointOfCare(ObjectId pointOfCare); + Task FindByPointOfCareId(ObjectId pointOfCare); + Task> FindDischargedPatients(); + Task UpdateOne(Patient updatedPatient); + Task> FindInActivePoC(); + Task> FindInInactivePoC(); + Task> FindPatientsNotUpdatedSince(DateTime date); + Task FindByPatientId(ObjectId patientId); + Task UpdatePatientIncomingData(ObjectId patientId, Patient person); + Task UpdatePatientDemographicData(ObjectId patientId, Patient person); + Task FindByUnitAndPocId(ObjectId unit, ObjectId pointOfCare); + Task CountByUnitId(ObjectId unitId); + Task SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId); + IFindFluent GetPaginatedPatients(PaginationFilter filter); + Task> FindAllPatientWithFinishedProcedures(int archiveProcedureEndDateAfterMinutes); + Task> FindAllPatientWithFinishedTests(int archiveTestEndDateAfterMinutes); + Task> FindAllPatientWithFinishedTreatment(int archiveTreatmentEndDateAfterMinutes); + Task> UpdateMasterListOption(List unitIds, UpdateOptionMasterListDto opt, string typeName); + Task> GetPatientsByUnitIds(List unitIds, string typeName); + Task> DeleteMasterListOption(List unitIds, OptionList opt, string typeName); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IPoCMappingRepository.cs b/adas-core.Application/Repositories/Interfaces/IPoCMappingRepository.cs new file mode 100644 index 00000000..1a1742ad --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IPoCMappingRepository.cs @@ -0,0 +1,9 @@ +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IPoCMappingRepository +{ + Task FindByKey(string key); + string GetCollectionName(); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IPoCSettingsRepository.cs b/adas-core.Application/Repositories/Interfaces/IPoCSettingsRepository.cs new file mode 100644 index 00000000..6a1f663d --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IPoCSettingsRepository.cs @@ -0,0 +1,18 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IPoCSettingsRepository : IMongoRepository +{ + Task Delete(ObjectId id); + + Task FindByLocation(PatientLocation location); + + Task FindById(ObjectId id); + + Task Update(PoCSettings pocSettings); + + Task> FindAll(); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IPointOfCareRepository.cs b/adas-core.Application/Repositories/Interfaces/IPointOfCareRepository.cs new file mode 100644 index 00000000..083721a4 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IPointOfCareRepository.cs @@ -0,0 +1,58 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IPointOfCareRepository : IMongoRepository +{ + Task Delete(ObjectId id); + + Task Update(PointOfCare pointOfCare); + + Task UpdateUnitId(ObjectId id, Unit unit); + + Task UpdateConfiguration(ObjectId id, PointOfCareConfiguration configuration); + + Task FindById(ObjectId id); + + Task?> FindAllByUnitId(ObjectId unit); + + Task?> FindByRoom(string room); + + Task> FindByFilter(FilterDefinition filter, + ProjectionDefinition? projection = null); + + Task?> FindByBed(string bed); + Task?> GetAll(); + Task?> GetAllConfigs(); + + Task?> GetAllLocationInfo(); + + Task GetPoCConfiguration(ObjectId pocId); + + + Task FindByBedAndUnitId(string? bed, ObjectId unitId); + + Task FindByPatientLocation(PatientLocation patientLocation); + + Task> FindByUnitAndStatus(ObjectId unitId, StatusEnum.PointOfCare status, + bool excludeVirtual = false); + + Task UpdateRelayConfig(ObjectId pocId, List relayConfig); + Task UpdateRelayConfig(ObjectId pocId, List relayConfig); + + IFindFluent GetPaginatedPoCs(PaginationFilter filter); + Task CountByUnitId(ObjectId unitId); + Task CountVirtualsByUnitId(ObjectId unitId); + Task DeleteManyByUnitId(ObjectId unitId); + Task FindByIdAllConfig(ObjectId id); + Task> FindAllIdCamerasInUse(); + Task> FindAllIdRelaysInUse(); + Task> FindAllIdBeaconsInUse(); + Task?> FindAllByUnitIdWithDevices(ObjectId unitId); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IPumpAlarmEventRepository.cs b/adas-core.Application/Repositories/Interfaces/IPumpAlarmEventRepository.cs new file mode 100644 index 00000000..2224ab24 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IPumpAlarmEventRepository.cs @@ -0,0 +1,22 @@ +using adas_core.Domain.Models.Pumps; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IPumpAlarmEventRepository +{ + Task InsertAsync(PumpAlarmEvent alarmEvent); + + Task> FindByDeviceIdAsync( + string deviceId, + DateTime? from = null, + DateTime? to = null, + int? limit = null); + + Task FindLastByDeviceIdAsync(string deviceId); + + // cleanup + Task DeleteByPatientId(ObjectId patientId); + + Task UpdateManyObjectIdByFiledNameAsync(string fieldName, ObjectId newId, ObjectId oldId); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IPumpAlarmStateRepository.cs b/adas-core.Application/Repositories/Interfaces/IPumpAlarmStateRepository.cs new file mode 100644 index 00000000..b858fd74 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IPumpAlarmStateRepository.cs @@ -0,0 +1,27 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.Pumps; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IPumpAlarmStateRepository +{ + Task FindActiveAsync( + string deviceId, + PumpEnum.AlarmType? alarmType, + string? alarmCodeMdc = null); + + Task> FindAllActiveByDeviceAsync(string deviceId); + + Task UpsertActiveAsync(PumpAlarmState state); + + Task RemoveAsync( + string deviceId, + PumpEnum.AlarmType? alarmType, + string? alarmCodeMdc = null); + + //cleanup + Task DeleteByPatientId(ObjectId patientId); + + Task UpdateManyObjectIdByFieldNameAsync(string fieldName, ObjectId newId, ObjectId oldId); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IPumpArchiveRepository.cs b/adas-core.Application/Repositories/Interfaces/IPumpArchiveRepository.cs new file mode 100644 index 00000000..58d20005 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IPumpArchiveRepository.cs @@ -0,0 +1,30 @@ +using MongoDB.Bson; +using MongoDB.Driver; +using adas_core.Domain.Models.Pumps; + +namespace adas_core.Application.Repositories.Interfaces +{ + public interface IPumpArchiveRepository + { + /// + /// Devuelve la colección subyacente + /// + IMongoCollection Collection { get; } + + /// + /// Inserta una observación de bomba en la colección de archivo. + /// + Task InsertAsync(PumpObservation obs); + + /// + /// Inserta múltiples observaciones en la colección de archivo. + /// + Task InsertManyAsync(IEnumerable observations); + + /// + /// Búsqueda por paciente para auditoría o restauración. + /// + Task> FindByPatientIdAsync(ObjectId patientId, + DateTime? from = null, DateTime? to = null, int? limit = null); + } +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IPumpObservationRepository.cs b/adas-core.Application/Repositories/Interfaces/IPumpObservationRepository.cs new file mode 100644 index 00000000..de00afe7 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IPumpObservationRepository.cs @@ -0,0 +1,28 @@ +using adas_core.Domain.Models.Pumps; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IPumpObservationRepository +{ + Task InsertAsync(PumpObservation obs); + Task InsertManyAsync(IEnumerable observations); + + Task> FindByDeviceIdAsync( + string deviceId, + DateTime? from = null, + DateTime? to = null, + int? limit = null); + + Task FindLastByDeviceIdAsync(string deviceId); + Task> FindAllLastPatientObservationTimeAsync(); + + Task> FindByPatientId(ObjectId? patientId); + + Task> AggregatedPatientLastObservations(ObjectId patientId, int num = 100); + Task DeleteByPatientId(ObjectId? patientId); + Task DeleteOlderThanDaysAsync(int days, string? name = null); + Task DeleteKeepLastNAsync(int maxCount); + + Task UpdateManyObjectIdByFieldAsync(string fieldName, ObjectId newId, ObjectId? oldId); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IPumpStateRepository.cs b/adas-core.Application/Repositories/Interfaces/IPumpStateRepository.cs new file mode 100644 index 00000000..abb9b65a --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IPumpStateRepository.cs @@ -0,0 +1,12 @@ +using adas_core.Domain.Models.Pumps; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IPumpStateRepository +{ + Task FindByDeviceIdAsync(string deviceId); + + Task UpsertAsync(PumpState state); + + Task> GetAllAsync(); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IRecordingAlertArchiveRepository.cs b/adas-core.Application/Repositories/Interfaces/IRecordingAlertArchiveRepository.cs new file mode 100644 index 00000000..26cb203e --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IRecordingAlertArchiveRepository.cs @@ -0,0 +1,10 @@ +using adas_core.Domain.Models; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IRecordingAlertArchiveRepository : IMongoRepository +{ + new Task InsertOneAsync(PatientRecordingAlert recordingAlert); + Task DeleteBeforeDate(DateTime date); + Task InsertBatch(IEnumerable recordingAlerts); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IRecordingAlertRepository.cs b/adas-core.Application/Repositories/Interfaces/IRecordingAlertRepository.cs new file mode 100644 index 00000000..f1b6ff8b --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IRecordingAlertRepository.cs @@ -0,0 +1,18 @@ +using adas_core.Domain.Models; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IRecordingAlertRepository : IMongoRepository +{ + Task> AggregatedPatientLastObservations(ObjectId patientId, int num); + + Task> FindLastObservations(ObjectId patientId, string name, int num = 2); + new Task InsertOneAsync(PatientRecordingAlert patientRecordingAlert); + Task DeleteOlderDaysAsync(string name, int value); + Task DeleteOlderNumberAsync(string name, int value); + Task DeleteByPatientId(ObjectId patientId); + Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId); + Task> FindByPatientIdAsync(ObjectId patientId); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IRelayRepository.cs b/adas-core.Application/Repositories/Interfaces/IRelayRepository.cs new file mode 100644 index 00000000..5b2c2954 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IRelayRepository.cs @@ -0,0 +1,18 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IRelayRepository : IMongoRepository +{ + Task GetById(ObjectId relayId); + List GetRelayByTypeInList(List configurationRelayList, RelayEnum.Type type); + List GetRelayInList(List configurationRelayList); + IFindFluent GetPaginatedRelays(PaginationFilter filter); + Task InsertOneRelayAsync(Relay request); + Task UpdateRelayAsync(ObjectId objectId, Relay relay); + Task GetByName(string? requestRelayName); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/ISectionRepository.cs b/adas-core.Application/Repositories/Interfaces/ISectionRepository.cs new file mode 100644 index 00000000..c8b717b5 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/ISectionRepository.cs @@ -0,0 +1,21 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface ISectionRepository : IMongoRepository
+{ + Task> FindByLocation(PatientLocation location); + Task FindById(object id); + Task FindById(string id); + Task FindBySection(string section); + Task FindByPointOfCare(string pointOfCare); + Task> GetAll(); + + Task UpdateSection(Section section); + //Task UpdateSectionItems(Section section); + //Task UpdateSectionConfig(Section section); + //Section UpdateSectionItems(string sectionId, string group, string boxName, BoxResponse boxUpdated); + + Task InsertOneSection(Section section); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IServiceConfigRepository.cs b/adas-core.Application/Repositories/Interfaces/IServiceConfigRepository.cs new file mode 100644 index 00000000..d8937053 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IServiceConfigRepository.cs @@ -0,0 +1,10 @@ +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IServiceConfigRepository : IMongoRepository +{ + Task FindById(ObjectId oid); + Task FindById(string id); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/ITreatmentArchiveRepository.cs b/adas-core.Application/Repositories/Interfaces/ITreatmentArchiveRepository.cs new file mode 100644 index 00000000..3abf6a1b --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/ITreatmentArchiveRepository.cs @@ -0,0 +1,13 @@ +using adas_core.Domain.Models; +using MongoDB.Bson; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface ITreatmentArchiveRepository +{ + Task InsertOneAsync(PatientTreatment patientTreatment); + Task DeleteBeforeDate(DateTime date); + Task InsertBatch(IEnumerable treatments); + + Task> FindAllFromPatient(ObjectId patientId); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/ITreatmentRepository.cs b/adas-core.Application/Repositories/Interfaces/ITreatmentRepository.cs new file mode 100644 index 00000000..723a973b --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/ITreatmentRepository.cs @@ -0,0 +1,23 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.Filter; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface ITreatmentRepository : IMongoRepository +{ + Task> GetByPatientId(ObjectId patientId); + Task> GetById(ObjectId id); + new Task InsertOneAsync(PatientTreatment treatment); + new Task DeleteAsync(ObjectId id); + Task Update(PatientTreatment treatment); + Task> FindByPatientIdAsync(ObjectId patientId); + Task> FindByPatientId(ObjectId patientId); + + Task DeleteByPatientId(ObjectId patientId); + Task> FindBolusTreatments(ObjectId patientId); + Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId); + IFindFluent GetPaginatedTreatments(PaginationFilter filter); + Task> GetActiveTreatmentsByPatientIdAndOrder(ObjectId patientId, string order); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IUnitRepository.cs b/adas-core.Application/Repositories/Interfaces/IUnitRepository.cs new file mode 100644 index 00000000..b75ee253 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IUnitRepository.cs @@ -0,0 +1,28 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IUnitRepository : IMongoRepository +{ + //Task FindByLocation(PatientLocation location); + Task FindById(object id); + + Task FindByName(string section); + + //Task FindByPointOfCare(PointOfCare pointOfCare); + Task> GetAll(); + Task UpdateUnit(Unit section); + Task InsertOneUnit(Unit unit); + Task> FindByMasterListId(ObjectId id, MasterListType masterListType); + Task> FindByMasterListId(ObjectId id); + Task UpdateUnitMasterList(UpdateUnitIdListDto updateUnitListDto); + Task UpdateUnitInfo(ObjectId unitId, string name, string title); + Task UpdateConfiguration(ObjectId unitIdParsed, UnitConfiguration unitConfiguration); + IFindFluent GetPaginatedUnits(PaginationFilter filter); + Task CountUnitsByMasterListId(ObjectId id, MasterListType masterListType); +} \ No newline at end of file diff --git a/adas-core.Application/Repositories/Interfaces/IUserRepository.cs b/adas-core.Application/Repositories/Interfaces/IUserRepository.cs new file mode 100644 index 00000000..71147fc6 --- /dev/null +++ b/adas-core.Application/Repositories/Interfaces/IUserRepository.cs @@ -0,0 +1,18 @@ +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Repositories.Interfaces; + +public interface IUserRepository : IMongoRepository +{ + Task GetUser(string username, string password); + Task GetById(ObjectId id); + Task GetByUserName(string name); + Task GetByName(string name); + Task GetByUserAndAuthoritesName(string name); + Task UpdateUser(User user, bool updatePass); + IFindFluent GetPaginatedUsers(PaginationFilter filter); + Task GetOrCreateSystemUser(); +} \ No newline at end of file diff --git a/adas-core.Application/Services/AdminPanelService.cs b/adas-core.Application/Services/AdminPanelService.cs new file mode 100644 index 00000000..ba522316 --- /dev/null +++ b/adas-core.Application/Services/AdminPanelService.cs @@ -0,0 +1,372 @@ +using adas_core.Application.Exceptions; +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.DTO; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Serilog; +using Patient = adas_core.Domain.Models.MongoModels.Patient; + +namespace adas_core.Application.Services; + +public class AdminPanelService( + IOptions apiSettings, + IPatientService patientService, + IConfigObservationService configObservationService, + IMedicineService medicineService, + IPointOfCareService pocService, + IUnitService unitService, + ILogger logger, + IAdmissionService admissionService, + IAuthService authService, + IDischargeService dischargeService, + IDisplayService displayService) + : IAdminPanelService +{ + private readonly List _defaultIdRecord = apiSettings.Value.DefaultIdRecord ?? []; + + + #region Patient + + public async Task ArchivePatient(Patient patient) + { + try + { + await patientService.ArchivePatient(patient); + + logger.LogDebug("archived patientid {patientid} from ADMPanel", patient.Id); + return true; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + public async Task CreatePatient(AdmPanelRequest admRequest) + { + var patient = new Patient + { + Id = ObjectId.GenerateNewId() + }; + await UpdateNewPatient(patient, admRequest); + await patientService.Insert(patient); + + logger.LogDebug("Inserted {patientid} from ADMPanel", patient.Id); + return patient; + } + + private async Task UpdateNewPatient(Patient patient, AdmPanelRequest admRequest) + { + if (!string.IsNullOrEmpty(admRequest.PatientNumber)) patient.PatientNumber = admRequest.PatientNumber; + if (admRequest.AdmTime.HasValue) patient.AdmTime = admRequest.AdmTime; + if (admRequest.Patient != null && !admRequest.Patient.IsEmptyDontCheckIds()) + patient.Person = admRequest.Patient; + if (admRequest.Patient != null && (admRequest.Patient.Ids == null || admRequest.Patient.Ids.Count == 0)) + { + if (patient.Person is { Ids: null }) patient.Person.Ids = new Dictionary(); + foreach (var item in _defaultIdRecord) patient.Person?.Ids?.Add(item, admRequest?.PatientNumber ?? "null"); + } + + patient.Person?.SetIds(patient.Person?.Ids ?? new Dictionary()); + + if (admRequest is { UnitId: not null }) + { + var unit = await unitService.FindById(admRequest.UnitId); + patient.UnitId = unit?.Id ?? admRequest.UnitId; + patient.UnitString = unit?.Name; + + + if (admRequest is { PointOfCareId: not null }) + { + //El paciente existe en la localizacion no te dejo insertarlo + var patientInLocation = await patientService.FindByPointOfCareId(admRequest.PointOfCareId.Value); + if (patientInLocation != null) + { + logger.LogError( + "trying to insert patientid {patientid} in to location already in use. PointOfcareId: {poc}", + patient.Id, patient.PointOfCareId); + throw new Exception($"patient already in location: {patient.Location}"); + } + + var poc = await pocService.FindById(admRequest.PointOfCareId.Value); + if (poc == null) + { + logger.LogError( + "trying to insert patientid {patientid} in to location not found. PointOfcareId: {poc}", + patient.Id, admRequest.PointOfCareId); + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + } + + await pocService.SetPointOfCareStatus(poc.Id, StatusEnum.PointOfCare.InUse); + + patient.Location = new PatientLocation + ( + bed: poc.Bed, + room: poc.Room, + unitName: unit?.Name + ); + patient.PointOfCareId = poc.Id; + } + else + { + var pocUnknown = + await pocService.FindByBedAndUnitId(VirtualPointOfCare.Unknown.ToString(), patient.UnitId); + + patient.PointOfCareId = pocUnknown?.Id; + } + + patient.UpdateDate = DateTime.UtcNow; + } + } + + public async Task FindPatientById(ObjectId id) + { + return await patientService.FindById(id) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + } + + public async Task FindPatientByLocation(PatientLocation location) + { + return await patientService.FindByLocation(location); + } + + public async Task FindPatientByPatientNumber(string patientNumber) + { + return await patientService.FindByPatientNumber(patientNumber); + } + + public async Task UpdatePatientData(AdmPanelRequest request, Patient oldPatient) + { + //traer el paciente que se quiere actualizar con los valores que tenga en la base de datos a una variable + //actualizar unicamente los campos que tengan que ver con los datos del paciente + if (request.PatientNumber == null || request.Patient == null || request.Patient.IsEmptyDontCheckIds()) + { + logger.LogError("Patient Data not updated. Old Patient:{oldPatient}. Api Request {request}", oldPatient, + request); + throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + } + + var patientNumberChanged = oldPatient.PatientNumber != request.PatientNumber; + await patientService.UpdatePatientData(oldPatient.Id, request.PatientNumber, request.Patient, + patientNumberChanged); + + return true; + } + + public async Task UpdatePatientLocation(AdmPanelRequest request) + { + var patient = await patientService.FindByLocation(request.OldLocation); + + var patientExistsInLocation = await patientService.FindByLocation(request.Location); + + //Si ya hay un paciente en esa localización + if (patientExistsInLocation != null) + //Si hay un paciente distinto en la nueva localización movemos al anterior + if (patient != null && patientExistsInLocation.PatientNumber != patient.PatientNumber) + await patientService.UpdateLocation(patientExistsInLocation.Id, + new PatientLocation(VirtualPointOfCare.Pushed.ToString(), ObjectId.GenerateNewId().ToString())); + + if (patient != null) + { + if (request.Location?.Bed == null || string.IsNullOrEmpty(request.Location.Bed)) + request.Location = new PatientLocation(VirtualPointOfCare.Pushed.ToString(), + ObjectId.GenerateNewId().ToString()); + + + //to solve problems when empty beds with not scapped "" + request.Location.Bed = request.Location?.Bed?.Replace("\"", ""); + + await patientService.UpdateLocation(patient.Id, request.Location); + patient = await patientService.FindById(patient.Id); + if (patient != null) await patientService.Update(patient); + } + else + { + patient = new Patient + { + Id = ObjectId.GenerateNewId() + }; + await patientService.Insert(patient); + } + + return true; + } + + public async Task FindPatient(AdmPanelRequest request) + { + var findByLocation = !request.Location?.IsFullEmpty(); + + return await patientService.FindPatient(request.PatientId, request.PatientNumber, request.Location, + findByLocation ?? false) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + } + + public async Task FindByLocation(PatientLocation location) + { + return await patientService.FindByLocation(location) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + } + + #endregion + + #region ConfigObservations + + public async Task CreateConfig(ConfigObservation configObservation) + { + _ = await configObservationService.CreateConfig(configObservation) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed); + return true; + } + + public async Task UpdateConfig(ConfigObservation configObservation) + { + _ = await configObservationService.UpdateConfig(configObservation) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + return true; + } + + public async Task DeleteConfigObservationItem(ObjectId id) + { + _ = await configObservationService.RemoveConfigItem(id) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); + return true; + } + + #endregion + + #region Unit + + public async Task InsertUnit(Unit unit) + { + //Insertamos la unidad y creamos los PoCs por defecto para esa unidad + var result = await unitService.InsertOne(unit); + if (result != null) + { + // Por cada valor del enum VirtualPointOfCare, creamos un PointOfCare asociado a la unidad + foreach (var pocEnum in Enum.GetValues(typeof(VirtualPointOfCare)).Cast()) + { + var poc = new PointOfCare + { + UnitId = result.Id, + Status = StatusEnum.PointOfCare.Available, + Room = pocEnum.ToString(), + Bed = pocEnum.ToString() + }; + var insertedPoc = await pocService.InsertPointOfCare(poc); + if (insertedPoc != null) + { + result.PointOfCareIds ??= []; + result.PointOfCareIds.Add(insertedPoc.Id); + } + } + + // Actualizamos la unidad con los nuevos PointOfCareIds + await unitService.UpdateUnit(result); + } + + return result; + } + + + public async Task GetUnitDependencyDto(Unit unit) + { + try + { + return new UnitInfoDto(unit) + { + Admissions = await admissionService.CountAdmissionsByUnitId(unit.Id), + Discharges = await dischargeService.CountDischargesByUnitId(unit.Id), + Displays = await displayService.CountDisplaysByUnitId(unit.Id), + Patients = await patientService.CountPatientsByUnitId(unit.Id), + PointOfCares = await pocService.CountPoCsByUnitId(unit.Id), + VirtualPointOfCares = await pocService.CountVirtualPoCsByUnitId(unit.Id) + }; + } + catch (Exception e) + { + logger.LogError(e.Message); + return null; + } + } + + public async Task DeleteUnitById(ObjectId unitId) + { + try + { + var unit = await unitService.FindById(unitId); + + if (unit == null) throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + + //No se puede borrar una unidad que est� en uso + var patients = await patientService.CountPatientsByUnitId(unitId); + if (patients > 0) + //Si tiene pacientes no se permite borrado + throw new ConflictException(HttpEnum.ErrorMessage.ConflictResourceInUse); + + //Borramos admisiones + await admissionService.DeleteAdmissionsByUnitId(unitId); + //discharges + await dischargeService.DeleteDischargesByUnitId(unitId); + //Borramos Authorizations + await authService.DeleteByUnitId(unitId); + + //Displays + await displayService.DeleteDisplaysByUnitId(unitId); + + //PointOfCares + await pocService.DeletePoCsByUnitId(unitId); + + + //Una vez borrados los recursos asociados a la unidad, borramos la unidad + await unitService.DeleteUnitById(unit); + + return true; + } + catch (Exception e) + { + Log.Error(e.Message); + return false; + } + } + + #endregion + + #region Medicine + + public async Task GetMedicineById(ObjectId medicineId) + { + return await medicineService.GetMedicineById(medicineId) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + } + + public async Task PostMedicine(Medicine medicine) + { + var newMedicine = await medicineService.PostMedicine(medicine) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed); + return newMedicine; + } + + public async Task UpdateMedicine(Medicine medicine) + { + var updatedMedicine = await medicineService.UpdateMedicine(medicine) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + return updatedMedicine; + } + + public async Task DeleteMedicineById(string medicineId) + { + if (!ObjectId.TryParse(medicineId, out var objectId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + await medicineService.DeleteMedicineById(objectId); + _ = await medicineService.GetMedicineById(ObjectId.Parse(medicineId)) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); + return true; + } + + #endregion +} \ No newline at end of file diff --git a/adas-core.Application/Services/AdmissionService.cs b/adas-core.Application/Services/AdmissionService.cs new file mode 100644 index 00000000..230a06af --- /dev/null +++ b/adas-core.Application/Services/AdmissionService.cs @@ -0,0 +1,744 @@ +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.DTO; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; + +namespace adas_core.Application.Services; + +public class AdmissionService( + ILogger logger, + ISubscribersService subscribersService, + IAdmissionRepository admissionRepository, + IClientMessageService clientMessageService, + IUnitService unitService, + IPatientService patientService, + IPointOfCareService pointOfCareService, + IDisplayService displayService, + IDischargeService dischargeService, + IPatientArchiveRepository patientArchiveRepository, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService, + IMasterListServiceFactory masterListServiceFactory) + : IAdmissionService +{ + // Auditory logs + + + public async Task DeleteAdmissionAsync(Admission admission) + { + await DeleteAdmissionByIdAsync(admission.Id); + } + + public async Task DeleteAdmissionByIdAsync(ObjectId admissionId) + { + var admissionAux = await admissionRepository.FindById(admissionId); + if (admissionAux == null) + { + logger.LogInformation("Error deleting Admission not found, id: {AdmissionId} ", admissionId); + return; + } + + await admissionRepository.Delete(admissionId); + + if (admissionAux.PointOfCareId.HasValue) + { + var poc = await pointOfCareService.GetInfo(admissionAux.PointOfCareId.Value); + if (poc != null && poc.AdmissionId == admissionId) + { + poc.AdmissionId = null; + poc.Admission = null; + await pointOfCareService.Update(poc); + + + if (poc.Status != StatusEnum.PointOfCare.Locked && poc.Status != StatusEnum.PointOfCare.InUse) + await pointOfCareService.SetPointOfCareStatus(poc.Id, StatusEnum.PointOfCare.Available); + } + } + + logger.LogInformation("Admission id: {AdmissionId} DELETED ", admissionId); + + SendAdmissionBroadcast(admissionAux, OperationType.DeleteAdmission); + + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, admissionAux, null); + } + + public async Task DeleteAdmissionsByUnitId(ObjectId unitId) + { + _ = await admissionRepository.DeleteAdmissionsByUnitId(unitId); + } + + + public async Task GetAdmissionByIdAsync(ObjectId admissionId) + { + var result = await admissionRepository.FindById(admissionId); + if (result?.PointOfCareId != null) + { + var poc = await pointOfCareService.GetInfo(result.PointOfCareId.Value, null,false); + result.PatientLocation = new PatientLocation(poc?.UnitName, poc?.Bed, poc?.Room); + } + + return result; + } + + public async Task> GetAdmissionsAsync() + { + var resultList = await admissionRepository.FindAll() ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + var admissionsAsync = resultList.ToList(); + foreach (var admission in admissionsAsync) + if (admission.PointOfCareId != null) + { + var poc = await pointOfCareService.GetInfo(admission.PointOfCareId.Value, null,false); + admission.PatientLocation = new PatientLocation(poc?.UnitName, poc?.Bed, poc?.Room); + } + + return admissionsAsync; + } + + public async Task InsertAdmission(Admission admission) + { + var admissionAux = await admissionRepository.FindByNhc(admission.Nhc); + if (admissionAux != null) throw new ConflictException(HttpEnum.ErrorMessage.BadRequestDuplicateData); + PointOfCare? pointOfCare = null; + //Bloqueamos el pointOfCare en el caso de que lo tenga asignado + if (admission.PointOfCareId.HasValue) + { + pointOfCare = await pointOfCareService.GetInfo(admission.PointOfCareId.Value) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + + //pointOfCare.AdmissionId = admission.Id; + //pointOfCare.Admission = admission; + admission.PatientLocation = new PatientLocation(pointOfCare.UnitName, pointOfCare.Bed, pointOfCare.Room); + } + + var insertedAdmission = await admissionRepository.InsertOneAsyncAndReturn(admission) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed); + + if (pointOfCare is { AdmissionId: null }) + { + pointOfCare.Admission = insertedAdmission; + pointOfCare.AdmissionId = insertedAdmission.Id; + if (pointOfCare.Status == StatusEnum.PointOfCare.Available) + pointOfCare.Status = StatusEnum.PointOfCare.Reserved; + + await pointOfCareService.Update(pointOfCare); + } + + SendAdmissionBroadcast(insertedAdmission, OperationType.NewAdmission); + // Obtener información el usuario autenticado + + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, insertedAdmission); + + return insertedAdmission; + } + + public async Task UpdateAdmissionAsync(Admission admission) + { + var oldAdmission = await admissionRepository.FindById(admission.Id); + if (oldAdmission == null) return; + if (oldAdmission.PointOfCareId.HasValue) + { + var pocOld = await pointOfCareService.GetInfo(oldAdmission.PointOfCareId.Value, null,false); + if (pocOld != null) + oldAdmission.PatientLocation = new PatientLocation(pocOld.UnitName, pocOld.Bed, pocOld.Room); + } + + if (admission.PointOfCareId.HasValue) + { + var poc = await pointOfCareService.GetInfo(admission.PointOfCareId.Value, null,false); + if (poc != null) admission.PatientLocation = new PatientLocation(poc.UnitName, poc.Bed, poc.Room); + } + + await admissionRepository.Update(admission); + + await HandlePointOfCareChange(admission, oldAdmission); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldAdmission, admission); + SendAdmissionBroadcast(admission, OperationType.UpdateAdmission); + } + + public async Task AdmitPatient(Admission admission, bool isNew = false) + { + if (admission.PointOfCareId == null) + { + logger.LogError("PointOfCare is required. Admission: {Admission}", admission); + return; + } + + var unit = await unitService.FindById(admission.UnitId); + if (unit == null) + { + logger.LogError("Unit {Name} not found. Unit Id: ", admission.UnitId); + return; + } + + var pointOfCare = await pointOfCareService.FindById(admission.PointOfCareId.Value); + if (pointOfCare == null) + { + logger.LogError("Point of Care not found. Patient not created. {Admission}", admission); + return; + } + + + Patient patient = new() + { + PointOfCareId = pointOfCare.Id, + UnitId = pointOfCare.UnitId, + PointOfCare = pointOfCare, + UnitString = pointOfCare.UnitName, + Bed = pointOfCare.Bed, + Room = pointOfCare.Room, + PatientNumber = admission.Nhc, + AdmTime = DateTime.UtcNow, + Person = admission.Person, + CreationDate = DateTime.UtcNow, + DischargeStatus = pointOfCare.Unit?.DischargeStatusList?.Options.FirstOrDefault(), + Origin = admission.Origin, + OriginAux = admission.OriginAux, + Diagnosis = admission.Diagnosis, + DiagnosisAux = admission.DiagnosisAux, + Allergies = admission.Allergies, + Insulation = admission.Insulation, + LanguageBarrier = admission.LanguageBarrier, + PassiveSitting = admission.PassiveSitting, + Altable = new OptionList + { + Name = "NotAltable", + IconDefault = "icNotAltable", + OptionType = "NotAltable" + }, + Visits = pointOfCare.Unit?.VisitOptionList?.Options.FirstOrDefault(), + AccessControl = pointOfCare.Unit?.AccessControlList?.Options.FirstOrDefault(), + Location = new PatientLocation + ( + unit.Name, + pointOfCare.Bed, + pointOfCare.Room + ) + }; + if (unit.AltableOptionListId is not null) + { + var list = await masterListServiceFactory.GetMasterListById(MasterListType.AltableOptionList, + unit.AltableOptionListId.Value, LocaleEnum.Default) as AltableOptionList; + var listOpt = list?.Options.FirstOrDefault(c => c.OptionType == "NotAltable"); + if (listOpt != null) + patient.Altable = listOpt; + } + + await patientService.Insert(patient); + await SetPointOfCareStatus(pointOfCare.Id, StatusEnum.PointOfCare.InUse); + + if (!isNew) + await DeleteAdmissionAsync(admission); + + if (admission.Insulation != null) + await patientService.UpdatePatientMasterList( + patient.Id, + MasterListType.InsulationList, + [admission.Insulation], + null, null); + + if (admission.Allergies != null) + await patientService.UpdatePatientMasterList( + patient.Id, + MasterListType.AllergyList, + admission.Allergies, + null, null); + if (admission.Diagnosis != null) + await patientService.UpdatePatientMasterList( + patient.Id, + MasterListType.DiagnosisList, + [admission.Diagnosis], + null, null); + if (admission.Origin != null) + await patientService.UpdatePatientMasterList( + patient.Id, + MasterListType.OriginList, + [admission.Origin], + null, null); + if (admission.LanguageBarrier != null) + await patientService.UpdatePatientMasterList( + patient.Id, + MasterListType.LanguageBarrierList, + admission.LanguageBarrier, + null, null); + if (admission.PassiveSitting != null) + await patientService.UpdatePatientMasterList( + patient.Id, + MasterListType.PassiveSittingList, + [admission.PassiveSitting], + null, null); + } + + public async Task ReturnPatientToAdmissions(ObjectId patientId) + { + var patient = await patientService.FindById(patientId); + if (patient == null) + { + logger.LogError("Error returning the patient Id: {Id} to admission", patientId); + return; + } + + var unit = await unitService.FindById(patient.UnitId); + if (unit == null) + { + logger.LogError("Unit not found by Id. {Name}", patient.UnitId); + return; + } + + if (patient.PointOfCareId != null) + { + var poc = await pointOfCareService.FindById(patient.PointOfCareId.Value); + Admission admission = new() + { + Nhc = patient.PatientNumber ?? string.Empty, + PointOfCareId = poc?.Id, + UnitId = unit.Id, + Person = patient.Person ?? + new Person(), //No debería ser null en este punto, pero así quito el warning + Origin = patient.Origin, + OriginAux = patient.OriginAux, + Diagnosis = patient.Diagnosis, + PassiveSitting = patient.PassiveSitting, + DiagnosisAux = patient.DiagnosisAux, + Allergies = patient.Allergies, + Insulation = patient.Insulation, + LanguageBarrier = patient.LanguageBarrier, + AdmissionDate = patient.AdmTime ?? DateTime.UtcNow, + PatientLocation = patient.Location + }; + + await InsertAdmission(admission); + } + + var dis = await dischargeService.GetDischargeByPatientId(patient.Id); + if (dis != null) await dischargeService.DeleteDischargeByIdAsync(dis.Id); + await patientService.ArchivePatient(patient); + + pointOfCareService.CheckNextAdmission(patient.PointOfCareId); + } + + // Used for temporal beds like PUSHED + public async Task ReturnPatientToAdmissions(ObjectId patientId, Admission adm) + { + var patient = await patientService.FindById(patientId); + if (patient == null) + { + logger.LogError("Error returning the patient Id: {Id} to admission", patientId); + return; + } + + + var unit = await unitService.FindById(adm.UnitId); + if (unit == null) + { + logger.LogError("Unit not found by Id. {Name}", patient.UnitId); + return; + } + + if (adm.PointOfCareId != null) + { + var poc = await pointOfCareService.FindById(adm.PointOfCareId.Value); + Admission admission = new() + { + Nhc = patient.PatientNumber ?? string.Empty, + PointOfCareId = poc?.Id, + UnitId = unit.Id, + Person = patient.Person ?? + new Person(), //No debería ser null en este punto, pero así quito el warning + Origin = patient.Origin, + OriginAux = patient.OriginAux, + Diagnosis = patient.Diagnosis, + DiagnosisAux = patient.DiagnosisAux, + Allergies = patient.Allergies, + PassiveSitting = patient.PassiveSitting, + Insulation = patient.Insulation, + LanguageBarrier = patient.LanguageBarrier, + AdmissionDate = patient.AdmTime ?? DateTime.UtcNow, + PatientLocation = patient.Location + }; + + await InsertAdmission(admission); + } + + var dis = await dischargeService.GetDischargeByPatientId(patient.Id); + if (dis != null) await dischargeService.DeleteDischargeByIdAsync(dis.Id); + await patientService.ArchivePatient(patient); + + pointOfCareService.CheckNextAdmission(patient.PointOfCareId); + } + + public async Task> GetAdmissionByLocation(PatientLocation location) + { + try + { + return await admissionRepository.FindByLocation(location); + } + catch (Exception e) + { + logger.LogError("Error getting admission by patient location {Location} exception: {Ex}", location, + e.Message); + return []; + } + } + + public async Task> GetAdmissionByPointOfCareId(ObjectId pocId) + { + try + { + var result = await admissionRepository.FindByPointOfCareId(pocId); + foreach (var admission in result) + { + var poc = await pointOfCareService.GetInfo(pocId, null,false); + if (poc != null) admission.PatientLocation = new PatientLocation(poc.UnitName, poc.Bed, poc.Room); + } + + return result; + } + catch (Exception e) + { + logger.LogError("Error getting admission by patient PocId {PocId} exception: {Ex}", pocId, e.Message); + return []; + } + } + + public async Task> GetAdmissionByPointOfCareIdAndLocale(ObjectId pocId, LocaleEnum locale) + { + var admissions = await GetAdmissionByPointOfCareId(pocId); + + // Ejecutar todas las traducciones en paralelo + var translatedAdmissions = await Task.WhenAll( + admissions.Select(adm => GetAdmissionWithLocale(adm, locale)) + ); + + return translatedAdmissions.ToList(); + } + + public async Task> GetAdmissionByUnitIdWithOutPoC(ObjectId unitId) + { + try + { + return await admissionRepository.GetAdmissionByUnitIdWithOutPoC(unitId); + } + catch (Exception e) + { + logger.LogError("Error getting admission by unitId with out PoCId {UnitId} exception: {Ex}", unitId, + e.Message); + return []; + } + } + + public async Task CountAdmissionsByUnitId(ObjectId unitId) + { + try + { + return await admissionRepository.CountByUnitId(unitId); + } + catch (Exception e) + { + logger.LogError("Error getting admission by unitId with out PoCId {UnitId} exception: {Ex}", unitId, + e.Message); + return 0; + } + } + + public async Task SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId) + { + var patient = await patientService.FindByPatientNumber(patientNumber); + if (patient != null) + { + if (patient.PointOfCareId.HasValue) + patient.PointOfCare = await pointOfCareService.FindById(patient.PointOfCareId.Value); + if (patient.UnitId.HasValue) + { + var uni = await unitService.FindById(patient.UnitId.Value); + if (uni != null) + patient.UnitString = uni.Name; + } + } + + var archivePatient = + await patientArchiveRepository.SearchByPatientNumberAndDistinctUnit(patientNumber, unitId); + var admission = await admissionRepository.SearchByPatientNumberAndDistinctUnit(patientNumber, unitId); + + var result = new PatientSearch(patient, archivePatient, admission, + patient == null && archivePatient != null, + patient != null || archivePatient != null || admission != null); + return result; + } + + public Task GetAdmissionByPatientNumber(string patientNumber) + { + return admissionRepository.FindByNhc(patientNumber); + } + + public async Task UpdatePatientMasterListItemChange(UpdateOptionMasterListDto opt, IEnumerable unitList, + string typeName) + { + var unitIds = unitList.Select(x => x.Id).ToList(); + var oldAdmissionList = await admissionRepository.FindByUnitIds(unitIds); + var admissionUpdatedList = await admissionRepository.UpdateMasterListOption(unitIds, opt, typeName); + foreach (var admission in admissionUpdatedList) + { + var oldAdmission = oldAdmissionList.Find(adm => adm.Id == admission.Id); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldAdmission!, admission); + SendAdmissionBroadcast(admission, OperationType.UpdateAdmission); + } + } + + public async Task DeletePatientMasterListItem(OptionList opt, IEnumerable unitList, string typeName) + { + var unitIds = unitList.Select(x => x.Id).ToList(); + var oldAdmissionList = await admissionRepository.FindByUnitIds(unitIds); + var admissionUpdatedList = await admissionRepository.DeleteMasterListOption(unitIds, opt, typeName); + foreach (var admission in admissionUpdatedList) + { + var admissionUpdated = await GetAdmissionByIdAsync(admission.Id); + var oldAdmission = oldAdmissionList.Find(adm => adm.Id == admission.Id); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldAdmission!, + admissionUpdated); + if (admissionUpdated != null) + SendAdmissionBroadcast(admissionUpdated, OperationType.UpdateAdmission); + } + } + + public async Task SaveRequest(ApiRequest apiRequest) + { + try + { + if (apiRequest.Admission == null) + return; + + switch (apiRequest.Type) + { + case "NewAdmission": + { + if (string.IsNullOrEmpty(apiRequest.Admission.Nhc) || + apiRequest.Admission.Origin == null || + apiRequest.Admission.Diagnosis == null) + { + logger.LogDebug( + "Error saving admission api request. Some values are required. Admission: {Admission}", + apiRequest.Admission); + return; + } + + await InsertAdmission(apiRequest.Admission); + break; + } + case "UpdateAdmission": + { + if (string.IsNullOrEmpty(apiRequest.Admission.Nhc) || + apiRequest.Admission.Origin == null || + apiRequest.Admission.Diagnosis == null) + { + logger.LogDebug( + "Error updating admission api request. Some values are required. Admission: {Admission}", + apiRequest.Admission); + return; + } + + await UpdateAdmissionAsync(apiRequest.Admission); + break; + } + case "DeleteAdmission": + { + await DeleteAdmissionAsync(apiRequest.Admission); + break; + } + } + } + catch (Exception ex) + { + logger.LogError("Exception updating admission {Admission} . Exception: {Ex}", apiRequest.Admission, + ex.Message); + throw; + } + } + + public Task SaveRequestAsync(ApiRequest apiRequest) + { + return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); })); + } + + private async Task HandlePointOfCareChange(Admission admission, Admission oldAdmission) + { + // Check if PointOfCare has changed. + if (admission.PointOfCareId == oldAdmission.PointOfCareId) return; + + // Disable new PointOfCare if it exists. + if (admission.PointOfCareId.HasValue) + { + var newPoc = await pointOfCareService.GetInfo(admission.PointOfCareId.Value); + if (newPoc == null) return; + + var patientOnNewPoc = await patientService.FindByPointOfCareId(newPoc.Id); + if (newPoc.AdmissionId == null && newPoc.Status != StatusEnum.PointOfCare.Locked) + { + newPoc.Admission = admission; + newPoc.AdmissionId = admission.Id; + newPoc.Status = patientOnNewPoc != null + ? StatusEnum.PointOfCare.InUse + : StatusEnum.PointOfCare.Reserved; + + await pointOfCareService.Update(newPoc); + } + + if (newPoc.Status == StatusEnum.PointOfCare.Available) + pointOfCareService.CheckNextAdmission(newPoc.Id); + } + + // Enable previous PointOfCare if it exists. + if (oldAdmission.PointOfCareId.HasValue) + { + var oldPoc = await pointOfCareService.GetInfo(oldAdmission.PointOfCareId.Value); + if (oldPoc != null) + { + var patientOnOldPoc = await patientService.FindByPointOfCareId(oldPoc.Id); + if (oldPoc.AdmissionId == oldAdmission.Id && oldPoc.Status != StatusEnum.PointOfCare.Locked) + { + oldPoc.Admission = null; + oldPoc.AdmissionId = null; + oldPoc.Status = patientOnOldPoc != null + ? StatusEnum.PointOfCare.InUse + : StatusEnum.PointOfCare.Available; + + await pointOfCareService.Update(oldPoc); + } + } + + if (oldPoc is { Status: StatusEnum.PointOfCare.Available }) + pointOfCareService.CheckNextAdmission(oldPoc.Id); + } + } + + private async Task SetPointOfCareStatus(ObjectId pointOfCareId, StatusEnum.PointOfCare status) + { + var pointOfCare = await pointOfCareService.FindById(pointOfCareId); + if (pointOfCare == null) return; + + await pointOfCareService.SetPointOfCareStatus(pointOfCareId, status); + } + + private async void SendAdmissionBroadcast(Admission admission, OperationType operation) + { + try + { + if (admission.PointOfCareId == null) + await SendAdmissionByUnitId(admission, operation); + else + await SendAdmissionBroadcastByPoC(admission, operation); + } + catch (Exception ex) + { + logger.LogError("Exception sending admission broadcast. Operation type: {Op}. Exception: {Ex}", + operation.ToString(), ex.Message); + } + } + + private async Task SendAdmissionBroadcastByPoC(Admission admission, OperationType operation) + { + if (!admission.PointOfCareId.HasValue) + { + logger.LogError("Error sending admission broadcast. PointOfCare id {Id} not found", + admission.PointOfCareId); + return; + } + + var pointOfCare = await pointOfCareService.FindById(admission.PointOfCareId.Value); + if (pointOfCare == null) + { + logger.LogError("Error sending admission broadcast. PointOfCare id {Id} not found", + admission.PointOfCareId); + return; + } + + var pocSubscribers = subscribersService.GetSubscribers().Where(s => + s.LocationIds.Contains(admission.PointOfCareId.Value)).GroupBy(h => h.Locale); + + foreach (var group in pocSubscribers) + { + var locale = group.Key ?? LocaleEnum.Default; + IEnumerable subscribers = group; + + foreach (var subscriber in subscribers) + { + var admissionWithLocale = await GetAdmissionWithLocale(admission, locale); + _ = clientMessageService.SendAsync(subscriber.Id, operation, admissionWithLocale); + } + } + } + + private async Task SendAdmissionByUnitId(Admission admission, OperationType operation) + { + if (admission.UnitId == ObjectId.Empty) + { + logger.LogError("Error sending admission broadcast. UnitId is null or empty {Admission}", admission); + return; + } + + var displays = await displayService.GetByUnitId(admission.UnitId); + var displayIds = displays.Select(c => c.Id).ToList(); + var unitSubscribers = subscribersService.GetSubscribers().Where(s => + s.DisplayId != null && displayIds.Contains(s.DisplayId.Value)).GroupBy(h => h.Locale); + foreach (var group in unitSubscribers) + { + var locale = group.Key ?? LocaleEnum.Default; + IEnumerable subscribers = group; + + foreach (var subscriber in subscribers) + { + var admissionWithLocale = await GetAdmissionWithLocale(admission, locale); + _ = clientMessageService.SendAsync(subscriber.Id, operation, admissionWithLocale); + } + } + } + + private async Task GetAdmissionWithLocale(Admission admission, LocaleEnum locale) + { + var unit = await unitService.FindById(admission.UnitId); + if (unit == null) return admission; + if (admission.Origin?.Name != null && unit.OriginListId.HasValue) + { + if (await masterListServiceFactory.GetMasterListById(MasterListType.OriginList, + unit.OriginListId.Value, locale) is OriginList list) + { + var listOrigin = list.Options.FirstOrDefault(c => c.Id == admission.Origin.Id)?.Name; + if (listOrigin != null) + admission.Origin.Name = listOrigin; + } + } + + if (admission.Diagnosis?.Name != null && unit.DiagnosisListId.HasValue) + { + if (await masterListServiceFactory.GetMasterListById(MasterListType.DiagnosisList, + unit.DiagnosisListId.Value, locale) is DiagnosisList list) + { + var listDiagnosis = list.Options.FirstOrDefault(c => c.Id == admission.Diagnosis.Id)?.Name; + if (listDiagnosis != null) + admission.Diagnosis.Name = listDiagnosis; + } + } + + if (admission.Insulation?.Name != null && unit.InsulationListId.HasValue) + { + if (await masterListServiceFactory.GetMasterListById(MasterListType.InsulationList, + unit.InsulationListId.Value, locale) is InsulationList list) + { + var listInsulation = list.Options.FirstOrDefault(c => c.Id == admission.Insulation.Id)?.Name; + if (listInsulation != null) + admission.Insulation.Name = listInsulation; + } + } + + return admission; + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/AlarmService.cs b/adas-core.Application/Services/AlarmService.cs new file mode 100644 index 00000000..927cb8be --- /dev/null +++ b/adas-core.Application/Services/AlarmService.cs @@ -0,0 +1,969 @@ +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.GroupedObservations; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Utils; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Serilog; +using Patient = adas_core.Domain.Models.MongoModels.Patient; + + +namespace adas_core.Application.Services; + +public class AlarmService : IAlarmService +{ + private readonly IAlarmRepository _alarmRepository; + private readonly IOptions _apiSettings; + private readonly ILocalAuditService _auditService; + private readonly Lazy _calculatedObservationsService; + private readonly IClientMessageService _clientMessageService; + private readonly IConfigObservationService _configObservationService; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly Lazy _lightBeaconService; + private readonly ILogger _logger; + private readonly Lazy _observationService; + private readonly IPatientService _patientService; + private readonly IPointOfCareService _pocService; + private readonly Lazy _recordingService; + private readonly List _relayAlarmList = []; + private readonly Lazy _relayService; + + private readonly SemaphoreSlim + _semaphore = new(1, 1); // Semáforo para evitar la ejecución simultánea del temporizador + + private readonly ISubscribersService _subscribersService; + + private readonly IUnitService _unitService; + //private readonly string _url; + + private List _beaconAlarmList = []; + + private TimeSpan _interval; + + public AlarmService(IAlarmRepository alarmRepository, + ILogger logger, + IPatientService patientService, + IConfigObservationService configObservationService, + Lazy observationService, + IClientMessageService clientMessageService, + ISubscribersService subscribersService, + Lazy calculatedObservationsService, + Lazy lightBeaconService, + Lazy recordingService, + Lazy relayService, + IOptions apiSettings, + IUnitService unitService, + IPointOfCareService pocService, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService, + bool startTimer = true + ) + { + _alarmRepository = alarmRepository; + _logger = logger; + _patientService = patientService; + _configObservationService = configObservationService; + _observationService = observationService; + _clientMessageService = clientMessageService; + _subscribersService = subscribersService; + _calculatedObservationsService = calculatedObservationsService; + _lightBeaconService = lightBeaconService; + _recordingService = recordingService; + _relayService = relayService; + _apiSettings = apiSettings; + _unitService = unitService; + _pocService = pocService; + _httpContextAccessor = httpContextAccessor; + _auditService = auditService; + if (apiSettings == null) throw new Exception("ApiSettings must be defined"); + + if (startTimer) StartTimer(); + } + + + public async Task SaveRequest(ApiRequest apiRequest) + { + if ( + string.IsNullOrEmpty(apiRequest.PatientNumber) && + string.IsNullOrEmpty(apiRequest.Location?.UnitName) + ) + { + _logger.LogDebug("Patient and PointOfCare are nulls"); + throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + } + + var patient = await _patientService.FindPatientByApiRequest(apiRequest); + + if (patient == null) + { + // NO PATIENTS OR LOCATIONS WERE FOUND + _logger.LogWarning( + "Observation for patient: {apiRequestpatientNumber} with location: {apiRequestlocation} not found, ignoring", + apiRequest.PatientNumber, apiRequest.Location); + return; + } + + var unitConfig = await _unitService.FindById(patient.UnitId); + if (!Hl7Utils.ManageAutoAdt(unitConfig, null, _logger, "ORU Alarm")) return; + + switch (apiRequest.Type) + { + /* + * ORU_R40 - Unsolicited transmission of an alert observation message + */ + case "ORU_R40": // UNSOLICITED ALERT OBSERVATION + + // OBSERVATIONS + if (apiRequest.Observation != null && apiRequest.Observations?.FirstOrDefault() == null) + apiRequest.Observations = [apiRequest.Observation]; + + //var obrcode = apiRequest.ObservationData?.Code ?? ""; + + if (!apiRequest.Alarms.IsNullOrEmpty()) + await ProcessAlarmObservations(apiRequest.Alarms ?? [], apiRequest.Observations ?? [], + patient, apiRequest.ObservationData?.Time ?? apiRequest.MessageTime, + apiRequest.ObservationData); + + break; + default: + _logger.LogWarning("ApiRequest type {apiRequesttype} sis not valid for Observations", + apiRequest.Type); + throw new ApiRequestException("ApiRequest type " + apiRequest.Type + + " is not valid for Observations"); + } + } + + public async Task SaveRequestAsync(ApiRequest apiRequest) + { + await SaveRequest(apiRequest); + } + + public async Task MapObservation(PatientObservationAlarm obs, bool onlyByName = false) + { + try + { + _logger.LogTrace("Mapping config Observation obs: {obs} onlyByName: {onlyByName}", obs, onlyByName); + var obs2 = await _configObservationService.Map(obs, onlyByName); + if (obs2 == null) + { + _logger.LogTrace("Mapping obs2 {obs}: Ignored", obs); + return null; + } + + _logger.LogTrace("Mapping _configObservationService.Map obs2: {obs2}", obs2); + + + var obs3 = await _calculatedObservationsService.Value.Map(obs2, onlyByName); + + if (obs3 == null) + { + _logger.LogTrace("Mapping obs3 {obs2}: Ignored", obs2); + return null; + } + + return obs3; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error Mapping Observation, Ignoring Observation: {obs} Exception:{ex}", obs, + ex.Message); + return null; + } + } + + public async Task> FindLastObservationsByField(ObjectId patientId, + List? filterObservations = null) + { + var result = + await _alarmRepository.AggregatedPatientLastObservationsByField(patientId, filterObservations); + return result; + } + + public async Task> FindLastValuesNotExpired(ObjectId patientId, + List filterObservations, List configAlarm) + { + var result = + await _alarmRepository.AggregatedPatientNotExpiredObservationsByField(patientId, filterObservations, + configAlarm); + return result; + } + + public async Task MapObservationsByName(PatientObservationAlarm obs) + { + return await _configObservationService.Map(obs, true); + } + + public async Task ProcessAlarmObservations(List alarmObservations, + List observations, Patient patient, + DateTime messageTime, ObservationData? observationData = null) + { + _logger.LogDebug( + "Patient: {patientid} PoC {patientpointOfCare} {patientbed} msg {messageTime} INSERTING {observationsCount} OBSERVATIONS", + patient.Id, patient.UnitId, patient.PointOfCareId, messageTime, alarmObservations.Count); + + ParentDataClass? parentData = null; + if (observationData != null) + parentData = new ParentDataClass + { + Code = observationData.Code, + CodingSystem = observationData.CodingSystem, + Name = observationData.Text + }; + + var listToInsert = new List(); + + foreach (var obs in alarmObservations) + { + obs.ParentData = parentData; + obs.MessageTime = messageTime; + obs.PatientId = patient.Id; + obs.Patient = patient; + obs.Id = ObjectId.GenerateNewId(); + + var intObsTime = new DateTimeOffset(obs.Time).ToUnixTimeSeconds(); + + if (obs.Time == DateTime.MinValue || intObsTime <= 10) + obs.Time = DateTime.UtcNow; + + var intMessageTime = new DateTimeOffset(obs.MessageTime).ToUnixTimeSeconds(); + + if (obs.MessageTime == DateTime.MinValue || intMessageTime <= 10) + obs.MessageTime = DateTime.UtcNow; + + if (obs.Value.ToString() == "System.Object") + { + obs.Value = obs.Event?.ToString()??string.Empty; + } + + _logger.LogDebug( + "Patient: {patientId} PoC {patientpointOfCare} {patientbed} msg {messageTime} INSERTING ALARM OBSERAVTION {obs}", + patient.Id, patient.PointOfCare, patient.Bed, messageTime, obs); + listToInsert.Add(obs); + } + + //listToInsert.ForEach(async obs => await InsertObservation(obs)); + foreach (var alarmToInsert in listToInsert) + { + var alarmData = + observations.FirstOrDefault(obs => obs.Value.ToString() == alarmToInsert.Value.ToString()); + if (alarmData != null) + { + alarmToInsert.Code = alarmData.Code; + alarmToInsert.Name = alarmData.Code; + alarmToInsert.CodingSystem = alarmData.CodingSystem; + } + else + { + alarmToInsert.CodingSystem = parentData?.CodingSystem?? "MDIL-ALARM"; + alarmToInsert.Code = alarmToInsert.Priority.ToString(); + + alarmToInsert.Name = alarmToInsert.Priority switch + { + AlarmEnum.ObservationAlarmPriority.Ph => "RedAlarm_Ph", + AlarmEnum.ObservationAlarmPriority.Pm => "YellowAlarm_Pm", + AlarmEnum.ObservationAlarmPriority.Pl => "BlueAlarm_Pl", + _ => "", + }; + } + + if (!alarmToInsert.Sources.IsNullOrEmpty()) + { + var apiRequestObs = new ApiRequest + { + Type = "ORU_R01", + MessageTime = messageTime, + ObservationData = observationData + }; + + var obsToInsert = new List(); + alarmToInsert.Sources?.ForEach(async void (c) => + { + try + { + var obs = new PatientObservation + { + Code = c.Code, + Name = c.OriginalName, + CodingSystem = c.CodeSystem, + Units = c.Units, + Value = c.Value?.ToString() ?? "No value", + Time = alarmToInsert.Time, + Result = c.Result + }; + var obs2 = await _calculatedObservationsService.Value + .MapSourceAlarm(obs, alarmToInsert); + obsToInsert.Add(obs2); + } + catch (Exception e) + { + _logger.LogError( + "Error processing source observation {source} for alarm {alarm}. Exception: {ex}", + c, alarmToInsert, e); + } + }); + + apiRequestObs.Observations = obsToInsert; + apiRequestObs.Location = patient.Location; + apiRequestObs.Patient = patient.Person; + apiRequestObs.PatientNumber = patient.PatientNumber; + await _observationService.Value.SaveRequestAsync(apiRequestObs); + } + + + await InsertObservation(alarmToInsert); + } + } + + private async Task InsertObservation(PatientObservationAlarm obs, bool persistObs = true, bool mapObs = true) + { + try + { + var obs2 = obs; + + //only will be false if the obs comes from the inner refactor job + if (mapObs) obs2 = await MapObservation(obs2, onlyByName: true); + + if (obs2 == null) + { + _logger.LogDebug("Mapped observation returns null. Ignored {obs}", obs); + } + else + { + if (obs2.Persist.HasValue && !obs2.Persist.Value) persistObs = false; + if (persistObs) + { + _logger.LogDebug("Mapped {obs2}", obs2); + await _alarmRepository.InsertOneAsync(obs2); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, obs2); + } + + _logger.LogDebug("Inserted {obs2}", obs2); + await SendObsBroadcast(obs2); + } + } + catch (Exception ex) + { + _logger.LogError("Error Inserting observation {obs}. Excepcion; {ex} ", obs, ex); + } + } + + private async Task SendObsBroadcast(BasePatientObservation obs) + { + if (obs.Name == null) return; + + const OperationType type = OperationType.Alarm; + + var patient = obs.Patient ?? await _patientService.FindById(obs.PatientId); + if (patient == null) + { + _logger.LogDebug("Not patient on bd to sendOnBroadcastObs: {obspatientid}", obs.PatientId); + return; + } + + _logger.LogTrace( + "sending obs name: {obsname} to patient id: {patientid}, PointOfCare: {patientpointOfCare} {patientbed}", + obs.Name, patient.Id, patient.UnitId, patient.Bed); + + var subscribers = _subscribersService.GetSubscribers().Where(s => + !s.LocationIds.IsNullOrEmpty() && s.LocationIds.Any(c => + c == patient.PointOfCareId + )).ToList(); + + foreach (var subscriber in subscribers) + { + _logger.LogTrace("sending obs name: {obsname} to subscriber id: {subscriberId}", obs.Name, + subscriber.Id); + await _clientMessageService.SendAsync(subscriber.Id, type, obs); + } + } + + + #region activación de alarmas con balizas, relé y grabaciones + + public async Task CheckObservationAlarm(PatientObservation obs) + { + //ConfigObservations + var configs = await _configObservationService.Get(new PatientObservation + { + Name = obs.Name, + PatientId = obs.PatientId + } + ); + + + if (configs?.CreateObservation == null) + return; + + var obsValue = obs.Value.ToString() ?? string.Empty; + + var observationsToCreate = configs.CreateObservation + .Where(c => obsValue.ToUpper().Contains(c.RequiredValue?.ToString()?.ToUpper() ?? string.Empty)) + .ToList(); + + foreach (var obsConfig in observationsToCreate) + { + var create = false; + + if (obsConfig.Preconditions == null) + create = true; + else + foreach (var preCondition in obsConfig.Preconditions) + { + if (preCondition.Name == null) + continue; + + var obsWithConditions = + await _observationService.Value.FindLastObservations(obs.PatientId, 1, + [preCondition.Name]); + if (!obsWithConditions.Any()) + continue; + + var foundObs = obsWithConditions.FirstOrDefault(); + + //Descartamos la observación si ha expirado + if (foundObs == null || (obsConfig.Expires.HasValue && + foundObs.Time.AddSeconds(obsConfig.Expires.Value) < DateTime.UtcNow)) + continue; + + var foundObsStr = foundObs.Value.ToString(); + var requiredValueStr = preCondition.RequiredValue?.ToString(); + + if (string.IsNullOrEmpty(requiredValueStr) || + (foundObsStr != null && foundObsStr.Contains(requiredValueStr))) + { + create = true; + break; + } + } + + + if (create) + { + var newObservation = CreateNewObservation(obs, obsConfig, StatusEnum.Type.Alert); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, newObservation); + newObservation = await CheckAlarmConfig(newObservation); + + var obsName = newObservation.Name ?? string.Empty; + + _ = SendAlarm(newObservation, obsName, null, AlarmEnum.Severity.None, AlarmEnum.Type.Auto); + _ = _observationService.Value.InsertObservation(newObservation); + } + } + } + + private static PatientObservation CreateNewObservation(PatientObservation obs, ConfigObservation config, + StatusEnum.Type type) + { + return new PatientObservation + { + CodingSystem = config.CodingSystem, + Code = config.Code, + Name = config.Name, + Value = obs.Value, + PatientId = obs.PatientId, + Time = obs.Time, + Alarm = config.Alarm, + Status = type + }; + } + + private async Task CheckAlarmConfig(PatientObservation pobs) + { + var configObs = await _configObservationService.Get(new PatientObservation + { + Name = pobs.Name, + PatientId = pobs.PatientId + }); + + pobs.Alarm = configObs?.Alarm ?? null; + + return pobs; + } + + + /// + /// Sends a new alarm + /// + /// Observation to generate the alarm + /// Name of the alarm + /// Code of the alarm for the recording + /// Severity of the alarm for the recording + /// + /// + /// New alarm created + public async Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code, AlarmEnum.Severity severity, + AlarmEnum.Type type) + { + try + { + var poc = await _pocService.FindPoCByPatientId(obs.PatientId); + switch (obs.Time.Kind) + { + // Convert obs.Time to UTC if it's not already + case DateTimeKind.Local: + obs.Time = obs.Time.ToUniversalTime(); + break; + case DateTimeKind.Unspecified: + _logger.LogWarning("obs.Time has unspecified kind. Assuming it to be UTC."); + obs.Time = DateTime.SpecifyKind(obs.Time, DateTimeKind.Utc); + break; + case DateTimeKind.Utc: + break; + default: + throw new ArgumentOutOfRangeException(); + } + + //ConfigObservations + var configObs = await _configObservationService.Get(new PatientObservation + { + Name = obs.Name, + PatientId = obs.PatientId + } + ); + + if (configObs == null) + return; + + if (configObs is { Alarm.Enabled: true }) + { + obs.Alarm = configObs.Alarm; + var now = DateTime.UtcNow; + + if (obs.Expired || (obs.Expires.HasValue && obs.Time.AddSeconds(obs.Expires.Value) < now)) + return; + + //En la prioridad de las alarmas 1 máxima prioridad + if (configObs.Alarm.Beacon is { Enabled: true }) + try + { + if (obs.Time.AddSeconds(configObs.Alarm.Beacon.EndAfter) >= now) + { + var patient = obs.Patient ?? await _patientService.FindById(obs.PatientId); + if (patient != null) + { + _logger.LogDebug("PatientId: {nObsPatientid}. Send Beacon alarmName {beaconColor}", + obs.PatientId, configObs.Alarm.Beacon.BeaconColor); + if (!_beaconAlarmList.Any(o => + o.PatientId == obs.PatientId && o.Alarm?.Priority < obs.Alarm.Priority)) + { + _ = SendBeaconCode(configObs.Alarm.Beacon.BeaconColor, patient); + + lock (_beaconAlarmList) + { + _beaconAlarmList.Add(obs); + } + } + } + } + else + { + _logger.LogDebug( + "PatientId: {nObsPatientid}.Beacon is Expired. EndAfter {endAfter} Time: {obsTime}", + obs.PatientId, configObs.Alarm.Beacon.EndAfter, obs.Time); + } + } + catch (Exception ex) + { + _logger.LogDebug("Exception sending beacon code for patient {patientId}. Exception: {ex}", + obs.PatientId, ex); + + + throw; + } + + if (configObs.Alarm.Recording is { Enabled: true }) + try + { + if (obs.Time.AddSeconds(configObs.Alarm.Recording.EndAfter) >= now) + { + //if(severity == AlarmSeverity.NONE) + severity = configObs.Alarm.Recording.Severity; + + var strValue = obs.Value.ToString(); + if (strValue == null) + { + _logger.LogError("Observation value to string is null observation:{nObs}", obs); + return; + } + + _logger.LogDebug("PatientId: {nObsPatientid}. Send Recording {nObsName}", obs.PatientId, + obs.Name); + + if (code == null && Enum.TryParse(configObs.Alarm.Name, out var result)) + code = result; + + + _ = StartRecording(obs.PatientId, obs.Time, configObs.Alarm.Recording, code, severity, + strValue, configObs.Alarm.Recording.EndAfter, type); + } + else + { + _logger.LogDebug( + "PatientId: {nObsPatientid}.Recording is Expired. EndAfter {endAfter} Time: {obsTime}", + obs.PatientId, configObs.Alarm.Recording.EndAfter, obs.Time); + } + } + catch (Exception ex) + { + _logger.LogDebug( + "Exception sending Alarm Recording for patient {patientId}. Exception: {ex}", + obs.PatientId, ex); + + throw; + } + + if (configObs.Alarm.OpenDoor is { Enabled: true }) + try + { + if (obs.Time.AddSeconds(configObs.Alarm.OpenDoor.EndAfter) >= now) + { + _logger.LogDebug("PatientId: {nObsPatientid}. Open door observation {obsName}", + obs.PatientId, obs.Name); + + lock (_relayAlarmList) + { + if (!_relayAlarmList.Any(o => + o.PatientId == obs.PatientId && o.Alarm?.Priority < obs.Alarm.Priority)) + { + PointOfCareConfiguration? poCSettings = null; + + if (poc is { Configuration.RelayIdList: not null }) + poCSettings = poc.Configuration; + + var status = _relayService.Value.GetRelayByTypeInList(poCSettings?.RelayIdList, + RelayEnum.Type.Door).FirstOrDefault()?.ManualRelayStatus; + + if (status is RelayEnum.Status.Off) + _ = RelayPowerOn(obs.PatientId, RelayEnum.Type.Door); + + lock (_relayAlarmList) + { + _relayAlarmList.Add(obs); + } + } + } + } + else + { + _logger.LogDebug( + "PatientId: {nObsPatientid}.Open door is Expired. EndAfter {endAfter} Time: {obsTime}", + obs.PatientId, configObs.Alarm.OpenDoor.EndAfter, obs.Time); + } + } + catch (Exception ex) + { + _logger.LogDebug("Exception opening door for patient {patientId}. Exception: {ex}", + obs.PatientId, ex); + + throw; + } + } + } + catch (Exception ex) + { + Log.Error("Exception sending alarm: {exMessage}", ex.Message); + } + } + + public async Task CalculateAlarmTest(BasePatientObservationValue source, string name) + { + if (source is not PatientObservation obs) return; + + //ConfigObservations + var configObs = await _configObservationService.Get(source, true); + + if (configObs is { Alarm.Enabled: true }) + { + obs.Alarm = configObs.Alarm; + string? alarmSeverityStr = null; + if (configObs.Alarm.Beacon is { Enabled: true }) + { + var patient = await _patientService.FindById(obs.PatientId); + if (patient == null) + return; + + _logger.LogDebug("PatientId: {nObsPatientid}. Send Beacon alarmName {beaconColor}", + obs.PatientId, configObs.Alarm.Beacon.BeaconColor); + _ = SendBeaconCode(configObs.Alarm.Beacon.BeaconColor, patient); + } + + if (configObs.Alarm.Recording is { Enabled: true }) + { + _logger.LogDebug("PatientId: {nObsPatientid}. Send Recording {nObsName}", obs.PatientId, + obs.Name); + + if (!string.IsNullOrEmpty(alarmSeverityStr) && + Enum.TryParse(alarmSeverityStr, out AlarmEnum.Severity alarmSeverity)) + _ = StartRecording(obs.PatientId, obs.Time, configObs.Alarm.Recording, AlarmEnum.Name.Test, + alarmSeverity, "test description", configObs.Alarm.Recording.EndAfter); + else + _ = StartRecording(obs.PatientId, obs.Time, configObs.Alarm.Recording, AlarmEnum.Name.Test, + AlarmEnum.Severity.Yellow, "test description", configObs.Alarm.Recording.EndAfter); + } + + if (configObs.Alarm.OpenDoor is { Enabled: true }) + { + _logger.LogDebug("PatientId: {nObsPatientid}. Open door", obs.PatientId); + _ = RelayPowerOn(obs.PatientId, RelayEnum.Type.Door); + } + } + } + + private Task SendBeaconCode(AlarmEnum.BeaconColor color, Patient patient) + { + if (!patient.PointOfCareId.HasValue) + { + _logger.LogError("Try to sen beacon code, but no PointOfCareId id is present in the Patient {Patient}", + patient.ToString()); + return Task.CompletedTask; + } + + switch (color) + { + case AlarmEnum.BeaconColor.Blue: + _lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Blue); + break; + case AlarmEnum.BeaconColor.Yellow: + _lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Yellow); + break; + case AlarmEnum.BeaconColor.Red: + _lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Red); + break; + case AlarmEnum.BeaconColor.None: + _lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Off); + break; + } + + return Task.CompletedTask; + } + + /// + /// + /// + /// Hora de la observación + /// + /// + /// + /// + /// + /// + private async Task StartRecording(ObjectId patientId, DateTime eventTime, AlarmItem? recording, + AlarmEnum.Name? alarmName, AlarmEnum.Severity severity, string? alarmDescription, int? endAfter, + AlarmEnum.Type type = AlarmEnum.Type.Manual) + { + try + { + var patient = await _patientService.FindById(patientId); + if (patient is not { PointOfCareId: not null }) return; + var poc = await _pocService.FindByIdAllConfig(patient.PointOfCareId.Value); + if (poc == null) + return; + + + //30 minutos antes y después de la fecha de la observación + var startTime = recording != null ? eventTime.AddSeconds(-recording.StartBefore) : eventTime; + var endDate = endAfter.HasValue ? eventTime.AddSeconds(endAfter.Value) : (DateTime?)null; + + await _recordingService.Value.SendRecordingDataToQueue(patient, poc, startTime, endDate, + eventTime, alarmName, severity, alarmDescription, true, type); + } + catch (Exception ex) + { + _logger.LogError( + "Error Starting recording from patientId: {patientId}. eventTime: {eventTime}. AlarmItem: {recording}. alarmSeverity: {alarmSeverity} Error: {ex}", + patientId, eventTime, recording, severity, ex); + } + } + + private async Task RelayPowerOn(ObjectId patientId, RelayEnum.Type type) + { + try + { + var patient = await _patientService.FindById(patientId); + if (patient is not { PointOfCareId: not null }) + { + Log.Error("can not power on relay because patient: {patientId} not found", patientId); + return; + } + + var poc = await _pocService.FindById(patient.PointOfCareId.Value); + if (poc?.Configuration == null) return; + var relayConfig = _relayService.Value.GetRelayByTypeInList(poc.Configuration.RelayIdList, type).FirstOrDefault(); + if (relayConfig != null) await _relayService.Value.PowerOn(relayConfig); + } + catch (Exception ex) + { + _logger.LogError("Error Relay Power On from patientId: {patientId}. Error: {ex}", patientId, ex); + } + } + + private async void StartTimer() + { + try + { + var pocList = await _pocService.GetAllLocationInfo(); + _interval = TimeSpan.FromSeconds(_apiSettings.Value.ExpireAlertIntervalSeconds); + _ = new Timer(async void (_) => + { + try + { + await _semaphore.WaitAsync(); // Esperar a adquirir el semáforo antes de ejecutar el temporizador + await CheckExpiredAlarms(pocList); + } + catch (Exception e) + { + _logger.LogError("Error in timer execution: {message}", e.Message); + //throw new Exception("Error in timer execution", e); + } + finally + { + _semaphore.Release(); // Liberar el semáforo después de ejecutar el temporizador + } + }, null, TimeSpan.Zero, _interval); + } + catch (Exception e) + { + _logger.LogError("Error starting timer: {message}", e.Message); + //throw new Exception("Error starting timer", e); + } + } + + + private async Task CheckExpiredAlarms(List pocList) + { + _logger.LogTrace("Checking Expired Alarms Started"); + + // Iniciar ambas tareas de forma asincrónica + var checkBeaconsTask = CheckExpiredBeaconsAsync(pocList); + var checkRelayTask = CheckExpiredRelayAsync(); + + // Esperar a que ambas tareas completen + await Task.WhenAll(checkBeaconsTask, checkRelayTask); + + _logger.LogTrace("Checking Expired Alarms Finished"); + } + + private readonly SemaphoreSlim _beaconListSemaphore = new(1, 1); + + private async Task CheckExpiredBeaconsAsync(List pocList) + { + await _beaconListSemaphore.WaitAsync(); + try + { + if (!_beaconAlarmList.Any()) + //var pocList = await _pocService.GetAllLocationInfo(); + if (pocList.Any()) + { + var tasks = pocList.Select(async poc => + { + await _lightBeaconService.Value.SendColor(poc, LightBeaconColor.Off); + }); + + await Task.WhenAll(tasks); + + return; + } + } + finally + { + _beaconListSemaphore.Release(); + } + + var now = DateTime.UtcNow; + List updatedList = []; + List ledTasks = []; + + await _beaconListSemaphore.WaitAsync(); + try + { + foreach (var obsGroup in _beaconAlarmList.GroupBy(o => o.PatientId)) + { + var nonExpiredObs = obsGroup.Where(obs => + obs.Alarm is { Beacon: not null } && + (obs.Time.Kind == DateTimeKind.Utc ? obs.Time : obs.Time.ToUniversalTime()) + .AddSeconds(obs.Alarm.Beacon.EndAfter) >= now + ).ToList(); + + if (!nonExpiredObs.Any()) + { + var patient = obsGroup.FirstOrDefault()?.Patient; + if (patient is { PointOfCareId: not null }) + ledTasks.Add(_lightBeaconService.Value.SendColor(patient.PointOfCareId.Value, + LightBeaconColor.Off)); + } + else + { + updatedList.AddRange(nonExpiredObs); + } + } + + _beaconAlarmList = updatedList; + } + finally + { + _beaconListSemaphore.Release(); + } + + await Task.WhenAll(ledTasks); + } + + + private async Task CheckExpiredRelayAsync() + { + var now = DateTime.UtcNow; + + IEnumerable> groupedByPatientId; + + lock (_relayAlarmList) + { + groupedByPatientId = _relayAlarmList.GroupBy(o => o.PatientId); + } + + foreach (var obsGroup in groupedByPatientId) + { + var patient = await _patientService.FindById(obsGroup.Key); + if (patient is not { PointOfCareId: null }) + continue; + + + var nonExpiredObs = obsGroup.Where(obs => + obs.Alarm is { OpenDoor: not null } && + (obs.Time.Kind == DateTimeKind.Utc ? obs.Time : obs.Time.ToUniversalTime()).AddSeconds( + obs.Alarm.OpenDoor.EndAfter) >= now + ).ToList(); + + // Apagar el LED si no hay observaciones no expiradas + if (!nonExpiredObs.Any()) + { + //buscamos en PoCSettings si está activado de forma manual + var pocSettings = await _pocService.FindById(patient.PointOfCareId!.Value); + var relays = _relayService.Value.GetRelayInList(pocSettings?.Configuration?.RelayIdList); + foreach (var relay in relays) + { + // Verificamos si NO tiene un estado manual activo (On) + // Si el estado es null o es diferente de On, lo apagamos + if (relay.ManualRelayStatus != null && relay.ManualRelayStatus != RelayEnum.Status.On) + { + await _relayService.Value.PowerOff(relay); + } + } + } + + // Reemplazar la lista original con las observaciones no expiradas + lock (_relayAlarmList) + { + _relayAlarmList.RemoveAll(obs => obs.PatientId == obsGroup.Key); + _relayAlarmList.AddRange(nonExpiredObs); + } + } + } +} + +#endregion \ No newline at end of file diff --git a/adas-core.Application/Services/AlertValuesService.cs b/adas-core.Application/Services/AlertValuesService.cs new file mode 100644 index 00000000..8e99573e --- /dev/null +++ b/adas-core.Application/Services/AlertValuesService.cs @@ -0,0 +1,17 @@ +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.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Services; + +public class AlertValuesService(IConfigObservationRepository alertValueRepository) : IAlertValuesService +{ + public async Task FindByKey(ObjectId key) + { + return await alertValueRepository.FindById(key) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/AppointmentService.cs b/adas-core.Application/Services/AppointmentService.cs new file mode 100644 index 00000000..2ea7223d --- /dev/null +++ b/adas-core.Application/Services/AppointmentService.cs @@ -0,0 +1,362 @@ +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.Utils; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Patient = adas_core.Domain.Models.MongoModels.Patient; + +namespace adas_core.Application.Services; + +public class AppointmentService( + IAppointmentRepository appointmentRepository, + IAppointmentArchiveRepository appointmentArchiveRepository, + Lazy patientService, + Lazy observationService, + IDiagnosisService diagnosisService, + IUnitService unitService, + IOptions apiSettings, + IOptions cacheSettings, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService, + IPointOfCareService pointOfCareService, + ISubscribersService subscribersService, + IClientMessageService clientMessageService, + ICacheService cacheService) + : IAppointmentService +{ + private readonly bool _createPatientWithSiu = apiSettings.Value.CreatePatientWithSiu; + private readonly CacheSettings? _cacheSettings = cacheSettings.Value ; + + public async Task SaveRequest(ApiRequest apiRequest) + { + if (string.IsNullOrEmpty(apiRequest.PatientNumber)) + { + logger.LogDebug("Patient is null"); + throw new ApiRequestException("Patient number is null"); + } + + logger.LogDebug("patientNumber: {apiRequestPatientNumber} location: {apiRequestLocation}", + apiRequest.PatientNumber, apiRequest.Location); + logger.LogDebug("RequestType: {apiRequestType}", apiRequest.Type); + + var patient = await patientService.Value.FindByPatientNumber(apiRequest.PatientNumber); + if (patient == null) + { + // PATIENT NOT FOUND + logger.LogWarning("Patient not Found. {patientNumber}", apiRequest.PatientNumber); + if (!_createPatientWithSiu) return; // IGNORE + patient = await patientService.Value.CreatePatientFromRequest(apiRequest, true); + } + + await ProcessApiRequest(apiRequest, patient); + + if (patient != null) + { + if (apiRequest is { Observations: not null, ObservationData: not null }) + observationService.Value.ProcessObservations(apiRequest.Observations, patient, + apiRequest.MessageTime, apiRequest.ObservationData); + else + logger.LogWarning("Observations not Found. {patientNumber} ", patient.PatientNumber); + + if (apiRequest.Diagnosis != null) + _ = diagnosisService.ProcessDiagnosis(apiRequest.Diagnosis, patient, apiRequest.MessageTime); + else + logger.LogWarning("Diagnosis not Found. {patientNumber}", patient.PatientNumber); + } + } + + public async Task ProcessApiRequest(ApiRequest apiRequest, Patient? patient) + { + if (patient == null) + return; + + var unitConfig = await unitService.FindById(patient.UnitId); + if (!Hl7Utils.ManageAutoAdt(unitConfig, null, logger, "ORU")) return; + + apiRequest.Appointments ??= []; + if (apiRequest.Appointment != null) apiRequest.Appointments.Add(apiRequest.Appointment); + //apiRequest.appointments.ForEach(ap => + foreach (var ap in apiRequest.Appointments) + { + if (ap.VisitNumber == null || patient.Person == null) return; + + ap.PatientId = patient.Id; + ap.Patient = patient.Person; + var apdb = ap.VisitNumber != null + ? await appointmentRepository.FindByPatientAndVisitNumber(ap.PatientId, ap.VisitNumber) + : null; + apdb ??= await appointmentRepository.FindByPatientAndReason(ap.PatientId, ap.AppointmentReason); + + if (apdb != null && DateTime.Compare(apdb.UpdateTime, apiRequest.MessageTime) > 0) return; + // TODO: Aux + var oldAp = await auditService.DeepCopyAsync(ap); + switch (apiRequest.Type) + { + //* SIU_S12 - Notification of new appointment booking + //* SIU_S13 - Notification of Appointment Rescheduling + //* SIU_S14 - Notification of Appointment Modification + //* SIU_S18 - Notification of Addition of Service/Resource on Appointment + //* SIU_S19 - Notification of Modification of Service/Resource on Appointment + //* SIU_S20 - Notification of Cancellation of Service/Resource on Appointment + //* SIU_S21 - Notification of Discontinuation of Service/Resource on Appointment + //* SIU_S22 - Notification of Deletion of Service/Resource on Appointment + + case "SIU_S12": + case "SIU_S13": + case "SIU_S14": + case "SIU_S18": + case "SIU_S19": + case "SIU_S20": + case "SIU_S21": + case "SIU_S22": + ap.UpdateTime = apiRequest.MessageTime; + if (apdb != null) + { + ap.Id = apdb.Id; + ap.CreateTime = apdb.CreateTime; + ap.AppointmentOperationType = OperationType.UpdatedAppointment; + ap.ApplyResourceGroups(apdb); + await appointmentRepository.Update(ap); + // Invalidar CACHE (colección completa) + await cacheService.DeleteObjectAsync(CacheKeys.PatientAppointmentsToday(ap.PatientId)); + //SendBroadcast(ap, OperationType.updatedAppointment); + } + else + { + ap.CreateTime = apiRequest.MessageTime; + ap.AppointmentOperationType = OperationType.NewAppointment; + ap.ApplyResourceGroups(); + await appointmentRepository.InsertOneAsync(ap); + //SendBroadcast(ap, OperationType.newAppointment); + } + + break; + + + //* SIU_S15 - Notification of Appointment Cancellation + //* SIU_S16 - Notification of Appointment Discontinuation + //* SIU_S17 - Notification of Appointment Deletion + + case "SIU_S15": + case "SIU_S16": + case "SIU_S17": + ap.UpdateTime = apiRequest.MessageTime; + if (apdb != null) + { + ap.Id = apdb.Id; + ap.CreateTime = apdb.CreateTime; + ap.AppointmentOperationType = OperationType.CanceledAppointment; + ap.ApplyResourceGroups(apdb); + await appointmentRepository.Update(ap); + await cacheService.DeleteObjectAsync(CacheKeys.PatientAppointmentsToday(ap.PatientId)); + } + else + { + ap.CreateTime = apiRequest.MessageTime; + ap.AppointmentOperationType = OperationType.CanceledAppointment; + ap.ApplyResourceGroups(); + await appointmentRepository.InsertOneAsync(ap); + } + + //SendBroadcast(ap, OperationType.canceledAppointment); + break; + + + //* SIU_S23 - Notification of Blocked Schedule Time Slot(S) + //* SIU_S24 - Notification of Opened (un-blocked) Schedule Time Slot(s) + //* SIU_S26 - Notification That Patient Did Not Show Up for Scheduled Appointment + + default: + + ap.UpdateTime = apiRequest.MessageTime; + if (apdb != null) + { + ap.Id = apdb.Id; + ap.CreateTime = apdb.CreateTime; + //ap.appointmentOperationType = OperationType.updatedAppointment; + ap.ApplyResourceGroups(apdb); + await appointmentRepository.Update(ap); + await cacheService.DeleteObjectAsync(CacheKeys.PatientAppointmentsToday(ap.PatientId)); + //SendBroadcast(ap, ap.appointmentOperationType); + } + else + { + ap.CreateTime = apiRequest.MessageTime; + //ap.appointmentOperationType = OperationType.newAppointment; + ap.ApplyResourceGroups(); + await appointmentRepository.InsertOneAsync(ap); + //SendBroadcast(ap, ap.appointmentOperationType); + } + + break; + } + + if (apdb != null) + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, apdb, ap); + + else await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, ap); + + if (ap.AppointmentOperationType.HasValue) + await SendBroadcast(ap, ap.AppointmentOperationType); + } + } + + public Task SaveRequestAsync(ApiRequest apiRequest) + { + return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); })); + } + + public async Task Archive(Patient patient) + { + await ArchiveByPatientId(patient.Id); + } + + public async Task ArchiveByPatientId(ObjectId id) + { + logger.LogDebug("Archive Appointments by patientId {id}", id); + using (var cursor = await FindByPatientIdAsync(id)) + { + while (await cursor.MoveNextAsync()) + foreach (var current in cursor.Current) + await appointmentArchiveRepository.InsertOneAsync(current); + } + + await DeleteByPatientId(id); + } + + public async Task> GetByPatient(ObjectId patientId) + { + return await appointmentRepository.GetByPatient(patientId); + } + + public async Task> GetTodayByPatient( + ObjectId patientId, + CancellationToken ct = default) + { + // Obtener clave + TTL según CacheSettings + var (key, ttl) = CacheKeys.PatientAppointmentsTodayKeyWithTtl(_cacheSettings, patientId); + + // Cachear la lista RAW de citas del paciente (sin filtrar) + var allAppointments = await cacheService.GetOrSetObjectAsync( + key, + async () => + { + // 1 - Consultar todas las citas del paciente + var list = await appointmentRepository.GetByPatient(patientId); + + // 2 - Devuelve RAW (List), nada filtrado + return list; + }, + ttl, + ct); + + // 3 - Ahora filtramos solo las de "hoy" + var today = DateTime.UtcNow.Date; + + var todayAppointments = allAppointments + .Where(a => a.Timings.Any(t => + t.StartTime.HasValue && + t.StartTime.Value.Date == today)) + .ToList(); + + return todayAppointments; + } + + + public async Task> GetTodayByPoc( + ObjectId pocId, + CancellationToken ct = default) + { + var poc = await pointOfCareService.FindById(pocId); + if(poc == null) return []; + + // Obtener clave + TTL según CacheSettings + var (key, ttl) = CacheKeys.PocAppointmentsTodayKeyWithTtl(_cacheSettings, pocId); + + // Cachear la lista RAW de citas del pointOfCare (sin filtrar) + var allAppointments = await cacheService.GetOrSetObjectAsync( + key, + async () => + { + // 1 - Consultar todas las citas del pointOfCare + var list = await appointmentRepository.FindByPoC(poc); + + // 2 - Devuelve RAW (List), nada filtrado + return list; + }, + ttl, + ct); + + // 3 - Ahora filtramos solo las de "hoy" + var today = DateTime.UtcNow.Date; + + var todayAppointments = allAppointments + .Where(a => a.Timings.Any(t => + t.StartTime.HasValue && + t.StartTime.Value.Date == today)) + .ToList(); + + return todayAppointments; + } + + public Task> FindByPatientIdAsync(ObjectId patientId) + { + return appointmentRepository.FindByPatientIdAsync(patientId); + } + + public async Task> FindByLocation(PatientLocation location) + { + return await appointmentRepository.FindByLocation(location); + } + + + public async Task DeleteByPatientId(ObjectId id) + { + var patientApp = await FindByPatientIdAsync(id); + logger.LogDebug("Delete Appointments by Patient Id {id}", id); + await appointmentRepository.DeleteByPatientId(id); + + // Invalidar CACHE (colección completa) + await cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.Appointments)); + + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, patientApp, null); + } + + public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId) + { + await appointmentRepository.UpdateManyObjectId(nameId, id, oldId); + } + + private async Task SendBroadcast(PatientAppointment appointment, OperationType? operationType) + { + //RECORRE LOS DIFERENTES LOCATIONS DE LA CITA + foreach (var resourceGroup in appointment.ResourceGroups) + if (resourceGroup.Locations != null) + foreach (var location in resourceGroup.Locations) + // TODO + if (!string.IsNullOrEmpty(location.UnitName) && !string.IsNullOrEmpty(location.Bed)) + { + var unit = await unitService.FindByName(location.UnitName); + if (unit == null) continue; + + var poc = await pointOfCareService.FindByBedAndUnitId(location.Bed, unit.Id); + if (poc == null) continue; + + var subscribers = subscribersService.GetSubscribers().Where(s => + !s.LocationIds.IsNullOrEmpty() && s.LocationIds.Any(c => + c == poc.Id + )).ToList(); + + foreach (var subscriber in subscribers) + _ = clientMessageService.SendAsync(subscriber.Id, operationType, appointment); + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/ArchivePatientCarePlanService.cs b/adas-core.Application/Services/ArchivePatientCarePlanService.cs new file mode 100644 index 00000000..0dae309c --- /dev/null +++ b/adas-core.Application/Services/ArchivePatientCarePlanService.cs @@ -0,0 +1,62 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models.MongoModels; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; + +namespace adas_core.Application.Services; + +public class ArchivePatientCarePlanService( + IArchivePatientCarePlanRepository archivedPatientRepository, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService) + : IArchivePatientCarePlanService +{ + #region Create + + public async Task InsertOneAsync(PatientCarePlan patient) + { + await archivedPatientRepository.InsertOneAsync(patient); + logger.LogInformation( + "Inserted archived patient care plan for patientId: {PatientId}, patientNumber: {PatientNumber}", + patient.PatientId, patient.PatientNumber); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, patient); + return patient; + } + + public async Task InsertManyAsync(List patientCarePla) + { + await archivedPatientRepository.InsertManyAsync(patientCarePla); + logger.LogInformation("Inserted {Count} archived patient care plans", patientCarePla.Count); + foreach (var patient in patientCarePla) + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, patient); + } + + #endregion + + #region Read + + public async Task?> FindByPatientId(ObjectId patientId) + { + return await archivedPatientRepository.FindByPatientId(patientId); + } + + public async Task?> FindByPatientId(string patientId) + { + return await archivedPatientRepository.FindByPatientId(patientId); + } + + public async Task?> FindByPatientNumber(string patientId) + { + return await archivedPatientRepository.FindByPatientNumber(patientId); + } + + public Task> FindAll() + { + return archivedPatientRepository.FindAll(); + } + + #endregion +} \ No newline at end of file diff --git a/adas-core.Application/Services/ArchivePatientObservationsService.cs b/adas-core.Application/Services/ArchivePatientObservationsService.cs new file mode 100644 index 00000000..be9c95c1 --- /dev/null +++ b/adas-core.Application/Services/ArchivePatientObservationsService.cs @@ -0,0 +1,15 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models; +using MongoDB.Bson; + +namespace adas_core.Application.Services; + +public class ArchivePatientObservationsService(IObservationArchiveRepository archivedPatientObservationService) + : IArchivedPatientObservationService +{ + public async Task> FindArchivedPatientObservationsFromPatient(ObjectId patientId) + { + return await archivedPatientObservationService.FindAllFromPatient(patientId); + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/ArchivedPatientService.cs b/adas-core.Application/Services/ArchivedPatientService.cs new file mode 100644 index 00000000..3a3f532a --- /dev/null +++ b/adas-core.Application/Services/ArchivedPatientService.cs @@ -0,0 +1,13 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Application.Services; + +public class ArchivedPatientService(IPatientArchiveRepository archivedPatientRepository) : IArchivedPatientService +{ + public async Task> FindAllPatients() + { + return await archivedPatientRepository.FindAll(); + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/ArchivedPatientTreatmentService.cs b/adas-core.Application/Services/ArchivedPatientTreatmentService.cs new file mode 100644 index 00000000..ecaf3ba3 --- /dev/null +++ b/adas-core.Application/Services/ArchivedPatientTreatmentService.cs @@ -0,0 +1,15 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models; +using MongoDB.Bson; + +namespace adas_core.Application.Services; + +public class ArchivedPatientTreatmentService(ITreatmentArchiveRepository archivedPatientTreatmentService) + : IArchivedPatientTreatmentService +{ + public async Task> FindAllPatientTreatmentsByPatient(ObjectId patientId) + { + return await archivedPatientTreatmentService.FindAllFromPatient(patientId); + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/AuthService.cs b/adas-core.Application/Services/AuthService.cs new file mode 100644 index 00000000..282dba09 --- /dev/null +++ b/adas-core.Application/Services/AuthService.cs @@ -0,0 +1,137 @@ +using System.Text; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using adas_core.Domain.Utils; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Newtonsoft.Json; + +namespace adas_core.Application.Services; + +public class AuthService : IAuthService +{ + private readonly IAuthorityRepository _authorityRepository; + private readonly ILogger _logger; + private readonly RecordingSettings _recordingSettings; + + //private LoginResponse? _loginResponse; + + public AuthService(IOptions recordingSettings, ILogger logger, + IAuthorityRepository authorityRepository) + { + _recordingSettings = recordingSettings.Value ?? + throw new Exception("RecordingSettings must be defined on appSettings"); + + _logger = logger; + + _authorityRepository = authorityRepository; + + _ = InstanceAuthUtils(); + } + + public async Task GetLoginResponse() + { + try + { + if (_recordingSettings.RecordingApiUrl.IsEmpty()) + { + _logger.LogInformation("Url not defined to get token on AuthUtils..."); + return null; + } + + var user = new + { + Username = _recordingSettings.RecordingOrApiClientId, + Password = _recordingSettings.RecordingOrApiClientSecret + }; + + var handler = new HttpClientHandler + { + ServerCertificateCustomValidationCallback = (_, _, _, _) => true + }; + + var client = new HttpClient(handler); + var json = JsonConvert.SerializeObject(user); + + var data = new StringContent(json, Encoding.UTF8, "application/json"); + var response = await client.PostAsync($"{_recordingSettings.RecordingApiUrl}/users/login", data); + var respToken = response.IsSuccessStatusCode ? await response.Content.ReadAsStringAsync() : null; + if (respToken != null) + { + var ton = JsonConvert.DeserializeObject(respToken); + if (ton != null && !ton.Token.IsEmpty()) + { + AuthUtils.Instance.LoginResponse = ton; + return ton; + } + } + + return null; + } + catch (Exception e) + { + _logger.LogError("Error trying to get token: exception: {eMessage}", e.Message); + return null; + } + } + + public async Task GetToken() + { + var loginResponse = AuthUtils.Instance.GetLoginResponse(); + if (!loginResponse.Token.IsEmptyOrWhiteSpace() && !loginResponse.IsExpired()) return loginResponse.Token; + var resp = await GetLoginResponse(); + if (resp != null) return resp.Token; + _logger.LogWarning("Failed Getting token check recordingSettings for user and pass"); + return ""; + } + + public async Task> GetByUnitId(ObjectId unitId) + { + return await _authorityRepository.GetByUnitId(unitId); + } + + public async Task> GetUserAuthorities(ObjectId id) + { + return await _authorityRepository.GetUserAuthorities(id); + } + + public async Task DeleteByUnitId(ObjectId unitId) + { + return await _authorityRepository.DeleteAllAuthoritiesByUnit(unitId); + } + + public async Task DeleteByDisplayId(ObjectId displayId) + { + return await _authorityRepository.DeleteAllAuthoritiesByDisplay(displayId); + } + + private async Task InstanceAuthUtils() + { + if (_recordingSettings.RecordingApiUrl.IsEmpty()) + { + _logger.LogInformation( + "RecordingOrApiUrl not defined on appsettings RecordingSettings AuthUtils not instanciated"); + return; + } + + if (AuthUtils.Instance.GetLoginResponse().Token.IsEmpty() || AuthUtils.Instance.GetLoginResponse().IsExpired()) + { + _logger.LogInformation("Token is not generated or is expired sending request for new token"); + var newLoginResponse = await GetLoginResponse(); + if (newLoginResponse == null) + { + _logger.LogError("Token request is null check recordingSettings for user, pass and authorities"); + } + else + { + _logger.LogInformation("New Token generated at UTC DATE: {DateTime} exires at: {newLoginResponse}", + DateTime.UtcNow, newLoginResponse.Expiration); + AuthUtils.Instance.LoginResponse = newLoginResponse; + } + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/Caching/CacheDispatcher.cs b/adas-core.Application/Services/Caching/CacheDispatcher.cs new file mode 100644 index 00000000..2bb1a910 --- /dev/null +++ b/adas-core.Application/Services/Caching/CacheDispatcher.cs @@ -0,0 +1,111 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Utils; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Caching +{ + /// + /// Orquestador de caché. Selecciona el backend (Redis, InMemory, None) + /// según CacheSettings y la entidad del key. + /// Implementa ICacheService y delega en el backend elegido. + /// + public class CacheDispatcher( + RedisService redis, + CacheService memory, + NoCacheService noop, + CacheSettings cacheSettings) + : ICacheService + { + + // Selección de backend + private ICacheService SelectBackend(string key) + { + var entity = CacheKeyClassifier.Classify(key); + var mode = entity switch + { + CacheEnum.EntityType.Patients => cacheSettings.Patients, + CacheEnum.EntityType.Displays => cacheSettings.Displays, + CacheEnum.EntityType.PumpObservations => cacheSettings.PumpObservations, + CacheEnum.EntityType.Appointments => cacheSettings.Appointments, + CacheEnum.EntityType.GroupedObservations => cacheSettings.GroupedObservations, + _ => CacheEnum.Mode.Cache + }; + + return mode switch + { + CacheEnum.Mode.Redis => redis, + CacheEnum.Mode.Cache => memory, + _ => noop + }; + } + + // Para GroupedObservations generamos la misma clave compuesta que el resto de servicios, + // de modo que el clasificador y la política de TTL funcionen igual. + private static string BuildGroupedKey(GroupedField gf, ObjectId patientId) + => $"GroupedObs:{patientId}:{gf.Name}"; + + private ICacheService SelectBackend(GroupedField groupedField, ObjectId patientId) + => SelectBackend(BuildGroupedKey(groupedField, patientId)); + + + // GetOrSet (KEY string) + public Task GetOrSetObjectAsync( + string key, + Func> factory, + TimeSpan? ttl = null, + CancellationToken cancellationToken = default) + => SelectBackend(key).GetOrSetObjectAsync(key, factory, ttl, cancellationToken); + + public Task GetOrSetValueAsync( + string key, + Func> loader, + TimeSpan? ttl = null) + => SelectBackend(key).GetOrSetValueAsync(key, loader, ttl); + + + // GetOrSet (GroupedField + PatientId) + public Task GetOrSetObjectAsync( + GroupedField groupedField, + ObjectId patientId, + Func> factory, + TimeSpan? ttl = null, + CancellationToken cancellationToken = default) + => SelectBackend(groupedField, patientId) + .GetOrSetObjectAsync(groupedField, patientId, factory, ttl, cancellationToken); + + + // Set/Get básicos + public void SetValue(string key, string value) + => SelectBackend(key).SetValue(key, value); + + public string? GetValue(string key) + => SelectBackend(key).GetValue(key); + + public Task GetObjectAsync(string key, bool upd = true) + => SelectBackend(key).GetObjectAsync(key, upd); + + public Task SetObjectAsync(string key, T obj, bool upd = true) + => SelectBackend(key).SetObjectAsync(key, obj, upd); + + public Task GetObjectAsync(string key, TimeSpan? ttl, bool upd) + => SelectBackend(key).GetObjectAsync(key, ttl, upd); + + public Task SetObjectAsync(string key, T obj, TimeSpan? ttl, bool upd) + => SelectBackend(key).SetObjectAsync(key, obj, ttl, upd); + + public Task DeleteObjectAsync(string key) + => SelectBackend(key).DeleteObjectAsync(key); + + public async Task DeleteByPatternAsync(string pattern) + => await redis.DeleteByPatternAsync(pattern) + await memory.DeleteByPatternAsync(pattern); + + public void CleanCache() + { + memory.CleanCache(); + redis.CleanCache(); + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/Caching/CacheService.cs b/adas-core.Application/Services/Caching/CacheService.cs new file mode 100644 index 00000000..dc85e758 --- /dev/null +++ b/adas-core.Application/Services/Caching/CacheService.cs @@ -0,0 +1,145 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models.GroupedObservations; +using MongoDB.Bson; +using System.Collections.Concurrent; + +namespace adas_core.Application.Services.Caching +{ + /// + /// Implementación de caché en memoria con soporte de locking seguro + /// mediante LockManagerService + InMemoryLockProvider. + /// Compatible con la interfaz ICacheService incluyendo GetOrSet. + /// + public class CacheService(LockManagerService lockManager) : ICacheService + { + private readonly ConcurrentDictionary _mem = new(); + + // HELPERS + private static string BuildGroupedKey(GroupedField gf, ObjectId patientId) + => $"GroupedObs:{patientId}:{gf.Name}"; + + + // GET OR SET (string key) + public async Task GetOrSetObjectAsync( + string key, + Func> factory, + TimeSpan? ttl = null, + CancellationToken cancellationToken = default) + { + // FAST PATH + if (_mem.TryGetValue(key, out var existing)) + return (T)existing; + + // LOCKED PATH + return await lockManager.WithLockAsync( + $"getorset:{key}", + TimeSpan.FromSeconds(5), + async () => + { + if (_mem.TryGetValue(key, out var again)) + return (T)again; + + var created = await factory(); + + if (created != null!) + _mem[key] = created; + + return created!; + }, + cancellationToken); + } + + public async Task GetOrSetValueAsync( + string key, + Func> loader, + TimeSpan? ttlOverride = null) + { + + var result = await GetOrSetObjectAsync(key, loader, ttlOverride); + return (string?)result; + + } + + + // GET OR SET (GroupedField + patientId) + public async Task GetOrSetObjectAsync( + GroupedField groupedField, + ObjectId patientId, + Func> factory, + TimeSpan? ttl = null, + CancellationToken cancellationToken = default) + { + var key = BuildGroupedKey(groupedField, patientId); + + if (_mem.TryGetValue(key, out var existing)) + return (T)existing; + + return await lockManager.WithLockAsync( + $"getorset:{key}", + TimeSpan.FromSeconds(5), + async () => + { + if (_mem.TryGetValue(key, out var again)) + return (T)again; + + var created = await factory(); + + if (created != null!) + _mem[key] = created; + + return created!; + }, + cancellationToken); + } + + + // GET / SET + public void SetValue(string key, string value) + => _mem[key] = value; + + public string? GetValue(string key) + => _mem.TryGetValue(key, out var v) ? v.ToString() : null; + + public Task GetObjectAsync(string key, bool updateExpiration = true) + { + return Task.FromResult( + _mem.TryGetValue(key, out var v) ? (T?)v : default + ); + } + + public Task SetObjectAsync(string key, T obj, bool updateExpiration = true) + { + _mem[key] = obj!; + return Task.CompletedTask; + } + + public Task GetObjectAsync(string key, TimeSpan? ttlOverride, bool upd) + => GetObjectAsync(key, upd); + + public Task SetObjectAsync(string key, T obj, TimeSpan? ttlOverride, bool upd) + => SetObjectAsync(key, obj, upd); + + + // DELETE / CLEAN + public Task DeleteObjectAsync(string key) + { + _mem.TryRemove(key, out _); + return Task.CompletedTask; + } + + public Task DeleteByPatternAsync(string pattern) + { + var p = pattern.Replace("*", ""); + var keys = _mem.Keys.Where(k => k.Contains(p)).ToList(); + + long removed = 0; + foreach (var k in keys) + if (_mem.TryRemove(k, out _)) + removed++; + + return Task.FromResult(removed); + } + + public void CleanCache() => _mem.Clear(); + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/Caching/InMemoryLockProvider.cs b/adas-core.Application/Services/Caching/InMemoryLockProvider.cs new file mode 100644 index 00000000..a74ad58d --- /dev/null +++ b/adas-core.Application/Services/Caching/InMemoryLockProvider.cs @@ -0,0 +1,62 @@ +using System.Collections.Concurrent; +using adas_core.Application.Services.Interfaces; + +namespace adas_core.Application.Services.Caching +{ + /// + /// Implementación local del sistema de locking por clave. + /// Se basa en SemaphoreSlim y solo controla concurrencia DENTRO del proceso. + /// Para CacheService (in-memory). + /// + public class InMemoryLockProvider : ILockProvider + { + private readonly ConcurrentDictionary _locks = new(StringComparer.Ordinal); + + /// + /// Crea u obtiene un semáforo asociado a la clave. + /// + private SemaphoreSlim GetOrCreate(string key) + { + return _locks.GetOrAdd(key, _ => new SemaphoreSlim(1, 1)); + } + + /// + /// Intenta adquirir el lock por clave. + /// + public async Task AcquireAsync(string key, TimeSpan timeout) + { + var sem = GetOrCreate(key); + + try + { + return await sem.WaitAsync(timeout).ConfigureAwait(false); + } + catch (ObjectDisposedException) + { + // Si el semáforo se eliminó entre medio, creamos uno nuevo. + _locks.TryRemove(key, out _); + return await GetOrCreate(key).WaitAsync(timeout).ConfigureAwait(false); + } + } + + /// + /// Libera el lock (si existe y no está ya liberado). + /// + public Task ReleaseAsync(string key) + { + if (!_locks.TryGetValue(key, out var sem)) + return Task.CompletedTask; + + try + { + sem.Release(); + } + catch (SemaphoreFullException) + { + // Idempotencia: ignoramos exceso de releases + } + + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/Caching/LockManagerService.cs b/adas-core.Application/Services/Caching/LockManagerService.cs new file mode 100644 index 00000000..843c1c10 --- /dev/null +++ b/adas-core.Application/Services/Caching/LockManagerService.cs @@ -0,0 +1,93 @@ +using adas_core.Application.Services.Interfaces; +using Microsoft.Extensions.Logging; + +namespace adas_core.Application.Services.Caching +{ + /// + /// Servicio que ejecuta acciones dentro de una sección crítica controlada por lock. + /// CacheService lo usará para evitar condiciones de carrera en GetOrSet. + /// Funciona igual para locks locales o distribuidos. + /// + public class LockManagerService( + ILogger logger, + ILockProvider provider) + { + /// + /// Ejecuta una función que devuelve un valor bajo un lock por clave. + /// + public async Task WithLockAsync( + string key, + TimeSpan timeout, + Func> action, + CancellationToken cancellationToken = default) + { + logger.LogDebug( + "LockManager — intentando adquirir lock para key={Key} timeout={TimeoutMs}ms", + key, timeout.TotalMilliseconds); + + var acquired = false; + + try + { + acquired = await provider.AcquireAsync(key, timeout).ConfigureAwait(false); + + if (!acquired) + { + logger.LogWarning( + "LockManager — timeout al adquirir lock para key={Key} tras {TimeoutMs}ms", + key, timeout.TotalMilliseconds); + throw new TimeoutException($"No se pudo adquirir el lock para '{key}'"); + } + + logger.LogDebug( + "LockManager — lock adquirido correctamente para key={Key}", + key); + + return await action().ConfigureAwait(false); + } + finally + { + if (acquired) + { + await SafeReleaseAsync(key); + } + } + } + + /// + /// Version Task (sin valor). + /// + public Task WithLockAsync( + string key, + TimeSpan timeout, + Func action, + CancellationToken cancellationToken = default) + { + return WithLockAsync( + key, + timeout, + async () => + { + await action(); + return null; + }, + cancellationToken); + } + + /// + /// Libera el lock y registra posibles errores sin interrumpir el flujo. + /// + private async Task SafeReleaseAsync(string key) + { + try + { + await provider.ReleaseAsync(key).ConfigureAwait(false); + logger.LogDebug("LockManager — key={Key} lock deleted ", key); + } + catch (Exception ex) + { + logger.LogError(ex, "LockManager — key={Key} error deleting lock", key); + } + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/Caching/NoCacheService.cs b/adas-core.Application/Services/Caching/NoCacheService.cs new file mode 100644 index 00000000..81e7fdbc --- /dev/null +++ b/adas-core.Application/Services/Caching/NoCacheService.cs @@ -0,0 +1,97 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models.GroupedObservations; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Caching +{ + /// + /// Implementación nula de ICacheService. + /// No almacena nada, no devuelve nada y no interfiere con el flujo. + /// Se usa cuando el CacheMode es "None". + /// + public class NoCacheService : ICacheService + { + public void SetValue(string key, string value) + { + // No hacer nada + } + + public string? GetValue(string key) + { + return null; + } + + public Task GetObjectAsync(string key, bool updateExpiration = true) + { + return Task.FromResult(default); + } + + public Task SetObjectAsync(string key, T obj, bool updateExpiration = true) + { + return Task.CompletedTask; + } + + public Task GetObjectAsync(string key, TimeSpan? ttlOverride, bool updateExpiration) + { + return Task.FromResult(default); + } + + public Task SetObjectAsync(string key, T obj, TimeSpan? ttlOverride, bool updateExpiration) + { + return Task.CompletedTask; + } + + public Task DeleteByPatternAsync(string pattern) + { + return Task.FromResult(0L); + } + + public Task DeleteObjectAsync(string key) + { + return Task.CompletedTask; + } + + public void CleanCache() + { + // Nada que limpiar + } + + // ============================================================ + // GET OR SET - STRING KEY + // ============================================================ + + public async Task GetOrSetObjectAsync( + string key, + Func> factory, + TimeSpan? ttl = null, + CancellationToken cancellationToken = default) + { + // En modo NONE no hay caché → siempre ejecutar factory + return await factory(); + } + + public async Task GetOrSetValueAsync( + string key, + Func> loader, + TimeSpan? ttl = null) + { + var result = await loader(); + return (string?)result; + } + + // ============================================================ + // GET OR SET - GroupedField + patientId + // ============================================================ + + public async Task GetOrSetObjectAsync( + GroupedField groupedField, + ObjectId patientId, + Func> factory, + TimeSpan? ttl = null, + CancellationToken cancellationToken = default) + { + // Igual que arriba: en modo NONE no hay caché + return await factory(); + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/Caching/RedisLockProvider.cs b/adas-core.Application/Services/Caching/RedisLockProvider.cs new file mode 100644 index 00000000..e4d03ee2 --- /dev/null +++ b/adas-core.Application/Services/Caching/RedisLockProvider.cs @@ -0,0 +1,67 @@ +using StackExchange.Redis; +using System.Collections.Concurrent; +using adas_core.Application.Services.Interfaces; + +namespace adas_core.Application.Services.Caching +{ + /// + /// Lock distribuido en Redis. Usa token por adquisición (owner) + /// y liberación segura con script Lua: borra la key solo si el valor coincide. + /// + public class RedisLockProvider(Func getDatabase) : ILockProvider + { + private readonly string _prefix = "lock:"; + private readonly TimeSpan _ttl = TimeSpan.FromSeconds(5); + private readonly TimeSpan _retryDelay = TimeSpan.FromMilliseconds(50); + + private static readonly string LuaReleaseScript = @" + if redis.call('get', KEYS[1]) == ARGV[1] then + return redis.call('del', KEYS[1]) + else + return 0 + end"; + + private readonly ConcurrentDictionary _tokens = + new(StringComparer.Ordinal); + + public async Task AcquireAsync(string key, TimeSpan timeout) + { + var redis = getDatabase(); + if (redis is null) return false; + + var redisKey = (RedisKey)(_prefix + key); + var token = Guid.NewGuid().ToString("N"); + var end = DateTime.UtcNow.Add(timeout); + + while (DateTime.UtcNow < end) + { + if (await redis.StringSetAsync(redisKey, token, _ttl, When.NotExists)) + { + _tokens[key] = token; + return true; + } + + await Task.Delay(_retryDelay); + } + + return false; + } + + public async Task ReleaseAsync(string key) + { + if (!_tokens.TryRemove(key, out var token)) + return; + + var redis = getDatabase(); + if (redis is null) return; + + var redisKey = (RedisKey)(_prefix + key); + + await redis.ScriptEvaluateAsync( + LuaReleaseScript, + [redisKey], + [token] + ); + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/Caching/RedisService.cs b/adas-core.Application/Services/Caching/RedisService.cs new file mode 100644 index 00000000..abf9ca28 --- /dev/null +++ b/adas-core.Application/Services/Caching/RedisService.cs @@ -0,0 +1,275 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Utils; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.GroupedObservations; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using MongoDB.Bson; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +using StackExchange.Redis; + +namespace adas_core.Application.Services.Caching +{ + public class RedisService : ICacheService + { + private readonly ILogger _logger; + private readonly CacheSettings _cacheSettings; + private readonly LockManagerService _lockManager; + private ConnectionMultiplexer? _connection; + private IDatabase? _database; + private IServer? _server; + + public IDatabase? Database => _database; + private bool _isRedisAvailable; + + public RedisService( + IOptions options, + ILogger logger, + LockManagerService lockManager) + { + _cacheSettings = options.Value; + _logger = logger; + _lockManager = lockManager; + + if (!string.IsNullOrEmpty(_cacheSettings.Redis.ConnectionString)) + _ = InitializeRedisConnectionAsync(); + } + + + // GET OR SET (string key) + public async Task GetOrSetObjectAsync( + string key, + Func> factory, + TimeSpan? ttl = null, + CancellationToken cancellationToken = default) + { + if (!_isRedisAvailable) + return await factory(); + + var direct = await GetObjectAsync(key); + + if (direct is not null) + return direct; + + return await _lockManager.WithLockAsync( + $"getorset:{key}", + TimeSpan.FromSeconds(5), + async () => + { + var again = await GetObjectAsync(key); + if (again is not null) + return again; + + var created = await factory(); + + if (created != null) + await SetObjectAsync(key, created, ttl, true); + + return created!; + }, + cancellationToken); + } + + public async Task GetOrSetValueAsync( + string key, + Func> loader, + TimeSpan? ttl = null) + { + if (!_isRedisAvailable) + return await loader(); + + var direct = GetValue(key); + if (direct is not null) + return direct; + + return await _lockManager.WithLockAsync( + $"getorset:{key}", + TimeSpan.FromSeconds(5), + async () => + { + var again = GetValue(key); + if (again is not null) + return again; + + var created = await loader(); + SetValue(key, created); + + return created; + }); + } + + + // GET OR SET (GroupedField + patientId) + private static string BuildGroupedKey(GroupedField gf, ObjectId patientId) + => $"GroupedObs:{patientId}:{gf.Name}"; + + public async Task GetOrSetObjectAsync( + GroupedField groupedField, + ObjectId patientId, + Func> factory, + TimeSpan? ttl = null, + CancellationToken cancellationToken = default) + { + var key = BuildGroupedKey(groupedField, patientId); + + if (!_isRedisAvailable) + return await factory(); + + var direct = await GetObjectAsync(key); + if (direct is not null) + return direct; + + return await _lockManager.WithLockAsync( + $"getorset:{key}", + TimeSpan.FromSeconds(5), + async () => + { + var again = await GetObjectAsync(key); + if (again is not null) + return again; + + var created = await factory(); + + if (created != null) + await SetObjectAsync(key, created, ttl, true); + + return created!; + }, + cancellationToken); + } + + + // BASIC OPERATIONS + public void SetValue(string key, string value) + => _database?.StringSet(key, value, GetEntityTtl(key), true); + + public string? GetValue(string key) + { + var val = _database?.StringGet(key); + if (val.HasValue && ShouldRenewTtl(key)) + _database?.KeyExpire(key, GetEntityTtl(key)); + return val; + } + + public async Task GetObjectAsync(string key, bool updateExpiration = true) + { + if (!_isRedisAvailable) + return default; + + var json = await _database!.StringGetAsync(key); + if (json.IsNullOrEmpty) + return default; + + if (updateExpiration) + _database!.KeyExpire(key, GetEntityTtl(key)); + + var settings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + Converters = new List { new StringEnumConverter(), new ObjectIdConverter() } + }; + try + { + return JsonConvert.DeserializeObject(json!, settings); + } + catch (Exception e) + { + _logger.LogError("Error deserializing object in RedisService {e}",e.ToString()); + throw new Exception($"Error deserializing object in RedisService {e}", e); + } + } + + public async Task SetObjectAsync( + string key, + T obj, + bool updateExpiration = true) + => await SetObjectAsync(key, obj, null, updateExpiration); + + public async Task SetObjectAsync( + string key, + T obj, + TimeSpan? ttlOverride, + bool updateExpiration) + { + if (!_isRedisAvailable) + return; + var settings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + Converters = new List { new StringEnumConverter(), new ObjectIdConverter() } + }; + var json = JsonConvert.SerializeObject(obj, settings); + await _database!.StringSetAsync(key, json, ttlOverride ?? GetEntityTtl(key), true); + } + + public async Task DeleteObjectAsync(string key) + { + if (_isRedisAvailable) + await _database!.KeyDeleteAsync(key); + } + + public async Task DeleteByPatternAsync(string pattern) + { + if (!_isRedisAvailable || _server == null) + return 0; + + var keys = _server.Keys(pattern: pattern).ToArray(); + + foreach (var key in keys) + await _database!.KeyDeleteAsync(key); + + return keys.Length; + } + + public void CleanCache() + => _server?.FlushDatabase(); + + + // TTL + private TimeSpan? GetEntityTtl(string key) + { + var entity = CacheKeyClassifier.Classify(key); + var ttl = _cacheSettings.Redis.Ttl; + + int? seconds = entity switch + { + CacheEnum.EntityType.Patients => ttl.PatientsSeconds, + CacheEnum.EntityType.Displays => ttl.DisplaysSeconds, + _ => ttl.GlobalSeconds + }; + + return seconds > 0 ? TimeSpan.FromSeconds(seconds.Value) : null; + } + + private bool ShouldRenewTtl(string key) + => GetEntityTtl(key) != null; + + + // INITIALIZATION + private async Task InitializeRedisConnectionAsync() + { + _isRedisAvailable = false; + try + { + if (_cacheSettings.Redis.ConnectionString == null) + return; + + _connection = await ConnectionMultiplexer.ConnectAsync(_cacheSettings.Redis.ConnectionString); + _database = _connection.GetDatabase(); + _server = _connection.GetServer(_cacheSettings.Redis.ConnectionString); + + _isRedisAvailable = true; + } + catch(Exception ex) + { + _logger.LogError( "Error Initializing Redis Connection. Exception:{ex}", ex.Message); + } + } + + public Task GetObjectAsync(string key, TimeSpan? ttlOverride, bool updateExpiration) + => GetObjectAsync(key, updateExpiration); + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/CalculatedObservationsService.cs b/adas-core.Application/Services/CalculatedObservationsService.cs new file mode 100644 index 00000000..b4497e96 --- /dev/null +++ b/adas-core.Application/Services/CalculatedObservationsService.cs @@ -0,0 +1,143 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Pumps; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; + +namespace adas_core.Application.Services; + +public class CalculatedObservationsService : ICalculatedObservationsService +{ + private static ICalculatedObservations? _service; + private readonly ILogger? _logger; + + + public CalculatedObservationsService( + IOptions apiSettings, + IServiceProvider serviceProvider, + ILogger logger) + { + _logger = logger; + + var customize = apiSettings.Value.Customize; + if (customize == null) + { + _logger.LogWarning("Not customization specified"); + _service = new DefaultCalculatedObservations(); + return; + } + + + var calculatedObservations = "adas_core.Application.Customizations." + customize + ".CalculatedObservations"; + + if (string.IsNullOrEmpty(customize)) + { + _service = new DefaultCalculatedObservations(); + } + else + { + var type = Type.GetType(calculatedObservations); + if (type == null) + { + _logger.LogWarning( + $"Type {calculatedObservations} not found for specification. Using DefaultCalculatedObservations"); + _service = new DefaultCalculatedObservations(); + return; + } + + var ctor = Type.GetType(calculatedObservations)?.GetConstructor([typeof(IServiceProvider)]); + if (ctor == null) + { + _logger.LogWarning( + $"Constructor not found for type {calculatedObservations} accepts one parameter with type IServiceProvider. Using DefaultCalculatedObservations"); + _service = new DefaultCalculatedObservations(); + return; + } + + _service = (ICalculatedObservations)ctor.Invoke([serviceProvider]); + } + } + + + public virtual async Task Map(PatientObservation obs, bool onlyByName = false) + { + if (_service == null) return obs; + var result = await _service.Map(obs, onlyByName); + if (result == null) + _logger?.LogDebug("Mapping calculated via service {serviceFullName}. {obs} Ignored", + _service.GetType().FullName, obs); + return result; + } + + public virtual async Task Map(PatientObservationAlarm obs, bool onlyByName = false) + { + if (_service == null) return obs; + var result = await _service.Map(obs, onlyByName); + if (result == null) + _logger?.LogDebug("Mapping calculated via service {serviceFullName}. {obs} Ignored", + _service.GetType().FullName, obs); + return result; + } + + public virtual async Task Map(PumpObservation obs) + { + if (_service == null) return null; + var result = await _service.Map(obs); + return result; + } + + public async Task Map(PatientRecordingAlert obs) + { + if (_service == null) return obs; + var result = await _service.Map(obs); + if (result == null) + _logger?.LogDebug("Mapping calculated via service {serviceFullName}. {obs} Ignored", + _service.GetType().FullName, obs); + return result; + } + + public async Task Map(PatientTreatment treatment) + { + if (_service == null) return treatment; + var result = await _service.Map(treatment); + return result; + } + + public virtual async Task Map(PatientDiagnosis diagnosis) + { + if (_service == null) return diagnosis; + var result = await _service.Map(diagnosis); + return result; + } + + public virtual async Task CalculateMedicineObservation(List activeMedicines, ObjectId patientId) + { + if (_service != null) await _service.CalculateMedicineObservation(activeMedicines, patientId); + } + + public async Task CalculateBolusOpiates(ObjectId patientId) + { + if (_service != null) await _service.CalculateActiveBolus(patientId); + } + + public virtual async Task> GetActiveTreatmentsByPatient(ObjectId id) + { + if (_service != null) return await _service.GetActiveTreatmentsByPatient(id); + return new List(); + } + + public virtual async Task> MapList(List listToInsert) + { + if (_service != null) return await _service.PreMapList(listToInsert); + return []; + } + + public virtual async Task MapSourceAlarm(PatientObservation observation, + PatientObservationAlarm observationAlarm) + { + if (_service != null) return await _service.MapSourceAlarm(observation, observationAlarm); + return observation; + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/CameraService.cs b/adas-core.Application/Services/CameraService.cs new file mode 100644 index 00000000..c3234af1 --- /dev/null +++ b/adas-core.Application/Services/CameraService.cs @@ -0,0 +1,99 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Services; + +public class CameraService(ILogger logger, ICameraRepository cameraRepository, IPointOfCareService pointOfCareService) : ICameraService +{ + private ICameraRepository _cameraRepository = cameraRepository; + public Task GetById(ObjectId relayId) + { + return _cameraRepository.GetById(relayId); + } + public List GetCameraInList(List configurationRelayList) + { + return _cameraRepository.GetCameraInList(configurationRelayList); + } + + public async Task> GetPaginatedCameras(PaginationFilter filter) + { + var usedCameraIds = await pointOfCareService.FindAllIdCamerasInUse(); + + var fluentQuery = _cameraRepository.GetPaginatedCameras(filter); + + if (filter.FilteredRequest?.InUse != null) + { + bool filterInUse = filter.FilteredRequest.InUse.Value; + var filterBuilder = Builders.Filter; + + var idFilter = filterInUse + ? filterBuilder.In(c => c.Id, usedCameraIds) + : filterBuilder.Not(filterBuilder.In(c => c.Id, usedCameraIds)); + + fluentQuery.Filter = filterBuilder.And(fluentQuery.Filter, idFilter); + } + + var count = await fluentQuery.CountDocumentsAsync(); + var data = await fluentQuery + .Skip((filter.PageNumber - 1) * filter.PageSize) + .Limit(filter.PageSize) + .ToListAsync(); + + if(data == null) return new PaginationResponse([], filter.PageNumber, filter.PageSize, count); + + foreach (var camera in data) + { + if (camera == null) continue; + + bool isInUse = usedCameraIds.Contains(camera.Id); + + // Asignación mediante reflexión para el private set + camera.GetType().GetProperty(nameof(Camera.InUse)) + ?.SetValue(camera, isInUse); + } + + return new PaginationResponse(data, filter.PageNumber, filter.PageSize, count); + } + + public async Task InsertCamera(Camera camera) + { + if (camera.Name == null) throw new Exception("Camera name cannot be null"); + var cameraFound = await _cameraRepository.GetByName(camera.Name); + if(cameraFound != null) throw new Exception($"Camera with name {camera.Name} already exists"); + return await _cameraRepository.InsertOneCamera(camera); + } + + public async Task UpdateCameraById(ObjectId objectId, Camera camera) + { + return await _cameraRepository.UpdateCameraAsync(objectId, camera); + } + + public async Task DeleteCamera(ObjectId objectId) + { + try + { + var cameraToDelete = await _cameraRepository.GetById(objectId); + if(cameraToDelete == null) return false; + + await _cameraRepository.DeleteAsync(cameraToDelete.Id); + return true; + } + catch (Exception e) + { + logger.LogError(e, e.Message); + return false; + } + + } + + public async Task> GetSearchByNameCameras(string textToSearch) + { + return await _cameraRepository.GetSearchByNameCameras(textToSearch); + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/ConfigObservationService.cs b/adas-core.Application/Services/ConfigObservationService.cs new file mode 100644 index 00000000..0ab13b68 --- /dev/null +++ b/adas-core.Application/Services/ConfigObservationService.cs @@ -0,0 +1,642 @@ +using System.Collections.Concurrent; +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.DTO; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.GroupedObservations; +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.CodeAnalysis.CSharp.Scripting; +using Microsoft.CodeAnalysis.Scripting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; + +namespace adas_core.Application.Services; + +public class ConfigObservationService : IConfigObservationService +{ + private static readonly ConcurrentDictionary CachedConfigObservations = new(); + + private static readonly ConcurrentDictionary + CachedConfigObservationKeys = new(); + + private readonly IOptions _apiSettings; + private readonly ILocalAuditService _auditService; + private readonly IConfigObservationRepository _configObservationRepository; + private readonly RetentionPolicy _defaultRetentionPolicy; + private readonly int _defaultRetentionPolicyValue; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly bool _ignoreUnknownTreatment; + private readonly ILogger _logger; + private readonly int? _refreshTimeout; + private readonly IUnitService _unitService; + private readonly ICacheService _cacheService; + private readonly CacheSettings? _cacheSettings; + + private bool IgnoreUnknownObservation => + _apiSettings.Value.ConfigObservation?.IgnoreUnknownObservation ?? false; + + public ConfigObservationService( + IConfigObservationRepository configObservationRepository, + IOptions apiSettings, + IOptions cacheSettings, + ILogger logger, + IUnitService unitService, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService, + ICacheService cacheService + ) + { + _cacheService = cacheService; + _cacheSettings = cacheSettings.Value; + _configObservationRepository = configObservationRepository; + _apiSettings = apiSettings; + _logger = logger; + _unitService = unitService; + + _refreshTimeout = _apiSettings.Value.ConfigObservation?.Refresh; + _ignoreUnknownTreatment = _apiSettings.Value.ConfigObservation?.IgnoreUnknownTreatment ?? false; + + _defaultRetentionPolicyValue = _apiSettings.Value.RetentionPolicyValue; + + _defaultRetentionPolicy = RetentionPolicy.NoDelete; + + _httpContextAccessor = httpContextAccessor; + _auditService = auditService; + } + + public async Task> GetAllConfigs(CancellationToken ct = default) + { + + var (key, ttl) = CacheKeys.ConfigObservationsAllKeyWithTtl(_cacheSettings); + + var result = await _cacheService.GetOrSetObjectAsync( + key, + async () => await _configObservationRepository.FindAll(), + ttl, + ct); + + return result; + + } + + public async Task GetAllCompact() + { + var count = await _configObservationRepository.Count(); + return new ConfigObservationDto { ItemCount = count }; + } + + public async Task> GetPaginatedItems(PaginationFilter filter) + { + var result = await _configObservationRepository.GetPaginatedItems(filter); + var count = await _configObservationRepository.Count(); + return new PaginationResponse(result.ToList(), filter.PageNumber, filter.PageSize, + count); + } + + + public async Task GetConfigById(ObjectId id) + { + return await _configObservationRepository.FindById(id) ?? null; + } + + public async Task> GetConfigNames(string id) + { + return await _configObservationRepository.GetConfigNames(id); + } + + public async Task> GetConfigNames() + { + return await _configObservationRepository.GetConfigNames(); + } + + + public async Task RetentionActions(T obs) where T : BasePatientObservation + { + var conf = await Get(obs); + return conf is { RetentionPolicy: not null } ? + new ObservatitonRetentionResult(conf.RetentionPolicy.Value, conf.RetentionPolicyValue) : + new ObservatitonRetentionResult(RetentionPolicy.NoDelete, null); + } + + + + public async Task GroupedObservationStatus(GroupedField groupedField, + GroupedObservationEnum.Result result, + string name, object value, double? min, double? max) + { + var conf = await Get(name); + if (conf == null) return StatusEnum.Type.Ok; + PatientObservation? mapObs; + if (conf.Grouped != null && groupedField.Group != null && + conf.Grouped.TryGetValue(groupedField.Group, out var grp)) + { + mapObs = await MapConf(new PatientObservation { Name = name, Value = value, Min = min, Max = max }, grp); + if (mapObs != null) return mapObs.Status; + } + + if (conf.Grouped != null && conf.Grouped.ContainsKey(result.ToString())) + { + mapObs = await MapConf(new PatientObservation { Name = name, Value = value, Min = min, Max = max }, + conf.Grouped[result.ToString()]); + if (mapObs != null) return mapObs.Status; + } + + mapObs = await MapConf(new PatientObservation { Name = name, Value = value, Min = min, Max = max }, conf); + if (mapObs != null) return mapObs.Status; + + return StatusEnum.Type.Ok; + } + + public async Task Map(T obs, bool onlyByName = false) where T : BasePatientObservation + { + var conf = onlyByName ? await Get(obs, onlyByName) : await Get(obs); + if (conf == null) + { + _logger.LogDebug("Ignore Unknown Observation. {Name} {Code} {CodingSystem}", obs.Name, obs.Code, + obs.CodingSystem); + return IgnoreUnknownObservation ? null : obs; + } + + if (string.IsNullOrEmpty(conf.Name)) + { + _logger.LogError("Config Name is null or empty. {conf}", conf); + return null; + } + + return await MapConf(obs, conf); + } + + public async Task RemoveConfigItem(ObjectId id) + { + var item = await _configObservationRepository.FindById(id); + if (item == null) return null; + + var deleted = await _configObservationRepository.Delete(id); + + // Invalidar CACHE (colección completa) + await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.ConfigObservations)); + + return deleted; + + } + + public async Task Map(PatientTreatment treatment) + { + ConfigObservation? conf = null; + + foreach (var requestGiveCode in treatment.RequestedGiveCodes) + conf = string.IsNullOrEmpty(requestGiveCode.CodingSystem) && + string.IsNullOrEmpty(requestGiveCode.Identifier) + ? await Get(requestGiveCode.Text) + : await GetByCodeSysAndCode(requestGiveCode.CodingSystem, requestGiveCode.Identifier); + if (conf == null) return _ignoreUnknownTreatment ? null : treatment; + + return conf.Name == null ? null : treatment; + } + + public async Task Get(T obs, bool onlyByName = false) + where T : BasePatientObservation + { + var items = await GetAllConfigs(); + if (items.Count == 0) return null; + + if (onlyByName) + { + var configItem = items.FirstOrDefault(i => i.Name == obs.Name); + if (configItem != null) return await Process(configItem); + _logger.LogError("Error getting Config observation Item. Observation: {obs}", obs); + return null; + + } + + ConfigObservation? item = null; + if ((!string.IsNullOrEmpty(obs.Code) && !string.IsNullOrEmpty(obs.CodingSystem)) || + obs.ParentData is { Code: not null, CodingSystem: not null }) + foreach (var obsConfig in items) + { + if (obs.Code != null && obs.Code != obsConfig.Code) + continue; + if (obsConfig.CodingSystem != null && obs.CodingSystem != obsConfig.CodingSystem) + continue; + if (obsConfig.OriginalName != null && obs.Name != obsConfig.OriginalName) + continue; + if (obsConfig.ParentCode != null && obs.ParentData?.Code != obsConfig.ParentCode) + continue; + if (obsConfig.ParentCodingSystem != null && + obs.ParentData?.CodingSystem != obsConfig.ParentCodingSystem) + continue; + if (obsConfig.ParentName != null && obs.ParentData?.Name != obsConfig.ParentName) + continue; + + if (obsConfig.OriginalName == null && obsConfig.Name != null && obs.Name != null && + obs.Name.Contains("Alarm") && obs.Name != obsConfig.Name) + continue; + + if (obsConfig.Code == null && obsConfig.CodingSystem == null && obsConfig.ParentCode == null && + obsConfig.ParentCodingSystem == null && obs.Name != null && !obs.Name.Contains("Alarm")) + continue; + if (obsConfig.Code == null && + obsConfig is { CodingSystem: not null, ParentCode: null, ParentCodingSystem: null } && + obs.Name != null && !obs.Name.Contains("Alarm")) + continue; + if (obsConfig.Code == null && obsConfig.CodingSystem == null && obsConfig.ParentCode == null && + obsConfig.ParentCodingSystem != null && obs.Name != null && !obs.Name.Contains("Alarm")) + continue; + if (obsConfig.Code == null && + obsConfig is { CodingSystem: not null, ParentCode: null, ParentCodingSystem: not null } && + obs.Name != null && !obs.Name.Contains("Alarm")) + continue; + + item = obsConfig; + break; + } + else item = items.FirstOrDefault(i => i.Name == obs.Name); + + return item != null ? await Process(item) : null; + } + + // public async Task> GetAlarmWithRecordingConfig(ObjectId patientId) + // { + // var configObservationId = await GetConfigObservationKeyFromPatientId(patientId); + // var configObservation = await GetConfig(configObservationId); + // return configObservation?.Items + // .Where(i => i is { CodingSystem: "ADAS_ALARM", Alarm.Recording.Enabled: true }) + // .ToList() ?? []; + // } + + public async Task GetByCodeSysAndCode(string? codingSystem, string? code) + { + var filteredResult = await _configObservationRepository.GetByCodeSysAndCode(codingSystem, code); + if (filteredResult != null) return await Process(filteredResult); + _logger.LogWarning("Config observation item not found. CodingSystem: {codingSystem} code: {code} ", + codingSystem ?? "null", code ?? "null"); + return null; + + } + + public async Task Get(string? name) + { + if (string.IsNullOrEmpty(name)) return null; + var result = await GetConfigByName(name); + if (result == null) + { + // _logger.LogWarning( + // "Config observation item not found. name: {name} configObservationId: {configObservationId} ", + // name, name); + return null; + } + + return await Process(result); + } + + public async Task UpdateConfig(ConfigObservation configObservationItem) + { + // if (!configObservationItem.Id.HasValue) + // return await _configObservationRepository.InsertOneAsyncAndReturn(configObservationItem); + var configObservation = await _configObservationRepository.FindById(configObservationItem.Id) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + + var updatedConfig = await _configObservationRepository.Update(configObservation); + + // Invalidar CACHE (colección completa) + await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.ConfigObservations)); + + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, configObservation, + updatedConfig!); + return updatedConfig; + } + + public async Task?> GetConfigObservationItem(string name) + { + var configObservationItems = await _configObservationRepository.FindAllByName(name); + if (configObservationItems.Count != 0) + return configObservationItems; + _logger.LogWarning("Cant get config item, config not found, name: {name} ", name); + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + } + + public async Task GetSingleConfigObservationItem(string? code, string? codingSystem, + string? name, string? originalName) + { + var matchingItem = + await _configObservationRepository.GetSingleConfigObservationItem(code, codingSystem, name, originalName); + + if (matchingItem == null) throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundNoMatches); + + return matchingItem; + } + + public async Task DeleteSingleConfigObservationItem(ConfigObservation configObservationItem) + { + var configObservation = await _configObservationRepository.FindById(configObservationItem.Id) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); + + var auxConfigObservation = await _auditService.DeepCopyAsync(configObservation); + + _ = await _configObservationRepository.DeleteAsync(configObservation.Id) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); + + // Invalidar CACHE (colección completa) + await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.ConfigObservations)); + + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, auxConfigObservation, + configObservation); + return true; + } + + public async Task> GetConfigObservationItemsByName(string name) + { + return await _configObservationRepository.FindAllByName(name); + } + + public async Task CreateConfig(ConfigObservation configObservation) + { + + var existing = await _configObservationRepository.FindById(configObservation.Id); + if (existing != null) + throw new BadRequestException(HttpEnum.ErrorMessage.ConflictCreationFailed); + + await _configObservationRepository.InsertOneAsyncAndReturn(configObservation); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, configObservation); + return configObservation; + } + + + public async Task RemoveConfigItem(string itemName) + { + var configObservation = await GetConfigByName(itemName) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); + var auxConfigObservation = await GetConfigByName(itemName) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, auxConfigObservation, + configObservation); + + var result = await _configObservationRepository.Delete(configObservation.Id!); + + // Invalidar CACHE (colección completa) + await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.ConfigObservations)); + + return result; + } + + public async Task GetConfig(ObjectId configObservationId) + { + RefreshCachedConfigObservations(); + + if (CachedConfigObservations.TryGetValue(configObservationId, out var cached) + && DateTime.Now <= cached.NextRefresh) + return cached.ConfigObservation; + + var config = await _configObservationRepository.FindById(configObservationId); + cached = new ConfigObservationCached + { + ConfigObservation = config ?? new ConfigObservation(), + NextRefresh = _refreshTimeout.HasValue ? DateTime.Now.AddSeconds(_refreshTimeout.Value) : DateTime.MinValue + }; + + CachedConfigObservations[configObservationId] = cached; + + return cached.ConfigObservation; + } + + public async Task GetConfigByName(string name) + { + if (string.IsNullOrWhiteSpace(name)) return null; + + RefreshCachedConfigObservations(); + + var cachedItem = CachedConfigObservations.Values + .FirstOrDefault(cached => + DateTime.Now <= cached.NextRefresh && + cached.ConfigObservation is { Name: not null } && + cached.ConfigObservation.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); + + if (cachedItem != null) return cachedItem.ConfigObservation; + var config = await _configObservationRepository.FindByName(name); + if (config == null) return null; + var newCachedItem = new ConfigObservationCached + { + ConfigObservation = config, + NextRefresh = _refreshTimeout.HasValue + ? DateTime.Now.AddSeconds(_refreshTimeout.Value) + : DateTime.MinValue + }; + + CachedConfigObservations[config.Id!] = newCachedItem; + + return config; + } + + private void RefreshCachedConfigObservations() + { + if (!_refreshTimeout.HasValue) return; + + // Use ConcurrentDictionary's thread-safe features to identify and remove expired keys + var keysToRemove = CachedConfigObservations + .Where(k => DateTime.Now > k.Value.NextRefresh) + .Select(k => k.Key) + .ToList(); + + foreach (var key in keysToRemove) CachedConfigObservations.TryRemove(key, out _); + } + + private Task Process(ConfigObservation confItem) + { + confItem.RetentionPolicy ??= _defaultRetentionPolicy; + if (confItem.RetentionPolicy != RetentionPolicy.NoDelete && !confItem.RetentionPolicyValue.HasValue) + { + confItem.RetentionPolicyValue = _defaultRetentionPolicyValue; + if (confItem.RetentionPolicyValue <= 0) confItem.RetentionPolicyValue = null; + } + else + { + confItem.RetentionPolicyValue = null; + } + + return Task.FromResult(confItem); + } + + private void RefreshCachedConfigObservationKeys() + { + try + { + if (!_refreshTimeout.HasValue) return; + + // Use ConcurrentDictionary's thread-safe features to identify and remove expired keys + var keysToRemove = CachedConfigObservationKeys + .Where(k => DateTime.Now > k.Value.NextRefresh) + .Select(k => k.Key) + .ToList(); + + foreach (var key in keysToRemove) CachedConfigObservationKeys.TryRemove(key, out _); + } + catch (Exception ex) + { + _logger.LogError("Error refreshing cached config observation keys. Exception: {ex}", ex); + } + } + + private async Task MapConf(T obs, ConfigObservation conf) where T : BasePatientObservation + { + _logger.LogTrace("Mapping observation: {obs}", obs); + + obs.Name = conf.Name; + if (obs is PatientObservation) + { + var noCalculateStatusWithCodingSystem = _apiSettings.Value.NoCalculateStatusWithCodingSystem; + if (noCalculateStatusWithCodingSystem != null && + obs.CodingSystem == noCalculateStatusWithCodingSystem) return obs; + if (obs is not PatientObservation pobs) + return obs; + + if (conf.MaxAlert != null && (conf.ForceAlert || !pobs.Max.HasValue)) pobs.Max = conf.MaxAlert; + + if (conf.MinAlert.HasValue && (conf.ForceAlert || !pobs.Min.HasValue)) pobs.Min = conf.MinAlert; + + if (conf.MaxWarn.HasValue && (conf.ForceWarn || !pobs.MaxWarn.HasValue)) pobs.MaxWarn = conf.MaxWarn; + + if (conf.MinWarn.HasValue && (conf.ForceWarn || !pobs.MinWarn.HasValue)) pobs.MinWarn = conf.MinWarn; + + if (conf.LevelCondition != null) + try + { + var evalCondition = await CSharpScript.EvaluateAsync(conf.LevelCondition, globals: pobs); + if (int.TryParse(evalCondition.ToString(), out var evalInt)) + pobs.Level = evalInt; + else + _logger.LogError("Error evaluating condition: {evalCondition} for obs: {pobs}", + evalCondition, pobs); + } + catch (CompilationErrorException e) + { + _logger.LogError("Error evaluating expression for obs: {obs} error: {diagnostics}", obs, + e.Diagnostics); + } + catch (Exception e) + { + _logger.LogError("Error evaluating expression for obs: {obs} error: {e}", obs, e); + } + + pobs.Status = StatusEnum.Type.Ok; + if (pobs.Value.IsNumber()) + { + if (pobs.Min.HasValue && pobs.Min > pobs.Value.ToDouble() && + conf.Alert is null or '<') + { + pobs.Status = StatusEnum.Type.Alert; + pobs.AlertColor = conf.AlertColor; + } + else if (pobs.Max.HasValue && pobs.Max < pobs.Value.ToDouble() && + conf.Alert is null or '>') + { + pobs.Status = StatusEnum.Type.Alert; + pobs.AlertColor = conf.AlertColor; + } + else if (pobs.MinWarn.HasValue && pobs.MinWarn > pobs.Value.ToDouble() && + conf.Alert is null or '<') + { + pobs.Status = StatusEnum.Type.Warning; + pobs.WarnColor = conf.WarnColor; + } + else if (pobs.MaxWarn.HasValue && pobs.MaxWarn < pobs.Value.ToDouble() && + conf.Alert is null or '>') + { + pobs.Status = StatusEnum.Type.Warning; + pobs.WarnColor = conf.WarnColor; + } + } + else if (pobs.Value is string) + { + if (conf.AlertValues != null && conf.AlertValues.Any() && conf.AlertValues.Contains(pobs.Value)) + pobs.Status = StatusEnum.Type.Alert; + else if (conf.WarningValues != null && conf.WarningValues.Any() && + conf.WarningValues.Contains(pobs.Value)) pobs.Status = StatusEnum.Type.Warning; + } + + if (conf.Expires is > 0) + { + pobs.Expires = conf.Expires; + //check if already is expired + var expireTime = obs.Time.ToLocalTime().AddSeconds(Convert.ToDouble(conf.Expires)); + pobs.Expired = DateTime.Now.CompareTo(expireTime) > 0; + } + + pobs.ShowOnExpired = conf.ShowOnExpired; + + if (conf.ColorOnExpired != null) pobs.ColorOnExpired = conf.ColorOnExpired; + + if (conf.Persist != null) + pobs.Persist = conf.Persist; + + if (conf.UiConfiguration != null && conf.UiConfiguration.Any()) pobs.UiConfiguration = conf.UiConfiguration; + + if (conf.Alarm != null) pobs.Alarm = conf.Alarm; + + if (conf.TimeFromMessageTime) + if (pobs.MessageTime.CompareTo(DateTime.MinValue) != 0) + pobs.Time = pobs.MessageTime; + + if (pobs.Units == null || conf.ForceUnits) pobs.Units = conf.Units; + + if (conf.CreateObservation != null) pobs.CreateObservation = conf.CreateObservation; + + pobs.CheckObservations = conf.CheckObservations; + } + + if (obs is PatientObservationAlarm) + { + if (obs is not PatientObservationAlarm pobs) + return obs; + + if (conf.Expires is > 0) + { + pobs.Expires = conf.Expires; + //check if already is expired + var expireTime = obs.Time.ToLocalTime().AddSeconds(Convert.ToDouble(conf.Expires)); + pobs.Expired = DateTime.Now.CompareTo(expireTime) > 0; + } + + if (conf.Persist != null) + pobs.Persist = conf.Persist; + + if (conf.Alarm != null) pobs.AlarmConfig = conf.Alarm; + + if (conf.AlertColor != null) pobs.AlertColor = conf.AlertColor; + + if (conf.TimeFromMessageTime) + if (pobs.MessageTime.CompareTo(DateTime.MinValue) != 0) + pobs.Time = pobs.MessageTime; + + if (pobs.Units == null || conf.ForceUnits) pobs.Units = conf.Units; + + if (conf.CreateObservation != null) pobs.CreateObservation = conf.CreateObservation; + + pobs.CheckObservations = conf.CheckObservations; + } + + _logger.LogTrace("Observation mapped: {obs}", obs); + + return obs; + } + + private class ConfigObservationCached + { + public DateTime NextRefresh { get; set; } + public ConfigObservation? ConfigObservation { get; set; } + } + + private class ConfigObservationKeyCached + { + public DateTime NextRefresh { get; set; } + public string? Key { get; set; } + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/ConfigPumpsService.cs b/adas-core.Application/Services/ConfigPumpsService.cs new file mode 100644 index 00000000..acbac573 --- /dev/null +++ b/adas-core.Application/Services/ConfigPumpsService.cs @@ -0,0 +1,163 @@ +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.MongoModels; +using adas_core.Domain.Models.Pumps; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace adas_core.Application.Services; + +public class ConfigPumpsService( + IConfigPumpsRepository configPumpsRepository, + IOptions apiSettings, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService) + : IConfigPumpsService +{ + private static ConfigPumps? _config; + private static DateTime _nextRefresh = DateTime.MinValue; + + private readonly bool _configPumpsRequired = apiSettings.Value.ConfigPumpsRequired; + private readonly string _key = apiSettings.Value.ConfigPumpsKey ?? "PV1"; + private readonly int? _refreshTimeout = apiSettings.Value.ConfigObservation?.Refresh; + + public async Task Map(PumpObservation obs) + { + if (!_configPumpsRequired) return obs; + + var conf = !string.IsNullOrEmpty(obs.AlarmType.ToString()) + ? await Get(obs.AlarmType.ToString() ?? string.Empty) + : null; + + if (conf == null) return obs; + + return await MapConf(obs, conf); + } + + public async Task?> GetAllPumpConfigs() + { + return await configPumpsRepository.GetAllConfigs(); + } + + public async Task GetPumpConfigById(string id) + { + return await configPumpsRepository.FindById(id); + } + + public async Task?> GetConfigItems(string id) + { + var result = await configPumpsRepository.FindById(id); + return result?.Items; + } + + public async Task UpdatePumpConfig(ConfigPumps pumpConfig) + { + var oldPumpConfig = configPumpsRepository.FindById(pumpConfig.Id); + var newPumpConfig = await configPumpsRepository.UpdateConfig(pumpConfig) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldPumpConfig, newPumpConfig); + return newPumpConfig; + } + + public async Task InsertPumpConfig(ConfigPumps pumpConfig) + { + try + { + await configPumpsRepository.InsertOneAsync(pumpConfig); + var newPumpConfig = await configPumpsRepository.FindById(pumpConfig.Id) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, newPumpConfig); + return newPumpConfig; + } + catch (Exception ex) + { + logger.LogError("ERROR inserting config_pumps: {key}. Exception: {exMessage} ", pumpConfig.Id, ex.Message); + return null; + } + } + + public async Task DeletePumpConfig(ConfigPumps config) + { + try + { + var result = await configPumpsRepository.DeleteConfig(config); + + if (result) + { + logger.LogError("ERROR deleting config_pumps: {key}. ", config.Id); + return false; + } + + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, config, null); + return true; + } + catch (Exception ex) + { + logger.LogError("ERROR deleting config_pumps: {key}. Exception: {exMessage} ", config.Id, ex.Message); + return false; + } + } + + public async Task RetentionActions(PumpObservation obs) + { + //TODO sacarlo de la configuración específica de Bombas + var conf = await Get(obs); + return conf is { RetentionPolicy: not null } ? + new ObservatitonRetentionResult(conf.RetentionPolicy.Value, conf.RetentionPolicyValue) : + new ObservatitonRetentionResult(RetentionPolicy.NoDelete, null); + } + + private static Task MapConf(PumpObservation obs, ConfigPumpItem conf) + { + if (conf.UiConfiguration != null && conf.UiConfiguration.Count != 0) + obs.UiConfiguration = conf.UiConfiguration; + + return Task.FromResult(obs); + } + + private async Task Get(string alarmType) + { + if (!Enum.TryParse(alarmType, out PumpEnum.AlarmType alarmTypeParsed)) + return null; + var result = await GetConfig(); + return result?.Items?.FirstOrDefault(i => i.AlarmType == alarmTypeParsed); + } + + private async Task Get(PumpObservation pobs) + { + var config = await GetConfig(); + return config?.Items?.FirstOrDefault(i => i.Type == pobs.MessageType); + } + public async Task?> Get() + { + var result = await GetConfig(); + return result?.Items; + } + + + private async Task GetConfig() + { + try + { + if (_config != null && DateTime.Now <= _nextRefresh) + return _config; + _config = await configPumpsRepository.FindById(_key); + _nextRefresh = _refreshTimeout.HasValue + ? DateTime.Now.AddSeconds(_refreshTimeout.Value) + : DateTime.MinValue; + + return _config; + } + catch (Exception ex) + { + logger.LogError("ERROR READ config_pumps: {key}. Exception: {exMessage} ", _key, ex.Message); + return null; + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/ConfigUnitsService.cs b/adas-core.Application/Services/ConfigUnitsService.cs new file mode 100644 index 00000000..51c3b20a --- /dev/null +++ b/adas-core.Application/Services/ConfigUnitsService.cs @@ -0,0 +1,119 @@ +using System.Reflection; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Pumps; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace adas_core.Application.Services; + +public class ConfigUnitsService( + IConfigUnitsRepository configUnitsRepository, + IOptions apiSettings, + ILogger logger) + : IConfigUnitsService +{ + private static ConfigUnits? _config; + private static DateTime _nextRefresh = DateTime.MinValue; + + private readonly bool _configUnitsRequired = apiSettings.Value.ConfigUnitsRequired; + + private readonly string _key = apiSettings.Value.ConfigUnitsKey ?? "PV1"; + private readonly int? _refreshTimeout = apiSettings.Value.ConfigObservation?.Refresh; + + public async Task Map(T obs) where T : BasePatientObservation + { + if (!_configUnitsRequired) return obs; + + var conf = !string.IsNullOrEmpty(obs.Units) ? await Get(obs.Units) : null; + + return conf == null ? obs : MapConf(obs, conf); + } + + public async Task Map(PumpObservation obs) + { + if (!_configUnitsRequired) return obs; + + //Para cada propiedad de la observación que sea del tipo PumpValue llama a GetByCodeSysAndCode(PumpValue) + await MapPumpValues(obs); + + // var conf = !string.IsNullOrEmpty(obs.Units) ? await Get(obs.Units) : null; + // return conf == null ? obs : MapConf(obs, conf); + + return obs; + } + + /// + /// Recorre recursivamente las propiedades del objeto para encontrar y convertir PumpValues. + /// + private async Task MapPumpValues(object? targetObject) + { + if (targetObject == null) return; + + var properties = targetObject.GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance); + + foreach (var property in properties) + { + var propertyValue = property.GetValue(targetObject); + + if (propertyValue == null) continue; + + + if (property.PropertyType == typeof(CommonPumpTypes.PumpValue)) + { + var pumpValue = (CommonPumpTypes.PumpValue)propertyValue; + + if (string.IsNullOrEmpty(pumpValue.Units)) continue; + + var conf = await Get(pumpValue.Units); + if (conf != null) pumpValue.Units = conf.Value; + } + // En este caso Syringe es una clase que tienen un pumpValue + else if (property.PropertyType.IsClass && property.PropertyType != typeof(string)) + { + // Llamada recursiva para inspeccionar las propiedades anidadas + await MapPumpValues(propertyValue); + } + } + } + + + private T MapConf(T obs, ConfigUnitItem conf) where T : BasePatientObservation + { + if (obs is not PatientObservation pobs) return obs; + + logger.LogDebug("Mapping config Unit obs: {obs} to units: {conf}", obs, conf.Value); + pobs.Units = conf.Value; + + return obs; + } + + public async Task Get(string code) + { + var result = await GetConfig(); + + return result?.Items?.FirstOrDefault(i => i.Code == code); + } + + private async Task GetConfig() + { + try + { + if (_config != null && DateTime.Now <= _nextRefresh) return _config; + + _config = await configUnitsRepository.FindById(_key); + _nextRefresh = _refreshTimeout.HasValue + ? DateTime.Now.AddSeconds(_refreshTimeout.Value) + : DateTime.MinValue; + return _config; + } + catch (Exception ex) + { + logger.LogError(ex, "ERROR READ config_units: {_key}: {ex}", _key, ex.Message); + return null; + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/DefaultCalculatedObservations.cs b/adas-core.Application/Services/DefaultCalculatedObservations.cs new file mode 100644 index 00000000..71c23e6a --- /dev/null +++ b/adas-core.Application/Services/DefaultCalculatedObservations.cs @@ -0,0 +1,65 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.Pumps; +using MongoDB.Bson; + +namespace adas_core.Application.Services; + +public class DefaultCalculatedObservations : ICalculatedObservations +{ + public Task CalculateMedicineObservation(List activeMedicines, ObjectId patientId) + { + return Task.CompletedTask; + } + + public Task CalculateActiveBolus(ObjectId patientId) + { + return Task.CompletedTask; + } + + public Task Map(T obs, bool onlyByName) where T : BasePatientObservation + { + return Task.FromResult(obs)!; + } + + public Task Map(PatientTreatment treatment) + { + return Task.FromResult(treatment); + } + + public Task> GetActiveTreatmentsByPatient(ObjectId id) + { + return Task.FromResult>(new List()); + } + + public Task Map(PatientDiagnosis diagnosis) + { + return Task.FromResult(diagnosis); + } + + public Task Map(PumpObservation pumpObservation) + { + throw new NotImplementedException(); + } + + public Task FixTimeInconsistencyWithLast(PatientObservation newObservation) + { + return Task.FromResult(newObservation); + } + + public Task> PreMapList(List listToInsert) + { + return Task.FromResult(listToInsert); + } + + public Task MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert) + { + return Task.FromResult(obs); + } + + public Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/DeviceService.cs b/adas-core.Application/Services/DeviceService.cs new file mode 100644 index 00000000..2d8777a2 --- /dev/null +++ b/adas-core.Application/Services/DeviceService.cs @@ -0,0 +1,246 @@ +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.DTO; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; + +namespace adas_core.Application.Services; + +public class DeviceService : IDeviceService +{ + private readonly IDeviceRepository _deviceRepository; + private readonly IObservationService _observationService; + private readonly IAlarmService _alarmService; + private readonly IConfigObservationService _configObservationService; + private readonly IPointOfCareService _pointOfCareService; + private readonly ILogger _logger; + + public DeviceService( + IDeviceRepository deviceRepository, + IPointOfCareService pointOfCareService, + ILogger logger, + IObservationService observationService, + IConfigObservationService configObservationService, + IAlarmService alarmService) + { + _deviceRepository = deviceRepository; + _pointOfCareService = pointOfCareService; + _logger = logger; + _observationService = observationService; + _configObservationService = configObservationService; + _alarmService = alarmService; + } + + public Device ToEntity(DeviceDto dto) + { + return new Device() + { + DeviceType = dto.DeviceType, + MacAddr = dto.MacAddr, + SerialNumber = dto.SerialNumber, + Name = dto.Name, + Battery = dto.Battery, + Color = dto.Color, + Connected = dto.Connected, + Ready = dto.Ready, + Uuid = dto.Uuid, + Key = dto.Key, + CreatedAt = dto.CreatedAt, + UpdatedAt = dto.UpdatedAt, + PointOfCareIds = dto.PointOfCareIds ?? new List(), + Settings = dto.Settings ?? new DeviceSettings() + }; + } + public async Task Create(DeviceDto deviceDto) + { + var device = ToEntity(deviceDto); + device.CreatedAt = DateTime.UtcNow; + device.UpdatedAt = DateTime.UtcNow; + await _deviceRepository.InsertOneAsync(device); + return device; + } + + public async Task Delete(ObjectId objectId) + { + return await _deviceRepository.DeleteAsync(objectId) != null; + } + + public async Task Update(DeviceDto deviceDto) + { + var device = ToEntity(deviceDto); + device.UpdatedAt = DateTime.UtcNow; + await _deviceRepository.UpdateOneAsync(device.Id, device); + return device; + } + + public async Task ReceiveEvent(DeviceDto deviceDto) + { + Device? deviceExist = null; + if (!string.IsNullOrEmpty(deviceDto.MacAddr)) + { + deviceExist = await _deviceRepository.FindByMacAddr(deviceDto.MacAddr); + } + + if (deviceExist == null && deviceDto.SerialNumber != null) + { + deviceExist = await _deviceRepository.FindBySerialNumber(deviceDto.SerialNumber); + } + + if (deviceExist == null && deviceDto.Uuid != null) + { + deviceExist = await _deviceRepository.FindByUuid(deviceDto.Uuid); + } + + if (deviceExist == null && deviceDto.Key != null) + { + deviceExist = await _deviceRepository.FindByKey(deviceDto.Key); + } + + if (deviceExist == null) + { + deviceExist = ToEntity(deviceDto); + deviceExist.CreatedAt = DateTime.UtcNow; + deviceExist.UpdatedAt = DateTime.UtcNow; + await _deviceRepository.InsertOneAsync(deviceExist); + } + else + { + await _deviceRepository.UpdateDeviceStats(deviceExist.Id, deviceDto); + } + + switch (deviceDto.DeviceType) + { + case DeviceType.Unknown: + break; + case DeviceType.Button: + await ManageDeviceButton(deviceExist, deviceDto); + break; + } + + return deviceExist; + } + + private async Task ManageDeviceButton(Device deviceExist, DeviceDto deviceDto) + { + if (deviceExist.Settings?.Action?.Type != null && deviceDto.Event?.ClickType != null) + { + switch (deviceExist.Settings.Action.Type) + { + case DeviceActionType.SendObs: + await SendObservationOnAction(deviceExist.Settings.Action, deviceDto.Event.ClickType.Value, deviceExist.PointOfCareIds); + break; + case DeviceActionType.SendAlarm: + await SendAlarmOnAction(deviceExist.Settings.Action, deviceDto.Event.ClickType.Value, deviceExist.PointOfCareIds); + break; + } + } + } + + private async Task SendAlarmOnAction( + DeviceAction settingsAction, + ClickType eventClickType, + List deviceExistPointOfCareIds) + { + var configObs = await _configObservationService.GetConfigById(settingsAction.ConfigObservationId); + if(configObs == null) return; + var obsData = new ObservationData + { + Code = configObs.Code, + CodingSystem = configObs.CodingSystem, + Time = DateTime.UtcNow, + Text = configObs.Name, + }; + var obs = new PatientObservationAlarm + { + InactivationState = new InactivationState{ Visual = AlarmEnum.AudioVideoState.Enabled, Audio = AlarmEnum.AudioVideoState.Enabled}, + MessageTime = DateTime.UtcNow, + Persist = true, + Code = obsData.Code, + CodingSystem = obsData.CodingSystem, + Name = configObs.Name, + Time = DateTime.UtcNow + }; + foreach (var pocId in deviceExistPointOfCareIds) + { + var data = await _pointOfCareService.GetInfo(pocId, null, true); + if (data != null && data.Patient?.Id != null) + { + obs.PatientId = data.Patient.Id; + obs.Patient = data.Patient; + switch (eventClickType) + { + case ClickType.SingleClick: + if(settingsAction.ValueOnSingleClick == null) return; + obs.Value = settingsAction.ValueOnSingleClick; + break; + case ClickType.DoubleClick: + if(settingsAction.ValueOnDoubleClick == null) return; + obs.Value = settingsAction.ValueOnDoubleClick; + break; + case ClickType.Hold: + if(settingsAction.ValueOnHoldClick == null) return; + obs.Value = settingsAction.ValueOnHoldClick; + break; + } + // Process Obs on service + await _alarmService.ProcessAlarmObservations(new List{obs},new List(){new (){Name = configObs.Name, Value = obs.Value, Code = obsData.Code, CodingSystem = obsData.CodingSystem}}, data.Patient, DateTime.UtcNow, obsData); + } + + } + } + + private async Task SendObservationOnAction( + DeviceAction settingsAction, + ClickType eventClickType, + List deviceExistPointOfCareIds) + { + var configObs = await _configObservationService.GetConfigById(settingsAction.ConfigObservationId); + if(configObs == null) return; + var obsData = new ObservationData + { + Code = configObs.Code, + CodingSystem = configObs.CodingSystem, + Time = DateTime.UtcNow, + Text = configObs.Name, + }; + var obs = new PatientObservation + { + MessageTime = DateTime.UtcNow, + Persist = true, + Code = obsData.Code, + CodingSystem = obsData.CodingSystem, + Name = configObs.Name, + Time = DateTime.UtcNow + }; + foreach (var pocId in deviceExistPointOfCareIds) + { + var data = await _pointOfCareService.GetInfo(pocId, null, true); + if (data != null && data.Patient?.Id != null) + { + obs.PatientId = data.Patient.Id; + obs.Patient = data.Patient; + switch (eventClickType) + { + case ClickType.SingleClick: + if(settingsAction.ValueOnSingleClick == null) return; + obs.Value = settingsAction.ValueOnSingleClick; + break; + case ClickType.DoubleClick: + if(settingsAction.ValueOnDoubleClick == null) return; + obs.Value = settingsAction.ValueOnDoubleClick; + break; + case ClickType.Hold: + if(settingsAction.ValueOnHoldClick == null) return; + obs.Value = settingsAction.ValueOnHoldClick; + break; + } + // Process Obs on service + _observationService.ProcessObservations(new List{obs}, data.Patient, DateTime.UtcNow, obsData); + } + + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/DiagnosisService.cs b/adas-core.Application/Services/DiagnosisService.cs new file mode 100644 index 00000000..94e1981c --- /dev/null +++ b/adas-core.Application/Services/DiagnosisService.cs @@ -0,0 +1,363 @@ +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.MongoModels; +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 DiagnosisService : IDiagnosisService +{ + private readonly ILocalAuditService _auditService; + private readonly Lazy _calculatedObservations; + private readonly IClientMessageService _clientMessageService; + private readonly IDiagnosisArchiveRepository _diagnosisArchiveRepository; + + private readonly List _diagnosisCode = []; + private readonly IDiagnosisRepository _diagnosisRepository; + + private readonly string _diagnosisSystem; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ILogger _logger; + private readonly Lazy _patientService; + private readonly ISubscribersService _subscribersService; + private readonly IUnitService _unitService; + + + public DiagnosisService( + Lazy patientService, + IOptions apiSettings, + IDiagnosisRepository diagnosisRepository, + IDiagnosisArchiveRepository diagnosisArchiveRepository, + ILogger logger, + IClientMessageService clientMessageService, + ISubscribersService subscribersService, + Lazy calculatedObservations, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService, + IUnitService unitService) + { + _patientService = patientService; + _diagnosisRepository = diagnosisRepository; + _diagnosisArchiveRepository = diagnosisArchiveRepository; + _logger = logger; + _clientMessageService = clientMessageService; + _subscribersService = subscribersService; + _calculatedObservations = calculatedObservations; + _unitService = unitService; + _httpContextAccessor = httpContextAccessor; + _auditService = auditService; + + + _diagnosisSystem = apiSettings.Value.DiagnosisSystem ?? "CUSTOM"; + + if (apiSettings.Value.DiagnosisCode.Any()) + _diagnosisCode = apiSettings.Value.DiagnosisCode; + } + + // private async Task SendBroadcast(PatientDiagnosis diagnosis) + // { + // var patient = await _patientService.Value.FindById(diagnosis.PatientId); + // if (patient == null) return; + // + // + // var subscribers = _subscribersService.GetSubscribers().Where(s => + // (s.SubscriptionType == SubscriptionType.Box && s.Box == patient.Bed && + // s.Section == patient.UnitString) || + // (s.SubscriptionType == SubscriptionType.Section && s.Section == patient.UnitString)).ToList(); + // subscribers.ForEach(Action); + // return; + // + // async void Action(WsSubscriber subscriber) => + // await _clientMessageService.SendAsync(subscriber.Id, OperationType.Diagnosis, diagnosis); + // } + + public async Task Archive(Patient patient) + { + await ArchiveByPatientId(patient.Id); + } + + public async Task ArchiveByPatientId(ObjectId id) + { + _logger.LogDebug("Archive Diagnoses by Patient Id {id}", id); + using (var cursor = await FindByPatientIdAsync(id)) + { + while (await cursor.MoveNextAsync()) + foreach (var current in cursor.Current) + await _diagnosisArchiveRepository.InsertOneAsync(current); + } + + await DeleteByPatientId(id); + } + + public async Task DeleteByPatientId(ObjectId id) + { + _logger.LogDebug("Delete Diagnoses by Patient Id {id}", id); + var oldPatient = await _diagnosisRepository.GetByPatient(id); + await _diagnosisRepository.DeleteByPatientId(id); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, null); + } + + public async Task> GetByPatientId(ObjectId patientId) + { + var diagnosis = await _diagnosisRepository.GetByPatient(patientId); + + return diagnosis; + } + + + public Task SaveRequest(ApiRequest apiRequest) + { + return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest, null); })); + } + + public Task SaveRequestAsync(ApiRequest apiRequest) + { + return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest, null); })); + } + + public async Task ProcessDiagnosisObservation(ApiRequest apiRequest, Patient patient) + { + _logger.LogDebug("INSERT DiagnosisObservation from obsservation"); + + var time = apiRequest.ObservationData?.Time; + if (time == null) + _logger.LogWarning("Processing diagnosis without time. Set Datetime now {DateTimeNow}. ", DateTime.Now); + + var obs = new PatientDiagnosis + { + CodingSystem = _diagnosisSystem, + Time = time ?? DateTime.Now, + PatientId = patient.Id, + MessageTime = apiRequest.MessageTime + }; + + if (apiRequest.Observations == null) + { + _logger.LogError("ApiRequest Observations null. "); + return; + } + + for (var i = 0; i <= apiRequest.Observations.Count - 1; i++) + { + var value = apiRequest.Observations[i].Value; + + var strValue = value.ToString() ?? "null"; + + switch (apiRequest.Observations[i].Code) + { + case "272099008": + obs.Description = strValue; + break; + + case "1000000013": + obs.Label = strValue; + break; + + case "1000000014": + obs.Code = strValue; + break; + + case "394731006": + obs.State = strValue; + break; + + case "272125009": + obs.Category = strValue; + break; + + case "398201009": + if (DateTime.TryParse(apiRequest.Observations[i].Value.ToString(), out var startTime)) + obs.StartTime = startTime; + break; + + case "397898000": + if (DateTime.TryParse(apiRequest.Observations[i].Value.ToString(), out var endTime)) + obs.EndTime = endTime; + break; + } + } + + await InsertDiagnosis(obs); + } + + public async Task SaveRequest(ApiRequest apiRequest, Patient? patient) + { + if (patient == null) + { + _logger.LogDebug("message:ApiRequest Diagnosis"); + if (string.IsNullOrEmpty(apiRequest.PatientNumber) && + string.IsNullOrEmpty(apiRequest.Location?.UnitName)) + { + _logger.LogDebug("person and PointOfCare are nulls"); + throw new ApiRequestException("person and PointOfCare are nulls"); + } + + _logger.LogDebug("patientNumber: {apiRequestpatientNumber} location: {apiRequestlocation}", + apiRequest.PatientNumber, apiRequest.Location); + _logger.LogDebug("RequestType: {apiRequesttype}", apiRequest.Type); + + patient = await _patientService.Value.FindPatientByApiRequest(apiRequest); + } + + if (patient == null) + { + // NO PATIENTS OR LOCATIONS WERE FOUND + _logger.LogWarning( + "Observation for patient: {apiRequestpatientNumber} with location: {apiRequestlocation} not found, ignoring", + apiRequest.PatientNumber, apiRequest.Location); + return; + } + + var unitConfig = await _unitService.FindById(patient.UnitId); + if (!Hl7Utils.ManageAutoAdt(unitConfig, null, _logger, "ORU Diagnosis")) return; + + switch (apiRequest.Type) + { + //* ORU_R01 - Unsolicited transmission of an observation message + //* ORU_R40 - Unsolicited transmission of an alert observation message + + case "ORU_R01": + case "ORU_R40": + + // OBSERVATIONS + if (apiRequest.Observation != null && apiRequest.Observations?.FirstOrDefault() == null) + apiRequest.Observations = [apiRequest.Observation]; + + if (apiRequest.Observations != null) + { + var obrcode = apiRequest.ObservationData?.Code; + + if (apiRequest.ObservationData?.Value != null) + apiRequest.Observations.Add(new PatientObservation + { Value = apiRequest.ObservationData.Value }); + + if (obrcode != null && _diagnosisCode.Contains(obrcode)) + _ = ProcessDiagnosisObservation(apiRequest, patient); + } + + + break; + default: + _logger.LogWarning("ApiRequest type {apiRequesttype} sis not valid for Diagnosis", + apiRequest.Type); + throw new ApiRequestException("ApiRequest type " + apiRequest.Type + + " is not valid for Diagnosis"); + } + } + + public async Task ProcessDiagnosis(List diagnosis, Patient patient, DateTime messageTime) + { + _logger.LogDebug("INSERT DiagnosisObservation from diagnosis"); + + foreach (var d in diagnosis) + { + d.PatientId = patient.Id; + d.Time = messageTime; + await InsertDiagnosis(d); + } + } + + public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId) + { + await _diagnosisRepository.UpdateManyObjectId(nameId, id, oldId); + } + + public async Task> GetByPatient(ObjectId id) + { + return await _diagnosisRepository.GetByPatient(id); + } + + public async Task Insert(PatientDiagnosis diagnosis) + { + _logger.LogDebug("Insert {diagnosis}", diagnosis); + var diag = await MapDiagnosis(diagnosis); + if (diag != null) + { + await _diagnosisRepository.InsertOneAsync(diag); + await SendBroadcast(diag); + } + } + + private async Task MapDiagnosis(PatientDiagnosis diagnosis) + { + var diagnosis2 = await _calculatedObservations.Value.Map(diagnosis); + if (diagnosis2 == null) _logger.LogDebug("Mapping {diagnosis}: Ignored", diagnosis2); + + return diagnosis2; + } + + private async Task SendBroadcast(PatientDiagnosis diagnosis) + { + var patient = await _patientService.Value.FindById(diagnosis.PatientId); + if (patient == null) return; + + var displaySubscribers = _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(); + + displaySubscribers.ForEach(Action); + return; + + void Action(WsSubscriber subscriber) + { + _clientMessageService.SendAsync(subscriber.Id, OperationType.Diagnosis, diagnosis); + } + } + + public Task> FindByPatientIdAsync(ObjectId patientId) + { + return _diagnosisRepository.FindByPatientIdAsync(patientId); + } + + public async Task InsertDiagnosis(PatientDiagnosis diagnosis) + { + try + { + _logger.LogDebug("Insert {diagnosis}", diagnosis); + var diag = await MapDiagnosis(diagnosis); + if (diag == null) + { + _logger.LogError("Mepped diagnosis is null. Diagnosis: {diagnosis}", diagnosis); + } + else + { + var dgdb = await _diagnosisRepository.FindByPatientIdAndCode(diag.PatientId, diag.Code, + diag.CodingSystem); + + if (dgdb != null) + { + var auxDgdb = dgdb; + diag.Id = dgdb.Id; + diag.Time = dgdb.Time; + diag.UpdateDate = diagnosis.Time; + await _diagnosisRepository.UpdateOneAsync(diag.Id, diag); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, auxDgdb, dgdb); + } + else + { + await _diagnosisRepository.InsertOneAsync(diagnosis); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, diagnosis); + } + + _ = SendBroadcast(diagnosis); + } + } + catch (Exception ex) + { + _logger.LogError("Error inserting diagnosis. Diagnosis: {diagnosis}. Exception:{ex}", diagnosis, ex); + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/DischargeService.cs b/adas-core.Application/Services/DischargeService.cs new file mode 100644 index 00000000..4ce7f211 --- /dev/null +++ b/adas-core.Application/Services/DischargeService.cs @@ -0,0 +1,400 @@ +using System.Reflection; +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.DTO; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; + +namespace adas_core.Application.Services; + +public class DischargeService : IDischargeService +{ + private readonly ILocalAuditService _auditService; + private readonly IClientMessageService _clientMessageService; + private readonly IDischargeRepository _dischargeRepository; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ILogger _logger; + private readonly IMasterListServiceFactory _masterListServiceFactory; + private readonly Lazy _patientServiceLazy; + private readonly IPointOfCareService _pointOfCareService; + private readonly ISubscribersService _subscribersService; + private readonly IUnitService _unitService; + + public DischargeService(ILogger logger, + ISubscribersService subscribersService, + IDischargeRepository dischargeRepository, + Lazy patientServiceLazy, + IClientMessageService clientMessageService, + IPointOfCareService pointOfCareService, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService, + IUnitService unitService, + IMasterListServiceFactory masterListServiceFactory) + { + _logger = logger; + _subscribersService = subscribersService; + _dischargeRepository = dischargeRepository; + _patientServiceLazy = patientServiceLazy; + _clientMessageService = clientMessageService; + _pointOfCareService = pointOfCareService; + _pointOfCareService = pointOfCareService; + _httpContextAccessor = httpContextAccessor; + _auditService = auditService; + _unitService = unitService; + _masterListServiceFactory = masterListServiceFactory; + } + + + public async Task DeleteDischargeAsync(Discharge discharge) + { + //var patient = await _patientServiceLazy.Value.FindById(discharge.PatientId); + //if (!discharge.MedicalDischarge.HasValue || !discharge.AdminDischarge.HasValue + // // || DischargeStatusType.NoAltable.ToString().Equals(patient?.DischargeStatus?.OptionType) + // ) + //{ + // _logger.LogError("The patient cannot be discharged"); + // return; + //} + await DeleteDischargeByIdAsync(discharge.Id); + } + + public async Task DeleteDischargeByIdAsync(ObjectId dischargeId) + { + try + { + var dischargeAux = await _dischargeRepository.FindById(dischargeId); + if (dischargeAux == null) + { + _logger.LogError("Error Discharge not found, id: {discharge} ", dischargeId); + return; + } + + await _dischargeRepository.Delete(dischargeId); + + _logger.LogInformation("Discharge id: {dischargeId} DELETED ", dischargeId); + + //var patient = await _patientServiceLazy.Value.FindById(dischargeAux.PatientId); + // if (patient != null) + // await _patientServiceLazy.Value.ArchivePatient(patient); + // else + // _logger.LogError("Patient not found on Discharge id: {discharge} ", dischargeId); + + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, dischargeAux, null); + SendDischargeBroadcast(dischargeAux, OperationType.DeleteDischarge); + } + catch (Exception ex) + { + _logger.LogError("Exception deleting discharge id:{admission} . Exception: {ex}", dischargeId, ex); + } + } + + public async Task CountDischargesByUnitId(ObjectId unitId) + { + return await _dischargeRepository.CountByUnitId(unitId); + } + + public async Task GetDischargeByIdAsync(ObjectId dischargeId) + { + var result = await _dischargeRepository.FindById(dischargeId) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + if (result.PointOfCareId == null) + return result; + + var poc = await _pointOfCareService.GetInfo(result.PointOfCareId.Value, null,false); + result.PatientLocation = new PatientLocation(poc?.UnitName, poc?.Bed, poc?.Room); + + return result; + } + + public async Task> GetDischargesAsync() + { + return await _dischargeRepository.FindAll(); + } + + public async Task GetDischargeByPatientId(ObjectId patientId) + { + try + { + var discharge = await _dischargeRepository.GetByPatientId(patientId); + if (discharge == null) + _logger.LogError("Discharge not found by patient Id {id}", patientId); + if (discharge is { PointOfCareId: not null }) + { + var poc = await _pointOfCareService.GetInfo(discharge.PointOfCareId.Value, null,false); + discharge.PatientLocation = new PatientLocation(poc?.UnitName, poc?.Bed, poc?.Room); + } + + return discharge; + } + catch (Exception ex) + { + _logger.LogError("Exception getting discharge by patient id: {id} . Exception: {ex}", patientId, ex); + return null; + } + } + + public async Task InsertDischarge(Discharge discharge) + { + await _dischargeRepository.InsertOneAsync(discharge); + + var dischargeAux = await _dischargeRepository.FindById(discharge.Id) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed); + _logger.LogInformation("Discharge: {discharge} INSERTED", discharge); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, dischargeAux); + SendDischargeBroadcast(discharge, OperationType.NewDischarge); + return dischargeAux; + } + + public async Task GetDischargeByLocation(PatientLocation location) + { + try + { + return await _dischargeRepository.GetDischargeByLocation(location); + } + catch (Exception e) + { + _logger.LogError($"Unable to get discharge by location on service Exception: {e}"); + return null; + } + } + + public async Task GetDischargeByPointOfCareId(ObjectId poc) + { + try + { + var discharge = await _dischargeRepository.GetDischargeByPointOfCareId(poc); + if (discharge == null) + _logger.LogInformation("Discharge not found by PointOfCareId {id}", poc); + if (discharge is { PointOfCareId: not null }) + { + var pocInfo = await _pointOfCareService.GetInfo(discharge.PointOfCareId.Value, null,false); + discharge.PatientLocation = new PatientLocation(pocInfo?.UnitName, pocInfo?.Bed, pocInfo?.Room); + } + + return discharge; + } + catch (Exception e) + { + _logger.LogError($"Unable to get discharge by location on service Exception: {e}"); + return null; + } + } + + public async Task GetDischargeByPointOfCareIdWithLocale(ObjectId location, LocaleEnum dataLocale) + { + var discharge = await GetDischargeByPointOfCareId(location); + if (discharge == null) return null; + var unit = await _unitService.FindById(discharge.UnitId); + if (unit == null) return discharge; + var dischargeWithLocale = await GetDischargeWithLocale(unit, discharge, dataLocale); + return dischargeWithLocale; + } + + public async Task UpdateDischargeAsync(Discharge discharge) + { + var oldDischarge = await GetDischargeByIdAsync(discharge.Id) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + await _dischargeRepository.Update(discharge); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldDischarge, discharge); + SendDischargeBroadcast(discharge, OperationType.UpdateDischarge); + _logger.LogInformation("Discharge: {discharge} UPDATED", discharge); + } + + + public async Task SaveRequest(ApiRequest apiRequest) + { + try + { + if (apiRequest.Discharge?.Patient == null) return; + + var patientId = apiRequest.Discharge.Patient.Id; + + var patient = await _patientServiceLazy.Value.FindById(patientId); + if (patient == null) + { + _logger.LogError("Error discharging patient id: {id} NOT FOUND", patientId); + return; + } + + await _patientServiceLazy.Value.Update(apiRequest.Discharge.Patient); + + switch (apiRequest.Type) + { + case "NewDischarge": + { + //TODO: ver qué tipos llegan + if (!"Altable".Equals(apiRequest.Discharge.Patient.DischargeStatus?.OptionType)) + { + _logger.LogError("Error discharging. Patient not altable: {patient}", patient); + return; + } + + + await _dischargeRepository.InsertOneAsync(apiRequest.Discharge); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, + apiRequest.Discharge); + SendDischargeBroadcast(apiRequest.Discharge, OperationType.NewDischarge); + + break; + } + case "UpdateDischarge": + { + await GetDischargeByIdAsync(apiRequest.Discharge.Id); + await UpdateDischargeAsync(apiRequest.Discharge); + break; + } + case "DeleteDischarge": + { + if (StatusEnum.Discharge.NoAltable.ToString().Equals(patient.DischargeStatus?.OptionType)) + { + _logger.LogError("Error deleting discharge. Patient altable: {patient}", patient); + return; + } + + await DeleteDischargeAsync(apiRequest.Discharge); + break; + } + } + } + catch (Exception ex) + { + _logger.LogError("Exception processing discharge api request {discharge} . Exception: {ex}", + apiRequest.Discharge, ex); + } + } + + public Task SaveRequestAsync(ApiRequest apiRequest) + { + return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); })); + } + + // Revisar + public async void SendDischargeBroadcast(Discharge discharge, OperationType operation) + { + try + { + if (discharge.PointOfCareId == null) + { + _logger.LogError("Error sending discharge broadcast. Unit name is null or empty {discharge} .", + discharge); + return; + } + + var subscribersGroup = _subscribersService.GetSubscribers() + .Where(s => s.LocationIds.Any(c => c == discharge.PointOfCareId)).GroupBy(h => h.Locale) + .ToList(); + + var unit = await _unitService.FindById(discharge.UnitId); + foreach (var group in subscribersGroup) + { + var locale = group.Key ?? LocaleEnum.Default; + IEnumerable subscribers = group; + + foreach (var subscriber in subscribers) + { + var dischargeWithLocale = await GetDischargeWithLocale(unit, discharge, locale); + _ = _clientMessageService.SendAsync(subscriber.Id, operation, dischargeWithLocale); + } + } + } + catch (Exception ex) + { + _logger.LogError("Exception sending discharge broadcast. Operation type: {op}. Exception: {ex}", + operation.ToString(), ex); + } + } + + public async Task UpdatePatientMasterListItemChange(UpdateOptionMasterListDto opt, IEnumerable unitList, + string typeName) + { + var unitIds = unitList.Select(x => x.Id).ToList(); + await _dischargeRepository.GetDischargesByUnitIds(unitIds); + var dischargeUpdatedList = await _dischargeRepository.UpdateMasterListOption(unitIds, opt, typeName); + var updatedList = dischargeUpdatedList as Discharge[] ?? dischargeUpdatedList.ToArray(); + foreach (var discharge in updatedList) + { + var oldDischarge = updatedList.FirstOrDefault(dis => dis.Id == discharge.Id); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldDischarge!, discharge); + SendDischargeBroadcast(discharge, OperationType.UpdateDischarge); + } + } + + public async Task DeletePatientMasterListItem(OptionList opt, IEnumerable unitList, string typeName) + { + var unitIds = unitList.Select(x => x.Id).ToList(); + var patientUpdatedList = await _dischargeRepository.DeleteMasterListOption(unitIds, opt, typeName); + foreach (var discharge in patientUpdatedList) + { + var dischargeUpdated = await GetDischargeByIdAsync(discharge.Id); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, discharge, null); + if (dischargeUpdated != null) + SendDischargeBroadcast(discharge, OperationType.UpdateDischarge); + } + } + + public async Task DeleteDischargesByUnitId(ObjectId unitId) + { + await _dischargeRepository.DeleteByUnitId(unitId); + } + + private async Task GetDischargeWithLocale(Unit? unit, Discharge discharge, LocaleEnum locale) + { + if (unit == null) + return discharge; + + if (locale == LocaleEnum.Default) + return discharge; + + // Campos del discharge que deben traducirse + var listMap = new List<(string field, ObjectId? listId, MasterListType type)> + { + ("serviceOption", unit.ServiceListId, MasterListType.ServiceList), + ("destinationOption", unit.DestinationListId, MasterListType.DestinationList) + }; + + foreach (var (field, listId, masterListType) in listMap) + { + if (listId == null) + continue; + + // Obtener propiedad desde DISCHARGE, no Patient + var prop = typeof(Discharge).GetProperty( + field, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + + if (prop == null) + continue; + + var propValue = prop.GetValue(discharge); + if (propValue == null) + continue; + + // Cargar master list traducida según locale + var listObj = await _masterListServiceFactory + .GetMasterListById(masterListType, listId.Value, locale); + + if (listObj == null) + continue; + + var master = listObj as dynamic; + + IEnumerable masterOptions = master.Options; + + // El campo puede ser OptionList simple + if (propValue is OptionList { Id: not null } option) + { + var translated = masterOptions.FirstOrDefault(o => o.Id == option.Id); + if (translated != null) option.Name = translated.Name; // Solo traducimos el nombre + } + } + + return discharge; + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/DisplayConfigService.cs b/adas-core.Application/Services/DisplayConfigService.cs new file mode 100644 index 00000000..18e690c5 --- /dev/null +++ b/adas-core.Application/Services/DisplayConfigService.cs @@ -0,0 +1,608 @@ +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.DTO.Display; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using adas_core.Domain.Utils; +using Microsoft.AspNetCore.Http; +using MongoDB.Bson; +using MongoDB.Driver; +using Newtonsoft.Json; +using Serilog; +using DisplayConfig = adas_core.Domain.Models.MongoModels.DisplayConfig; + +namespace adas_core.Application.Services; + +public class DisplayConfigService( + IDisplayConfigRepository displayConfigRepository, + Lazy displayService, + ISubscribersService subscribersService, + IClientMessageService clientMessageService, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService, + IMasterListServiceFactory masterListServiceFactory, + IDisplayCardConfigRepository displayCardConfigRepository, + IDisplayDetailConfigRepository displayDetailConfigRepository, + IDisplayChartConfigRepository displayChartRepository) + : IDisplayConfigService +{ + public async Task> GetAll() + { + var result = await displayConfigRepository.GetAll(); + var resultToReturn = new List(); + foreach (var config in result) + { + var c = await AddDisplaySectionMinimal(config); + if (c != null) resultToReturn.Add(c); + } + + return resultToReturn; + } + + public async Task> GetAllCompactPaginated(PaginationFilter filter) + { + var result = displayConfigRepository.GetAllPaginated(filter); + var count = await result.CountDocumentsAsync(); + var resultToReturn = new List(); + + var data = await result.Skip((filter.PageNumber - 1) * filter.PageSize) + .Limit(filter.PageSize) + .ToCursorAsync(); + + var dataList = await data.ToListAsync(); + foreach (var config in dataList) + { + var isInUse = await displayService.Value.IsDisplayConfigInUse(config.Id); + resultToReturn.Add(new DisplayConfigMinimalResponse(config, isInUse)); + } + + + return new PaginationResponse(resultToReturn, filter.PageNumber, filter.PageSize, + count); + } + + public async Task> GetByType(DisplayConfigEnums.DisplayType type) + { + var result = await displayConfigRepository.GetByType(type); + var resultToReturn = new List(); + foreach (var config in result) + { + var c = await AddDisplaySectionMinimal(config); + if (c != null) resultToReturn.Add(c); + } + + return resultToReturn; + } + + public async Task GetById(ObjectId id) + { + return await AddDisplaySectionMinimal(await displayConfigRepository.GetById(id)) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + } + + public async Task GetById(ObjectId? configId, ObjectId unitId, + DisplayConfigEnums.DisplayType displayType) + { + if (configId.HasValue) + { + DisplayConfig? currentConfig; + DisplayConfig? defaultConfig; + try + { + currentConfig = await GetById(configId.Value); + } + catch (NotFoundException) + { + currentConfig = null; + } + + try + { + defaultConfig = await GetDefaultByUnitIdAndType(unitId, displayType); + } + catch (NotFoundException) + { + defaultConfig = null; + } + + if (defaultConfig != null && currentConfig != null) return currentConfig.MergeConfig(defaultConfig); + + return defaultConfig ?? currentConfig; + } + + return await GetDefaultByUnitIdAndType(unitId, displayType); + } + + public async Task InsertOne(DisplayConfig config) + { + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, config); + return await displayConfigRepository.InsertOneAsyncAndReturn(config); + } + + public async Task InsertOneMinimal(CreateDisplayConfigDto config) + { + DisplayConfig newDisplayConfig; + + if (config.Type == DisplayConfigEnums.DisplayType.DisplayNurse) + newDisplayConfig = new DisplayNurse + { + Hospital = config.Hospital, + Type = config.Type + }; + else + newDisplayConfig = new DisplayConfig + { + Hospital = config.Hospital, + Type = config.Type + }; + newDisplayConfig.Id = ObjectId.GenerateNewId(); + + var result = await displayConfigRepository.InsertOneAsyncAndReturn(newDisplayConfig); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, newDisplayConfig); + return result; + } + + public async Task InsertOneTest() + { + var d = new DisplayNurse + { + Type = DisplayConfigEnums.DisplayType.DisplayNurse + }; + await displayConfigRepository.InsertOneAsyncAndReturn(d); + var e = new SmartDisplay + { + Type = DisplayConfigEnums.DisplayType.SmartDisplay + }; + await displayConfigRepository.InsertOneAsyncAndReturn(e); + return d; + } + + public async Task UpdateConfig(ObjectId displayConfigId, object newDisplayConfig) + { + var baseType = JsonConvert.DeserializeObject(newDisplayConfig.ToString()!); + // var cardConfigUpdate = await UpdateDisplayCardConfig(displayConfigId, baseType); + var oldDisplayConfig = await displayConfigRepository.GetById(displayConfigId); + switch (baseType!.Type) + { + case DisplayConfigEnums.DisplayType.SmartDisplay: + + var smartConfigToReturn = await displayConfigRepository.UpdateSmartDisplay(displayConfigId, + JsonConvert.DeserializeObject(newDisplayConfig.ToString()!)); + if (smartConfigToReturn != null) + { + // smartConfigToReturn.CardConfig = cardConfigUpdate; + SendSmartDisplayConfigBroadcast(displayConfigId, smartConfigToReturn, + oldDisplayConfig as SmartDisplay); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldDisplayConfig, + newDisplayConfig); + return smartConfigToReturn; + } + + break; + + case DisplayConfigEnums.DisplayType.DisplayNurse: + var obsNurseList = masterListServiceFactory.StringNurseObs(); + var displayConfigUpdate = + JsonConvert.DeserializeObject(newDisplayConfig.ToString()!); + var nurseConfigToReturn = + await displayConfigRepository.UpdateDisplayNurse(displayConfigId, displayConfigUpdate, + obsNurseList); + if (nurseConfigToReturn != null) + { + // nurseConfigToReturn.CardConfig = cardConfigUpdate; + SendDisplayConfigBroadcast(displayConfigId, OperationType.UpdateDisplayConfig, nurseConfigToReturn); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldDisplayConfig, + nurseConfigToReturn); + return nurseConfigToReturn; + } + + break; + default: + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundNoMatches); + } + + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + } + + public Task UpdateFieldList(ObjectId objectIdConfigDisplay, List fields) + { + return displayConfigRepository.UpdateFieldList(objectIdConfigDisplay, fields); + } + + public async Task UpdateConfigColor(ObjectId objectIdConfigDisplay, ColorConfig colorConfig) + { + var oldDisplayConfig = await displayConfigRepository.GetById(objectIdConfigDisplay) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + if (oldDisplayConfig.ColorConfig == null) + { + oldDisplayConfig.ColorConfig = new ColorConfig(); + await displayConfigRepository.UpdateOneAsync(objectIdConfigDisplay, oldDisplayConfig); + } + + var result = await displayConfigRepository.UpdateConfigColor(objectIdConfigDisplay, colorConfig); + var newDisplayConfig = await displayConfigRepository.GetById(objectIdConfigDisplay) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldDisplayConfig, + newDisplayConfig); + return result; + } + + public async Task UpdateHeaderConfig(ObjectId objectIdConfigDisplay, HeaderConfig headerConfig) + { + var oldDisplayConfig = await displayConfigRepository.GetById(objectIdConfigDisplay) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + var result = await displayConfigRepository.UpdateHeaderConfig(objectIdConfigDisplay, headerConfig); + var newDisplayConfig = await displayConfigRepository.GetById(objectIdConfigDisplay) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldDisplayConfig, + newDisplayConfig); + return result; + } + + public async Task UpdateSetHomeBanner(ObjectId objectIdConfigDisplay, List bannerItems) + { + var oldDisplayConfig = await displayConfigRepository.GetById(objectIdConfigDisplay) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + var result = await displayConfigRepository.UpdateSetHomeBanner(objectIdConfigDisplay, bannerItems); + var newDisplayConfig = await displayConfigRepository.GetById(objectIdConfigDisplay) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldDisplayConfig, + newDisplayConfig); + return result; + } + + public async Task UpdateBaseConfig(DisplayConfig baseConfig) + { + var oldDisplayConfig = await displayConfigRepository.GetById(baseConfig.Id) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + var result = await displayConfigRepository.UpdateBaseConfig(baseConfig.Id, baseConfig); + var newDisplayConfig = await displayConfigRepository.GetById(baseConfig.Id) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldDisplayConfig, + newDisplayConfig); + return result; + } + + public async Task UpdateDisplayConfigHospitalName(ObjectId objectIdConfigDisplay, string name) + { + var oldDisplayConfig = await displayConfigRepository.GetById(objectIdConfigDisplay) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + var result = await displayConfigRepository.UpdateDisplayConfigHospitalName(objectIdConfigDisplay, name); + var newDisplayConfig = await displayConfigRepository.GetById(objectIdConfigDisplay) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldDisplayConfig, + newDisplayConfig); + return result; + } + + public async Task DeleteDisplayConfig(ObjectId objectIdConfigDisplay) + { + var displays = await displayService.Value.GetByConfigId(objectIdConfigDisplay); + if (displays.Count > 0) + { + var type = displays.First().Type; + var defaultConfig = await GetDefaultConfig(type) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); + foreach (var display in displays) + _ = await displayService.Value.UpdateConfigId(display, defaultConfig.Id) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); + } + + var result = await displayConfigRepository.DeleteDisplayConfig(objectIdConfigDisplay); + if (result != null) + { + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, result, null); + return true; + } + + return false; + } + + public async Task> GetDisplayConfigLocations(ObjectId objectIdConfigDisplay) + { + return await displayService.Value.GetDisplayConfigLocations(objectIdConfigDisplay); + } + + public async Task> GetAllCompact() + { + return await displayConfigRepository.GetAllCompact(); + } + + public async Task InsertOneWithTemplate(string objectId, + DisplayConfigEnums.DisplayType configType, string? configHospital) + { + if (ObjectId.TryParse(objectId, out var objectIdConfigDisplay)) + { + var template = await GetById(objectIdConfigDisplay); + switch (configType) + { + case DisplayConfigEnums.DisplayType.StandarDisplay: + if (template is StandarDisplay standardTemplate) + { + var standarConfigg = new StandarDisplay + { Hospital = configHospital, Type = DisplayConfigEnums.DisplayType.StandarDisplay }; + standarConfigg.MergeConfig(standardTemplate); + await InsertOne(standarConfigg); + return standarConfigg; + } + + break; + + case DisplayConfigEnums.DisplayType.DisplayNurse: + if (template is DisplayNurse nurseTemplate) + { + var nurseConfig = new DisplayNurse + { Hospital = configHospital, Type = DisplayConfigEnums.DisplayType.DisplayNurse }; + nurseConfig.MergeConfig(nurseTemplate); + await InsertOne(nurseConfig); + return nurseConfig; + } + + break; + + case DisplayConfigEnums.DisplayType.SmartDisplay: + if (template is SmartDisplay smartTemplate) + { + var smartConfig = new SmartDisplay + { Hospital = configHospital, Type = DisplayConfigEnums.DisplayType.DisplayNurse }; + smartConfig.MergeConfig(smartTemplate); + await InsertOne(smartConfig); + return smartConfig; + } + + break; + + default: + return null; + } + } + + return null; + } + + public async Task UpdateCardConfig(CardConfig baseConfig) + { + var result = await displayCardConfigRepository.UpdateOne(baseConfig); + + if (result.Changes > 0) + { + var displayConfigs = await displayConfigRepository.GetAllByCardConfigIdAndRotating(baseConfig.Id); + foreach (var displayConfig in displayConfigs) + { + await displayConfigRepository.UpdateDisplayNurse(displayConfig, + new DisplayNurseDto() { CardConfig = result.Data }, masterListServiceFactory.StringNurseObs()); + } + + var displays = await displayConfigRepository.GetAllByCardConfigId(baseConfig.Id); + foreach (var display in displays) + SendDisplayConfigBroadcast(display, OperationType.UpdateCardDisplayConfig, result.Data); + + return true; + } + + return false; + } + + public async Task UpdateDetailConfig(CardDetailsConfig baseConfig) + { + var result = await displayDetailConfigRepository.UpdateOne(baseConfig); + var displays = await displayConfigRepository.GetAllByCardDetailConfigId(baseConfig.Id); + foreach (var display in displays) + SendDisplayConfigBroadcast(display, OperationType.UpdateDetailDisplayConfig, result.Data); + if (result.Changes > 0) return true; + return false; + } + + public async Task UpdateChartConfig(ChartConfig baseConfig) + { + var result = await displayChartRepository.UpdateOne(baseConfig); + if (result.Changes > 0) return true; + return false; + } + + public async Task DeleteChartConfig(ObjectId objectIdConfigDisplay) + { + var res = await displayChartRepository.DeleteOne(objectIdConfigDisplay); + if (res != null) + { + await UpdateDeletedChartConfig(objectIdConfigDisplay); + return true; + } + + return false; + } + + public async Task GetChartConfig(ObjectId objectIdConfigChart) + { + return await displayChartRepository.GetById(objectIdConfigChart); + } + + public async Task InsertCardDetailConfig(CreateDisplayConfigCardDto updateDisplayConfigNameDto) + { + if (updateDisplayConfigNameDto.DetailConfig == null) return null; + var result = + await displayDetailConfigRepository.InsertOneAsyncAndReturn(updateDisplayConfigNameDto.DetailConfig); + if (updateDisplayConfigNameDto.DisplayConfigId != null && result != null) + { + var res = await UpdateDetailConfigId(updateDisplayConfigNameDto.DisplayConfigId, result.Id); + if (res) + SendDisplayConfigBroadcast(updateDisplayConfigNameDto.DisplayConfigId.Value, + OperationType.UpdateDetailDisplayConfig, result); + } + + return result; + } + + public async Task InsertChartConfig(CreateDisplayConfigCardDto updateDisplayConfigNameDto) + { + if (updateDisplayConfigNameDto.ChartConfig == null) return null; + var result = await displayChartRepository.InsertOneAsyncAndReturn(updateDisplayConfigNameDto.ChartConfig); + if (updateDisplayConfigNameDto.DisplayConfigId != null && result != null) + { + var res = await AddChartId(updateDisplayConfigNameDto.DisplayConfigId, result.Id); + if (res) + SendDisplayConfigBroadcast(updateDisplayConfigNameDto.DisplayConfigId.Value, + OperationType.UpdateChartConfig, result); + } + + return result; + } + + public async Task InsertCardConfig(CreateDisplayConfigCardDto updateDisplayConfigNameDto) + { + if(updateDisplayConfigNameDto.CardConfig == null) return null; + var result = await displayCardConfigRepository.InsertOneAsyncAndReturn(updateDisplayConfigNameDto.CardConfig); + if (updateDisplayConfigNameDto.DisplayConfigId != null && result != null) + { + var res = await UpdateCardConfigId(updateDisplayConfigNameDto.DisplayConfigId, result.Id); + if (res) + SendDisplayConfigBroadcast(updateDisplayConfigNameDto.DisplayConfigId.Value, + OperationType.UpdateDetailDisplayConfig, result); + } + + return result; + } + + public async Task> GetCardConfigAll() + { + return await displayCardConfigRepository.GetAll(); + } + + public async Task GetCardConfigById(ObjectId id) + { + return await displayCardConfigRepository.GetById(id); + } + + public async Task GetDefaultConfig(DisplayConfigEnums.DisplayType type) + { + return await displayConfigRepository.GetDefault(type); + } + + private async Task GetDefaultByUnitIdAndType(ObjectId unitId, + DisplayConfigEnums.DisplayType displayType) + { + return + await displayConfigRepository.GetDefaultByUnitIdAndType(unitId, + displayType); //?? throw new NotFoundException(ErrorMessage.NotFound_ResourceMissing); + } + + private async Task AddDisplaySectionMinimal(DisplayConfig? displayConfig) + { + if (displayConfig == null) return null; + var displayConfigAux = displayConfigRepository.GetById(displayConfig.Id) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + if (displayConfig.Type == DisplayConfigEnums.DisplayType.SmartDisplay && + !displayConfig.DisplaySectionIdList.IsNullOrEmpty()) + { + foreach (var displayId in displayConfig.DisplaySectionIdList) + { + var d = await displayService.Value.GetById(displayId); + if (d != null) + { + var minimalDisplay = new MinimalDisplaySection + { + Id = displayId, + Name = d.Name + }; + displayConfig.DisplaySectionList.Add(minimalDisplay); + } + } + + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, displayConfigAux, + displayConfig); + } + + return displayConfig; + } + + private async Task UpdateDeletedChartConfig(ObjectId deletedId) + { + await displayConfigRepository.UpdateDeletedChartConfig(deletedId); + } + + private async Task AddChartId(ObjectId? displayConfigId, ObjectId resultId) + { + var result = await displayConfigRepository.AddChartId(displayConfigId, resultId); + return result; + } + + private async Task UpdateCardConfigId(ObjectId? displayConfigId, ObjectId? resultId) + { + var result = await displayConfigRepository.UpdateCardConfigId(displayConfigId, resultId); + return result; + } + + private async Task UpdateDetailConfigId(ObjectId? displayConfigId, ObjectId? resultId) + { + var result = await displayConfigRepository.UpdateDetailConfigId(displayConfigId, resultId); + return result; + } + + private async void SendDisplayConfigBroadcast(ObjectId displayConfigId, OperationType operationType, + object? newDisplayConfig) + { + try + { + var listDisplayId = await displayService.Value.GetByConfigId(displayConfigId); + var listDisplayIdList = listDisplayId.Select(c => c.Id).ToList(); + var subscribers = subscribersService.GetSubscribers().Where(s => + s.DisplayId != null && listDisplayIdList.Contains((ObjectId)s.DisplayId)).ToList(); + + foreach (var sub in subscribers) + _ = clientMessageService.SendAsync(sub.Id, operationType, + newDisplayConfig); + } + catch (Exception e) + { + Log.Error("Error sending update for DisplayNurse config: {message}", e.Message); + //throw new ConflictException(ErrorMessage.Conflict_UpdateFailed, e); + } + } + + private async void SendSmartDisplayConfigBroadcast(ObjectId displayConfigId, SmartDisplay? newDisplayConfig, + SmartDisplay? oldDisplayConfig) + { + try + { + var listDisplayId = await displayService.Value.GetByConfigId(displayConfigId); + var listDisplayIdList = listDisplayId.Select(c => c.Id).ToList(); + var subscribers = subscribersService.GetSubscribers().Where(s => + s.DisplayId != null && listDisplayIdList.Contains((ObjectId)s.DisplayId)).ToList(); + + if (newDisplayConfig != null && oldDisplayConfig != null) + SendSmartDisplayConfigUpdate(subscribers, oldDisplayConfig, newDisplayConfig); + else Log.Error("Error sending update for SmartDisplay config config is null"); + } + catch (Exception e) + { + Log.Error("Error sending update for SmartDisplay config: {message}", e.Message); + //throw new ConflictException(ErrorMessage.Conflict_UpdateFailed, e); + } + finally + { + // Enviar un mensaje a todos los clientes para que actualicen la configuraci�n del display + var displayConfig = await displayConfigRepository.GetById(displayConfigId); + if (displayConfig != null) + _ = clientMessageService.SendToAllAsync(OperationType.UpdateDisplayConfig, displayConfig); + } + } + + private void SendSmartDisplayConfigUpdate(List subscribers, + SmartDisplay? oldDisplayDisplayConfig, SmartDisplay? newDisplayDisplayConfig) + { + // Obtener las propiedades que han cambiado + var differentProperties = oldDisplayDisplayConfig?.GetDifferentProperties(newDisplayDisplayConfig); + + // Enviar un mensaje a los clientes por cada propiedad que haya cambiado + if (differentProperties != null) + foreach (var property in differentProperties) + foreach (var sub in subscribers) + _ = clientMessageService.SendAsync(sub.Id, OperationType.UpdateDisplayConfig, + newDisplayDisplayConfig?.GetType().GetProperty(property)?.GetValue(newDisplayDisplayConfig)); + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/DisplayService.cs b/adas-core.Application/Services/DisplayService.cs new file mode 100644 index 00000000..51ef35f8 --- /dev/null +++ b/adas-core.Application/Services/DisplayService.cs @@ -0,0 +1,712 @@ +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.AppSettings; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.DTO.Display; +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 DisplayService( + IDisplayRepository displayRepository, + IPointOfCareService pointOfCareService, + Lazy unitService, + IDisplayConfigService displayConfigService, + ISubscribersService subscribersService, + IClientMessageService clientMessageService, + IUserRepository userRepository, + IAuthService authorityService, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService, + Lazy permissionService, + ICacheService cacheService, + IOptions cacheSettings) + : IDisplayService +{ + private readonly CacheSettings? _cacheSettings = cacheSettings.Value ; + + #region Methods + + #region Create + + public async Task InsertOne(Display display) + { + var defaultConfig = await displayConfigService.GetDefaultConfig(display.Type) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + display.DisplayConfigId = defaultConfig.Id; + await displayRepository.InsertOneAsync(display); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, display); + return display; + } + + public async Task InsertOneTest() + { + var d = new Display + { + Name = "DisplayTEST", + UnitId = new ObjectId("65ba5f89d5ba8e273cf9cd96"), + DisplayConfigId = new ObjectId("45ba5f89d5ba8e273cf9cd96") + }; + await displayRepository.InsertOneAsync(d); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, d); + return d; + } + + #endregion + + #region Read + + public async Task> GetAllCompact() + { + var result = await displayRepository.GetAll(); + List listToReturn = []; + foreach (var res in result) listToReturn.Add(new DisplayMinimalDto(res)); + return listToReturn; + } + + public Task> GetAll(string? userName) + { + throw new NotImplementedException(); + } + + public async Task> GetPaginatedDisplays(PaginationFilter filter) + { + var result = displayRepository.GetPaginatedDisplays(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(dataList, filter.PageNumber, filter.PageSize, count); + } + + public async Task> GetAllByUser(string? userName) + { + var start = DateTime.Now; + var listToReturn = new List(); + if (userName == null) return listToReturn; + var user = await userRepository.GetByUserAndAuthoritesName(userName); + if (user == null) return listToReturn; + if (user.Authorization == null || user.Authorization.Count == 0) + user.Authorization = await authorityService.GetUserAuthorities(user.Id); + + if (user.Authorization == null) + return listToReturn; + + foreach (var e in user.Authorization) + if (e.UnitId != null) + { + var isParsed = ObjectId.TryParse(e.UnitId, out var dId); + if (isParsed) + { + var dis = await displayRepository.GetByUnitId(dId); + foreach (var display in dis) + { + var toAdd = await GetInfo(display.Id, userName, user.Authorization,null, false, false, false, false); + + if (toAdd != null) + { + var newDto = new DisplayWithPermissionsDto + { + Display = toAdd, + Permissions = + await permissionService.Value.GetPermissionsForUnit(dId.ToString(), user) ?? + throw new ForbbidenException(HttpEnum.ErrorMessage.ForbiddenNoPermission) + }; + listToReturn.Add(newDto); + } + } + } + } + else if (e.DisplayId != null && !FindDisplayInPerms(e.DisplayId, listToReturn)) + { + var isParsed = ObjectId.TryParse(e.DisplayId, out var dId); + if (isParsed) + { + var toAdd = await GetInfo(dId, userName, user.Authorization, null, false, false, false, false); + + if (toAdd != null) + { + var newDto = new DisplayWithPermissionsDto + { + Display = toAdd, + Permissions = await permissionService.Value.GetPermissionsForDisplay(toAdd, user) + }; + listToReturn.Add(newDto); + } + } + } + + var end = DateTime.Now; + logger.LogDebug("Finished GetAllByUser Displays for user {user} in {TotalSeconds:F1} seconds", userName, + (end - start).TotalSeconds); + return listToReturn; + } + + private static bool FindDisplayInPerms(string displayId, List perms) + { + return perms.Any(p => p.Display != null && p.Display.Id.ToString() == displayId); + } + + public async Task> GetByType(DisplayConfigEnums.DisplayType type) + { + var configs = await displayConfigService.GetByType(type); + var listToReturn = new List(); + foreach (var config in configs) + { + var displayToAdd = await displayRepository.GetByConfigId(config.Id); + displayToAdd.ForEach(c => c.DisplayConfig = config); + if (!displayToAdd.IsNullOrEmpty()) listToReturn.AddRange(displayToAdd); + } + + return listToReturn; + } + + public async Task> GetByPointOfCare(PointOfCare pointOfCare) + { + return await displayRepository.GetByPointOfCare(pointOfCare); + } + + public Task> GetByConfigId(ObjectId configId) + { + return displayRepository.GetByConfigId(configId); + } + + public Task> GetByCardConfigId(ObjectId configId) + { + return displayRepository.GetByCardConfigId(configId); + } + + public async Task GetByName(string name) + { + return await displayRepository.GetByName(name) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + } + + public async Task GetById(ObjectId id) + { + return await displayRepository.GetById(id); + } + + public async Task GetByIdWithPermissions(ObjectId id, LocaleEnum localeEnum) + { + var username = JwtHelper.GetUsernameFromPrincipal(httpContextAccessor.HttpContext?.User!) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + var user = await userRepository.GetByUserName(username); + var display = await GetById(id); + + if (display == null) throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + + foreach (var poc in display.PointOfCareIdList) + { + var c = await pointOfCareService.GetInfo(poc, localeEnum); + if (c != null) display.PointOfCares.Add(c); + } + + display.DisplayConfig = + await displayConfigService.GetById(display.DisplayConfigId, display.UnitId, display.Type); + display.DisplayConfig!.DisplaySectionList = []; + try + { + display.DisplayConfig!.DisplaySectionList = + await GetDisplaySectionByUser(display.Type, id, username, user?.Authorization); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + + return new DisplayWithPermissionsDto + { + Display = display, + Permissions = await permissionService.Value.GetPermissionsForDisplay(display, user!) + }; + } + + public async Task CountDisplaysByUnitId(ObjectId unitId) + { + return await displayRepository.CountByUnitId(unitId); + } + + public async Task GetInfo(ObjectId id, + string? userName, + List? authorizations, + LocaleEnum? locale, + bool fillPointOfCare = true, + bool fillPatientData = false, + bool fillDisplayList = true, + bool fillDisplayConfig = true, + CancellationToken ct = default) + { + var start = DateTime.Now; + + Display? display; + if (fillDisplayConfig) + { + + // Clave: display con configuración + var (key, ttl) = CacheKeys.DisplayWithConfigKeyWithTtl(_cacheSettings, id); + + display = await cacheService.GetOrSetObjectAsync( + key, + async () => await BuildDisplayWithConfig(id, ct), + ttl, + ct); + + } + else + { + // Clave: display base + var (key, ttl) = CacheKeys.DisplayBaseKeyWithTtl(_cacheSettings, id); + + display = await cacheService.GetOrSetObjectAsync( + key, + async () => await displayRepository.GetById(id), + ttl, + ct); + + } + if (display == null) return null; + + // PointOfCare (cacheado en su propio servicio) + if (fillPointOfCare) + foreach (var poc in display.PointOfCareIdList) + { + var c = await pointOfCareService.GetInfo(poc, locale, fillPatientData); + if (c != null) display.PointOfCares.Add(c); + } + + if (authorizations == null && userName != null) + { + var c = await userRepository.GetByUserAndAuthoritesName(userName); + authorizations = c?.Authorization; + } + + // DisplayList (depende de autorizaciones → NO cacheable) + if (fillDisplayList) + if (display.DisplayConfig != null) + display.DisplayConfig.DisplaySectionList = + await GetDisplaySectionByUser(display.Type, id, userName, authorizations); + else + logger.LogError("DISPLAY CONFIG NULL on fill display list"); + + var end = DateTime.Now; + + logger.LogDebug("Finished GetInfo Displays for user {user} in {TotalSeconds:F1} seconds", userName, + (end - start).TotalSeconds); + + return display; + } + + private async Task BuildDisplayWithConfig(ObjectId id, CancellationToken ct) + { + var display = await displayRepository.GetById(id); + if (display == null) return null; + + var type = display.Type; + + // DisplayConfig + display.DisplayConfig = + await displayConfigService.GetById(display.DisplayConfigId, display.UnitId, type); + + if (type != DisplayConfigEnums.DisplayType.SmartDisplay || + display.DisplayConfig is not SmartDisplay { CardRotatingLayout: not null } smart) + return display; + + if(smart.CardRotatingLayout== null) + return display; + + foreach (var card in smart.CardRotatingLayout) + card.Data = await displayConfigService.GetCardConfigById(card.DataId) + ?? new CardConfig(); + + return display; + + } + + + public async Task> GetDisplaySectionByUser( + DisplayConfigEnums.DisplayType type, + ObjectId? currentDisplay, + string? userName, + List? authorizations) + { + try + { + var listToReturn = new List(); + if (userName != null) + { + var user = await userRepository.GetByUserName(userName); + + if (user != null) + { + var displayIdByAuthorities = authorizations ?? await authorityService.GetUserAuthorities(user.Id); + + foreach (var e in displayIdByAuthorities) + { + var isParsed = ObjectId.TryParse(e.DisplayId, out var dId); + if (isParsed) + { + if (listToReturn.All(c => c.Id != dId)) + { + var toAdd = await GetById(dId); + if (toAdd != null) + if (type == toAdd.Type) + { + var minDisSec = new MinimalDisplaySection + { + Name = toAdd.Name, + Id = toAdd.Id, + IsSelected = currentDisplay != null && toAdd.Id == currentDisplay + }; + listToReturn.Add(minDisSec); + } + } + } + else + { + var isParsedUnitId = ObjectId.TryParse(e.UnitId, out var uId); + if (isParsedUnitId) + { + var unitDisplays = await GetByUnitId(uId); + foreach (var disp in unitDisplays) + if (type == disp.Type && listToReturn.All(c => c.Id != dId)) + { + var minDisSec = new MinimalDisplaySection + { + Name = disp.Name, + Id = disp.Id, + IsSelected = currentDisplay != null && disp.Id == currentDisplay + }; + listToReturn.Add(minDisSec); + } + } + } + } + } + } + + return listToReturn; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + public async Task GetAllDisplaySection() + { + var minimalDisplayListDto = new MinimalDisplayListDto(); + var listDisplayNurse = await GetByType(DisplayConfigEnums.DisplayType.DisplayNurse); + foreach (var displayForAdmin in listDisplayNurse) + { + var minDisSec = new MinimalDisplaySection + { + Name = displayForAdmin.Name, + Id = displayForAdmin.Id + }; + minimalDisplayListDto.DisplayNurse.Add(minDisSec); + } + + var listDisplaySmart = await GetByType(DisplayConfigEnums.DisplayType.SmartDisplay); + foreach (var displayForAdmin in listDisplaySmart) + { + var minDisSec = new MinimalDisplaySection + { + Name = displayForAdmin.Name, + Id = displayForAdmin.Id + }; + minimalDisplayListDto.SmartDisplay.Add(minDisSec); + } + + return minimalDisplayListDto; + } + + public async Task> GetByUnitId(ObjectId unitId) + { + return await displayRepository.GetByUnitId(unitId); + } + + public async Task GetAllAvailablePoc(List displayIds, bool excludeVirtual = false) + { + try + { + var listToReturn = new PocAndUnitDto(); + var listObjectId = new List(); + foreach (var displayId in displayIds) + { + var isParsed = ObjectId.TryParse(displayId, out var dId); + if (isParsed) + { + var diplay = await displayRepository.GetById(dId); + if (diplay != null) + listObjectId.Add(diplay.UnitId); + } + } + + var distinctObjectIds = listObjectId.Distinct().ToList(); + foreach (var distinctObjectId in distinctObjectIds) + { + var unit = await unitService.Value.FindById(distinctObjectId) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + var pocList = await pointOfCareService.FindByUnitAndStatus(distinctObjectId, + StatusEnum.PointOfCare.Available, excludeVirtual); + foreach (var pointOfCare in pocList) + { + var pocAv = new MinimalPocAndUnitDto + { + PocId = pointOfCare.Id, + PocName = pointOfCare.Bed, + UnitId = pointOfCare.UnitId, + UnitName = unit.Name + }; + listToReturn.PocList.Add(pocAv); + } + } + + return listToReturn; + } + catch (Exception e) + { + logger.LogError("Error GetAllAvailablePoc {Error}", e.Message); + return new PocAndUnitDto(); + } + } + + public async Task> GetAllPocsByDisplayId(ObjectId id) + { + try + { + var pocList = new List(); + var display = await displayRepository.GetById(id); + if (display == null) + return pocList; + + foreach (var pocId in display.PointOfCareIdList) + { + var poc = await pointOfCareService.FindById(pocId); + if (poc != null) pocList.Add(poc); + } + + return pocList; + } + catch (Exception e) + { + logger.LogError("Error GetAllAvailablePoc {Error}", e.Message); + return []; + } + } + + public async Task> GetDisplayConfigLocations(ObjectId displayConfigId) + { + var displays = await GetByConfigId(displayConfigId); + var locations = new List(); + foreach (var display in displays) + { + var unit = await unitService.Value.FindById(display.UnitId); + DisplayConfigLocationDto newLocation = new() + { + DisplayName = display.Name, + UnitName = unit?.Name, + }; + locations.Add(newLocation); + } + + return locations; + } + + public async Task IsDisplayConfigInUse(ObjectId displayConfigId) + { + return await displayRepository.IsDisplayConfigInUse(displayConfigId) > 0; + } + + #endregion + + #region Update + + /* + * En esta actualización se espera una resubscipción al id del display ya que actualizar los PoC conlleva actualizar + * subscrioptor y locations para las observaciones + */ + public async Task UpdatePointOfCareList(ObjectId objectId, List listPocObId) + { + var oldDisplay = await displayRepository.GetById(objectId) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + var displayToReturn = await displayRepository.UpdatePointOfCareList(objectId, listPocObId) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + + await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(objectId)); + + SendDisplayBroadcast(displayToReturn, OperationType.UpdateDisplayPoC); + + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldDisplay, displayToReturn); + return displayToReturn; + } + + public async Task UpdateConfig(Display oldDisplay, DisplayConfig? newDisplayConfig) + { + var newDisplayConfigCast = new DisplayConfig(); + switch (newDisplayConfig?.Type) + { + case DisplayConfigEnums.DisplayType.DisplayNurse: + newDisplayConfigCast = newDisplayConfig as DisplayNurse; + break; + case DisplayConfigEnums.DisplayType.SmartDisplay: + newDisplayConfigCast = newDisplayConfig as SmartDisplay; + break; + } + + if (newDisplayConfigCast != null) + { + var displayToReturn = await displayRepository.UpdateConfig(oldDisplay.Id, newDisplayConfigCast); + if (displayToReturn != null) + { + SendDisplayBroadcast(displayToReturn, OperationType.UpdateDisplayConfig); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldDisplay, + displayToReturn); + } + + await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(oldDisplay.Id)); + + return displayToReturn; + } + + return null; + } + + public async Task UpdateConfigId(Display oldDisplay, ObjectId configId) + { + var displayToReturn = await displayRepository.UpdateConfigId(oldDisplay.Id, configId); + if (displayToReturn != null) + { + await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(oldDisplay.Id)); + SendDisplayBroadcast(displayToReturn, OperationType.UpdateDisplayConfig); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldDisplay, displayToReturn); + } + + return displayToReturn; + } + + public async Task UpdateConfigPreset(ObjectId objectIdDisplay, ObjectId objectIdConfigDisplay) + { + var oldConfig = await displayConfigService.GetById(objectIdConfigDisplay); + var result = await displayRepository.UpdateConfigPreset(objectIdDisplay, objectIdConfigDisplay); + + await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(objectIdDisplay)); + + var config = await displayConfigService.GetById(objectIdConfigDisplay); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldConfig, config); + if (result == null || config == null) + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + + var subscribers = subscribersService.GetSubscribers().Where(s => s.DisplayId == objectIdDisplay).ToList(); + switch (config.Type) + { + case DisplayConfigEnums.DisplayType.DisplayNurse: + SendNurseDisplayBroadcast(subscribers, config as DisplayNurse); + break; + case DisplayConfigEnums.DisplayType.SmartDisplay: + SendSmartDisplayBroadcast(subscribers, config as SmartDisplay); + break; + case DisplayConfigEnums.DisplayType.Unknown: + break; + default: + throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + } + + return result; + } + + public async Task UpdateName(ObjectId id, string name) + { + var display = await displayRepository.GetById(id) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + var newDisplay = await displayRepository.UpdateName(display, name); + + await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(id)); + + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, display, newDisplay); + return newDisplay; + } + + #endregion + + #region Delete + + public async Task DeleteDisplay(ObjectId id) + { + var display = await displayRepository.GetById(id) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + await displayRepository.DeleteAsync(id); + + // Invalidar CACHE (colección completa) + await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(id)); + + await authorityService.DeleteByDisplayId(id); + + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, display, null); + return true; + } + + public async Task DeleteDisplaysByUnitId(ObjectId unitId) + { + await displayRepository.DeleteManyByUnitId(unitId); + // Invalidar CACHE (colección completa) + await cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.Displays)); + + await authorityService.DeleteByUnitId(unitId); + } + + #endregion + + #region Send Notification + + private void SendSmartDisplayBroadcast(List subscribers, SmartDisplay? config) + { + foreach (var subscriber in subscribers) + _ = clientMessageService.SendAsync(subscriber.Id, OperationType.UpdateSmartDisplayConfig, config); + } + + private void SendNurseDisplayBroadcast(List subscribers, DisplayNurse? config) + { + foreach (var subscriber in subscribers) + _ = clientMessageService.SendAsync(subscriber.Id, OperationType.UpdateNurseDisplayConfig, config); + } + + private void SendDisplayBroadcast(Display display, OperationType operation) + { + var subscribers = subscribersService.GetSubscribers().Where(s => + s.DisplayId == display.Id).ToList(); + + switch (operation) + { + case OperationType.UpdateDisplayPoC: + foreach (var subscriber in subscribers) + _ = clientMessageService.SendAsync(subscriber.Id, operation, null); + break; + } + } + + #endregion + + #endregion +} \ No newline at end of file diff --git a/adas-core.Application/Services/FileService.cs b/adas-core.Application/Services/FileService.cs new file mode 100644 index 00000000..e029ba91 --- /dev/null +++ b/adas-core.Application/Services/FileService.cs @@ -0,0 +1,113 @@ +//using Microsoft.AspNetCore.Http; + +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.DTO; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Serilog; + +namespace adas_core.Application.Services; + +public class FileService : IFileService +{ + private readonly string? _assetsDirectory; + + private readonly ILogger _logger; + private readonly string? _updateDirectory; + + public FileService( + IOptions apiSettings, + ILogger logger + ) + { + _logger = logger; + + if (apiSettings.Value.PathUpdateFiles != null) + _updateDirectory = Path.Combine(apiSettings.Value.PathUpdateFiles); + if (apiSettings.Value.PathToDisplayAssets != null) + _assetsDirectory = Path.Combine(apiSettings.Value.PathToDisplayAssets); + } + + + public async Task CopyUpdateFiles(ICollection files) + { + if (string.IsNullOrWhiteSpace(_updateDirectory)) return false; + + foreach (var file in files) + { + await using var stream = new FileStream(Path.Combine(_updateDirectory, file.FileName), FileMode.Create); + await file.CopyToAsync(stream); + } + + return true; + } + + public async Task UploadAssetFiles(ICollection files, FileEnum.AssetTheme themeParse) + { + if (string.IsNullOrWhiteSpace(_assetsDirectory)) return false; + var directoryInfo = new DirectoryInfo(Path.Combine(_assetsDirectory, themeParse.ToString())); + if (!directoryInfo.Exists) directoryInfo.Create(); + foreach (var file in files) + { + await using var stream = + new FileStream(Path.Combine(_assetsDirectory, themeParse.ToString(), file.FileName), FileMode.Create); + await file.CopyToAsync(stream); + } + + return true; + } + + public List GetAllAssetFile(FileEnum.AssetTheme themeParse) + { + try + { + if (string.IsNullOrWhiteSpace(_assetsDirectory)) return []; + var listToReturn = new List(); + var directoryInfo = new DirectoryInfo(Path.Combine(_assetsDirectory, themeParse.ToString())); + + if (!directoryInfo.Exists) directoryInfo.Create(); + + var files = directoryInfo.GetFiles(); // Obtener todos los archivos en el directorio + foreach (var file in files) + { + var assetDto = new AssetDto + { + Name = file.Name, + Extension = file.Extension, + Path = file.FullName // Obtener la ruta completa del archivo + }; + listToReturn.Add(assetDto); + } + + return listToReturn; + } + catch (Exception e) + { + _logger.LogError("Error while retrieving assets: {eMessage} {eStackTrace}", e.Message, e.StackTrace); + return []; + } + } + + public List GetFilesInDirectory(string directoryPath) + { + List fileList = []; + + try + { + if (Directory.Exists(directoryPath)) + // Obtiene todos los archivos en el directorio + fileList.AddRange(Directory.GetFiles(directoryPath)); + else + Log.Warning("La ruta proporcionada no existe: {directoryPath}", directoryPath); + } + catch (Exception ex) + { + Log.Error("Ocurrió un error al buscar archivos: {exMessage}", ex.Message); + } + + return fileList; + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/GroupedObservationService.cs b/adas-core.Application/Services/GroupedObservationService.cs new file mode 100644 index 00000000..b6750bd1 --- /dev/null +++ b/adas-core.Application/Services/GroupedObservationService.cs @@ -0,0 +1,1262 @@ +using System.Globalization; +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.GroupedObservations; +using adas_core.Domain.Utils; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using static adas_core.Domain.Models.GroupedObservation; + +namespace adas_core.Application.Services; + +public class GroupedObservationService : IGroupedObservationService +{ + private readonly CacheSettings? _cacheSettings; + private readonly ICacheService _cacheService; + private readonly IConfigObservationService _configObservationService; + private readonly ILogger _logger; + private readonly IObservationRepository _observationRepository; + + public GroupedObservationService( + IObservationRepository observationRepository, + IConfigObservationService configObservationService, + ILogger logger, + ICacheService cacheService, + IOptions apiSettings, + IOptions cacheSettings) + { + _observationRepository = observationRepository; + _configObservationService = configObservationService; + _logger = logger; + _cacheService = cacheService; + _cacheSettings = cacheSettings.Value; + } + + + // PUBLIC API (Use cases) + public async Task GenerateGroupedObservation( + ObjectId patientId, + GroupedField groupedField, + string timeZoneId = "Romance Standard Time", + bool cacheIsChecked = false, + CancellationToken ct = default) +{ + // Loader que calcula TODA la lista de observaciones agrupadas + async Task> ComputeObservationsAsync() + { + var result = await _observationRepository.AggregatedPatientGroupedObservations(patientId, groupedField); + + var tz = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId); + + result = FillHours(result, groupedField); + + if (groupedField.StartTimeShift.Count > 0 && + groupedField.Regularity == GroupedObservationEnum.Regularity.Shift) + { + var shiftGroupObservations = GenerateShiftObservations(result, groupedField); + result = CalculateShiftObservations(shiftGroupObservations, groupedField); + } + + if (groupedField.Result.Contains(GroupedObservationEnum.Result.HalfHour)) + { + result = CalculateHalfHourObservations(result); + result = FillHours(result, groupedField); + } + + if (groupedField.Result.Contains(GroupedObservationEnum.Result.LastFilled)) + result = await CalculateLastFilledObservations(result, groupedField, patientId); + + var list = new List(); + + foreach (var it in result.ToList()) + { + var id = it.TryGetElement("_id", out var _) ? it.GetElement("_id").Value.AsBsonDocument : null; + var name = id?.Get("name")?.AsString; + if (name is null or "_id") + { + var idValue = id?.Get("value") as BsonDocument; + name = idValue?.Get("name")?.AsString; + } + + var uTime = it.Get("time")?.ToUniversalTime(); + if (!uTime.HasValue) + { + _logger.LogError("Error getting time to universal time. Bson document: {it}", it); + continue; + } + + var obsTime = TimeZoneInfo.ConvertTimeFromUtc(uTime.Value, tz); + obsTime = DateTime.SpecifyKind(obsTime, DateTimeKind.Local); + + var obs = new GroupedObservationObs + { + Time = obsTime, + Name = name ?? string.Empty + }; + + if (it.TryGetValue("shift", out var shiftValue) && it.TryGetValue("day", out _)) + { + obs.Shift = shiftValue.ToInt32(); + obs.ShiftDate = obs.Time; + } + + if (id != null && id.Contains("min")) obs.MinAlert = id.Get("min")?.ToDouble(); + if (id != null && id.Contains("max")) obs.MaxAlert = id.Get("max")?.ToDouble(); + if (id != null && it.Contains("isFilled")) obs.IsFilled = (bool)it.Get("isFilled"); + + if (groupedField.Result.Contains(GroupedObservationEnum.Result.First)) + { + var g = !it.Contains("first") ? null : it.Get("first")?.AsBsonDocument; + if (g != null) + { + var value = g.Get("value")?.ToObject(); + var min = g.Get("min")?.ToDouble(); + var max = g.Get("max")?.ToDouble(); + var uTim = g.Get("time")?.ToUniversalTime(); + if (!uTim.HasValue) { _logger.LogError("Error getting utc time in 'first': {it}", it); goto addObs; } + + var time = TimeZoneInfo.ConvertTimeFromUtc(uTim.Value, tz); + time = DateTime.SpecifyKind(time, DateTimeKind.Local); + if (string.IsNullOrEmpty(name) || value == null) + { _logger.LogError("Invalid 'first' grouped obs (name/value) patient:{patientId}", patientId); goto addObs; } + + obs.First = new GroupedObservationObsValue( + value, time, + await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.First, name, value, min, max) + ); + } + } + + if (groupedField.Result.Contains(GroupedObservationEnum.Result.Last)) + { + var g = !it.Contains("last") ? null : it.Get("last")?.AsBsonDocument; + if (g != null) + { + var value = g.Get("value")?.ToObject(); + var min = g.Get("min")?.ToDouble(); + var max = g.Get("max")?.ToDouble(); + var uTim = g.Get("time")?.ToUniversalTime(); + if (!uTim.HasValue) { _logger.LogError("Error getting utc time in 'last': {it}", it); goto addObs; } + + var time = TimeZoneInfo.ConvertTimeFromUtc(uTim.Value, tz); + time = DateTime.SpecifyKind(time, DateTimeKind.Local); + if (string.IsNullOrEmpty(name) || value == null) + { _logger.LogError("Invalid 'last' grouped obs (name/value) patient:{patientId}", patientId); goto addObs; } + + obs.Last = new GroupedObservationObsValue( + value, time, + await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Last, name, value, min, max) + ); + } + } + + if (groupedField.Result.Contains(GroupedObservationEnum.Result.Min)) + { + var g = !it.Contains("min") ? null : it.Get("min")?.AsBsonDocument; + if (g != null) + { + var value = g.Get("value")?.ToObject(); + var min = g.Get("min")?.ToDouble(); + var max = g.Get("max")?.ToDouble(); + var uTim = g.Get("time")?.ToUniversalTime(); + if (!uTim.HasValue) { _logger.LogError("Error getting utc time in 'min': {it}", it); goto addObs; } + + var time = TimeZoneInfo.ConvertTimeFromUtc(uTim.Value, tz); + time = DateTime.SpecifyKind(time, DateTimeKind.Local); + if (string.IsNullOrEmpty(name) || value == null) + { _logger.LogError("Invalid 'min' grouped obs (name/value) patient:{patientId}", patientId); goto addObs; } + + obs.Min = new GroupedObservationObsValue( + value, time, + await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Min, name, value, min, max) + ); + } + } + + if (groupedField.Result.Contains(GroupedObservationEnum.Result.Max)) + { + var g = !it.Contains("max") ? null : it.Get("max")?.AsBsonDocument; + if (g != null) + { + var value = g.Get("value")?.ToObject(); + var min = g.Get("min")?.ToDouble(); + var max = g.Get("max")?.ToDouble(); + var uTim = g.Get("time")?.ToUniversalTime(); + if (!uTim.HasValue) { _logger.LogError("Error getting utc time in 'max': {it}", it); goto addObs; } + + var time = TimeZoneInfo.ConvertTimeFromUtc(uTim.Value, tz); + time = DateTime.SpecifyKind(time, DateTimeKind.Local); + if (string.IsNullOrEmpty(name) || value == null) + { _logger.LogError("Invalid 'max' grouped obs (name/value) patient:{patientId}", patientId); goto addObs; } + + obs.Max = new GroupedObservationObsValue( + value, time, + await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Max, name, value, min, max) + ); + } + } + + if (groupedField.Result.Contains(GroupedObservationEnum.Result.Sum)) + { + var value = !it.Contains("sum") ? null : it.Get("sum")?.ToObject(); + if (value != null && !string.IsNullOrEmpty(name)) + { + obs.Sum = new GroupedObservationObsValue( + value, null, + await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Sum, name, value, null, null) + ); + } + } + + if (groupedField.Result.Contains(GroupedObservationEnum.Result.Average)) + { + var value = !it.Contains("average") ? null : it.Get("average")?.ToObject(); + if (value != null && !string.IsNullOrEmpty(name)) + { + obs.Average = new GroupedObservationObsValue( + value, null, + await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Average, name, value, null, null) + ); + } + } + + if (groupedField.Result.Contains(GroupedObservationEnum.Result.Count)) + { + var value = !it.Contains("count") ? null : it.Get("count")?.AsNullableInt32; + if (value != null && !string.IsNullOrEmpty(name)) + { + obs.Count = new GroupedObservationObsValue( + value, null, + await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Count, name, value, null, null) + ); + } + } + + if (groupedField.Result.Contains(GroupedObservationEnum.Result.LastFilled)) + { + var g = !it.Contains("lastfilled") ? null : it.Get("lastfilled")?.AsBsonDocument; + if (g != null) + { + var value = g.Get("value")?.ToObject(); + var min = g.Get("min")?.ToDouble(); + var max = g.Get("max")?.ToDouble(); + var uTim = g.Get("time")?.ToUniversalTime(); + if (!uTim.HasValue) { _logger.LogError("Error getting utc time in 'lastfilled': {it}", it); goto addObs; } + + var time = TimeZoneInfo.ConvertTimeFromUtc(uTim.Value, tz); + time = DateTime.SpecifyKind(time, DateTimeKind.Local); + + if (value != null && !string.IsNullOrEmpty(name)) + { + obs.LastFilled = new GroupedObservationObsValue( + value, time, + await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.LastFilled, name, value, min, max) + ); + } + } + } + + if (groupedField.Result.Contains(GroupedObservationEnum.Result.HalfHour)) + { + var g = !it.Contains("halfhour") ? null : it.Get("halfhour")?.AsBsonDocument; + if (g != null) + { + var value = g.Get("value")?.ToObject(); + var min = g.Get("min")?.ToDouble(); + var max = g.Get("max")?.ToDouble(); + var uTim = g.Get("time")?.ToUniversalTime(); + if (!uTim.HasValue) { _logger.LogError("Error getting utc time in 'halfhour': {it}", it); goto addObs; } + + var time = TimeZoneInfo.ConvertTimeFromUtc(uTim.Value, tz); + time = DateTime.SpecifyKind(time, DateTimeKind.Local); + + if (value != null && !string.IsNullOrEmpty(name)) + { + obs.HalfHour = new GroupedObservationObsValue( + value, time, + await GroupedObservationStatus(groupedField, GroupedObservationEnum.Result.Last, name, value, min, max) + ); + } + } + } + + addObs: + _logger.LogTrace("PatientId: {patientId}. Add to grouped observation: {obs}", patientId, obs); + list.Add(obs); + } + + return list; + } + + // caché explícita por clave + TTL --- + if (!cacheIsChecked) + { + var (key, ttl) = CacheKeys.GroupedObsKeyWithTtl(_cacheSettings, patientId, groupedField.Name ?? string.Empty); + + var observations = await _cacheService.GetOrSetObjectAsync( + key, + ComputeObservationsAsync, + ttl, + ct); + + return new GroupedObservation + { + PatientId = patientId, + Group = groupedField.Group, + Name = groupedField.Name, + Observations = observations + }; + } + + // Re-cálculo forzado (sin tocar caché) + var computed = await ComputeObservationsAsync(); + + return new GroupedObservation + { + PatientId = patientId, + Group = groupedField.Group, + Name = groupedField.Name, + Observations = computed + }; +} + + public async Task GenerateGroupedObservation( + ObjectId patientId, + GroupedField groupedField, + List wsgLastGroupedObservationObs, + PatientObservation obs, + string timeZoneId = "Romance Standard Time") + { + + // Estos resultados requieren recálculo completo (no incremental) + if (groupedField.Result.Contains(GroupedObservationEnum.Result.Average) || + groupedField.Result.Contains(GroupedObservationEnum.Result.HalfHour) || + groupedField.Result.Contains(GroupedObservationEnum.Result.Sum)) + { + return await GenerateGroupedObservation(patientId, groupedField, timeZoneId, true); + } + + // Clave consistente con CacheDispatcher / backends + var cacheKey = $"GroupedObs:{patientId}:{groupedField.Name}"; + + // Intentar leer caché actual + var cached = await _cacheService.GetObjectAsync>(cacheKey, true); + if (cached != null) + { + _logger.LogInformation("Return cached grouped obs (incremental)"); + + // Actualizar lista en memoria + var updated = LocateCurrentObsInGroup(cached, obs, groupedField); + + // Persistir nueva lista (SET incremental) + await _cacheService.SetObjectAsync(cacheKey, updated, true); + + return new GroupedObservation + { + PatientId = patientId, + Group = groupedField.Group, + Name = groupedField.Name, + Observations = updated + }; + } + // Si MISS, recálculo completo y salida + return await GenerateGroupedObservation(patientId, groupedField, timeZoneId, true); + } + + public async Task> FindLastObservations(ObjectId patientId, int num = 2, + List? filterObservations = null) + { + var result = await _observationRepository.AggregatedPatientLastObservations(patientId, num, filterObservations); + return result; + } + + public async Task CreateNextEmptyObs(WsSubscriberGrouped ws) + { + var gf = new GroupedField + { + Max = ws.Max, + Since = ws.Since, + StartTimeShift = ws.StartTimeShift, + Regularity = ws.Regularity, + Result = ws.Result, + Names = ws.Names, + Name = ws.Names.First(), + Group = ws.Group.FirstOrDefault().Value + }; + + // Mantenemos el uso del HashCode + var currentObsList = await _cacheService.GetObjectAsync>(ws.HashCode, false); + + if (currentObsList is { Count: > 0 }) + { + var orderCurrentObsList = currentObsList.OrderBy(c => c.Time).ToList(); + + foreach (var name in gf.Names) + { + var obs = orderCurrentObsList.LastOrDefault(c => c.Name == name) + ?? orderCurrentObsList.LastOrDefault(); + if (obs != null) + { + var (timesLeft, multiplierFactor) = TimesLeftToCreateEmptyObs(obs, gf); + for (var i = 1; i <= timesLeft; i++) + orderCurrentObsList.Add(CreateNextObs(obs, gf, i * multiplierFactor)); + } + } + + // Ajuste de ventana temporal a mostrar según Regularity + DateTime dateLimit; + var requireMinutes = false; + var requireSeconds = false; + + switch (gf.Regularity) + { + case GroupedObservationEnum.Regularity.Minute: + dateLimit = new DateTime( + DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, + DateTime.Now.Hour, DateTime.Now.Minute, 0 + ).AddMinutes(gf.Max * -1); + requireMinutes = true; + break; + + case GroupedObservationEnum.Regularity.Second: + dateLimit = new DateTime( + DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, + DateTime.Now.Hour, DateTime.Now.Minute, DateTime.Now.Second + ).AddSeconds(gf.Max * -1); + requireMinutes = true; + requireSeconds = true; + break; + + default: + dateLimit = new DateTime( + DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, + DateTime.Now.Hour, 0, 0 + ).AddHours(gf.Max * -1); + break; + } + + var cacheResult = orderCurrentObsList + .Where(c => + new DateTime( + c.Time.Year, c.Time.Month, c.Time.Day, c.Time.Hour, + requireMinutes ? c.Time.Minute : 0, + requireSeconds ? c.Time.Second : 0 + ) >= dateLimit) + .ToList(); + + // Persistimos la lista incrementada en la misma clave para no romper el flujo actual. + await _cacheService.SetObjectAsync(ws.HashCode, cacheResult, false); + + return new GroupedObservation + { + PatientId = ws.PatientId, + Group = gf.Group, + Name = gf.Name, + Observations = cacheResult + }; + } + + + // Si no hay caché o hay MISS → recálculo completo (sin usar caché en esta ruta). + return await GenerateGroupedObservation(ws.PatientId, gf, ws.TimeZoneId, true); + } + + + // PRIVATE: High-level composition steps + public List FillHours(List result, GroupedField groupedField) + { + var now = DateTime.UtcNow; + //var resultNumber = groupedField.Max; + var names = groupedField.Names.Count > 1 ? groupedField.Names : [groupedField.Name ?? string.Empty]; + + if (result.Count >= groupedField.Max * names.Count) + return result; //We have data from each hour/day/minute, dont need to calc. + List fillResult = []; + //var first = result.FirstOrDefault(); + foreach (var name in names) + { + var filteredDocuments = result.Where(doc => doc["_id"]["name"] == name).ToList(); + if (filteredDocuments.Count >= groupedField.Max) + { + filteredDocuments = filteredDocuments.OrderBy(e => e.Get("time")?.ToLocalTime()).ToList(); + fillResult.AddRange(filteredDocuments); + continue; + } + + var expectedDate = DateTime.MinValue; + var inverseCounter = 0; + for (var i = groupedField.Max; i != 0; i--) + { + var expectedMatch = true; + + var value = filteredDocuments.ElementAtOrDefault(inverseCounter); + + DateTime? time = null; + + if (value == null) + expectedMatch = false; + else + //Estamos recogiendo la hora en UTC + time = value.Get("time")?.ToUniversalTime(); + + + switch (groupedField.Regularity) + { + case GroupedObservationEnum.Regularity.Day: + expectedDate = now.AddDays(-i); + if (!time.HasValue || expectedDate.Day != time.Value.Day) expectedMatch = false; + break; + case GroupedObservationEnum.Regularity.Hour: + case GroupedObservationEnum.Regularity.Shift: + expectedDate = now.AddHours(-i); + if (!time.HasValue || expectedDate.Hour != time.Value.Hour) expectedMatch = false; + break; + case GroupedObservationEnum.Regularity.Minute: + expectedDate = now.AddMinutes(-i); + if (!time.HasValue || expectedDate.Minute != time.Value.Minute) expectedMatch = false; + break; + case GroupedObservationEnum.Regularity.Second: + expectedDate = now.AddSeconds(-i); + if (!time.HasValue || expectedDate.Second != time.Value.Second) expectedMatch = false; + break; + } + + + if (!expectedMatch) + { + var newEntry = new BsonDocument + { + { + "_id", new BsonDocument + { + { "year", expectedDate.Year }, + { "month", expectedDate.Month }, + { "day", expectedDate.Day }, + { "hour", expectedDate.Hour }, + { "minute", expectedDate.Minute }, + { "name", groupedField.Name } + } + }, + { "time", new BsonDateTime(expectedDate) }, //TODO loop del result entero + //{ Result.HalfHour.ToString().ToLower(), new BsonDocument { { "value", 0 }, { "min",0 }, { "max",0 }, { "time", new BsonDateTime(expectedDate) } } } + // { groupedField.regularity.ToString().ToLower(), new BsonDocument { { "value", 0 }, { "min",0 }, { "max",0 }, { "time", new BsonDateTime(expectedDate) } } } + { "isFilled", true } //Marcamos como hora añadida + }; + + foreach (var r in groupedField.Result) + switch (r) + { + case GroupedObservationEnum.Result.LastFilled: + case GroupedObservationEnum.Result.LastAll: + continue; + case GroupedObservationEnum.Result.Sum: + case GroupedObservationEnum.Result.Average: + case GroupedObservationEnum.Result.Count: + newEntry.Add(new BsonElement(r.ToString().ToLower(), 0)); + continue; + case GroupedObservationEnum.Result.HalfHour: + newEntry.Add("all", + new BsonArray + { + new BsonDocument + { + { "value", 0 }, { "min", 0 }, { "max", 0 }, + { "time", new BsonDateTime(expectedDate) } + } + }); + continue; + default: + newEntry.Add(r.ToString().ToLower(), + new BsonDocument + { + { "value", 0 }, { "min", 0 }, { "max", 0 }, + { "time", new BsonDateTime(expectedDate) } + }); + continue; + } + + if (groupedField.Result.Count != 1 || + !groupedField.Result.Contains(GroupedObservationEnum.Result.LastFilled)) + filteredDocuments.Add(newEntry); + + filteredDocuments = filteredDocuments.OrderBy(e => e.Get("time")?.ToLocalTime()).ToList(); + } + + inverseCounter++; + } + + if (groupedField.Max <= filteredDocuments.Count) + { + var value = filteredDocuments.ElementAtOrDefault(inverseCounter); + + if (value == null) + { + var newEntry = new BsonDocument + { + { + "_id", new BsonDocument + { + { "year", DateTime.UtcNow.Year }, + { "month", DateTime.UtcNow.Month }, + { "day", DateTime.UtcNow.Day }, + { "hour", DateTime.UtcNow.Hour }, + { "minute", DateTime.UtcNow.Minute }, + { "name", groupedField.Name } + } + }, + { "time", new BsonDateTime(DateTime.UtcNow) }, //TODO loop del result entero + { "isFilled", true } //Marcamos como hora añadida + }; + + foreach (var r in groupedField.Result) + switch (r) + { + case GroupedObservationEnum.Result.LastFilled: + case GroupedObservationEnum.Result.LastAll: + continue; + case GroupedObservationEnum.Result.Sum: + case GroupedObservationEnum.Result.Average: + case GroupedObservationEnum.Result.Count: + newEntry.Add(new BsonElement(r.ToString().ToLower(), 0)); + continue; + case GroupedObservationEnum.Result.HalfHour: + newEntry.Add("all", + new BsonArray + { + new BsonDocument + { + { "value", 0 }, { "min", 0 }, { "max", 0 }, + { "time", new BsonDateTime(expectedDate) } + } + }); + continue; + default: + newEntry.Add(r.ToString().ToLower(), + new BsonDocument + { + { "value", 0 }, { "min", 0 }, { "max", 0 }, + { "time", new BsonDateTime(expectedDate) } + }); + continue; + } + + if (groupedField.Result.Count != 1 || + !groupedField.Result.Contains(GroupedObservationEnum.Result.LastFilled)) + filteredDocuments.Add(newEntry); + + filteredDocuments = filteredDocuments.OrderBy(e => e.Get("time")?.ToLocalTime()).ToList(); + } + + fillResult.AddRange(filteredDocuments); + } + } + + fillResult = fillResult.OrderBy(e => e.Get("time")?.ToLocalTime()).ToList(); + + return fillResult; + } + + public async Task> CalculateLastFilledObservations(List result, + GroupedField groupedField, ObjectId patientId) + { + List resultToReturn = []; + if (groupedField.Names.Any()) + foreach (var name in groupedField.Names) + { + var resultFiltedByName = result.FindAll(b => b.Get("_id")?.AsBsonDocument.Get("name") == name); + var resultCalculated = await LastFilledCalc(resultFiltedByName, groupedField, patientId, name); + resultToReturn.AddRange(resultCalculated); + } + else + return await LastFilledCalc(result, groupedField, patientId, groupedField.Name); + + + return resultToReturn; + } + + private async Task> LastFilledCalc(List result, GroupedField groupedField, + ObjectId patientId, string? name) + { + var now = DateTime.UtcNow; + + if (result.Count == groupedField.Max) + return result; //We have data from each hour/day/minute, dont need to calc. + + var first = result.FirstOrDefault(); + + + object? lastObservationValue = null; + + var expectedFirstDate = groupedField.Regularity switch + { + GroupedObservationEnum.Regularity.Day => now.AddDays(-groupedField.Max), + GroupedObservationEnum.Regularity.Hour => now.AddHours(-groupedField.Max), + GroupedObservationEnum.Regularity.Minute => now.AddMinutes(-groupedField.Max), + GroupedObservationEnum.Regularity.Second => now.AddSeconds(-groupedField.Max), + _ => DateTime.MinValue + }; + + //first > fecha minima tenemos que buscar el ultimo valor + if (first == null || first.Get("time")?.ToUniversalTime().CompareTo(expectedFirstDate) > 0) + { + var lastObservation = + await _observationRepository.FindLastObservationBeforeDate(patientId, groupedField.Name, + expectedFirstDate); + + if (lastObservation != null) lastObservationValue = lastObservation.Value; + } + + var expectedDate = DateTime.MinValue; + var inverseCounter = 0; + for (var i = groupedField.Max; i != 0; i--) + { + var expectedMatch = true; + + var value = result.ElementAtOrDefault(inverseCounter); + + DateTime? time = null; + + if (value == null) + expectedMatch = false; + else + time = value.Get("time")?.ToUniversalTime(); + + + switch (groupedField.Regularity) + { + case GroupedObservationEnum.Regularity.Day: + expectedDate = now.AddDays(-i); + if (!time.HasValue || expectedDate.Day != time.Value.Day) expectedMatch = false; + break; + case GroupedObservationEnum.Regularity.Hour: + expectedDate = now.AddHours(-i); + if (!time.HasValue || expectedDate.Hour != time.Value.Hour) expectedMatch = false; + break; + case GroupedObservationEnum.Regularity.Minute: + expectedDate = now.AddMinutes(-i); + if (!time.HasValue || expectedDate.Minute != time.Value.Minute) expectedMatch = false; + break; + case GroupedObservationEnum.Regularity.Second: + expectedDate = now.AddSeconds(-i); + if (!time.HasValue || expectedDate.Second != time.Value.Second) expectedMatch = false; + break; + } + + + if (!expectedMatch) + { + var lastResult = result.ElementAtOrDefault(inverseCounter - 1); + object? lastFilledValue; + if (lastResult == null) + { + lastFilledValue = lastObservationValue; + } + else + { + var lastfilled = GroupedObservationEnum.Result.LastFilled.ToString().ToLower(); + lastFilledValue = lastResult.Get(lastfilled)?.AsBsonDocument.Get("value"); + } + + lastFilledValue ??= 0; + + var newEntry = new BsonDocument + { + { + "_id", new BsonDocument + { + { "year", expectedDate.Year }, + { "month", expectedDate.Month }, + { "day", expectedDate.Day }, + { "hour", expectedDate.Hour }, + { "minute", expectedDate.Minute }, + { "name", name } + } + }, + { "time", new BsonDateTime(expectedDate) }, + { + GroupedObservationEnum.Result.LastFilled.ToString().ToLower(), + new BsonDocument + { + { "value", lastFilledValue.ToString() }, { "min", 0 }, { "max", 0 }, + { "time", new BsonDateTime(expectedDate) } + } + } + }; + result.Add(newEntry); + result = result.OrderBy(e => e.Get("time")?.ToLocalTime()).ToList(); + } + + inverseCounter++; + } + + return result; + } + + public List CalculateShiftObservations(List shiftGroupObservations, + GroupedField groupedField) + { + var shiftGrouped = new List(); + + if (groupedField.Result.Contains(GroupedObservationEnum.Result.Sum)) + { + var shiftGroups = + shiftGroupObservations.GroupBy(shift => new + { V1 = shift.GetElement("shift").ToString(), V = shift.GetElement("day").ToString() }) + .Select(s => s.ToList()) + .ToList(); + + + shiftGroups.ForEach(shiftGroup => + { + var shiftSum = shiftGroup.Select(shift => shift.Get("sum")?.ToInt32()).Sum(); + var shiftToAdd = shiftGroup.FirstOrDefault(); + + shiftToAdd?.SetElement(new BsonElement("sum", shiftSum)); + if (shiftGroup.Select(shift => shift.Get("isFilled")).Any(s => s == false)) + shiftToAdd?.SetElement(new BsonElement("isFilled", false)); + + if (shiftToAdd != null) + shiftGrouped.Add(shiftToAdd); + }); + + + return shiftGrouped; + } + + return shiftGroupObservations; + } + + //MIGUEL: EJEMPLO SI LAS 12 TIENEN 4 VALORES 12:15 => 5, 12:20 => 10, 12:45 => 20 Y LAS 13 NO TIENE VALOR. 12 DEBERÍA 5 Y 13 DEBERÍA SER + // ACTUALMENTE A LAS 12 MOSTRARIA EL ULTIMO DE LA MEDIA HORA Y A LAS 13? + public List CalculateHalfHourObservations(List result) + { + //HalfHour añadir que se rellenen las horas que no existen + for (var i = 0; i < result.Count; i++) + { + var document = result[i]; + BsonDocument? nextDocument = null; + + if (i + 1 < result.Count) + nextDocument = result[i + 1]; + + document.TryGetValue("all", out var docvalues); + + + List groups = []; + + if (docvalues != null) + groups = docvalues.AsBsonArray.ToList().GroupBy(x => + { + var stamp = x.AsBsonDocument.Get("time")?.ToLocalTime(); + if (!stamp.HasValue) return null; + stamp = stamp.Value.AddMinutes(-(stamp.Value.Minute % 30)); + stamp = stamp.Value.AddMilliseconds(-stamp.Value.Millisecond - 1000 * stamp.Value.Second); + return stamp; + }).Select(g => + g.OrderByDescending(c => c.AsBsonDocument.Get("time")?.ToLocalTime()).FirstOrDefault()) + .ToList(); + + + if (groups.Count > 0) + { + if (nextDocument == null) + { + document.Set("halfhour", groups.Last()); + break; + } + + if (groups.Count > 1) + { + var nextDocumentValues = nextDocument.Get("all")?.AsBsonArray.ToList(); + + if (nextDocumentValues == null || nextDocumentValues.Count == 0 + || (nextDocumentValues.Count == 1 && + nextDocument.Get("all")?.AsBsonArray.ToList()[0].AsBsonDocument + .Get("value")?.ToString() == + "0")) //El siguiente registro no tiene tomas + { + nextDocument.Set("halfhour", groups.LastOrDefault()); + document.Set("halfhour", groups.FirstOrDefault()); + } + else + { + document.Set("halfhour", groups.LastOrDefault()); + } + } + else + { + if (document.Get("halfhour")?.AsBsonDocument == null) + document.Set("halfhour", groups.LastOrDefault()); + } + } + else + { + if (!document.TryGetValue("halfhour", out _)) document.Set("halfhour", new BsonDocument()); + } + } + + + return result; + } + + public List GenerateShiftObservations(List result, GroupedField groupedField) + { + result.ForEach(item => + { + DateTime? obsDateTime = null; + var id = item.Get("_id")?.AsBsonDocument; + if (id == null) + return; + + + var timeStr = id.Get("hour")?.ToString()?.PadLeft(2, '0') ?? "00"; + if (id.TryGetValue("minute", out _)) + { + timeStr += ":" + id.Get("minute")?.ToString()?.PadLeft(2, '0'); + if (DateTime.TryParseExact(timeStr, "HH:mm", CultureInfo.InvariantCulture, DateTimeStyles.None, + out var r1)) + obsDateTime = r1; + } + else + { + if (DateTime.TryParseExact(timeStr, "HH", CultureInfo.InvariantCulture, DateTimeStyles.None, + out var r2)) + obsDateTime = r2; + } + + + obsDateTime = obsDateTime?.ToLocalTime(); + int shift; + int day; + //Return all dates in shift array before obsDateTime, and take last, that will be the closest shift date. + var expectedHour = obsDateTime?.Hour.ToString().Length < 2 + ? $"0{obsDateTime.Value.Hour}" + : obsDateTime?.Hour.ToString(); + + var expectedMinutes = obsDateTime?.Minute.ToString().Length < 2 + ? $"0{obsDateTime.Value.Minute}" + : obsDateTime?.Minute.ToString(); + + + var shiftSelected = groupedField.StartTimeShift.LastOrDefault + (i => DateTime.ParseExact(i, "HH:mm", CultureInfo.InvariantCulture).Ticks <= DateTime + .ParseExact($"{expectedHour}:{expectedMinutes}", "HH:mm", CultureInfo.InvariantCulture) + .Ticks); + + var havemin = id.TryGetValue("minute", out var minuteV); + var year = id.Get("year")?.ToInt32(); + var month = id.Get("month")?.ToInt32(); + var dayy = id.Get("day")?.ToInt32(); + var hour = id.Get("hour")?.ToInt32(); + + if (!year.HasValue || !month.HasValue || !dayy.HasValue || !hour.HasValue) + { + _logger.LogError("Error getting dateTime from Bson: {id}", id); + return; + } + + var dateUtc = new DateTime(year.Value, month.Value, dayy.Value, hour.Value, havemin ? minuteV.ToInt32() : 0, + 0); + + if (!groupedField.StartTimeShift.Any()) + { + _logger.LogError( + "Error generating shift observations. StartTimeShift List in null or empty. {groupedField}", + groupedField); + return; + } + + if (shiftSelected == null) + { + day = dateUtc.ToLocalTime().AddDays(-1).Day; + shift = groupedField.StartTimeShift.Count - 1; + } + else + { + day = dateUtc.ToLocalTime().Day; + shift = groupedField.StartTimeShift.IndexOf(shiftSelected); + } + + item.Add(new BsonElement("day", day)); + item.Add(new BsonElement("shift", shift)); + }); + + return result; + } + + + + // PRIVATE: Incremental item adjustment + private List LocateCurrentObsInGroup(List group, + PatientObservation obs, GroupedField groupedField) + { + // Check if we need to modify the current value or add new one + var specifyMinutes = + groupedField.Regularity is GroupedObservationEnum.Regularity.Minute + or GroupedObservationEnum.Regularity.Second; + var specifySeconds = groupedField.Regularity == GroupedObservationEnum.Regularity.Second; + var obsTime = new DateTime(obs.Time.Year, obs.Time.Month, obs.Time.Day, obs.Time.Hour, + specifyMinutes ? obs.Time.Minute : 0, + specifySeconds ? obs.Time.Second : 0); + var obsIndexInThisHour = group.FindIndex(c => + new DateTime(c.Time.Year, c.Time.Month, c.Time.Day, c.Time.Hour, + specifyMinutes ? c.Time.Minute : 0, + specifySeconds ? c.Time.Second : 0) == obsTime && c.Name == obs.Name); + if (obsIndexInThisHour >= 0) + { + // Obs to modify found + var valueIsParsed = double.TryParse(obs.Value.ToString(), out var valueParsed); + if (!valueIsParsed) + return group; + group[obsIndexInThisHour].IsFilled = false; + group[obsIndexInThisHour].Time = obsTime; + foreach (var result in groupedField.Result) + switch (result) + { + case GroupedObservationEnum.Result.Max: + var valueMaxIsParsed = double.TryParse(group[obsIndexInThisHour].Max?.Value.ToString(), + out var valueMaxParsed); + if (!valueMaxIsParsed) continue; + //actual value is higher, is relevant + if (group[obsIndexInThisHour].Max?.Value != null && valueParsed > valueMaxParsed) + { + group[obsIndexInThisHour].Max!.Value = obs.Value; + group[obsIndexInThisHour].Max!.Time = obs.Time; + group[obsIndexInThisHour].Max!.Type = obs.Status; + group[obsIndexInThisHour].Max!.Type = obs.Status; + } + + break; + case GroupedObservationEnum.Result.Min: + var valueMinIsParsed = double.TryParse(group[obsIndexInThisHour].Max?.Value.ToString(), + out var valueMinParsed); + if (!valueMinIsParsed) continue; + if (group[obsIndexInThisHour].Max?.Value != null && valueParsed < valueMinParsed) + { + group[obsIndexInThisHour].Min!.Value = obs.Value; + group[obsIndexInThisHour].Min!.Time = obs.Time; + group[obsIndexInThisHour].Min!.Type = obs.Status; + } + + break; + case GroupedObservationEnum.Result.Last: + if (group[obsIndexInThisHour].Last?.Time < obs.Time) + { + group[obsIndexInThisHour].Last!.Value = obs.Value; + group[obsIndexInThisHour].Last!.Time = obs.Time; + group[obsIndexInThisHour].Last!.Type = obs.Status; + } + + break; + case GroupedObservationEnum.Result.LastFilled: + if (group[obsIndexInThisHour].LastFilled?.Time < obs.Time) + { + group[obsIndexInThisHour].LastFilled!.Value = obs.Value; + group[obsIndexInThisHour].LastFilled!.Time = obs.Time; + group[obsIndexInThisHour].LastFilled!.Type = obs.Status; + } + + break; + } + } + else + { + // Delete obs autofilled and in time of the new obs + var obsIndexInThisHourToRemove = group.FindIndex(c => + new DateTime(c.Time.Year, c.Time.Month, c.Time.Day, c.Time.Hour, + specifyMinutes ? c.Time.Minute : 0, + specifySeconds ? c.Time.Second : 0) == obsTime && c.IsFilled); + if (obsIndexInThisHourToRemove >= 0) + group.RemoveAt(obsIndexInThisHourToRemove); + // Obs to modify not found add it + var ngo = new GroupedObservationObs + { + Name = obs.Name ?? string.Empty, + MinAlert = obs.Min, + MaxAlert = obs.Max, + Time = obsTime, + IsFilled = false, + ShiftDate = obs.Time, + Shift = GetShift(obs.Time, groupedField.StartTimeShift) + }; + + foreach (var result in groupedField.Result) + switch (result) + { + case GroupedObservationEnum.Result.Max: + ngo.Max = new GroupedObservationObsValue(obs.Value, obs.Time, obs.Status); + break; + case GroupedObservationEnum.Result.Min: + ngo.Min = new GroupedObservationObsValue(obs.Value, obs.Time, obs.Status); + break; + case GroupedObservationEnum.Result.Last: + ngo.Last = new GroupedObservationObsValue(obs.Value, obs.Time, obs.Status); + break; + case GroupedObservationEnum.Result.LastFilled: + ngo.LastFilled = new GroupedObservationObsValue(obs.Value, obs.Time, obs.Status); + break; + } + + group.Add(ngo); + } + + // Check if we have more obs than we require to show + DateTime dateLimit; + switch (groupedField.Regularity) + { + case GroupedObservationEnum.Regularity.Minute: + dateLimit = new DateTime(obs.Time.Year, obs.Time.Month, obs.Time.Day, obs.Time.Hour, obs.Time.Minute, 0) + .AddMinutes(groupedField.Max * -1); + break; + case GroupedObservationEnum.Regularity.Second: + dateLimit = new DateTime(obs.Time.Year, obs.Time.Month, obs.Time.Day, obs.Time.Hour, obs.Time.Minute, + obs.Time.Second).AddSeconds(groupedField.Max * -1); + break; + default: + dateLimit = + new DateTime(obs.Time.Year, obs.Time.Month, obs.Time.Day, obs.Time.Hour, 0, 0).AddHours( + groupedField.Max * -1); + break; + } + + return group.Where(c => + new DateTime(c.Time.Year, c.Time.Month, c.Time.Day, c.Time.Hour, specifyMinutes ? c.Time.Minute : 0, + specifySeconds ? c.Time.Second : 0) >= dateLimit).ToList(); + } + + + + // PRIVATE: Low-level utilities + private int? GetShift(DateTime currentObsDate, List groupedFieldStartTimeShift) + { + if (groupedFieldStartTimeShift.Count == 0) return null; + var listTime = GetTimeSpan(groupedFieldStartTimeShift); + var dateTimeSpan = currentObsDate.TimeOfDay; + + for (var i = 0; i < listTime.Count; i++) + { + var nextIndex = (i + 1) % listTime.Count; + var nextTimeSpan = listTime[nextIndex]; + var currentTimeSpan = listTime[i]; + + if (dateTimeSpan >= currentTimeSpan && dateTimeSpan < nextTimeSpan) return i; + } + + return -1; + } + + private List GetTimeSpan(List groupedFieldStartTimeShift) + { + List timeSpans = []; + timeSpans.AddRange(groupedFieldStartTimeShift.Select(TimeSpan.Parse)); + + List newTimeSpans = []; + + for (var i = 0; i < timeSpans.Count; i++) + { + var nextIndex = (i + 1) % timeSpans.Count; + var nextTimeSpan = timeSpans[nextIndex]; + var currentTimeSpan = timeSpans[i]; + var addedTimeSpan = currentTimeSpan.Add(TimeSpan.FromMinutes(1)); + + newTimeSpans.Add(addedTimeSpan < nextTimeSpan ? addedTimeSpan : nextTimeSpan); + } + + return newTimeSpans; + } + + private async Task GroupedObservationStatus(GroupedField groupedField, + GroupedObservationEnum.Result result, string name, object value, double? min, double? max) + { + return await _configObservationService.GroupedObservationStatus(groupedField, result, name, value, min, max); + } + + private (int timesLeft, int multiplierFactor) TimesLeftToCreateEmptyObs(GroupedObservationObs obs, GroupedField gf) + { + var currentDateTime = DateTime.Now; + var timeElapsed = currentDateTime - obs.Time; + double timeToCurrent; + var multipliesFactor = 1; + // Calcula el tiempo transcurrido en la unidad deseada + switch (gf.Regularity) + { + case GroupedObservationEnum.Regularity.Second: + timeToCurrent = timeElapsed.TotalSeconds; + break; + + case GroupedObservationEnum.Regularity.Minute: + timeToCurrent = timeElapsed.TotalMinutes; + multipliesFactor = 60; + break; + + case GroupedObservationEnum.Regularity.Day: + timeToCurrent = timeElapsed.TotalDays; + multipliesFactor = 86400; + break; + + default: + timeToCurrent = timeElapsed.TotalHours; + multipliesFactor = 3600; + break; + } + + var timesLeft = (int)Math.Ceiling(timeToCurrent); + return (timesLeft, multipliesFactor); + } + + private GroupedObservationObs CreateNextObs(GroupedObservationObs sourceObs, GroupedField gf, int seconsToAdd) + { + var lastObs = new GroupedObservationObs + { + Time = sourceObs.Time.AddSeconds(seconsToAdd), + Name = sourceObs.Name, + IsFilled = true + }; + //Log.Debug("Result list:", string.Join(',', Result)); + //Se rellena el tiempo con la hora de la obs anterior por que no hay dato de la actual + GroupedObservationObsValue defaultValue = new(0, sourceObs.Time); + foreach (var r in gf.Result) + { + //Log.Debug("Processing {r}", r.ToString()); + //var props = typeof(GroupedObservationObs).GetProperties(); + //Log.Debug("GroupedObservationObs Poperties list: {list}", string.Join(',', props.ToList())); + + var propInfo = typeof(GroupedObservationObs).GetProperty(r.ToString()); + if (propInfo == null) + { + _logger.LogError("Error Creating Next Obs. Getting property: {R} not found. SourceObs:{Obs}", + r.ToString(), sourceObs); + continue; + } + + propInfo.SetValue(lastObs, defaultValue); + + var gObsValue = propInfo.GetValue(sourceObs); + if (gObsValue == null) + { + _logger.LogWarning("Error Creating Next Obs.Getting property value: {R} not found. SourceObs:{Obs}", + r.ToString(), sourceObs); + continue; + } + + var prevValue = (GroupedObservationObsValue)gObsValue; + + prevValue.Time = sourceObs.Time; + + propInfo.SetValue(sourceObs, prevValue); + } + + if (gf.Regularity == GroupedObservationEnum.Regularity.Shift) + { + lastObs.ShiftDate = lastObs.Time; + lastObs.Shift = GetShift(lastObs.ShiftDate, gf.StartTimeShift); + } + + return lastObs; + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/HistoricalConfigChangesService.cs b/adas-core.Application/Services/HistoricalConfigChangesService.cs new file mode 100644 index 00000000..a579eea5 --- /dev/null +++ b/adas-core.Application/Services/HistoricalConfigChangesService.cs @@ -0,0 +1,116 @@ +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.MongoModels; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Services; + +public class HistoricalConfigChangesService( + IHistoricalConfigChangesRepository historicalConfigChangesRepository, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService) + : IHistoricalConfigChangesService +{ + private readonly ILogger _logger = logger; + + public async Task DeleteHistoricalConfigChange(ObjectId id) + { + var filter = Builders.Filter.Eq("_id", id); + var result = historicalConfigChangesRepository.FindById(id); + await historicalConfigChangesRepository.Collection.DeleteOneAsync(filter); + _logger.LogInformation("Deleted historicalConfigChanges with id: {id}", id); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, result, null); + } + + public async Task FindLastConfigChanges(DisplayConfigEnums.ConfigTypes configType) + { + var result = await historicalConfigChangesRepository.FindLastHistoricalConfigChangesByType(configType); + + return result.FirstOrDefault(); + } + + public async Task Get(ObjectId id) + { + return await historicalConfigChangesRepository.FindById(id); + } + + public async Task> GetAll() + { + return await historicalConfigChangesRepository.FindAll(); + } + + public async Task> GetByType(DisplayConfigEnums.ConfigTypes type, int num = 10) + { + return await historicalConfigChangesRepository.FindLastHistoricalConfigChangesByType(type, num); + } + + + public async Task> GetByUser(string user, + DisplayConfigEnums.ConfigTypes? configTypes = null, int num = 10) + { + return await historicalConfigChangesRepository.FindLastHistoricalConfigChangesByUser(user, configTypes, num); + } + + public async Task InsertOne(HistoricalConfigChanges historicalConfigChanges) + { + try + { + var result = await historicalConfigChangesRepository.InsertOneAsync(historicalConfigChanges) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, result); + return await historicalConfigChangesRepository.InsertOneAsync(historicalConfigChanges); + } + catch (Exception ex) + { + _logger.LogError("Exception inserting historicalConfigChanges {changes} exception:{e} ", + historicalConfigChanges.ToJson(), ex); + return null; + } + } + + public async Task UpdateHistoricalConfigChange( + HistoricalConfigChanges historicalConfigChanges) + { + try + { + var oldHistorical = await historicalConfigChangesRepository.FindById(historicalConfigChanges.Id) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + var result = await historicalConfigChangesRepository.Update(historicalConfigChanges); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldHistorical, result); + return result; + } + catch (Exception ex) + { + _logger.LogError("Exception updating historicalConfigChanges {changes} exception:{e} ", + historicalConfigChanges.ToJson(), ex); + return null; + } + } + + public async Task LogConfigChanged(string user, DisplayConfigEnums.ConfigTypes configType, string newConfig, + string oldConfig) + { + HistoricalConfigChanges historicalConfigChanges = new() + { + ConfigType = configType, + Time = DateTime.Now, + Username = user, + OldConfig = oldConfig, + NewConfig = newConfig + }; + + var result = await InsertOne(historicalConfigChanges); + if (result == null) + _logger.LogError("Error logging config changes. newConfig: {newConfig}, oldConfig: {oldConfig}", + historicalConfigChanges.NewConfig, historicalConfigChanges.OldConfig); + else + _logger.LogDebug("Config changes logged. newConfig: {newConfig}, oldConfig: {oldConfig}", + historicalConfigChanges.NewConfig, historicalConfigChanges.OldConfig); + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IAPIRequestService.cs b/adas-core.Application/Services/Interfaces/IAPIRequestService.cs new file mode 100644 index 00000000..612ebd21 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IAPIRequestService.cs @@ -0,0 +1,9 @@ +using adas_core.Domain.Models; + +namespace adas_core.Application.Services.Interfaces; + +public interface IApiRequestService +{ + Task SaveRequestAsync(ApiRequest apiRequest); + Task SaveRequest(ApiRequest apiRequest); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IAdminPanelService.cs b/adas-core.Application/Services/Interfaces/IAdminPanelService.cs new file mode 100644 index 00000000..ed02102e --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IAdminPanelService.cs @@ -0,0 +1,46 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IAdminPanelService +{ + Task DeleteUnitById(ObjectId unitId); + Task InsertUnit(Unit unit); + + #region Patient + + Task CreatePatient(AdmPanelRequest apiRequest); + Task FindPatientByLocation(PatientLocation location); + Task FindPatientByPatientNumber(string patientNumber); + + Task FindPatientById(ObjectId id); + + //List FindAllPatient(); + Task FindPatient(AdmPanelRequest request); + Task GetUnitDependencyDto(Unit unit); + + Task UpdatePatientLocation(AdmPanelRequest request); + Task UpdatePatientData(AdmPanelRequest request, Patient oldPatient); + Task ArchivePatient(Patient patient); + + #endregion + + #region ConfigObservations + Task CreateConfig(ConfigObservation configObservation); + Task UpdateConfig(ConfigObservation configObservation); + Task DeleteConfigObservationItem(ObjectId id); + + #endregion + + #region Medicienes + + Task GetMedicineById(ObjectId medicineId); + Task PostMedicine(Medicine medicine); + Task UpdateMedicine(Medicine medicine); + Task DeleteMedicineById(string medicineId); + + #endregion +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IAdmissionService.cs b/adas-core.Application/Services/Interfaces/IAdmissionService.cs new file mode 100644 index 00000000..940c0b2f --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IAdmissionService.cs @@ -0,0 +1,34 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IAdmissionService : IApiRequestService +{ + Task GetAdmissionByIdAsync(ObjectId admissionId); + Task DeleteAdmissionByIdAsync(ObjectId admissionId); + Task DeleteAdmissionAsync(Admission admission); + Task DeleteAdmissionsByUnitId(ObjectId unitId); + + Task UpdateAdmissionAsync(Admission admission); + Task> GetAdmissionsAsync(); + Task InsertAdmission(Admission admission); + Task AdmitPatient(Admission admission, bool isNew = false); + Task ReturnPatientToAdmissions(ObjectId patientId); + Task ReturnPatientToAdmissions(ObjectId patientId, Admission adm); + Task> GetAdmissionByLocation(PatientLocation location); + Task> GetAdmissionByPointOfCareId(ObjectId id); + Task> GetAdmissionByPointOfCareIdAndLocale(ObjectId pocId, LocaleEnum locale); + Task> GetAdmissionByUnitIdWithOutPoC(ObjectId unitId); + Task CountAdmissionsByUnitId(ObjectId unitId); + + Task SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId); + Task GetAdmissionByPatientNumber(string patientNumber); + + Task UpdatePatientMasterListItemChange(UpdateOptionMasterListDto opt, IEnumerable unitList, string typeName); + Task DeletePatientMasterListItem(OptionList opt, IEnumerable unitList, string typeName); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IAlarmService.cs b/adas-core.Application/Services/Interfaces/IAlarmService.cs new file mode 100644 index 00000000..dd867926 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IAlarmService.cs @@ -0,0 +1,30 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IAlarmService : IApiRequestService +{ + public Task> FindLastObservationsByField(ObjectId patientId, + List? filterObservations = null); + + Task> FindLastValuesNotExpired(ObjectId patientId, List dataAlarmfields, + List configAlarm); + + public Task MapObservation(PatientObservationAlarm obs, bool onlyByName = false); + public Task MapObservationsByName(PatientObservationAlarm obs); + + Task CalculateAlarmTest(BasePatientObservationValue source, string name); + + Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code, AlarmEnum.Severity severity, + AlarmEnum.Type type); + + Task CheckObservationAlarm(PatientObservation obs4); + + Task ProcessAlarmObservations(List alarmObservations, + List observations, Patient patient, + DateTime messageTime, ObservationData? observationData = null); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IAlertValuesService.cs b/adas-core.Application/Services/Interfaces/IAlertValuesService.cs new file mode 100644 index 00000000..384bfc39 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IAlertValuesService.cs @@ -0,0 +1,9 @@ +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IAlertValuesService +{ + Task FindByKey(ObjectId key); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IAppointmentService.cs b/adas-core.Application/Services/Interfaces/IAppointmentService.cs new file mode 100644 index 00000000..76af07bf --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IAppointmentService.cs @@ -0,0 +1,20 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Services.Interfaces; + +public interface IAppointmentService : IApiRequestService +{ + Task> FindByLocation(PatientLocation location); + Task> GetByPatient(ObjectId patientId); + Task> GetTodayByPatient(ObjectId patientId, CancellationToken ct = default); + Task> GetTodayByPoc(ObjectId pocId, CancellationToken ct = default); + Task> FindByPatientIdAsync(ObjectId patientId); + Task Archive(Patient patient); + Task ArchiveByPatientId(ObjectId id); + Task DeleteByPatientId(ObjectId id); + Task ProcessApiRequest(ApiRequest apiRequest, Patient patient); + Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IArchivePatientCarePlanService.cs b/adas-core.Application/Services/Interfaces/IArchivePatientCarePlanService.cs new file mode 100644 index 00000000..424a5bef --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IArchivePatientCarePlanService.cs @@ -0,0 +1,21 @@ +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IArchivePatientCarePlanService +{ + Task?> FindByPatientId(ObjectId id); + Task?> FindByPatientId(string id); + Task?> FindByPatientNumber(string id); + + Task> FindAll(); + + // Task UpdateTreatment(PatientCarePlan patientCarePla, List options); + // Task UpdateProcedure(PatientCarePlan patientCarePla, List options); + Task InsertOneAsync(PatientCarePlan patientCarePla); + Task InsertManyAsync(List patientCarePla); + + // Task Update(PatientCarePlan oldPatientCarePla, PatientCarePlan newPatientCarePla); + // Task FindByIds(ObjectId oldPatientCarePlaId, string? oldPatientCarePlaPatientId, string? oldPatientCarePlaPatientNumber); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IArchivedPatientObservationService.cs b/adas-core.Application/Services/Interfaces/IArchivedPatientObservationService.cs new file mode 100644 index 00000000..0d15a50c --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IArchivedPatientObservationService.cs @@ -0,0 +1,9 @@ +using adas_core.Domain.Models; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IArchivedPatientObservationService +{ + public Task> FindArchivedPatientObservationsFromPatient(ObjectId patientId); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IArchivedPatientService.cs b/adas-core.Application/Services/Interfaces/IArchivedPatientService.cs new file mode 100644 index 00000000..fd342a72 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IArchivedPatientService.cs @@ -0,0 +1,8 @@ +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Application.Services.Interfaces; + +public interface IArchivedPatientService +{ + public Task> FindAllPatients(); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IArchivedPatientTreatmentService.cs b/adas-core.Application/Services/Interfaces/IArchivedPatientTreatmentService.cs new file mode 100644 index 00000000..fc5cfdac --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IArchivedPatientTreatmentService.cs @@ -0,0 +1,9 @@ +using adas_core.Domain.Models; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IArchivedPatientTreatmentService +{ + Task> FindAllPatientTreatmentsByPatient(ObjectId patientId); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IAuthService.cs b/adas-core.Application/Services/Interfaces/IAuthService.cs new file mode 100644 index 00000000..909568db --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IAuthService.cs @@ -0,0 +1,15 @@ +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IAuthService +{ + Task GetLoginResponse(); + Task GetToken(); + Task> GetByUnitId(ObjectId unitId); + Task DeleteByDisplayId(ObjectId displayId); + Task DeleteByUnitId(ObjectId unitId); + Task> GetUserAuthorities(ObjectId id); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/ICacheService.cs b/adas-core.Application/Services/Interfaces/ICacheService.cs new file mode 100644 index 00000000..2ed78ac8 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/ICacheService.cs @@ -0,0 +1,39 @@ +using adas_core.Domain.Models.GroupedObservations; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces +{ + public interface ICacheService + { + //Métodos básicos + void SetValue(string key, string value); + string? GetValue(string key); + Task GetObjectAsync(string key, bool updateExpiration = true); + Task SetObjectAsync(string key, T obj, bool updateExpiration = true); + Task GetObjectAsync(string key, TimeSpan? ttlOverride, bool updateExpiration); + Task SetObjectAsync(string key, T obj, TimeSpan? ttlOverride, bool updateExpiration); + Task DeleteObjectAsync(string key); + Task DeleteByPatternAsync(string pattern); + void CleanCache(); + + + // Métodos para transparencia y gestión de locks + + // GetOrSet (string key) + Task GetOrSetValueAsync(string key, Func> loader, TimeSpan? ttlOverride = null); + Task GetOrSetObjectAsync( + string key, + Func> factory, + TimeSpan? ttl = null, + CancellationToken cancellationToken = default); + + // GetOrSet especializado para GroupedObservations + Task GetOrSetObjectAsync( + GroupedField groupedField, + ObjectId patientId, + Func> factory, + TimeSpan? ttl = null, + CancellationToken cancellationToken = default); + + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/ICalculatedObservations.cs b/adas-core.Application/Services/Interfaces/ICalculatedObservations.cs new file mode 100644 index 00000000..9371928e --- /dev/null +++ b/adas-core.Application/Services/Interfaces/ICalculatedObservations.cs @@ -0,0 +1,27 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.Pumps; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface ICalculatedObservations +{ + Task Map(T obs, bool onlyByName = false) where T : BasePatientObservation; + Task Map(PatientTreatment treatment); + Task Map(PatientDiagnosis diagnosis); + + Task Map(PumpObservation pumpObservation); + + Task CalculateMedicineObservation(List activeMedicines, ObjectId patientId); + + Task CalculateActiveBolus(ObjectId patientId); + + Task> GetActiveTreatmentsByPatient(ObjectId id); + + Task FixTimeInconsistencyWithLast(PatientObservation newObservation); + Task> PreMapList(List listToInsert); + Task MapSourceAlarm(PatientObservation obs, PatientObservationAlarm alarmToInsert); + + Task SendAlarm(PatientObservation obs, string name, AlarmEnum.Name? code); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/ICalculatedObservationsService.cs b/adas-core.Application/Services/Interfaces/ICalculatedObservationsService.cs new file mode 100644 index 00000000..4cd6a3ef --- /dev/null +++ b/adas-core.Application/Services/Interfaces/ICalculatedObservationsService.cs @@ -0,0 +1,20 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.Pumps; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface ICalculatedObservationsService +{ + Task Map(PatientObservation obs, bool onlyByName = false); + Task Map(PatientObservationAlarm obs, bool onlyByName = false); + Task Map(PatientTreatment treatment); + Task Map(PumpObservation obs); + Task Map(PatientRecordingAlert obs); + Task Map(PatientDiagnosis obs); + Task> MapList(List listToInsert); + Task CalculateBolusOpiates(ObjectId patientId); + Task CalculateMedicineObservation(List activeMedicines, ObjectId patientId); + Task> GetActiveTreatmentsByPatient(ObjectId id); + Task MapSourceAlarm(PatientObservation observation, PatientObservationAlarm observationAlarm); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/ICameraService.cs b/adas-core.Application/Services/Interfaces/ICameraService.cs new file mode 100644 index 00000000..a88b0ec5 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/ICameraService.cs @@ -0,0 +1,17 @@ +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface ICameraService +{ + Task GetById(ObjectId relayId); + List GetCameraInList(List configurationRelayList); + Task> GetPaginatedCameras(PaginationFilter request); + Task InsertCamera(Camera camera); + Task UpdateCameraById(ObjectId objectId, Camera camera); + Task DeleteCamera(ObjectId objectId); + Task> GetSearchByNameCameras(string textToSearch); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IClientMessageService.cs b/adas-core.Application/Services/Interfaces/IClientMessageService.cs new file mode 100644 index 00000000..804d3644 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IClientMessageService.cs @@ -0,0 +1,13 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.SignalR; + +namespace adas_core.Application.Services.Interfaces; + +public interface IClientMessageService +{ + Task SendAsync(string receiverId, OperationType? type, object? msg); + Task SendToAllAsync(OperationType type, object? msg); + Task ProcessMessage(Message msg, string contextConnectionId); + void SendUpdateMessageToBoxes(List boxes); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IConfigObservationService.cs b/adas-core.Application/Services/Interfaces/IConfigObservationService.cs new file mode 100644 index 00000000..5c5aefc1 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IConfigObservationService.cs @@ -0,0 +1,44 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Pumps; +using adas_core.Domain.Models.Responses; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IConfigObservationService +{ + Task GetByCodeSysAndCode(string codingSystem, string code); + Task Get(string name); + + Task Get(T obs, bool onlyByName = false) where T : BasePatientObservation; + + Task RetentionActions(T obs) where T : BasePatientObservation; + Task Map(T obs, bool onlyByName = false) where T : BasePatientObservation; + Task Map(PatientTreatment treatment); + + Task GroupedObservationStatus(GroupedField groupedField, GroupedObservationEnum.Result result, + string name, object value, double? min, double? max); + + Task> GetAllConfigs(CancellationToken ct = default); + Task> GetPaginatedItems(PaginationFilter filter); + Task GetAllCompact(); + Task GetConfigById(ObjectId id); + Task> GetConfigNames(string id); + Task> GetConfigNames(); + Task UpdateConfig(ConfigObservation configObservation); + Task CreateConfig(ConfigObservation configObservation); + Task RemoveConfigItem(string itemName); + Task RemoveConfigItem(ObjectId id); + Task?> GetConfigObservationItem(string name); + + Task GetSingleConfigObservationItem(string? code, string? codingSystem, string? name, + string? originalName); + + Task DeleteSingleConfigObservationItem(ConfigObservation configObservationItem); + Task> GetConfigObservationItemsByName(string name); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IConfigPumpsService.cs b/adas-core.Application/Services/Interfaces/IConfigPumpsService.cs new file mode 100644 index 00000000..b128fe65 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IConfigPumpsService.cs @@ -0,0 +1,18 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Pumps; + +namespace adas_core.Application.Services.Interfaces; + +public interface IConfigPumpsService +{ + Task Map(PumpObservation obs); + Task?> GetAllPumpConfigs(); + Task?> GetConfigItems(string id); + Task GetPumpConfigById(string id); + Task UpdatePumpConfig(ConfigPumps pumpConfig); + Task InsertPumpConfig(ConfigPumps pumpConfig); + Task DeletePumpConfig(ConfigPumps config); + Task RetentionActions(PumpObservation obs); + +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IConfigUnitsService.cs b/adas-core.Application/Services/Interfaces/IConfigUnitsService.cs new file mode 100644 index 00000000..65a6d41b --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IConfigUnitsService.cs @@ -0,0 +1,10 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.Pumps; + +namespace adas_core.Application.Services.Interfaces; + +public interface IConfigUnitsService +{ + Task Map(T obs) where T : BasePatientObservation; + Task Map(PumpObservation obs); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IDeviceService.cs b/adas-core.Application/Services/Interfaces/IDeviceService.cs new file mode 100644 index 00000000..6fce203d --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IDeviceService.cs @@ -0,0 +1,13 @@ +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IDeviceService +{ + Task Create(DeviceDto device); + Task Delete(ObjectId objectId); + Task Update(DeviceDto device); + Task ReceiveEvent(DeviceDto device); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IDiagnosisService.cs b/adas-core.Application/Services/Interfaces/IDiagnosisService.cs new file mode 100644 index 00000000..0141e03c --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IDiagnosisService.cs @@ -0,0 +1,17 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IDiagnosisService : IApiRequestService +{ + Task> GetByPatientId(ObjectId patientId); + Task DeleteByPatientId(ObjectId id); + Task Archive(Patient patient); + Task ArchiveByPatientId(ObjectId id); + Task ProcessDiagnosisObservation(ApiRequest apiRequest, Patient patient); + Task SaveRequest(ApiRequest apiRequest, Patient patient); + Task ProcessDiagnosis(List diagnosis, Patient patient, DateTime messageTime); + Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IDischargeService.cs b/adas-core.Application/Services/Interfaces/IDischargeService.cs new file mode 100644 index 00000000..51f48259 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IDischargeService.cs @@ -0,0 +1,27 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IDischargeService : IApiRequestService +{ + Task GetDischargeByIdAsync(ObjectId dischargeId); + Task CountDischargesByUnitId(ObjectId unitId); + Task DeleteDischargeByIdAsync(ObjectId dischargeId); + Task DeleteDischargeAsync(Discharge discharge); + Task UpdateDischargeAsync(Discharge discharge); + Task> GetDischargesAsync(); + Task InsertDischarge(Discharge discharge); + Task GetDischargeByLocation(PatientLocation location); + Task GetDischargeByPatientId(ObjectId patientId); + Task GetDischargeByPointOfCareId(ObjectId location); + Task GetDischargeByPointOfCareIdWithLocale(ObjectId location, LocaleEnum dataLocale); + void SendDischargeBroadcast(Discharge discharge, OperationType operation); + Task UpdatePatientMasterListItemChange(UpdateOptionMasterListDto opt, IEnumerable unitList, string typeName); + Task DeletePatientMasterListItem(OptionList opt, IEnumerable unitList, string typeName); + Task DeleteDischargesByUnitId(ObjectId unitId); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IDisplayConfigService.cs b/adas-core.Application/Services/Interfaces/IDisplayConfigService.cs new file mode 100644 index 00000000..17c747d4 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IDisplayConfigService.cs @@ -0,0 +1,46 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.DTO.Display; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IDisplayConfigService +{ + Task> GetAll(); + Task> GetAllCompactPaginated(PaginationFilter request); + Task> GetByType(DisplayConfigEnums.DisplayType type); + Task GetById(ObjectId id); + Task GetById(ObjectId? configId, ObjectId unitId, DisplayConfigEnums.DisplayType displayType); + Task InsertOne(DisplayConfig config); + Task InsertOneMinimal(CreateDisplayConfigDto config); + Task InsertOneTest(); + Task UpdateConfig(ObjectId displayConfigId, object newDisplayConfig); + Task UpdateFieldList(ObjectId objectIdConfigDisplay, List fields); + Task UpdateConfigColor(ObjectId objectIdConfigDisplay, ColorConfig colorConfigDto); + Task UpdateHeaderConfig(ObjectId objectIdConfigDisplay, HeaderConfig headerConfig); + Task UpdateSetHomeBanner(ObjectId objectIdConfigDisplay, List bannerItems); + Task UpdateBaseConfig(DisplayConfig baseConfig); + Task UpdateDisplayConfigHospitalName(ObjectId objectIdConfigDisplay, string name); + Task DeleteDisplayConfig(ObjectId objectIdConfigDisplay); + Task GetDefaultConfig(DisplayConfigEnums.DisplayType type); + Task> GetDisplayConfigLocations(ObjectId objectIdConfigDisplay); + Task> GetAllCompact(); + + Task InsertOneWithTemplate(string objectId, + DisplayConfigEnums.DisplayType configType, string? configHospital); + + Task UpdateCardConfig(CardConfig baseConfig); + Task InsertCardConfig(CreateDisplayConfigCardDto updateDisplayConfigNameDto); + Task> GetCardConfigAll(); + Task GetCardConfigById(ObjectId id); + Task UpdateDetailConfig(CardDetailsConfig baseConfig); + Task InsertCardDetailConfig(CreateDisplayConfigCardDto updateDisplayConfigNameDto); + Task InsertChartConfig(CreateDisplayConfigCardDto updateDisplayConfigNameDto); + Task UpdateChartConfig(ChartConfig baseConfig); + Task DeleteChartConfig(ObjectId objectIdConfigDisplay); + Task GetChartConfig(ObjectId objectIdConfigChart); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IDisplayService.cs b/adas-core.Application/Services/Interfaces/IDisplayService.cs new file mode 100644 index 00000000..54058999 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IDisplayService.cs @@ -0,0 +1,63 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.DTO.Display; +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 MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IDisplayService +{ + //Task> GetAll(string? userName, List displayIdByAuthorities); + Task> GetAllCompact(); + Task> GetAllByUser(string? userName); + Task> GetByType(DisplayConfigEnums.DisplayType type); + Task> GetByPointOfCare(PointOfCare pointOfCare); + Task> GetByConfigId(ObjectId configId); + Task> GetByCardConfigId(ObjectId configId); + + // Task> GetByUser(); + Task GetByName(string name); + Task GetById(ObjectId id); + Task GetByIdWithPermissions(ObjectId id, LocaleEnum localeEnum); + Task CountDisplaysByUnitId(ObjectId unitId); + + Task GetInfo( + ObjectId id, + string? userName, + List? authorizations, + LocaleEnum? locale, + bool fillPointOfCare = true, + bool fillPatientData = false, + bool fillDisplayList = true, + bool fillDisplayConfig = true, + CancellationToken ct = default + ); + + + Task> GetDisplaySectionByUser( + DisplayConfigEnums.DisplayType type, + ObjectId? currentDisplay, + string? userName, + List? authorizations); + + Task GetAllDisplaySection(); + Task> GetByUnitId(ObjectId id); + Task GetAllAvailablePoc(List displayIds, bool excludeVirtual = false); + Task> GetAllPocsByDisplayId(ObjectId id); + Task InsertOne(Display display); + Task InsertOneTest(); + Task UpdateConfig(Display oldDisplay, DisplayConfig? newDisplayConfig); + Task UpdateConfigId(Display oldDisplay, ObjectId configId); + Task UpdatePointOfCareList(ObjectId objectId, List listPocObId); + Task UpdateConfigPreset(ObjectId objectIdDisplay, ObjectId objectIdConfigDisplay); + Task UpdateName(ObjectId id, string name); + Task> GetPaginatedDisplays(PaginationFilter filter); + Task DeleteDisplay(ObjectId id); + Task> GetDisplayConfigLocations(ObjectId displayConfigId); + Task IsDisplayConfigInUse(ObjectId displayConfigId); + Task DeleteDisplaysByUnitId(ObjectId unitId); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IFileService.cs b/adas-core.Application/Services/Interfaces/IFileService.cs new file mode 100644 index 00000000..83aa993a --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IFileService.cs @@ -0,0 +1,16 @@ +//using Microsoft.AspNetCore.Http; + +using adas_core.Domain.Enums; +using adas_core.Domain.Models.DTO; +using Microsoft.AspNetCore.Http; + +namespace adas_core.Application.Services.Interfaces; + +public interface IFileService +{ + Task CopyUpdateFiles(ICollection files); + Task UploadAssetFiles(ICollection files, FileEnum.AssetTheme themeParse); + List GetAllAssetFile(FileEnum.AssetTheme themeParse); + + List GetFilesInDirectory(string directoryPath); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IGroupedObservationService.cs b/adas-core.Application/Services/Interfaces/IGroupedObservationService.cs new file mode 100644 index 00000000..8704d1b3 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IGroupedObservationService.cs @@ -0,0 +1,26 @@ +using adas_core.Application.Subscriptions; +using adas_core.Domain.Models; +using adas_core.Domain.Models.GroupedObservations; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IGroupedObservationService +{ + Task GenerateGroupedObservation(ObjectId id, GroupedField groupedField, + string timeZoneId = "Romance Standard Time", bool cacheIsChecked = false, CancellationToken ct = default); + + Task GenerateGroupedObservation(ObjectId obsPatientId, GroupedField groupedField, + List wsgLastGroupedObservationObs, PatientObservation obs, + string timeZoneId = "Romance Standard Time"); + + Task> FindLastObservations(ObjectId patientId, int num = 2, + List? filterObservations = null); + + Task CreateNextEmptyObs(WsSubscriberGrouped ws); + /* + * + List CalculateShiftObservations(List shiftGroupObservations, GroupedField groupedField); + List GenerateShiftObservations(List result, GroupedField groupedField); + */ +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IHistoricalConfigChangesService.cs b/adas-core.Application/Services/Interfaces/IHistoricalConfigChangesService.cs new file mode 100644 index 00000000..9c3d1067 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IHistoricalConfigChangesService.cs @@ -0,0 +1,25 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IHistoricalConfigChangesService +{ + Task> GetAll(); + Task Get(ObjectId id); + Task> GetByType(DisplayConfigEnums.ConfigTypes type, int num); + + Task> GetByUser(string user, DisplayConfigEnums.ConfigTypes? configTypes, + int num); + + Task FindLastConfigChanges(DisplayConfigEnums.ConfigTypes configTypes); + + + Task InsertOne(HistoricalConfigChanges historicalConfigChanges); + Task UpdateHistoricalConfigChange(HistoricalConfigChanges historicalConfigChanges); + + Task DeleteHistoricalConfigChange(ObjectId id); + + Task LogConfigChanged(string user, DisplayConfigEnums.ConfigTypes configType, string newConfig, string oldConfig); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/ILightBeaconService.cs b/adas-core.Application/Services/Interfaces/ILightBeaconService.cs new file mode 100644 index 00000000..d55c0db0 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/ILightBeaconService.cs @@ -0,0 +1,28 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface ILightBeaconService +{ + Task SendColor(ObjectId pocId, LightBeaconColor color); + Task SendColor(PointOfCare pocId, LightBeaconColor color); + Task SendBeaconBroadcast(PointOfCare poc, LightBeaconColor color); + Task SendBeaconBroadcast(ObjectId poc, LightBeaconColor color); + Task PowerOffLed(ObjectId pocId); + Task PowerOffLed(PointOfCare poc); + + void GenerateColorAlert(PatientObservation obs); + + //TODO refactor, one patient can have multiple beacons + public Task GetColor(ObjectId pocId); + public Task GetColor(PointOfCare poc); + Task UpdateOne(LightBeacon beacon); + Task InsertOne(LightBeacon beacon); + Task> GetPaginatedBeacons(PaginationFilter request); + Task> GetSearchByName(string textToSearch); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/ILocalAuditService.cs b/adas-core.Application/Services/Interfaces/ILocalAuditService.cs new file mode 100644 index 00000000..edd3c2f2 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/ILocalAuditService.cs @@ -0,0 +1,9 @@ +using System.Security.Claims; + +namespace adas_core.Application.Services.Interfaces; + +public interface ILocalAuditService +{ + Task CreateAuditLogAsync(ClaimsPrincipal? user, object? dataOriginal, object? dataModified, string? reason = null); + Task DeepCopyAsync(T data); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/ILockProvider.cs b/adas-core.Application/Services/Interfaces/ILockProvider.cs new file mode 100644 index 00000000..18f310ed --- /dev/null +++ b/adas-core.Application/Services/Interfaces/ILockProvider.cs @@ -0,0 +1,20 @@ +namespace adas_core.Application.Services.Interfaces +{ + /// + /// Contrato genérico para un proveedor de locks por clave. + /// CacheService usará una implementación local en memoria. + /// RedisService podría usar una distribuida (si fuera necesario). + /// + public interface ILockProvider + { + /// + /// Intenta adquirir el lock para una clave dentro del timeout. + /// + Task AcquireAsync(string key, TimeSpan timeout); + + /// + /// Libera el lock para la clave (idempotente). + /// + Task ReleaseAsync(string key); + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IMasterListService.cs b/adas-core.Application/Services/Interfaces/IMasterListService.cs new file mode 100644 index 00000000..250045e7 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IMasterListService.cs @@ -0,0 +1,46 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.Responses; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IMasterListService where T : MasterList +{ + Task> GetAllMasterList(); + Task> GetAllMasterListWithoutOptions(); + Task> GetAllMasterListWithPaginatedOptions(PaginationFilter request); + Task GetMasterListById(ObjectId id, LocaleEnum? locale); + + Task GetMasterListByIdWithPaginatedOptions(ObjectId id, + PaginationFilter request); + + Task> GetMasterListOptionsNamesById(ObjectId id); + Task> GetMasterListByIdAndTextSearch(ObjectId id, string? textSearch); + Task> GetMasterListByIdAndSearchOptions(ObjectId id, FilterOptionListElement filterOption); + Task GetMasterListByName(string name); + Task InsertMasterList(T item); + Task UpdateMasterList(T item); + Task DeleteMasterListById(ObjectId id); + Task AddOptionToMasterList(ObjectId id, FilterOptionListElement opt); + Task UpdateMasterListOption(ObjectId id, OptionList opt, string typeName, LocaleEnum locale); + Task UpdateFullMasterListOption(ObjectId id, OptionList opt, string typeName); + Task UpdateMasterListOption(ObjectId id, OptionList opt, string typeName); + Task FindOptionItemById(ObjectId masterId, ObjectId optionId, LocaleEnum locale); + Task UpdateOptionDetailsToMasterList(ObjectId id, UpdateMasterListDetailsDto opt); + Task UpdateMasterListName(ObjectId id, string name); + Task UpdateMasterListDescription(ObjectId id, string name); + Task RemoveMasterListOption(ObjectId id, OptionList oldOpt); + Task GetAllMasterListCount(); + Task> GetPaginatedMasterList(PaginationFilter filter); + Task DeleteMasterListOption(ObjectId id, ObjectId optId, string typeName); + + Task> GetPaginatedMasterListWithPaginatedOptions( + PaginationFilter listFilter, PaginationFilter optionsFilter); + + Task> GetPaginatedOptions(PaginationFilter filter, ObjectId listId); + Task GetAssociatedList(ObjectId id, MasterListType masterListType1, MasterListType masterListType2); + Task> GetOptionsOfList(ObjectId id); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IMasterListServiceFactory.cs b/adas-core.Application/Services/Interfaces/IMasterListServiceFactory.cs new file mode 100644 index 00000000..abba1548 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IMasterListServiceFactory.cs @@ -0,0 +1,23 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IMasterListServiceFactory +{ + object GetService(Type serviceType); + object GetService(MasterListType serviceName); + object? GetTypedMasterList(MasterListType masterListType, MasterList masterList); + Type GetMasterListSpecificType(MasterListType masterListType); + Task InsertMasterList(MasterListType masterListType, MasterList masterList); + Task UpdateMasterList(MasterListType masterListType, MasterList masterList); + Task GetMasterListById(MasterListType masterListType, ObjectId masterListId, LocaleEnum? dataLocale); + List StringNurseObs(); + Task GetPatientTraslated(Unit? unit, LocaleEnum? locale, Patient? patient); + + Task GetMasterListOptionById(MasterListType masterListType, ObjectId masterListId, + ObjectId masterListOptionId, + LocaleEnum? dataLocale); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IMedicineService.cs b/adas-core.Application/Services/Interfaces/IMedicineService.cs new file mode 100644 index 00000000..d5dfb2d4 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IMedicineService.cs @@ -0,0 +1,25 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.Responses; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IMedicineService +{ + Task GetByCode(string code); + Task> GetByCodeOrNote(List codeNote); + Task GetByName(string name); + Task> GetMedicinesOfTreatments(IEnumerable treatments); + Task> GetActiveMedicinesByPatient(ObjectId patientId); + Task> GetPaginatedMedicines(PaginationFilter filter); + Task> GetAll(); + Task GetMedicineById(ObjectId medicineId); + Task PostMedicine(Medicine medicine); + Task UpdateMedicine(Medicine medicine); + Task DeleteMedicineById(ObjectId medicineId); + Task> GetAllTypes(); + Task> GetAllGroups(); + Task> GetAllNames(); + Task> GetAllCodes(); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/INoticeService.cs b/adas-core.Application/Services/Interfaces/INoticeService.cs new file mode 100644 index 00000000..d802be67 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/INoticeService.cs @@ -0,0 +1,19 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface INoticeService +{ + Task DeleteNoticeAsync(Notice notice); + Task DeleteNoticeByIdAsync(ObjectId noticeId); + Task GetNoticeByIdAsync(ObjectId noticeId); + Task?> GetNoticeByTypeAsync(string noticeType); + Task> GetNoticesAsync(); + Task InsertNotice(Notice notice); + Task UpdateNoticeAsync(Notice notice); + Task SaveRequest(ApiRequest apiRequest); + Task SaveRequestAsync(ApiRequest apiRequest); + Task?> GetNoticesByDisplayId(ObjectId displayId); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IObservationDemoService.cs b/adas-core.Application/Services/Interfaces/IObservationDemoService.cs new file mode 100644 index 00000000..4e3edac0 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IObservationDemoService.cs @@ -0,0 +1,12 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Application.Services.Interfaces; + +public interface IObservationDemoService +{ + Task> GenerateObservationByField(Patient patient, List dataFields); + Task GenerateGroupedObservation(Patient patient, GroupedField groupedField); + Task> GenerateAlarmByField(Patient patient, List dataAlarmfields); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IObservationService.cs b/adas-core.Application/Services/Interfaces/IObservationService.cs new file mode 100644 index 00000000..1f089dbb --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IObservationService.cs @@ -0,0 +1,85 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Services.Interfaces; + +public interface IObservationService : IApiRequestService +{ + //REMOVE + /* + List FindLastObservations(ObjectId patientId, string codingSystem, string code, int num = 2); + */ + Task> FindByPatientIdAsync(ObjectId patientId); + + Task> FindByPatientIdAndCodingSystemAsync(ObjectId patientId, string codingSystem, + string name); + + Task> FindLastObservations(ObjectId patientId, int num = 2, + List? filterObservations = null); + + Task UpdateExpiredObservations(List expiredObservations); + Task ExpireObservationsAndRecalculateAsync(); + Task ExpireAlertsAndPowerOffAsync(); + + Task> FindLatestUniqueValuesByName(ObjectId patientId, string name, int? expires); + + Task> FindLastObservationsByField(ObjectId patientId, + List? filterObservations = null, bool mapped = true, CancellationToken ct = default); + + Task> FindLastIntravenousLinesObservationByLocation(ObjectId patientId); + Task InsertObservation(PatientObservation patientObservation, bool persistObs = true, bool mapObs = true); + Task InsertIfChanged(string name, PatientObservation observation, bool persistObs = true, bool mapObs = true); + Task InsertNurseObservation(PatientObservation obs); + Task DeleteByPatientId(ObjectId id); + Task Archive(PatientObservation observation); + Task MapObservation(PatientObservation obs, bool onlyByName = false); + Task> FindAllLastPatientObservationTime(); + Task MapObservationsByName(PatientObservation obs); + Task Archive(Patient patient); + Task ArchiveByPatientId(ObjectId id); + + Task UpdateObservation(PatientObservation observation); + Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId); + + Task SendObsBroadcast(BasePatientObservation obs); + Task SendObsBroadcast(List obs, PatientLocation location); + Task SendObsBroadcast(List obs, ObjectId pocId); + + Task FindLastBeforeDate(ObjectId patientId, DateTime date, string? obsName); + + Task?> FindAnyWithSameDate(ObjectId patientId, DateTime date, string? obsName); + + //TODO To implement + Task> FindAllBeforeDate(ObjectId patientId, DateTime date, + List? filterObservations = null); + + //TODO To implement + Task> FindAllAfterDate(ObjectId patientId, DateTime date, + List? filterObservations = null); + + Task> FindAllBetweenDates(ObjectId patientId, DateTime? startDate, DateTime? endDate, + List? filterObservations = null, bool fromArchived = false, PaginationFilter? filter = null); + + Task> FindLastNotExpiredObservatonsByPatient(ObjectId patientId, string name, + int? endAfter = null, int? num = null); + + + Task CheckAndExpireObservations(); + + IAsyncEnumerable FindNotExpiredObservationsShouldBeExpired(); + + + void ProcessObservations(List observations, Patient patient, DateTime messageTime, + ObservationData? observationData = null); + + Task ExpireObservations(); + + Task InsertSimpleObservation(PatientObservation observation); + Task> GetPaginatedObservations(PaginationFilter filter); + Task SaveRequestNurseObsAsync(ApiRequest request); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IPatientCarePlanService.cs b/adas-core.Application/Services/Interfaces/IPatientCarePlanService.cs new file mode 100644 index 00000000..0d1c47cf --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IPatientCarePlanService.cs @@ -0,0 +1,17 @@ +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IPatientCarePlanService +{ + Task> FindByPatientId(ObjectId patientId); + Task> FindByUserId(ObjectId userId); + Task> FindAll(); + Task InsertOneAsync(PatientCarePlan patientCarePlan); + Task ArchiveCarePlanFromJob(Patient patientWithFinishedProcedure, List itemsToArchive); + + Task ArchiveByPatientId(ObjectId patientid); + Task UpdateManyObjectId(string patientid, ObjectId patientId, ObjectId oldId); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IPatientService.cs b/adas-core.Application/Services/Interfaces/IPatientService.cs new file mode 100644 index 00000000..b7fb95b7 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IPatientService.cs @@ -0,0 +1,84 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.DTO; +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 MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IPatientService : IApiRequestService +{ + Task FindByPatientId(string patientId, bool withLocation = false); + Task FindByPatientIdWithLocale(string patientId, LocaleEnum localeEnum); + Task FindByPatientNumberArchived(string patientNumber); + Task FindByPatientNumber(string patientNumber, bool withLocation = false); + Task FindByLocation(PatientLocation? location); + Task FindByPointOfCareId(ObjectId pocId); + Task FindByUnitAndPocId(ObjectId unit, ObjectId pointOfCare); + Task> FindByPointOfCare(string pointOfCare); + Task CountPatientsByUnitId(ObjectId unitId); + + Task FindPatient(string? patientId, string? patientNumber, PatientLocation? location, + bool findByLocation = false); + + Task Insert(Patient patient); + Task InsertAsync(Patient patient); + Task ArchivePatient(Patient patient); + Task ArchivePatientData(ObjectId patientid); + Task MergePatient(Patient patient, string oldPatienNumber); + Task UpdateLocation(ObjectId id, PatientLocation? location); + Task UpdateAttendingDoctor(ObjectId id, Person doctor); + Task UpdatePatientData(ObjectId id, string patientNumber, Person data, bool updatePatientNumber = true); + Task UpdatePatientData(ObjectId id, string patientNumber, Patient patient, bool updatePatientNumber = true); + Task Update(Patient patient); + Task Move(Patient patient, ObjectId newPocId, ObjectId oldPocId); + Task FindById(ObjectId id, bool withLocation = false); + Task GetBox(PointOfCare poc, bool observations = false, List? filterObservations = null); + Task CreatePatientFromRequest(ApiRequest apiRequest, bool ignoreLocation = false); + Task FindPatientByApiRequest(ApiRequest apiRequest); + Task ArchivePatientWithoutObservationsSinceDate(DateTime date); + + Task ArchiveDischargedPatients(int hoursBeforeArchive); + Task> FindAll(bool withLocation = false); + Task> GetPaginatedPatients(PaginationFilter filter); + + Task DischargeInactivePatients(DateTime sinceDate, int hoursBeforeArchive); + Task UpdateOne(Patient updatedPatient); + + Task SendNewPatientBroadcast(Patient patient); + Task SendPatientUpdateBroadcast(Patient patient); + Task> FindInActivePoC(); + Task> FindInInactivePoC(); + + Task GetByPointOfCare(PointOfCare item, bool observations = false, + List? filterObservations = null); + + Task GetByPointOfCareAndLocale(PointOfCare item, Unit? unit, LocaleEnum? localeEnum); + Task UpdatePatientAltable(ObjectId patientId, OptionList altable, User? user); + Task ExitPatientById(ObjectId id, bool archivePatient = true); + + Task UpdatePatientMasterList(ObjectId patientId, MasterListType typeName, + List updatedOptions, + User? user, List? carePlanLog); + + Task GenerateNurseCarePlanAndInsert(MasterListType carePlanType, List? options, + Patient patient, User? user); + + Task UpdatePatientIncomingData(ObjectId patientId, Patient person); + Task UpdatePatientDemographicData(ObjectId patientId, Patient person, User? user); + Task SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId); + Task> FindAllPatientWithFinishedProcedures(int archiveProcedureEndDateAfterMinutes); + Task> FindAllPatientWithFinishedTest(int archiveTestEndDateAfterMinutes); + Task> FindAllPatientWithFinishedTreatment(int archiveTreatmentEndDateAfterMinutes); + + Task UpdatePatientIncomingData(ObjectId patientId, Patient person, PatientIncomeData personDataChange, + User user); + + Task UpdatePatientMasterListItemChange(UpdateOptionMasterListDto opt, IEnumerable unitList, + string typeName); + + Task DeletePatientMasterListItem(OptionList opt, IEnumerable unitList, string typeName); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IPermissionService.cs b/adas-core.Application/Services/Interfaces/IPermissionService.cs new file mode 100644 index 00000000..7a0cb583 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IPermissionService.cs @@ -0,0 +1,22 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Application.Services.Interfaces; + +public interface IPermissionService +{ + public Task GetPermissionsForDisplay(Display display, User user); + public Task GetPermissionsForUnit(string unitId, User user); + public PanelPermissionTypes GetPermissionsForPanel(User user); + public Task GetPermissionsForPanel(string user); + + Task HasAccessToDisplay(string username, PermissionEnum.RolesType role, + PermissionEnum.SourcePermissionsEnum source, string displayId); + + Task HasAccessToUnit(string username, PermissionEnum.RolesType role, + PermissionEnum.SourcePermissionsEnum source, string unitId); + + Task HasAccessToPanel(string username, PermissionEnum.RolesType role, + PermissionEnum.SourcePermissionsEnum source); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IPoCMappingService.cs b/adas-core.Application/Services/Interfaces/IPoCMappingService.cs new file mode 100644 index 00000000..88649ed3 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IPoCMappingService.cs @@ -0,0 +1,8 @@ +using adas_core.Domain.Models; + +namespace adas_core.Application.Services.Interfaces; + +public interface IPoCMappingService +{ + Task Map(PatientLocation original); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IPointOfCareService.cs b/adas-core.Application/Services/Interfaces/IPointOfCareService.cs new file mode 100644 index 00000000..da47f057 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IPointOfCareService.cs @@ -0,0 +1,58 @@ +using adas_core.Domain.Enums; +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 MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IPointOfCareService +{ + Task Delete(ObjectId id); + + Task Update(PointOfCare pointOfCare); + + Task UpdateUnit(ObjectId id, Unit unit); + Task> GetAll(); + + Task> GetAllConfigs(); + Task> GetAllLocationInfo(); + + Task UpdateConfiguration(ObjectId id, PointOfCareConfiguration configuration); + + Task FindById(ObjectId id); + Task FindByIdAllConfig(ObjectId id); + + Task?> FindAllByUnitId(ObjectId unit); + + Task?> FindByRoom(string room); + + Task?> FindByBed(string bed); + + Task> FindAllByUnitIds(List unitIds); + + Task GetInfo(ObjectId id, LocaleEnum? locale = null, bool fillPatientData = true, CancellationToken ct = default); + + Task InsertPointOfCare(PointOfCare pointOfCare); + Task FindPoCByPatientId(ObjectId patientId); + + Task SetPointOfCareStatus(ObjectId id, StatusEnum.PointOfCare status); + + Task> FindByUnitAndStatus(ObjectId unitId, StatusEnum.PointOfCare poc, + bool excludeVirtual = false); + + void CheckNextAdmission(ObjectId? patientLocation); + Task FindByBedAndUnitId(string? bed, ObjectId? unitId); + Task UpdateRelayConfig(PointOfCare poc); + Task FindPoCByPatientNumber(string patientNumber); + + Task CountPoCsByUnitId(ObjectId unitId); + Task CountVirtualPoCsByUnitId(ObjectId unitId); + Task> GetPaginatedPoCs(PaginationFilter filter); + Task DeletePoCsByUnitId(ObjectId unitId); + Task> FindAllIdCamerasInUse(); + Task> FindAllIdRelaysInUse(); + Task> FindAllIdBeaconsInUse(); + Task?> FindAllByUnitIdWithDevices(ObjectId unitId); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IPublisherService.cs b/adas-core.Application/Services/Interfaces/IPublisherService.cs new file mode 100644 index 00000000..4b6746b2 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IPublisherService.cs @@ -0,0 +1,9 @@ +namespace adas_core.Application.Services.Interfaces; + +public interface IPublisherService +{ + Task CreateQueue(string queueName); + Task SendMessageError(object obj, string queueName); + Task SendMessage(object obj, string queueName); + Task SendMessage(string message, string queueName); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IPumpService.cs b/adas-core.Application/Services/Interfaces/IPumpService.cs new file mode 100644 index 00000000..941548cd --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IPumpService.cs @@ -0,0 +1,40 @@ +using MongoDB.Bson; +using adas_core.Domain.Models; +using adas_core.Domain.Models.Pumps; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; + +namespace adas_core.Application.Services.Interfaces; + +public interface IPumpService: IApiRequestService +{ + // Insert manual + Task InsertPumpObservation(PumpObservation obs); + + // Mapping + Task MapPumpObservation(PumpObservation obs); + + // Consultas por paciente + Task> FindLastPumpObservations(ObjectId patientId, int num = 1); + Task> FindAllLastPatientObservationTime(); + + // Gestión de configuración + Task?> GetItemsById(string id); + Task?> GetAllPumpConfig(); + Task GetPumpConfigsById(string id); + Task UpdatePumpConfig(ConfigPumps config); + Task InsertPumpConfig(ConfigPumps config); + Task DeletePumpConfig(ConfigPumps config); + + // Paginación + Task?> GetPaginatedPump(PaginationFilter filter); + + // Archivado / limpieza + Task DeleteByPatientId(ObjectId id); + Task Archive(Patient patient); + Task ArchiveByPatientId(ObjectId id); + + // Mantenimiento ids + Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IRecordingAlertService.cs b/adas-core.Application/Services/Interfaces/IRecordingAlertService.cs new file mode 100644 index 00000000..5bb4e3f4 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IRecordingAlertService.cs @@ -0,0 +1,15 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IRecordingAlertService : IApiRequestService +{ + Task> FindLastRecordingAlert(ObjectId patientId, int num = 2); + Task DeleteByPatientId(ObjectId id); + Task Archive(Patient patient); + Task ArchiveByPatientId(ObjectId id); + + Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IRecordingService.cs b/adas-core.Application/Services/Interfaces/IRecordingService.cs new file mode 100644 index 00000000..c4d43536 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IRecordingService.cs @@ -0,0 +1,23 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Recording; +using Patient = adas_core.Domain.Models.MongoModels.Patient; + +namespace adas_core.Application.Services.Interfaces; + +public interface IRecordingService : IApiRequestService +{ + Task SendCancelRecordingToRecordingApi(Patient patient, PointOfCare poc); + + Task SendRecordingDataToQueue(Patient patient, PointOfCare poc, DateTime? date, DateTime? endDate, + DateTime? eventDate, AlarmEnum.Name? alarmName, AlarmEnum.Severity severity, string? alarmDescription, + bool start = true, AlarmEnum.Type type = AlarmEnum.Type.Manual); + + Task SendRecordingData(Patient patient, PointOfCare poc, ManualRecording manualRecording, bool start); + + Task SendAutoRecordingData(Patient patient, PointOfCare poc, RecordingData automaticRecording); + + Task?> GetRecordings(int roomName); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IRelayService.cs b/adas-core.Application/Services/Interfaces/IRelayService.cs new file mode 100644 index 00000000..25b28fd2 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IRelayService.cs @@ -0,0 +1,24 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IRelayService +{ + Task CheckRelayStatus(Relay relay); + Task CheckRelayStatus(ObjectId relayId); + + Task PowerOn(Relay relay); + Task PowerOff(Relay relay); + Task SetManualRelay(RelayEnum.Status status, ObjectId pocId, RelayEnum.Type type); + Task GetById(ObjectId relay); + List GetRelayInList(List? relayList); + List GetRelayByTypeInList(List? configurationRelayList, RelayEnum.Type type); + + Task> GetPaginatedRelays(PaginationFilter request); + Task InsertRelay(Relay request); + Task UpdateRelayById(ObjectId objectId, Relay relay); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/ISendAlertService.cs b/adas-core.Application/Services/Interfaces/ISendAlertService.cs new file mode 100644 index 00000000..a4a69bca --- /dev/null +++ b/adas-core.Application/Services/Interfaces/ISendAlertService.cs @@ -0,0 +1,10 @@ +using adas_core.Domain.Models.SystemAlerts; + +namespace adas_core.Application.Services.Interfaces; + +public interface ISendAlertService +{ + Task> GetQueues(); + List GetPerformance(); + Task> GetApiClients(); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IServiceConfigService.cs b/adas-core.Application/Services/Interfaces/IServiceConfigService.cs new file mode 100644 index 00000000..ec58b9ca --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IServiceConfigService.cs @@ -0,0 +1,8 @@ +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Application.Services.Interfaces; + +public interface IServiceConfigService +{ + Task Get(string id); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/ISubscriberGroupedService.cs b/adas-core.Application/Services/Interfaces/ISubscriberGroupedService.cs new file mode 100644 index 00000000..1b00f220 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/ISubscriberGroupedService.cs @@ -0,0 +1,21 @@ +using adas_core.Application.Subscriptions; +using adas_core.Domain.Models; +using adas_core.Domain.Models.GroupedObservations; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface ISubscriberGroupedService +{ + List GetGrouped(); + void RemoveGroupedObsByPatientId(string patientId); + void RemoveWsSubscriberByLocation(string patientId, PatientLocation? newLocation); + void RemoveWsSubscriberPatientIdAndWsId(string patientId, string wsIdToRemove); + List CheckEmptySubscriberGroup(WsSubscriberGrouped wsl); + void AddSubscriberGrouped(WsSubscriberGrouped wsSubscriberGrouped); + + void CheckOnSubscriptionGroup(GroupedField groupedField, ObjectId patientId, string timeZoneId, string connectionId, + GroupedObservation go); + + void UpdateLastGroupedObsInGroup(string wsgHashCode, GroupedObservation newGroupedObservation); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/ISubscribersService.cs b/adas-core.Application/Services/Interfaces/ISubscribersService.cs new file mode 100644 index 00000000..3f86bd1a --- /dev/null +++ b/adas-core.Application/Services/Interfaces/ISubscribersService.cs @@ -0,0 +1,13 @@ +using adas_core.Application.Subscriptions; +using MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface ISubscribersService +{ + List GetSubscribers(); + WsSubscriber? GetById(string contextConnectionId); + List GetByPocId(ObjectId pocId); + int RemoveConnectionById(string contextConnectionId); + void AddSubscriber(WsSubscriber subscriber); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/ITreatmentService.cs b/adas-core.Application/Services/Interfaces/ITreatmentService.cs new file mode 100644 index 00000000..167d0001 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/ITreatmentService.cs @@ -0,0 +1,26 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Services.Interfaces; + +public interface ITreatmentService : IApiRequestService +{ + Task Insert(PatientTreatment treatment); + Task DeleteByPatientId(ObjectId id); + Task DeleteById(ObjectId id); + Task ArchiveByPatientId(ObjectId id); + Task Archive(Patient patient); + Task UpdateTreatment(PatientTreatment patientTreatment); + Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId); + Task> GetTreatmentsByPatientId(ObjectId id); + Task> GetActiveTreatmentsByPatient(ObjectId id); + Task> FindByPatientIdAsync(ObjectId patientId); + Task> FindByPatientId(ObjectId patientId); + Task> GetBolusTreatments(ObjectId patientId); + Task> GetPaginatedTreatments(PaginationFilter filter); + Task> GetActiveTreatmentsByPatientIdAndOrder(ObjectId patientId, string order); +} \ No newline at end of file diff --git a/adas-core.Application/Services/Interfaces/IUnitService.cs b/adas-core.Application/Services/Interfaces/IUnitService.cs new file mode 100644 index 00000000..cbdfff64 --- /dev/null +++ b/adas-core.Application/Services/Interfaces/IUnitService.cs @@ -0,0 +1,44 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.DTO; +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 MongoDB.Bson; + +namespace adas_core.Application.Services.Interfaces; + +public interface IUnitService +{ + Task> GetAll(bool withPocs = false); + Task> GetAllCompact(); + + Task GetOneCompact(ObjectId id); + + // Task GetByCodeSysAndCode(string unit); + Task GetByName(string unit); + + // Task GetByPointOfCare(PointOfCare pointOfCare); + Task GetInfo(ObjectId id, LocaleEnum? dataLocale, bool fillLists = true, bool withPoCs = false); + Task GetInfo(ObjectId id, bool withPoCs = true, bool withDevices = true); + + Task FindByPatientId(ObjectId patientId); + + // Task?> FindByLocation(PatientLocation location); + Task FindById(ObjectId? id); + Task FindByName(string? name); + + Task FindByUnitNameOrPocName(string? name, string? pocName); + + //Task FindByPointOfCare(PointOfCare pointOfCare); + Task InsertOne(Unit unit); + Task UpdateUnit(Unit unit); + Task?> FindUnitsByMasterListId(ObjectId masterListId, MasterListType masterListType); + Task CountUnitsByMasterListId(ObjectId masterListId, MasterListType masterListType); + Task> FindUnitsByMasterListId(ObjectId masterListId); + Task UpdateUnitMasterList(UpdateUnitIdListDto updateUnitListDto); + Task UpdateConfiguration(ObjectId unitIdParsed, UnitConfiguration unitConfiguration); + Task DeleteUnitById(Unit unit); + Task UpdateUnitInfo(ObjectId unitId, string name, string title, string? configObsId = null); + Task> GetPaginatedUnits(PaginationFilter filter, bool withPoCs); +} \ No newline at end of file diff --git a/adas-core.Application/Services/LocalAuditService.cs b/adas-core.Application/Services/LocalAuditService.cs new file mode 100644 index 00000000..3861daca --- /dev/null +++ b/adas-core.Application/Services/LocalAuditService.cs @@ -0,0 +1,56 @@ +using System.Security.Claims; +using adas_core.Application.Services.Interfaces; +using audit_logs.Services.Interfaces; +using Microsoft.Extensions.Logging; + +namespace adas_core.Application.Services; + +public class LocalAuditService( + IAuditService auditService, + ILogger logger) + : ILocalAuditService +{ + public async Task CreateAuditLogAsync(ClaimsPrincipal? user, object? dataOriginal, object? dataModified, + string? reason) + { + try + { + if (reason != null) + { + await auditService.CreateAuditLogAsync(user, dataOriginal!, dataModified!, reason); + } + else + { + _ = user?.FindFirst(ClaimTypes.Name)?.Value ?? "Not specified"; + + await auditService.CreateAuditLogAsync(user, dataOriginal!, dataModified!); + } + } + catch (Exception ex) + { + logger.LogError(ex.Message); + } + } + + public async Task DeepCopyAsync(T data) + { + try + { + return await auditService.DeepCopyAsync(data); + } + catch (Exception ex) + { + logger.LogWarning("Error copying object, trying with JsonDeepCopyAsync. Exception: {Message}", ex.Message); + try + { + var copy = await auditService.JsonDeepCopyAsync(data); + return copy; + } + catch (Exception e) + { + logger.LogError(e, "Error in fallback copy method. Exception: {Message}", e.Message); + return default; + } + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/MasterListService.cs b/adas-core.Application/Services/MasterListService.cs new file mode 100644 index 00000000..888f9b57 --- /dev/null +++ b/adas-core.Application/Services/MasterListService.cs @@ -0,0 +1,825 @@ +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.DTO; +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 Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Services; + +public class MasterListService : IMasterListService where T : MasterList, new() +{ + private readonly Lazy _admissionService; + private readonly string? _assetsDirectory; + private readonly ILocalAuditService _auditService; + private readonly Lazy _clientMessageService; + private readonly Lazy _dischargeService; + private readonly Lazy _displayService; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ILogger> _logger; + private readonly Lazy _patientService; + private readonly IServiceProvider _serviceProvider; + private readonly ISubscribersService _subscribersService; + private readonly Lazy _unitService; + + public MasterListService( + ILogger> logger, + IServiceProvider serviceProvider, + Lazy clientMessageService, + ISubscribersService subscribersService, + Lazy unitService, + Lazy displayService, + Lazy patientService, + Lazy dischargeService, + Lazy admissionService, + IOptions apiSettings, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService) + { + _logger = logger; + _serviceProvider = serviceProvider; + _clientMessageService = clientMessageService; + _subscribersService = subscribersService; + _unitService = unitService; + _displayService = displayService; + _patientService = patientService; + _dischargeService = dischargeService; + _admissionService = admissionService; + _httpContextAccessor = httpContextAccessor; + _auditService = auditService; + if (apiSettings.Value.PathToDisplayAssets != null) + _assetsDirectory = Path.Combine(apiSettings.Value.PathToDisplayAssets); + } + + public async Task DeleteMasterListById(ObjectId id) + { + var repository = GetRepository(); + var masterList = await repository.FindById(id, LocaleEnum.Default); + if (masterList == null) + { + _logger.LogInformation("Error deleting masterList not found, id: {id} ", id); + return; + } + + if (await IsInUseCount(masterList) > 0) throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); + await repository.Delete(id); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, masterList, null); + await SendMasterListBroadcast(masterList, OperationType.DeleteMasterList); + } + + // public async Task AddOptionToMasterList(ObjectId id, OptionList opt) + // { + // try + // { + // var repository = GetRepository(); + // var result = await repository.AddOptionToMasterList(id, opt); + // if (result) + // { + // var masterList = await repository.FindById(id); + // await SendMasterListBroadcast(masterList, OperationType.UpdateMasterList); + // await SendMasterListItemBroadcast(masterList,opt, opt, OperationType.AddMasterListItem); + // + // } + // + // return result; + // + // } + // catch (Exception ex) + // { + // _logger.LogError("Error AddOptionToMasterList {name}. Exception: {ex}", typeof(T).Name, ex); + // return false; + // } + // } + public async Task AddOptionToMasterList(ObjectId id, FilterOptionListElement opt) + { + try + { + var repository = GetRepository(); + //TODO: LOCALE + var oldMasterList = await repository.FindById(id, opt.Locale); + var result = await repository.AddOptionToMasterList(id, opt); + + if (result != null) + { + //TODO: LOCALE + var masterList = await repository.FindById(id); + if (masterList == null) + return null; + + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldMasterList, + masterList); + await SendMasterListBroadcast(masterList, OperationType.UpdateMasterList); + await SendMasterListItemBroadcast(masterList, result, result, OperationType.AddMasterListItem); + } + + return result; + } + catch (Exception ex) + { + _logger.LogError("Error AddOptionToMasterList {name}. Exception: {ex}", typeof(T).Name, ex); + return null; + } + } + + public async Task UpdateMasterListOption(ObjectId id, OptionList opt, string typeName, + LocaleEnum locale) + { + try + { + var repository = GetRepository(); + //TODO: LOCALE + var oldMasterList = await repository.FindById(id); + var oldOption = oldMasterList?.Options.FirstOrDefault(o => o.Id == opt.Id); + var result = await repository.UpdateMasterListOption(id, opt, locale); + if (result != null) + { + //TODO: LOCALE + var masterList = await repository.FindById(id, LocaleEnum.Default) ?? new T(); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldMasterList, + masterList); + await SendMasterListItemBroadcast(masterList, opt, oldOption, OperationType.UpdateMasterListItem); + await UpdatePatientItemList(id, + new UpdateOptionMasterListDto + { OldOption = oldOption, UpdatedOption = opt, OptionId = opt.Id.ToString() }, typeName); + } + + return result; + } + catch (Exception ex) + { + _logger.LogError("Error UpdateMasterListOption {name}. Exception: {ex}", typeof(T).Name, ex); + return null; + } + } + + public async Task UpdateMasterListOption(ObjectId id, OptionList opt, string typeName) + { + try + { + var repository = GetRepository(); + //TODO: LOCALE + var oldMasterList = await repository.FindById(id); + var oldOption = oldMasterList?.Options.FirstOrDefault(o => o.Id == opt.Id); + var result = await repository.UpdateMasterListOption(id, opt); + if (result != null) + { + //TODO: LOCALE + var masterList = await repository.FindById(id, LocaleEnum.Default) ?? new T(); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldMasterList, + masterList); + await SendMasterListItemBroadcast(masterList, opt, oldOption, OperationType.UpdateMasterListItem); + await UpdatePatientItemList(id, + new UpdateOptionMasterListDto + { OldOption = oldOption, UpdatedOption = opt, OptionId = opt.Id.ToString() }, typeName); + } + + return result; + } + catch (Exception ex) + { + _logger.LogError("Error UpdateMasterListOption {name}. Exception: {ex}", typeof(T).Name, ex); + return null; + } + } + + public async Task UpdateFullMasterListOption(ObjectId id, OptionList opt, string typeName) + { + try + { + var repository = GetRepository(); + //TODO: LOCALE + var oldMasterList = await repository.FindById(id); + var oldOption = oldMasterList?.Options.FirstOrDefault(o => o.Id == opt.Id); + var result = await repository.UpdateFullMasterListOption(id, opt); + if (result != null) + { + //TODO: LOCALE + var masterList = await repository.FindById(id, LocaleEnum.Default) ?? new T(); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldMasterList, + masterList); + await SendMasterListItemBroadcast(masterList, opt, oldOption, OperationType.UpdateMasterListItem); + await UpdatePatientItemList(id, + new UpdateOptionMasterListDto + { OldOption = oldOption, UpdatedOption = opt, OptionId = opt.Id.ToString() }, typeName); + } + + return result; + } + catch (Exception ex) + { + _logger.LogError("Error UpdateMasterListOption {name}. Exception: {ex}", typeof(T).Name, ex); + return null; + } + } + + public async Task DeleteMasterListOption(ObjectId id, ObjectId optId, string typeName) + { + try + { + var repository = GetRepository(); + var oldMasterList = await repository.FindById(id, LocaleEnum.Default); + var opt = oldMasterList?.Options.First(c => c.Id == optId); + var result = await repository.DeleteMasterListOption(id, optId); + if (result && opt != null) + { + var masterList = await repository.FindById(id, LocaleEnum.Default) ?? new T(); + + await DeletePatientItemList(id, opt, typeName); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldMasterList, + masterList); + await SendMasterListBroadcast(masterList, OperationType.UpdateMasterList); + await SendMasterListItemBroadcast(masterList, opt, opt, OperationType.DeleteMasterListItem); + + return true; + } + + return false; + } + catch (Exception ex) + { + _logger.LogError("Error DeleteMasterListOption {name}. Exception: {ex}", typeof(T).Name, ex); + return false; + } + } + + public async Task UpdateOptionDetailsToMasterList(ObjectId id, + UpdateMasterListDetailsDto opt) + { + try + { + var repository = GetRepository(); + var oldMasterList = await repository.FindById(id); + var result = await repository.UpdateOptionDetailsToMasterList(id, opt); + if (result != null) + { + var masterList = await repository.FindById(id) ?? new T(); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldMasterList, + masterList); + await SendMasterListBroadcast(masterList, OperationType.UpdateMasterList); + } + + return result; + } + catch (Exception ex) + { + _logger.LogError("Error UpdateMasterListOption {name}. Exception: {ex}", typeof(T).Name, ex); + return null; + } + } + + public async Task UpdateMasterListName(ObjectId id, string name) + { + try + { + var repository = GetRepository(); + var oldMasterList = await repository.FindById(id, LocaleEnum.Default); + var result = await repository.UpdateMasterListName(id, name); + if (result) + { + var masterList = await repository.FindById(id, LocaleEnum.Default) ?? new T(); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldMasterList, + masterList); + await SendMasterListBroadcast(masterList, OperationType.UpdateMasterList); + } + + return result; + } + catch (Exception ex) + { + _logger.LogError("Error UpdateMasterListOption {name}. Exception: {ex}", typeof(T).Name, ex); + return false; + } + } + + public async Task UpdateMasterListDescription(ObjectId id, string name) + { + try + { + var repository = GetRepository(); + var oldMasterList = await repository.FindById(id, LocaleEnum.Default); + var result = await repository.UpdateMasterListDescription(id, name); + if (result) + { + var masterList = await repository.FindById(id, LocaleEnum.Default) ?? new T(); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldMasterList, + masterList); + await SendMasterListBroadcast(masterList, OperationType.UpdateMasterList); + } + + return result; + } + catch (Exception ex) + { + _logger.LogError("Error UpdateMasterListOption {name}. Exception: {ex}", typeof(T).Name, ex); + return false; + } + } + + public async Task RemoveMasterListOption(ObjectId id, OptionList oldOpt) + { + try + { + var repository = GetRepository(); + var oldMasterList = await repository.FindById(id, LocaleEnum.Default); + var result = await repository.RemoveMasterListOption(id, oldOpt); + if (result) + { + var masterList = await repository.FindById(id, LocaleEnum.Default) ?? new T(); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldMasterList, + masterList); + await SendMasterListBroadcast(masterList, OperationType.UpdateMasterList); + await SendMasterListItemBroadcast(masterList, oldOpt, oldOpt, OperationType.DeleteMasterListItem); + } + + return result; + } + catch (Exception ex) + { + _logger.LogError("Error UpdateMasterListOption {name}. Exception: {ex}", typeof(T).Name, ex); + return false; + } + } + + public async Task> GetAllMasterList() + { + try + { + var repository = GetRepository(); + return await repository.GetAll(); + } + catch (Exception ex) + { + _logger.LogError("Error getting all {name}. Exception: {ex}", typeof(T).Name, ex); + return []; + } + } + + public async Task> GetAllMasterListWithoutOptions() + { + try + { + var repository = GetRepository(); + return await repository.GetAllWithoutOptions(); + } + catch (Exception ex) + { + _logger.LogError("Error getting all {name}. Exception: {ex}", typeof(T).Name, ex); + return []; + } + } + + public async Task FindOptionItemById(ObjectId masterId, ObjectId optionId, LocaleEnum locale) + { + try + { + var repository = GetRepository(); + return await repository.FindOptionItemById(masterId, optionId, locale); + } + catch (Exception ex) + { + _logger.LogError("Error getting all {name}. Exception: {ex}", typeof(T).Name, ex); + return null; + } + } + + public async Task> GetAllMasterListWithPaginatedOptions( + PaginationFilter request) + { + try + { + var repository = GetRepository(); + var masterLists = await repository.GetAll(); + List results = []; + foreach (var masterList in masterLists) + { + var units = await IsInUse(masterList); + var list = new MasterListWithPaginatedOptionsDto(masterList, request.PageSize, units); + results.Add(list); + } + + return results; + } + catch (Exception ex) + { + _logger.LogError("Error getting all {name}. Exception: {ex}", typeof(T).Name, ex); + return []; + } + } + + public async Task GetAllMasterListCount() + { + try + { + var repository = GetRepository(); + return await repository.Count(); + } + catch (Exception ex) + { + _logger.LogError("Error getting all {name}. Exception: {ex}", typeof(T).Name, ex); + return 0; + } + } + + public async Task GetMasterListById(ObjectId id, LocaleEnum? locale) + { + try + { + var repository = GetRepository(); + return await repository.FindById(id, locale); + } + catch (Exception ex) + { + _logger.LogError("Error getting by id: {id}. Exception: {ex}", id, ex); + return null; + } + } + + public async Task GetMasterListByIdWithPaginatedOptions(ObjectId id, + PaginationFilter request) + { + try + { + var repository = GetRepository(); + var list = await repository.FindById(id); + if (list != null) + { + var units = await IsInUse(list); + return new MasterListWithPaginatedOptionsDto(list, request.PageSize, units); + } + + return null; + } + catch (Exception ex) + { + _logger.LogError("Error getting by id: {id}. Exception: {ex}", id, ex); + return null; + } + } + + public async Task> GetMasterListOptionsNamesById(ObjectId id) + { + try + { + var repository = GetRepository(); + var list = await repository.FindById(id, LocaleEnum.Default); + var nameList = new List(); + + list?.Options.ForEach(o => nameList.Add(o.Name)); + return nameList; + } + catch (Exception ex) + { + _logger.LogError("Error getting by id: {id}. Exception: {ex}", id, ex); + return []; + } + } + + public async Task> GetMasterListByIdAndTextSearch(ObjectId id, string? textSearch) + { + try + { + var repository = GetRepository(); + return await repository.GetMasterListByIdAndTextSearchContaining(id, textSearch); + } + catch (Exception ex) + { + _logger.LogError("Error getting by id: {id}. Exception: {ex}", id, ex); + return []; + } + } + + public async Task> GetMasterListByIdAndSearchOptions(ObjectId id, + FilterOptionListElement filterOption) + { + var repository = GetRepository(); + return await repository.GetMasterListByIdAndSearchOptions(id, filterOption); + } + + public async Task GetMasterListByName(string name) + { + try + { + var repository = GetRepository(); + var result = await repository.FindByName(name); + return result; + } + catch (Exception ex) + { + _logger.LogError("Error getting by name: {name}. Exception: {ex}", name, ex); + return null; + } + } + + public async Task> GetPaginatedMasterList(PaginationFilter filter) + { + var repository = GetRepository(); + + var result = repository.GetPaginatedMasterList(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(dataList, filter.PageNumber, filter.PageSize, count); + } + + + public async Task> GetPaginatedMasterListWithPaginatedOptions( + PaginationFilter listFilter, PaginationFilter optionFilter) + { + var repository = GetRepository(); + + var result = repository.GetPaginatedMasterList(listFilter); + + var count = await result.CountDocumentsAsync(); + + var data = await result.Skip((listFilter.PageNumber - 1) * listFilter.PageSize) + .Limit(listFilter.PageSize) + .ToCursorAsync(); + + + var dataList = await data.ToListAsync(); + List results = []; + + foreach (var list in dataList) + { + var units = await IsInUse(list); + var listDto = new MasterListWithPaginatedOptionsDto(list, optionFilter.PageSize, units); + results.Add(listDto); + } + + return new PaginationResponse(results, listFilter.PageNumber, + listFilter.PageSize, count); + } + + public async Task> GetPaginatedOptions(PaginationFilter filter, ObjectId listId) + { + var repository = GetRepository(); + var result = await repository.GetPaginatedOptions(filter, listId); + var count = result.Count; + var data = result.Skip((filter.PageNumber - 1) * filter.PageSize) + .Take(filter.PageSize) + .ToList(); + return new PaginationResponse(data, filter.PageNumber, filter.PageSize, count); + } + + public async Task InsertMasterList(T item) + { + try + { + var repository = GetRepository(); + await repository.InsertOneAsync(item); + await SendMasterListBroadcast(item, OperationType.NewMasterList); + var result = await repository.FindById(item.Id); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, result); + return result; + } + catch (Exception ex) + { + _logger.LogError("Error inserting {name}: {item}. Exception: {ex}", typeof(T).Name, item, ex); + return null; + } + } + + public async Task UpdateMasterList(T item) + { + try + { + var repository = GetRepository(); + var oldItem = repository.FindById(item.Id, LocaleEnum.Default); + await repository.Update(item); + await SendMasterListBroadcast(item, OperationType.UpdateMasterList); + var result = await repository.FindById(item.Id, LocaleEnum.Default); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldItem, result); + return result; + } + catch (Exception ex) + { + _logger.LogError("Error updating {name}: {item}. Exception: {ex}", typeof(T).Name, item, ex); + return null; + } + } + + + public async Task GetAssociatedList(ObjectId id, MasterListType masterListType1, + MasterListType masterListType2) + { + var repository = GetRepository(); + await repository.FindById(id, LocaleEnum.Default); + + var units = await _unitService.Value.FindUnitsByMasterListId(id, masterListType1); + var unitArray = units as Unit[] ?? (units ?? []).ToArray(); + if (!unitArray.Any()) return null; + var unit = unitArray.First(); + + switch (masterListType2) + { + case MasterListType.AltableOptionList: return unit.AltableOptionListId; + case MasterListType.AllergyList: return unit.AllergyListId; + case MasterListType.DestinationList: return unit.DestinationListId; + case MasterListType.DiagnosisList: return unit.DiagnosisListId; + case MasterListType.DischargeStatusList: return unit.DischargeStatusListId; + case MasterListType.DoctorList: return unit.DoctorListId; + case MasterListType.DoctorTypeList: return unit.DoctorTypeListId; + case MasterListType.InternalDestinationList: return unit.InternalDestinationListId; + case MasterListType.InsulationList: return unit.InsulationListId; + case MasterListType.LanguageBarrierList: return unit.LanguageBarrierListId; + case MasterListType.PassiveSittingList: return unit.PassiveSittingListId; + case MasterListType.GenericList: return unit.GenericListId; + case MasterListType.MobilityOptionList: return unit.MobilityOptionListId; + case MasterListType.OriginList: return unit.OriginListId; + case MasterListType.PatientStatusList: return unit.PatientStatusListId; + case MasterListType.ProcedureList: return unit.ProcedureListId; + case MasterListType.TestList: return unit.TestListId; + case MasterListType.ServiceList: return unit.ServiceListId; + case MasterListType.TherapeuticCeilingList: return unit.TherapeuticCeilingListId; + case MasterListType.TreatmentList: return unit.TreatmentListId; + case MasterListType.VisitOptionList: return unit.VisitOptionListId; + case MasterListType.AccessControlList: return unit.AccessControlListId; + } + + return null; + } + + public async Task> GetOptionsOfList(ObjectId id) + { + var repository = GetRepository(); + var list = await repository.FindById(id, LocaleEnum.Default); + List options = []; + if (list == null) return options; + foreach (var item in list.Options) options.Add(item.Name); + return options; + } + + private IMasterListRepository GetRepository() + { + return _serviceProvider.GetRequiredService>(); + } + + private async Task UpdatePatientItemList(ObjectId id, UpdateOptionMasterListDto opt, string typeName) + { + // Necesito saber que unidades tienen el id de lista que estamos modificando + var unitList = await _unitService.Value.FindUnitsByMasterListId(id); + // Que pacientes dentro de esa/s unidades tienen el valor antiguo de la lista que estamos modificando + var units = unitList as Unit[] ?? unitList.ToArray(); + await _patientService.Value.UpdatePatientMasterListItemChange(opt, units, typeName); + await _dischargeService.Value.UpdatePatientMasterListItemChange(opt, units, typeName); + await _admissionService.Value.UpdatePatientMasterListItemChange(opt, units, typeName); + } + + private async Task DeletePatientItemList(ObjectId id, OptionList opt, string typeName) + { + // Necesito saber que unidades tienen el id de lista que estamos modificando + var unitList = await _unitService.Value.FindUnitsByMasterListId(id); + // Que pacientes dentro de esa/s unidades tienen el valor antiguo de la lista que estamos modificando + var units = unitList as Unit[] ?? unitList.ToArray(); + await _patientService.Value.DeletePatientMasterListItem(opt, units, typeName); + await _dischargeService.Value.DeletePatientMasterListItem(opt, units, typeName); + await _admissionService.Value.DeletePatientMasterListItem(opt, units, typeName); + } + + public Task SaveRequestAsync(ApiRequest apiRequest) + { + return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); })); + } + + public Task SaveRequest(ApiRequest apiRequest) + { + return Task.CompletedTask; + } + + private async Task SendMasterListBroadcast(T masterList, OperationType operation) + { + try + { + var type = masterList.GetType(); + var masterListTypeName = type.Name; + if (!Enum.TryParse(masterListTypeName, out var masterListType)) + return; + + var units = await _unitService.Value.FindUnitsByMasterListId(masterList.Id, masterListType); + + if (units == null) + return; + + List diplayIds = []; + foreach (var unit in units) + { + var displayList = await _displayService.Value.GetByUnitId(unit.Id); + displayList.ForEach(d => diplayIds.Add(d.Id)); + } + + var subscribers = _subscribersService.GetSubscribers() + .Where(s => s.DisplayId.HasValue && diplayIds.Contains(s.DisplayId.Value)).ToList(); + + foreach (var subscriber in subscribers) + _ = _clientMessageService.Value.SendAsync(subscriber.Id, operation, + masterList.ReturnMasterListOptionsInLocaleIfExist(subscriber.Locale)); + } + catch (Exception ex) + { + _logger.LogError("Exception sending masterList broadcast. Operation type: {op}. Exception: {ex}", + operation.ToString(), ex); + } + } + + private async Task SendMasterListItemBroadcast(T masterList, OptionList? updatedItem, OptionList? oldItem, + OperationType operation) + { + try + { + var type = masterList.GetType(); + var masterListTypeName = type.Name; + if (!Enum.TryParse(masterListTypeName, out var masterListType)) + return; + + var units = await _unitService.Value.FindUnitsByMasterListId(masterList.Id, masterListType); + + if (units == null) + return; + + List diplayIds = []; + foreach (var unit in units) + { + var displayList = await _displayService.Value.GetByUnitId(unit.Id); + displayList.ForEach(d => diplayIds.Add(d.Id)); + } + + var subscribers = _subscribersService.GetSubscribers() + .Where(s => s.DisplayId.HasValue && diplayIds.Contains(s.DisplayId.Value)).ToList(); + object message; + switch (operation) + { + case OperationType.AddMasterListItem: + message = new + { + newItem = updatedItem, + listType = masterListTypeName, + name = masterList.Name + }; + break; + case OperationType.DeleteMasterListItem: + message = new + { + deletedItem = oldItem, + listType = masterListTypeName, + name = masterList.Name + }; + break; + default: + message = new + { + updatedItem, + oldItem, + listType = masterListTypeName, + name = masterList.Name + }; + break; + } + + foreach (var subscriber in subscribers) + _ = _clientMessageService.Value.SendAsync(subscriber.Id, operation, message); + } + catch (Exception ex) + { + _logger.LogError("Exception sending masterList broadcast. Operation type: {op}. Exception: {ex}", + operation.ToString(), ex); + } + } + + private async Task> IsInUse(T list) + { + var units = await _unitService.Value.FindUnitsByMasterListId(list.Id, list.ListType); + List unitInfoDtos = []; + var unitArray = units as Unit[] ?? (units ?? []).ToArray(); + if (units != null && unitArray.Any()) + foreach (var unit in unitArray) + { + var unitInfoDto = new UnitInfoDto(unit); + unitInfoDtos.Add(unitInfoDto); + } + + return unitInfoDtos; + } + + private async Task IsInUseCount(T list) + { + return await _unitService.Value.CountUnitsByMasterListId(list.Id, list.ListType); + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/MasterListServiceFactory .cs b/adas-core.Application/Services/MasterListServiceFactory .cs new file mode 100644 index 00000000..043c66b0 --- /dev/null +++ b/adas-core.Application/Services/MasterListServiceFactory .cs @@ -0,0 +1,299 @@ +using System.Reflection; +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; + +namespace adas_core.Application.Services; + +public class MasterListServiceFactory( + IServiceProvider serviceProvider, + ILogger logger, + IOptions listSettings) + : IMasterListServiceFactory +{ + private readonly IOptions _listSettings = listSettings; + private readonly ILogger _logger = logger; + + // Obtiene el servicio basado en MasterListType + public object GetService(MasterListType serviceName) + { + var typeParameter = Type.GetType($"adas_core.Domain.Models.Masters.{serviceName}, adas-core.Domain") ?? + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var genericTypeDef = typeof(MasterListService<>); + var specificServiceType = genericTypeDef.MakeGenericType(typeParameter) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + + return GetService(specificServiceType); + } + + // Obtiene el servicio basado en un Type genérico + public object GetService(Type serviceType) + { + // Verifica si el tipo es genérico y está basado en MasterList + if (serviceType.IsGenericType && serviceType.GetGenericTypeDefinition() == typeof(MasterListService<>)) + { + // Obtiene el tipo genérico base + var itemType = serviceType.GenericTypeArguments[0]; + + // Construye el tipo del servicio genérico con base en el tipo T + var specificServiceType = typeof(IMasterListService<>).MakeGenericType(itemType); + + // Obtiene el servicio utilizando el tipo construido + var service = serviceProvider.GetService(specificServiceType); + + return service ?? + throw new InvalidOperationException( + $"No se pudo resolver el servicio de tipo {serviceType.FullName}"); + } + + _logger.LogError("El tipo de servicio {serviceType} no es compatible.", serviceType.FullName); + throw new InvalidOperationException($"El tipo de servicio {serviceType.FullName} no es compatible."); + } + + public Type GetMasterListSpecificType(MasterListType masterListType) + { + // mapear cada valor de MasterListType a su respectivo tipo específico + return masterListType switch + { + MasterListType.AltableOptionList => typeof(AltableOptionList), + MasterListType.AllergyList => typeof(AllergyList), + MasterListType.DestinationList => typeof(DestinationList), + MasterListType.DiagnosisList => typeof(DiagnosisList), + MasterListType.DischargeStatusList => typeof(DischargeStatusList), + MasterListType.DoctorList => typeof(DoctorList), + MasterListType.DoctorTypeList => typeof(DoctorTypeList), + MasterListType.InternalDestinationList => typeof(InternalDestinationList), + MasterListType.InsulationList => typeof(InsulationList), + MasterListType.MobilityOptionList => typeof(MobilityOptionList), + MasterListType.OriginList => typeof(OriginList), + MasterListType.PatientStatusList => typeof(PatientStatusList), + MasterListType.ProcedureList => typeof(ProcedureList), + MasterListType.TestList => typeof(TestList), + MasterListType.ServiceList => typeof(ServiceList), + MasterListType.TherapeuticCeilingList => typeof(TherapeuticCeilingList), + MasterListType.PassiveSittingList => typeof(PassiveSittingList), + MasterListType.GenericList => typeof(GenericList), + MasterListType.TreatmentList => typeof(TreatmentList), + MasterListType.VisitOptionList => typeof(VisitOptionList), + MasterListType.AccessControlList => typeof(AccessControlList), + MasterListType.LanguageBarrierList => typeof(LanguageBarrierList), + _ => typeof(MasterList) + }; + } + + public object? GetTypedMasterList(MasterListType masterListType, MasterList masterList) + { + // Obtiene el tipo específico de MasterList basado en masterListType + var specificType = GetMasterListSpecificType(masterListType); + + // Utiliza reflexión para crear una instancia del tipo específico + var specificMasterList = Activator.CreateInstance(specificType); + if (specificMasterList == null) + return null; + + // Realiza el mapeo de propiedades de masterList a specificMasterList + MapMasterListProperties(masterList, specificMasterList); + + return specificMasterList; + } + + public async Task InsertMasterList(MasterListType masterListType, MasterList masterList) + { + dynamic service = GetService(masterListType) ?? + throw new InvalidOperationException("Service not found for type " + masterListType); + var typedMasterList = GetTypedMasterList(masterListType, masterList) ?? + throw new InvalidOperationException("Failed to convert MasterList to specific type."); + + // Reflexión para invocar el método InsertMasterList + var method = service.GetType().GetMethod("InsertMasterList", new[] { typedMasterList.GetType() }) ?? + throw new InvalidOperationException("Method InsertMasterList not found."); + var task = (Task)method.Invoke(service, new[] { typedMasterList }); + await task.ConfigureAwait(false); + + var resultProperty = task.GetType().GetProperty("Result"); + return resultProperty?.GetValue(task); + } + + public async Task UpdateMasterList(MasterListType masterListType, MasterList masterList) + { + dynamic service = GetService(masterListType) ?? + throw new InvalidOperationException("Service not found for type " + masterListType); + var typedMasterList = GetTypedMasterList(masterListType, masterList) ?? + throw new InvalidOperationException("Failed to convert MasterList to specific type."); + + // Reflexión para invocar el método InsertMasterList + var method = service.GetType().GetMethod("UpdateMasterList", new[] { typedMasterList.GetType() }) ?? + throw new InvalidOperationException("Method UpdateMasterList not found."); + var task = (Task)method.Invoke(service, new[] { typedMasterList }); + await task.ConfigureAwait(false); + + var resultProperty = task.GetType().GetProperty("Result"); + return resultProperty?.GetValue(task); + } + + public async Task GetMasterListById(MasterListType masterListType, ObjectId masterListId, + LocaleEnum? dataLocale) + { + dynamic service = GetService(masterListType) ?? + throw new InvalidOperationException("Service not found for type " + masterListType); + + // Reflexión para invocar el método InsertMasterList + var method = + service.GetType().GetMethod("GetMasterListById", new[] { typeof(ObjectId), typeof(LocaleEnum?) }) ?? + throw new InvalidOperationException("Method GetMasterListById not found."); + var task = (Task)method.Invoke(service, new object[] { masterListId, dataLocale! }); + + await task.ConfigureAwait(false); + + var resultProperty = task.GetType().GetProperty("Result"); + return resultProperty?.GetValue(task); + } + + public async Task GetMasterListOptionById(MasterListType masterListType, ObjectId masterListId, + ObjectId masterListOptionId, + LocaleEnum? dataLocale) + { + dynamic service = GetService(masterListType) ?? + throw new InvalidOperationException("Service not found for type " + masterListType); + + // Reflexión para invocar el método InsertMasterList + var method = + service.GetType().GetMethod("FindOptionItemById", + new[] { typeof(ObjectId), typeof(ObjectId), typeof(LocaleEnum) }) ?? + throw new InvalidOperationException("Method GetMasterListById not found."); + var task = (Task)method.Invoke(service, + new object[] { masterListId, masterListOptionId, dataLocale ?? LocaleEnum.Default }); + + await task.ConfigureAwait(false); + + var resultProperty = task.GetType().GetProperty("Result"); + return resultProperty?.GetValue(task); + } + + public List StringNurseObs() + { + var stringNurseObs = new List(); + + // 1. Obtener la instancia configurada de ListSettings + var listSettingsInstance = _listSettings.Value; + var listSettingsType = typeof(ListSettings); + + // 2. Obtener todas las propiedades públicas de la instancia de ListSettings + // Estas propiedades son las que están inicializadas con ListSettingItem + var properties = listSettingsType.GetProperties(BindingFlags.Public | BindingFlags.Instance); + + foreach (var prop in properties) + { + // 3. Leer el valor de la propiedad (que es un ListSettingItem) de la instancia configurada + // prop.GetValue(listSettingsInstance) devuelve el ListSettingItem que contiene el ManualObservationName + + // 4. Extraer el ManualObservationName + if (prop.GetValue(listSettingsInstance) is ListSettingItem listSettingItem && !string.IsNullOrEmpty(listSettingItem.ManualObservationName)) + stringNurseObs.Add(listSettingItem.ManualObservationName); + } + + // 5. Añadir otras observaciones fijas + // (Añadir observaciones referentes a las acciones de NursePlan) + stringNurseObs.Add("PatientIncomingData"); + + return stringNurseObs; + } + + public async Task GetPatientTraslated(Unit? unit, LocaleEnum? locale, Patient? patient) + { + if (patient == null) + return null; + // Si es Default, devolvemos tal cual + if (locale == LocaleEnum.Default || unit == null) + return patient; + + var listMap = new List<(string field, ObjectId? listId, MasterListType type)> + { + ("origin", unit.OriginListId, MasterListType.OriginList), + ("diagnosis", unit.DiagnosisListId, MasterListType.DiagnosisList), + ("visits", unit.VisitOptionListId, MasterListType.VisitOptionList), + ("insulation", unit.InsulationListId, MasterListType.InsulationList), + ("languageBarrier", unit.LanguageBarrierListId, MasterListType.LanguageBarrierList), + ("allergies", unit.AllergyListId, MasterListType.AllergyList), + ("therapeuticCeiling", unit.TherapeuticCeilingListId, MasterListType.TherapeuticCeilingList), + ("tests", unit.TestListId, MasterListType.TestList), + ("procedures", unit.ProcedureListId, MasterListType.ProcedureList), + ("treatment", unit.TreatmentListId, MasterListType.TreatmentList) + }; + + foreach (var (field, listId, masterListType) in listMap) + { + if (listId == null) + continue; + + // Obtener propiedad del paciente (origin, diagnosis, procedures…) + var prop = typeof(Patient).GetProperty( + field, + BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); + + if (prop == null) + continue; + + var propValue = prop.GetValue(patient); + if (propValue == null) + continue; + + // var master = listObj as dynamic; + // IEnumerable masterOptions = master.Options; + + // --------------------------------------------------------- + // CASO 1 → OptionList simple + // --------------------------------------------------------- + if (propValue is OptionList single) + { + if (single.Id != null) + { + var translated = + await GetMasterListOptionById(masterListType, listId.Value, single.Id.Value, locale); + if (translated is OptionList translatedSingle) + single.Name = translatedSingle.Name; + } + + continue; + } + + // --------------------------------------------------------- + // CASO 2 → Lista de OptionList (procedures, tests, treatment…) + // --------------------------------------------------------- + if (propValue is IEnumerable list) + foreach (var item in list) + { + if (item.Id == null) + continue; + + var translated = await GetMasterListOptionById(masterListType, listId.Value, item.Id.Value, locale); + if (translated is OptionList translatedSingle) + item.Name = translatedSingle.Name; + } + } + + return patient; + } + + private void MapMasterListProperties(MasterList source, object destination) + { + // Obtiene las propiedades del tipo de origen y destino + var sourceProps = source.GetType().GetProperties(); + var destProps = destination.GetType().GetProperties(); + + foreach (var sourceProp in sourceProps) + foreach (var destProp in destProps) + if (destProp.Name == sourceProp.Name && destProp.PropertyType == sourceProp.PropertyType) + { + // Asigna el valor de la propiedad de origen a la propiedad de destino + destProp.SetValue(destination, sourceProp.GetValue(source)); + break; + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/MedicineService.cs b/adas-core.Application/Services/MedicineService.cs new file mode 100644 index 00000000..198b45f1 --- /dev/null +++ b/adas-core.Application/Services/MedicineService.cs @@ -0,0 +1,273 @@ +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.Responses; +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 MedicineService( + IMedicineRepository medicineRepository, + ITreatmentService treatmentService, + IOptions apiSettings, + ILogger logger, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService) + : IMedicineService +{ + private readonly List _notesIndicatingMedication = apiSettings.Value.NotesIndicatingMedication ?? []; + + public async Task> GetAll() + { + return await medicineRepository.GetAll(); + } + + public async Task GetByCode(string code) + { + return await medicineRepository.GetMedicine(code); + } + + public async Task> GetByCodeOrNote(List codeNote) + { + return await medicineRepository.GetMedicineByCodeOrNote(codeNote); + } + + public async Task GetByName(string name) + { + return await medicineRepository.GetMedicineByName(name); + } + + public async Task> GetMedicinesOfTreatments(IEnumerable treatments) + { + var totalMedicines = new List(); + + try + { + foreach (var treatment in treatments) + { + if (treatment == null) + continue; + + var medicines = new List(); + + foreach (var code in treatment.RequestedGiveCodes) + { + var medicineList = await medicineRepository.GetMedicineByCodeOrNote([code.Identifier]); + var medicine = medicineList.FirstOrDefault(); + //Cant retrieve from medicine list check for parental nutrition and if is not and any note indicate that is medication it is added + //to collection + + if (medicine == null) + { + var parentalNutritionMedicine = await CalculateParentalNutritionMedicine(treatment); + if (parentalNutritionMedicine != null) + { + medicines.Add(parentalNutritionMedicine); + continue; + } + + var isMedicine = treatment.Notes.Any(n => _notesIndicatingMedication.Contains(n.Comment)); + if (isMedicine) + medicines.Add(new Medicine + { + Codes = [code.Identifier], + Name = code.Text + }); + } + else if (!medicine.Type.Contains("Nutrition")) + { + medicines.Add(medicine); + } + } + + if (!string.IsNullOrEmpty(treatment.RequestedGiveTreatment)) + try + { + medicines = + [ + new Medicine + { + Name = treatment.RequestedGiveTreatment, + Type = medicines.FindAll(t => t.Type.Any()) + .Select(m => m.Type.Aggregate((x, y) => x + "," + y)).Distinct() + .ToList(), + Codes = medicines.FindAll(t => t.Codes.Any()) + .Select(m => m.Codes.Aggregate((x, y) => x + "," + y)).Distinct() + .ToList(), + Group = medicines.FindAll(t => t.Group.Any()) + .Select(m => m.Group.Aggregate((x, y) => x + "," + y)).Distinct() + .ToList(), + Notes = medicines.FindAll(t => t.Notes.Any()) + .Select(m => m.Notes.Aggregate((x, y) => x + "," + y)).Distinct() + .ToList() + } + ]; + } + catch (Exception ex) + { + logger.LogError("Error Getting Medicines from Treatment. {medicines} . Exception {ex}", + string.Join(",", medicines), ex); + } + + totalMedicines.AddRange(medicines); + } + } + catch (Exception e) + { + logger.LogError("Error get Medicines Of Treatments: {eMessage} {eStackTrace}", e.Message, e.StackTrace); + } + + return totalMedicines.AsEnumerable(); + } + + public async Task> GetActiveMedicinesByPatient(ObjectId patientId) + { + var activeTreatments = await treatmentService.GetActiveTreatmentsByPatient(patientId); + return await GetMedicinesOfTreatments(activeTreatments); + } + + + public async Task> GetPaginatedMedicines(PaginationFilter filter) + { + var result = medicineRepository.GetPaginatedMedicines(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(dataList, filter.PageNumber, filter.PageSize, count); + } + + + public async Task GetMedicineById(ObjectId medicineId) + { + return await medicineRepository.GetMedicineById(medicineId) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + } + + public async Task PostMedicine(Medicine medicine) + { + var result = await medicineRepository.PostMedicine(medicine) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, result); + return result; + } + + public async Task UpdateMedicine(Medicine medicine) + { + var oldMedicine = GetMedicineById(medicine.Id); + var newMedicine = await medicineRepository.UpdateMedicine(medicine); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldMedicine, newMedicine); + return newMedicine; + } + + public async Task DeleteMedicineById(ObjectId medicineId) + { + var oldMedicine = GetMedicineById(medicineId); + await medicineRepository.DeleteMedicineById(medicineId); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldMedicine, null); + } + + public async Task> GetAllTypes() + { + try + { + var bsonDocuments = await medicineRepository.GetDistinctFieldDataQuery("type").ToListAsync(); + var result = bsonDocuments.Select(doc => doc["type"].AsString).ToList(); + return result; + } + catch (Exception) + { + return []; + } + } + + public async Task> GetAllGroups() + { + try + { + var bsonDocuments = await medicineRepository.GetDistinctFieldDataQuery("group").ToListAsync(); + var result = bsonDocuments.Select(doc => doc["group"].AsString).ToList(); + return result; + } + catch (Exception) + { + return []; + } + } + + public async Task> GetAllNames() + { + try + { + var bsonDocuments = await medicineRepository.GetDistinctFieldDataQuery("name").ToListAsync(); + var result = bsonDocuments.Select(doc => doc["name"].AsString).ToList(); + return result; + } + catch (Exception) + { + return []; + } + } + + public async Task> GetAllCodes() + { + try + { + var bsonDocuments = await medicineRepository.GetDistinctFieldDataQuery("codes").ToListAsync(); + var result = bsonDocuments.Select(doc => doc["codes"].AsString).ToList(); + return result; + } + catch (Exception) + { + return []; + } + } + + //Exclusive for H12O, not in calculatedObservations of H12O to dont repeat code in multiple places. + private Task CalculateParentalNutritionMedicine(PatientTreatment treatment) + { + Medicine? medicine = null; + + try + { + if (treatment is { Notes: not null } && treatment.Notes.FirstOrDefault(n => n.Comment == "NPT") != null) + { + medicine = new Medicine + { + Name = treatment.Notes.FirstOrDefault(n => n.CommentType == "formularybaseformulation")?.Comment ?? + "UNKNOWN" + }; + + if (treatment.Notes is { Count: > 0 }) medicine.Notes = treatment.Notes.Select(t => t.Comment).ToList(); + + + if (treatment.Notes.FirstOrDefault(n => + n.Comment is "LÍPIDOS NEONATALES AL 20%" or "LÍPIDOS NEONATALES AL 20% CON...") != null) + medicine.Type = [MedicineEnum.Types.ParenteralNutritionLipids.ToString()]; + else + medicine.Type = [MedicineEnum.Types.ParenteralNutrition.ToString()]; + } + } + catch (Exception e) + { + logger.LogError("Error calculate Parental Nutrition Medicine: {eMessage} {eStackTrace}", e.Message, + e.StackTrace); + } + + return Task.FromResult(medicine); + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/NoticeService.cs b/adas-core.Application/Services/NoticeService.cs new file mode 100644 index 00000000..bcc54895 --- /dev/null +++ b/adas-core.Application/Services/NoticeService.cs @@ -0,0 +1,198 @@ +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.MongoModels; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; + +namespace adas_core.Application.Services; + +public class NoticeService( + ILogger logger, + ISubscribersService subscribersService, + INoticeRepository noticeRepository, + IClientMessageService clientMessageService, + IDisplayService displayService, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService) + : INoticeService +{ + public async Task DeleteNoticeAsync(Notice notice) + { + await DeleteNoticeByIdAsync(notice.Id); + } + + public async Task DeleteNoticeByIdAsync(ObjectId noticeId) + { + try + { + var noticeAux = await noticeRepository.FindById(noticeId); + if (noticeAux == null) + { + logger.LogInformation("Error deleting Notice not found, id: {noticeId} NOT DELETED ", noticeId); + return; + } + + await noticeRepository.Delete(noticeId); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, noticeAux, null); + logger.LogInformation("Notice id: {noticeId} DELETED ", noticeId); + + SendNoticeBroadcast(noticeAux, OperationType.DeleteNotice); + } + catch (Exception ex) + { + logger.LogError("Exception deleting notice id:{notice} . Exception: {ex}", noticeId, ex); + } + } + + public async Task GetNoticeByIdAsync(ObjectId noticeId) + { + try + { + return await noticeRepository.FindById(noticeId); + } + catch (Exception ex) + { + logger.LogError("Exception deleting notice id:{notice} . Exception: {ex}", noticeId, ex); + return null; + } + } + + public async Task?> GetNoticeByTypeAsync(string noticeType) + { + return await noticeRepository.FindByType(noticeType) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + } + + public async Task> GetNoticesAsync() + { + return await noticeRepository.FindAll(); + } + + public async Task InsertNotice(Notice notice) + { + try + { + notice.Id = new ObjectId(); + await noticeRepository.InsertOneAsync(notice); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, notice); + logger.LogInformation("Notice: {notice} INSERTED", notice); + + SendNoticeBroadcast(notice, OperationType.NewNotice); + + return notice; + } + catch (Exception ex) + { + logger.LogError("Exception inserting notice {notice} . Exception: {ex}", notice, ex); + return null; + } + } + + public async Task UpdateNoticeAsync(Notice notice) + { + try + { + var auxNotice = await noticeRepository.FindById(notice.Id) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + await noticeRepository.Update(notice); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, auxNotice, notice); + SendNoticeBroadcast(notice, OperationType.UpdateNotice); + } + catch (Exception ex) + { + logger.LogError("Exception updating notice {notice} . Exception: {ex}", notice, ex); + } + } + + public async Task SaveRequest(ApiRequest apiRequest) + { + try + { + if (apiRequest.Notice == null) + return; + + switch (apiRequest.Type) + { + case "NewNotice": + { + if (string.IsNullOrEmpty(apiRequest.Notice.Description)) + { + logger.LogDebug("Error saving notice api request. Some values are required. Notice: {notice}", + apiRequest.Notice); + return; + } + + await InsertNotice(apiRequest.Notice); + break; + } + case "UpdateNotice": + { + if (string.IsNullOrEmpty(apiRequest.Notice.Description)) + { + logger.LogDebug("Error updating notice api request. Some values are required. Notice: {notice}", + apiRequest.Notice); + return; + } + + await UpdateNoticeAsync(apiRequest.Notice); + break; + } + case "DeleteNotice": + { + await DeleteNoticeAsync(apiRequest.Notice); + break; + } + } + } + catch (Exception ex) + { + logger.LogError("Exception updating notice {notice} . Exception: {ex}", apiRequest.Notice, ex); + } + } + + public Task SaveRequestAsync(ApiRequest apiRequest) + { + return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); })); + } + + public async Task?> GetNoticesByDisplayId(ObjectId displayId) + { + try + { + return await noticeRepository.FindByDisplayId(displayId); + } + catch (Exception e) + { + logger.LogError("Unable to get notice by unit on service Exception: {e}", e); + return null; + } + } + + private async void SendNoticeBroadcast(Notice notice, OperationType operation) + { + try + { + var display = await displayService.GetById(notice.DisplayId); + + if (display == null) + { + logger.LogError("Error sending notice broadcast. display is null or empty. Notice: {notice}", notice); + return; + } + + var subscribers = subscribersService.GetSubscribers().Where(s => s.DisplayId == display.Id).ToList(); + + foreach (var subscriber in subscribers) + _ = clientMessageService.SendAsync(subscriber.Id, operation, notice); + } + catch (Exception ex) + { + logger.LogError("Exception sending notice broadcast. Operation type: {op}. Exception: {ex}", + operation.ToString(), ex); + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/ObservationDemoService.cs b/adas-core.Application/Services/ObservationDemoService.cs new file mode 100644 index 00000000..fbb00cb9 --- /dev/null +++ b/adas-core.Application/Services/ObservationDemoService.cs @@ -0,0 +1,271 @@ +using System.Collections; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Utils; + +namespace adas_core.Application.Services; + +public class ObservationDemoService( + IConfigObservationService configObservationService, + Lazy alarmService) + : IObservationDemoService +{ + public async Task> GenerateAlarmByField(Patient patient, List dataAlarmfields) + { + var listToReturn = new List(); + foreach (var field in dataAlarmfields) + { + var sendingRandom = new Random(); + + if (sendingRandom.Next(0, 2) == 0 || string.IsNullOrEmpty(field.Name)) + continue; + + var firstCof = await configObservationService.GetConfigObservationItemsByName(field.Name); + var conf = firstCof.FirstOrDefault(); + if (conf == null) + continue; + + var alarmObs = new PatientObservationAlarm + { + Patient = patient, + PatientId = patient.Id, + AlarmConfig = conf.Alarm, + InactivationState = new InactivationState + { + Audio = AlarmEnum.AudioVideoState.Enabled, Acknowledge = true, + Visual = AlarmEnum.AudioVideoState.Enabled + }, + EventPhase = AlarmEnum.EventPhase.Continue, + Type = AlarmEnum.ObservationAlarmType.Sp, + Expires = conf.Expires, + State = AlarmEnum.ObservationAlarmState.Active, + Time = DateTime.UtcNow, + Persist = false, + MessageTime = DateTime.UtcNow, + Code = conf.Code, + PriorityLevel = conf.DemoConfig?.PriorityAlarm, + Name = conf.Name, + ParentData = new ParentDataClass + { + Code = conf.ParentCode, + Name = conf.ParentName, + CodingSystem = conf.CodingSystem + }, + Units = conf.Units, + Value = GenerateValueRandom(conf, null) ?? 0 + }; + var obs = await alarmService.Value.MapObservationsByName(alarmObs); + + + if (obs != null) + listToReturn.Add(obs); + } + + return listToReturn; + } + + public async Task> GenerateObservationByField(Patient patient, List dataFields) + { + var listToReturn = new List(); + var rnd = new Random(); + foreach (var field in dataFields) + { + if (string.IsNullOrEmpty(field.Name)) + continue; + + var conf = await configObservationService.GetConfigObservationItemsByName(field.Name); + var firstCof = conf.FirstOrDefault(); + if (firstCof == null) + continue; + + var patientObs = new PatientObservation + { + Patient = patient, + PatientId = patient.Id, + Max = firstCof.MaxAlert, + Min = firstCof.MinAlert, + MaxWarn = firstCof.MaxWarn, + MinWarn = firstCof.MinWarn, + ShowOnExpired = firstCof.ShowOnExpired, + Expires = firstCof.Expires, + UiConfiguration = firstCof.UiConfiguration, + MessageTime = DateTime.UtcNow, + Code = firstCof.Code, + ColorOnExpired = firstCof.ColorOnExpired, + AlertColor = firstCof.AlertColor, + WarnColor = firstCof.WarnColor, + CodingSystem = firstCof.CodingSystem, + Alarm = firstCof.Alarm, + InsertMode = firstCof.InsertMode, + Name = field.Name, + Time = DateTime.UtcNow, + ParentData = new ParentDataClass + { + Code = firstCof.ParentCode, + Name = firstCof.ParentName, + CodingSystem = firstCof.CodingSystem + }, + Units = firstCof.Units, + Value = GenerateValueRandom(firstCof, null) ?? 0 + }; + var result = await configObservationService.Map(patientObs, true); + if (firstCof.DemoConfig?.RequireInitDate == true && result != null) + { + var rndRestNmbHours = rnd.Next(3, 23); + var initDate = DateTime.UtcNow.AddHours(-rndRestNmbHours); + + // Si se requiere fecha de fin, devolvemos un objeto con ambas + if (firstCof.DemoConfig?.RequireEndDate == true) + { + result.Time = initDate; + result.EndTime = initDate.AddHours(1); + } + + // Si no, solo devolvemos la fecha inicial + result.Time = initDate; + } + + if (result != null) + listToReturn.Add(result); + } + + return listToReturn; + } + + public async Task GenerateGroupedObservation(Patient patient, GroupedField groupedField) + { + if (groupedField.Names.IsNullOrEmpty() && groupedField.Name != null) groupedField.Names.Add(groupedField.Name); + var rnd = new Random(); + var startTime = DateTime.UtcNow; + var go = new GroupedObservation + { + Name = groupedField.Name, + PatientId = patient.Id, + Group = groupedField.Group, + Observations = [] + }; + foreach (var groupedFieldName in groupedField.Names) + { + var conf = await configObservationService.GetConfigObservationItemsByName(groupedFieldName); + var firstCof = conf.FirstOrDefault(); + if (firstCof == null) + continue; + + foreach (var result in groupedField.Result) + { + object? prevValue = null; + for (var i = 0; i <= groupedField.Max; i++) + { + var time = startTime; + time = groupedField.Regularity switch + { + GroupedObservationEnum.Regularity.Day => time.AddDays(-i), + GroupedObservationEnum.Regularity.Minute => time.AddMinutes(-i), + GroupedObservationEnum.Regularity.Second => time.AddSeconds(-i), + _ => time.AddHours(-i) + }; + var val = new GroupedObservation.GroupedObservationObs + { + Name = groupedFieldName, + MaxAlert = firstCof?.MaxAlert, + MinAlert = firstCof?.MinAlert, + Time = time, + IsFilled = false + }; + var value = new GroupedObservation.GroupedObservationObsValue( + GenerateValueRandom(firstCof, prevValue) ?? rnd.Next(0, 30), time); + prevValue = value.Value; + var propertyInfo = val.GetType().GetProperty(result.ToString()); + + if (propertyInfo != null && propertyInfo.CanWrite) propertyInfo.SetValue(val, value); + var valueType = await configObservationService.GroupedObservationStatus( + groupedField, + result, + groupedFieldName, + val, + firstCof?.MaxAlert, + firstCof?.MinAlert + ); + value.Type = valueType; + go.Observations.Add( + val + ); + } + + if (firstCof?.DemoConfig?.RequireInitDate.HasValue == true && firstCof.DemoConfig.RequireInitDate.Value) + break; + } + } + + return go; + } + + private static object? GenerateValueRandom(ConfigObservation? firstCof, object? prevValue) + { + if (firstCof?.DemoConfig == null) + return "-"; + + // 1. Determinar cuántos valores necesitamos + var count = firstCof.DemoConfig?.ValueIsArray == true ? firstCof.DemoConfig?.RandomCount ?? 1 : 1; + var results = new List(); + + for (var i = 0; i < count; i++) + { + var singleValue = PickSingleValue(firstCof.DemoConfig, prevValue); + if (singleValue != null) + results.Add(singleValue); + } + + // 2. Si no es un array, devolvemos solo el primer elemento + return firstCof.DemoConfig?.ValueIsArray == true ? results : results.FirstOrDefault(); + } + + private static object? PickSingleValue(DemoConfig? config, object? prevValue) + { + if (config == null) return null; + + var rnd = new Random(); + // Prioridad 1: Selección de una lista de opciones (SetInValue) + if (config is { SetInValue: true, ValueOption: not null }) + { + if (config.ValueOption is List { Count: > 0 } options) + { + var index = rnd.Next(options.Count); + return options.ElementAt(index); + } + + // Caso especial si ValueOption llega como un JArray o lista genérica + if (config.ValueOption is IEnumerable list) + { + var tempList = list.Cast().ToList(); + return tempList[rnd.Next(tempList.Count)]; + } + } + + // Prioridad 2: Generación numérica por rango (SetRandomValue) + if (config.SetRandomValue == false) + return null; + + // 1. Extraemos los límites primero para usarlos en cualquier caso + var minLimit = config.MinValue ?? 0; + var maxLimit = config.MaxValue ?? 100; + + if (prevValue is not int values) + // Caso base: Si no hay valor previo, generamos uno aleatorio dentro del rango permitido + return rnd.Next(minLimit, maxLimit + 1); + + if (values == 0) values = 1; + // 2. Calculamos la variación (entre 5% y 10%) + var porcentaje = rnd.Next(5, 10) / 100.0; + var direccion = rnd.Next(0, 2) == 0 ? -1 : 1; + + // 3. Calculamos el valor base redondeado + var calculado = (int)Math.Round(values + values * porcentaje * direccion); + + // 4. Forzamos que esté dentro del rango [minLimit, maxLimit] + return Math.Clamp(calculado, minLimit, maxLimit); + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/ObservationService.cs b/adas-core.Application/Services/ObservationService.cs new file mode 100644 index 00000000..09e7f029 --- /dev/null +++ b/adas-core.Application/Services/ObservationService.cs @@ -0,0 +1,1568 @@ +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.GroupedObservations; +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; +using Serilog; +using Patient = adas_core.Domain.Models.MongoModels.Patient; + +namespace adas_core.Application.Services; + +public class ObservationService : IObservationService +{ + private readonly ICacheService _cacheService; + private readonly CacheSettings? _cacheSettings; + + private readonly IAlarmService _alarmService; + private readonly List _allergies; + private readonly ILocalAuditService _auditService; + private readonly Lazy _calculatedObservationsService; + private readonly IClientMessageService _clientMessageService; + private readonly IConfigObservationService _configObservationService; + private readonly IConfigUnitsService _configUnitsService; + private readonly List _diagnosisCode; + private readonly IDiagnosisService _diagnosisService; + private readonly List _drainageCode; + private readonly IGroupedObservationService _groupedObservationService; + private readonly IHttpContextAccessor _httpContextAccessor; + + private readonly List _intravenousLines; + private readonly List _isolationCode; + private readonly ILightBeaconService _lightBeaconService; + private readonly ILogger _logger; + private readonly IObservationArchiveRepository _observationArchiveRepository; + private readonly IObservationRepository _observationRepository; + private readonly IPatientService _patientService; + + //Suprimo el warning porque detecta que no se usa en el código, pero sí se usa en los test + //#pragma warning disable IDE0051 // Quitar miembros privados no utilizados + //#pragma warning disable CS0169 // El campo '_observationService.createPatientWithORU' nunca se usa + /*private readonly bool createPatientWithORU; + private readonly bool createPatientWithoutLocation; + + private readonly bool createPatientWithLocation; + + //private readonly bool updatePatientLocationWithORU; + private readonly bool updatePatientDataWithORU; + private readonly bool ArchivePatientWithOru;*/ + //#pragma warning restore CS0169 // El campo '_observationService.createPatientWithORU' nunca se usa + //#pragma warning restore IDE0051 // Quitar miembros privados no utilizados + + + private readonly bool _persistObservationCodes; + private readonly IPointOfCareService _pointOfCareService; + private readonly List _positionCode; + private readonly IRecordingService _recordingService; + private readonly IRelayService _relayService; + private readonly ISubscriberGroupedService _subscriberGroupedService; + private readonly ISubscribersService _subscribersService; + + + public ObservationService( + IPatientService patientService, + IConfigObservationService configObservationService, + IObservationRepository observationRepository, + IObservationArchiveRepository observationArchiveRepository, + IConfigUnitsService configUnitsService, + IDiagnosisService diagnosisService, + IOptions apiSettings, + IOptions cacheSettings, + ILightBeaconService lightBeaconService, + IRelayService relayService, + IRecordingService recordingService, + ILogger logger, + IGroupedObservationService groupedObservationService, + IAlarmService alarmService, + IClientMessageService clientMessageService, + ISubscribersService subscribersService, + ISubscriberGroupedService subscriberGroupedService, + Lazy calculatedObservationsService, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService, + IPointOfCareService pointOfCareService, + ICacheService cacheService + ) + + { + _patientService = patientService; + _configObservationService = configObservationService; + _observationRepository = observationRepository; + _observationArchiveRepository = observationArchiveRepository; + _configUnitsService = configUnitsService; + _diagnosisService = diagnosisService; + _lightBeaconService = lightBeaconService; + _relayService = relayService; + _recordingService = recordingService; + _httpContextAccessor = httpContextAccessor; + _auditService = auditService; + _groupedObservationService = groupedObservationService; + _cacheService = cacheService; + + + _logger = logger; + + if (apiSettings == null) throw new Exception("ApiSettings must be defined"); + + + _persistObservationCodes = apiSettings.Value.PersistObservationCodes; + _intravenousLines = apiSettings.Value.IntravenousLinesCode ?? []; + _allergies = apiSettings.Value.AllergiesCode; + _drainageCode = apiSettings.Value.DrainageCode; + _isolationCode = apiSettings.Value.IsolationCode; + _positionCode = apiSettings.Value.PositionCode; + _diagnosisCode = apiSettings.Value.DiagnosisCode; + _alarmService = alarmService; + _clientMessageService = clientMessageService; + _subscribersService = subscribersService; + _subscriberGroupedService = subscriberGroupedService; + _calculatedObservationsService = calculatedObservationsService; + _pointOfCareService = pointOfCareService; + _cacheSettings = cacheSettings.Value; + } + + public async Task> FindLastObservations(ObjectId patientId, int num = 2, + List? filterObservations = null) + { + var result = + await _observationRepository.AggregatedPatientLastObservations(patientId, num, filterObservations); + return result; + } + + private async Task> AggregatedLastObsCached( + ObjectId patientId, + List? filterObservations, + CancellationToken ct = default) + { + // Normalizar campos + var fieldNames = (filterObservations ?? new()) + .Select(f => f.Name ?? string.Empty) + .Where(n => !string.IsNullOrWhiteSpace(n)) + .ToList(); + + // Clave + TTL según PatientObservation / backend configurado + var (key, ttl) = CacheKeys.LatestObservationsKeyWithTtl( + _cacheSettings, + patientId, + fieldNames + ); + + // GET → MISS → LOCK → AGGREGATE → SET + var result = await _cacheService.GetOrSetObjectAsync( + key, + async () => + { + var raw = await _observationRepository + .AggregatedPatientLastObservationsByField(patientId, filterObservations); + + return raw; + }, + ttl, ct); + + return result; + } + + public async Task> FindLastObservationsByField( + ObjectId patientId, + List? filterObservations = null, + bool mapped = true, + CancellationToken ct = default) + { + //lista RAW desde la caché + var raw = await AggregatedLastObsCached(patientId, filterObservations, ct); + + if (!mapped) + return raw; + + // Mapeo + var mappedList = new List(); + foreach (var o in raw) + { + var mo = await MapObservationsByName(o); + if (mo != null) + mappedList.Add(mo); + } + + return mappedList; + } + + public async Task MapObservationsByName(PatientObservation obs) + { + return await _configObservationService.Map(obs, true); + } + + public async Task MapObservation(PatientObservation obs, bool onlyByName = false) + { + try + { + _logger.LogTrace("Mapping config Observation obs: {obs} onlyByName: {onlyByName}", obs, onlyByName); + var obs2 = await _configObservationService.Map(obs, onlyByName); + if (obs2 == null) + { + _logger.LogTrace("Mapping obs2 {obs}: Ignored", obs); + return null; + } + + var obs3 = await _configUnitsService.Map(obs2); + + var obs4 = await _calculatedObservationsService.Value.Map(obs3, onlyByName); + + _logger.LogTrace("Mapping calculatedObservations.Map obs4: {obs4}", obs4); + + if (obs4 == null) + { + _logger.LogTrace("Mapping obs4 {obs3}: Ignored", obs3); + return null; + } + + if (obs4.CheckObservations) + _ = _alarmService.CheckObservationAlarm(obs4); + + + return obs4; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error Mapping Observation, Ignoring Observation: {obs} Exception:{ex}", obs, + ex.Message); + return null; + } + } + + /// + /// Inserts a new observation if needed + /// + /// Observation + /// + /// True if it needs to be inserted + public async Task InsertObservation(PatientObservation obs, bool persistObs = true, bool mapObs = true) + { + try + { + var obs2 = obs; + + //only will be false if the obs comes from the inner refactor job + if (mapObs) obs2 = await MapObservation(obs2); + + if (obs2 == null) + { + _logger.LogDebug("Mapped observation returns null. Ignored {obs}", obs); + } + else + { + _logger.LogDebug("Mapped {obs2}", obs2); + + if (!_persistObservationCodes) + { + obs2.Code = null; + obs2.CodingSystem = null; + obs2.ParentData = null; + } + + _ = SendObsBroadcast(obs2); + + if (obs2.Persist.HasValue) + persistObs = obs2.Persist.Value; + + _logger.LogDebug("Insert {obs}, persist is {persistObs}", obs2, persistObs); + + if (persistObs) + { + await _observationRepository.InsertOneAsync(obs2); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, obs2); + _logger.LogDebug("Inserted {obs2}", obs2); + + _ = Task.Run(() => + CheckForGroupedObs(obs2) + ); + } + + _ = DoRetentionActions(obs2); + } + } + catch (Exception ex) + { + _logger.LogError("Error Inserting observation {obs}. Excepcion; {ex} ", obs, ex); + } + } + + public async Task InsertNurseObservation(PatientObservation obs) + { + try + { + var obs2 = obs; + + obs2 = await MapObservation(obs2, true); + if (obs2 == null || obs2.Name == null) + { + _logger.LogError("Error mapping observation {obs}", obs.ToString()); + return; + } + + await _observationRepository.InsertOneAsync(obs2); + + var (key, ttl) = CacheKeys.LatestObservationsKeyWithTtl( + _cacheSettings, + obs2.PatientId, + [obs2.Name] + ); + + // GET → MISS → LOCK → AGGREGATE → SET + var result = _cacheService.GetOrSetObjectAsync( + key, + async () => + { + return obs2; + }, + ttl, default); + + _ = SendObsBroadcast(obs2); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, obs2); + } + catch (Exception ex) + { + _logger.LogError("Error Inserting nurse observation {obs}. Excepcion; {ex} ", obs, ex); + } + } + + public async Task InsertIfChanged(string name, PatientObservation observation, bool persistObs = true, + bool mapObs = true) + { + var changedList = await FindLastObservations(observation.PatientId, 1, [name]); + var changed = changedList.All(o => observation.Value != o.Value); + + if (!changed) return false; + + await InsertObservation(observation, persistObs, mapObs); + return true; + } + + + public Task SendObsBroadcast(List obsList, PatientLocation location) + { + // Display Subscription + var displaySubscribers = _subscribersService.GetSubscribers().Where(s => + !s.Locations.IsNullOrEmpty() && s.Locations.Any(c => + c.UnitName == location.UnitName && + c.Bed == location.Bed && + c.Room == location.Room + )).ToList(); + + + foreach (var subscriber in displaySubscribers) + foreach (var obs in obsList) + _ = _clientMessageService.SendAsync(subscriber.Id, OperationType.Observation, obs); + + return Task.CompletedTask; + } + + public Task SendObsBroadcast(List obsList, ObjectId pocId) + { + // Display Subscription + var displaySubscribers = _subscribersService.GetSubscribers().Where(s => + !s.LocationIds.IsNullOrEmpty() && s.LocationIds.Any(c => + c == pocId + )).ToList(); + + + foreach (var subscriber in displaySubscribers) + foreach (var obs in obsList) + _ = _clientMessageService.SendAsync(subscriber.Id, OperationType.Observation, obs); + + return Task.CompletedTask; + } + + public async Task SendObsBroadcast(BasePatientObservation obs) + { + if (obs.Name == null) return; + + var patient = obs.Patient ?? await _patientService.FindById(obs.PatientId); + if (patient == null) + { + _logger.LogDebug("Not patient on bd to sendOnBroadcastObs: {obspatientid}", obs.PatientId); + return; + } + + _logger.LogTrace( + "sending obs name: {obsname} to patient id: {patientid}, PointOfCare: {patientpointOfCare} {patientbed}", + obs.Name, patient.Id, patient.UnitString, patient.Bed); + + + var displaySubscribers = _subscribersService.GetSubscribers().Where(s => + !s.LocationIds.IsNullOrEmpty() && s.LocationIds.Any(c => + c == patient.PointOfCareId + )).ToList(); + + + foreach (var subscriber in displaySubscribers) + _ = _clientMessageService.SendAsync(subscriber.Id, OperationType.Observation, obs); + } + + public async void ProcessObservations(List observations, Patient patient, + DateTime messageTime, ObservationData? observationData = null) + { + try + { + _logger.LogDebug( + "Patient: {patientid} PoC {patientpointOfCare} {patientbed} msg {messageTime} INSERTING {observationsCount} OBSERVATIONS", + patient.Id, patient.PointOfCare, patient.Bed, messageTime, observations.Count); + + ParentDataClass? parentData = null; + if (observationData != null) + parentData = new ParentDataClass + { + Code = observationData.Code, + CodingSystem = observationData.CodingSystem, + Name = observationData.Text + }; + + var obsToInsert = new List(); + // Alarm to insert + // var alrmToInsert = new List(); + + foreach (var obs in observations) + { + obs.ParentData = parentData; + obs.MessageTime = messageTime; + obs.PatientId = patient.Id; + obs.Patient = patient; + obs.Id = ObjectId.GenerateNewId(); + + if (obs.Time == DateTime.MinValue) + obs.Time = DateTime.UtcNow; + + if (obs.MessageTime == DateTime.MinValue) + obs.MessageTime = DateTime.UtcNow; + + _logger.LogDebug( + "Patient: {patientId} PoC {patientpointOfCare} {patientbed} msg {messageTime} INSERTING observation {obs}", + patient.Id, patient.PointOfCare, patient.Bed, messageTime, obs); + obsToInsert.Add(obs); + } + + obsToInsert = await _calculatedObservationsService.Value.MapList(obsToInsert); + + _ = Task.WhenAll(obsToInsert.Select(obs => + Task.Run(() => InsertObservation(obs)))); //.GetAwaiter().GetResult(); + } + catch (Exception ex) + { + _logger.LogError( + "Patient: {patientid} PoC {patientpointOfCare} {patientbed} msg {messageTime} ERROR PROCESSING observations. Exception: {ex}", + patient.Id, patient.PointOfCare, patient.Bed, messageTime, ex); + } + } + + public Task SaveRequestNurseObsAsync(ApiRequest request) + { + return Task.Run(() => SaveRequestNurseObs(request)); + } + + public async Task SaveRequest(ApiRequest apiRequest) + { + if ( + string.IsNullOrEmpty(apiRequest.PatientNumber) && + apiRequest.Location != null && apiRequest.Location.IsEmpty() + ) + { + _logger.LogDebug("Patient and PointOfCare are nulls"); + throw new ApiRequestException("Patient and PointOfCare are nulls"); + } + + _logger.LogDebug("patientNumber: {PatientNumber} location: {Location}", apiRequest.PatientNumber, + apiRequest.Location); + _logger.LogDebug("RequestType: {Type}", apiRequest.Type); + + var patient = await _patientService.FindPatientByApiRequest(apiRequest); + + if (patient == null) + { + // NO PATIENTS OR LOCATIONS WERE FOUND + _logger.LogWarning( + "Observation for patient: {apiRequestpatientNumber} with location: {apiRequestlocation} not found, ignoring", + apiRequest.PatientNumber, apiRequest.Location); + return; + } + + + try + { + switch (apiRequest.Type) + { + /* + * ORU_R01 - Unsolicited transmission of an observation message + * ORU_R40 - Unsolicited transmission of an alert observation message + */ + case "ORU_R40": // UNSOLICITED ALERT OBSERVATION + await _alarmService.SaveRequestAsync(apiRequest); + return; + case "ORU_R01": // UNSOLICITED OBSERVATON + + if (!apiRequest.Alarms.IsNullOrEmpty()) + { + await _alarmService.SaveRequestAsync(apiRequest); + return; + } + + + // OBSERVATIONS + if (apiRequest.Observation != null && + (apiRequest.Observations == null || !apiRequest.Observations.Any())) + apiRequest.Observations = [apiRequest.Observation]; + + var obrcode = apiRequest.ObservationData?.Code ?? ""; + + if (apiRequest.Observations != null) + { + if (_intravenousLines.Contains(obrcode)) + await ProcessIntravenousLinesObservation(apiRequest, patient); + else if (_allergies.Contains(obrcode)) + await ProcessAllergiesObservation(apiRequest, patient); + else if (_drainageCode.Contains(obrcode)) + await ProcessDrainageObservation(apiRequest, patient); + //TODO PARA SALIR EN EL RYC REFACTOR + else if (_isolationCode.Contains(obrcode)) + await ProcessIsolationObservation(apiRequest, patient); + //TODO REFACTOR, CAMBIO RAPIDO PARA SALIR RYC, PENSAR SI NO SOLO HAY QUE FIARSE POR EL CÓDIGO SI NO POR TODA LA ESTRUCTURA + //DE CONFIG OBSERVATIONS, A LO MEJOR LA BD TIENE QUE INDICAR SI ES UN PROCESAMIENTO ESPECIAL? REVISAR. + else if (_positionCode.Contains(obrcode) && + !"Temperatura(ºC)".Equals(apiRequest.ObservationData?.Text)) + await ProcessPositionObservation(apiRequest, patient); + else if (_diagnosisCode.Contains(obrcode)) + await _diagnosisService.SaveRequest(apiRequest, patient); + else + ProcessObservations(apiRequest.Observations, patient, apiRequest.MessageTime, + apiRequest.ObservationData); + } + + break; + default: + _logger.LogWarning("ApiRequest type {apiRequesttype} sis not valid for Observations", + apiRequest.Type); + throw new ApiRequestException("ApiRequest type " + apiRequest.Type + + " is not valid for Observations"); + } + } + catch (Exception ex) + { + _logger.LogError( + "ERROR SAVING REQUEST: exception: {exMessage} trace: {exStackTrace}", + ex.Message, ex.StackTrace); //add traceability + throw new Exception(ex.Message); + } + } + + public Task SaveRequestAsync(ApiRequest apiRequest) + { + //return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); })); + return Task.Run(() => SaveRequest(apiRequest)); + } + + + public async Task> FindByPatientIdAsync(ObjectId patientId) + { + return await _observationRepository.FindByPatientIdAsync(patientId); + } + + public async Task> FindByPatientIdAndCodingSystemAsync(ObjectId patientId, + string codingSystem, string name) + { + return await _observationRepository.FindByPatientIdAndCodingSystemAsync(patientId, codingSystem, name); + } + + public async Task DeleteByPatientId(ObjectId id) + { + try + { + var deletedObservationList = await _observationRepository.FindByPatientId(id); + + if (deletedObservationList == null || deletedObservationList.Count == 0) + return; + + _logger.LogDebug("Delete Observations by Patient Id {id}", id); + await _observationRepository.DeleteByPatientId(id); + + await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ByPatient("patients:latestObs", id.ToString())); + + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, deletedObservationList, + null); + } + catch (Exception ex) + { + _logger.LogError("Error Deleting by PatientId. Exception: {ex}", ex); + } + } + + public async Task Archive(PatientObservation observation) + { + await _observationArchiveRepository.InsertOneAsync(observation); + await _observationRepository.DeleteAsync(observation.Id); + await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ByPatient("patients:latestObs", observation.PatientId.ToString())); + + } + + public async Task Archive(Patient patient) + { + await ArchiveByPatientId(patient.Id); + } + + public async Task ArchiveByPatientId(ObjectId id) + { + _logger.LogDebug("Archive Observations by Patient Id {id}", id); + var cursor = await FindByPatientIdAsync(id); + + while (await cursor.MoveNextAsync()) + foreach (var current in cursor.Current) + { + current.Id = ObjectId.GenerateNewId(); + await _observationArchiveRepository.InsertOneAsync(current); + } + + await DeleteByPatientId(id); + await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ByPatient("patients:latestObs", id.ToString())); + + } + + public async Task> FindLastIntravenousLinesObservationByLocation(ObjectId patientId) + { + return await _observationRepository.AggregatedPatientActiveIntravenousLinesObservations(patientId); + } + + public async Task> FindAllLastPatientObservationTime() + { + return await _observationRepository.FindAllLastPatientObservationTime(); + } + + + public async Task UpdateObservation(PatientObservation observation) + { + var obs = await _observationRepository.FindById(observation.Id); + if (obs != null) + { + await _observationRepository.Update(observation); + + await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ByPatient("patients:latestObs",observation.PatientId.ToString())); + + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, obs, observation); + } + } + + public async Task FindLastBeforeDate(ObjectId patientId, DateTime date, string? obsName) + { + return await _observationRepository.FindLastObservationBeforeDate(patientId, obsName, date); + } + + public async Task?> FindAnyWithSameDate(ObjectId patientId, DateTime date, + string? obsName) + { + return await _observationRepository.FindAnyWithSameDate(patientId, obsName, date); + } + + //TODO To implement + public async Task> FindAllBeforeDate(ObjectId patientId, DateTime date, + List? filterObservations = null) + { + return await _observationRepository.FindAnyBeforeDate(patientId, date); + } + + public async Task> FindLatestUniqueValuesByName(ObjectId patientId, string name, + int? expires) + { + return await _observationRepository.FindLatestUniqueValuesByName(patientId, name, expires); + } + + //TODO To implement + public Task> FindAllAfterDate(ObjectId patientId, DateTime date, + List? filterObservations = null) + { + throw new NotImplementedException(); + } + + public async Task> FindLastNotExpiredObservatonsByPatient(ObjectId patientId, + string name, int? endAfter = null, int? num = null) + { + return await _observationRepository.FindLastNotExpiredObservatonsByPatient(patientId, name, endAfter, num); + } + + /// + /// Retrieve all observations with expire time from configObservation service + /// Check all of this observations in patient_observations and expire them if (obs.time + config.expires) smaller than + /// current time + /// mark as expired in bd. + /// + public async Task CheckAndExpireObservations() + { + var count = 0; + _logger.LogDebug("start checking expired observations"); + + + await using var enumerator = FindNotExpiredObservationsShouldBeExpired().GetAsyncEnumerator(); + while (await enumerator.MoveNextAsync()) + { + count++; + + var current = enumerator.Current; + current.Expired = true; + + await _observationRepository.Update(current); + } + + _logger.LogDebug("found observations {count} ", count); + } + + public async Task UpdateExpiredObservations(List patientObservations) + { + await _observationRepository.UpdateExpiredObservations(patientObservations); + var id = patientObservations.First().Id; + await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ByPatient("patients:latestObs", id.ToString())); + + foreach (var observation in patientObservations) + { + var auxObs = await _auditService.DeepCopyAsync(observation); + observation.Expired = true; + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, auxObs, observation); + } + } + + public async IAsyncEnumerable FindNotExpiredObservationsShouldBeExpired() + { + var expiringObservations = await _configObservationService.GetAllConfigs(); + var expiringList = expiringObservations.ToList(); + expiringList.RemoveAll(z => z.Expires == null); + var count = 0; + + var uniqueNames = expiringList + .Select(item => item.Name) + .Distinct() + .ToList(); + + + using var enumerator = (await _observationRepository.FindNotExpired(uniqueNames)).GetEnumerator(); + while (enumerator.MoveNext()) + { + count++; + + var current = enumerator.Current; + var patientid = current.PatientId; + var patient = await _patientService.FindById(patientid); + if (patient == null) + { + await _observationRepository.FindByPatientId(patientid); + + await _observationRepository.DeleteByPatientId(patientid); + await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ByPatient("patients:latestObs", patientid.ToString())); + + continue; + } + if(current.Name == null) continue; + await _configObservationService.GetConfigObservationItemsByName(current.Name); + var configObs = expiringObservations? + .FirstOrDefault(); + if (configObs == null) continue; + + if (!double.TryParse(configObs.Expires.ToString(), out var expires)) + continue; + + var expectedExpireTime = current.Time.AddMinutes(expires); + if (DateTime.Now > expectedExpireTime) yield return current; + } + + _logger.LogDebug("expired observations retrieved {count} observations", count); + } + + public async Task ExpireObservations() + { + //TODO expire each section + var expiringObservations = await _configObservationService.GetAllConfigs(); + var expiringList = expiringObservations.ToList(); + expiringList.RemoveAll(z => z.Expires is null or <= 0); + + await _observationRepository.ExpireExpiredObservations(expiringList.Distinct() + .ToList()); + } + + public async Task ExpireObservationsAndRecalculateAsync() + { + try + { + GlobalData.AddData("isCheckingExpiration", true); + + var configObservations = + await _configObservationService.GetAllConfigs(); + + var names = configObservations.Select(o => o.Name).ToList(); + + var patients = await _patientService.FindAll(); + + foreach (var patient in patients) + { + var listField = names?.Select(n => new Field + { + Name = n, + Last = 1, + OnlyExpired = true + }).ToList(); + + var lastPatientObservationsByName = + await _observationRepository.AggregatedPatientLastObservationsByField(patient.Id, listField); + + foreach (var obs in + lastPatientObservationsByName) + await InsertObservation(obs, false, false); //Not really insert, only makes calcs + } + + var obsToExpireList = new List(); + var i = 0; + var count = 0; + await using var enumerator = FindNotExpiredObservationsShouldBeExpired().GetAsyncEnumerator(); + while (await enumerator.MoveNextAsync()) + { + i++; + //every 1000 cut list to avoid memory leaks when there are tons of data. + + if (i > 1000) + { + await UpdateExpiredObservations(obsToExpireList); + i = 0; + obsToExpireList.Clear(); + } + + var current = enumerator.Current; + current.Expired = true; + + obsToExpireList.Add(current); + count++; + } + + await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.PatientObservations)); + + _logger.LogDebug("number of expired observations should be expired:{count}", count); + } + catch (Exception ex) + { + _logger.LogError("ERROR expire observations And recalculate {exMessage} trace: {exStackTrace}", + ex.Message, ex.StackTrace); + } + + GlobalData.AddData("isCheckingExpiration", false); + } + + public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId) + { + await _observationRepository.UpdateManyObjectId(nameId, id, oldId); + } + + public async Task InsertSimpleObservation(PatientObservation observation) + { + await _observationRepository.InsertOneAsync(observation); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, observation); + } + + public async Task> FindAllBetweenDates( + ObjectId patientId, + DateTime? startDate = null, + DateTime? endDate = null, + List? filterObservations = null, + bool fromArchived = false, + PaginationFilter? filter = null + ) + { + try + { + var collection = fromArchived + ? _observationArchiveRepository.Collection + : _observationRepository.Collection; + + var filterBuilder = Builders.Filter; + + var conditions = new List> + { + filterBuilder.Eq(o => o.PatientId, patientId), + filterBuilder.Ne(o => o.Name, null), + //filterBuilder.In(o => o.Name, filterObservations ?? new List()), + filterBuilder.Gt(o => o.Time, startDate ?? DateTime.MinValue), + filterBuilder.Lt(o => o.Time, endDate ?? DateTime.MaxValue) + }; + + if (filterObservations != null && filterObservations.Any()) + conditions.Add(filterBuilder.In(o => o.Name, filterObservations)); + + var combinedFilter = filterBuilder.And(conditions); + + var findOptions = new FindOptions + { + Skip = (filter?.PageNumber - 1) * filter?.PageSize, + Limit = filter?.PageSize + }; + + var cursor = await collection.FindAsync(combinedFilter, findOptions); + + var observationsList = await cursor.ToListAsync(); + + return observationsList; + } + catch (Exception ex) + { + Log.Error("Error while getting PatientObservation between dates. Exception: {ex}", ex); + throw; + } + } + + + public async Task ExpireAlertsAndPowerOffAsync() + { + try + { + GlobalData.AddData("isCheckingAlertsExpiration", true); + + + var pointOfCares = await _pointOfCareService.GetAllConfigs(); + + pointOfCares = pointOfCares + .Where(box => + box.Configuration is { BeaconList: not null } && + box.Configuration!.BeaconList.Any() && + !box.Configuration!.BeaconList.First().Options.Emulate) + .ToList(); + + + var configObs = await _configObservationService.GetAllConfigs(); + var configList = configObs.ToList(); + + configList.RemoveAll(z => z.Alarm is not { Enabled: true }); + + + if (configList.Count == 0) + return; + var patients = await _patientService.FindAll(); + if (!patients.Any()) + return; + + //apagamos balizas de los boxes que no tienen pacientes por si se ha quedado alguna encendida + var emptyBoxes = pointOfCares + .Where(box => box.Status != StatusEnum.PointOfCare.InUse) + .ToList(); + + emptyBoxes.ForEach(async void (b) => + { + try + { + await _lightBeaconService.PowerOffLed(b.Id); + } + catch (Exception ex) + { + _logger.LogError("Exception powerOffLed {exMessage} trace: {exStackTrace}", + ex.Message, ex.StackTrace); + } + }); + + var configObservationItems = configObs.ToList(); + var endAfterDefaultValue = configObservationItems + .Where(c => c.Alarm != null) + .Select(c => + c.Alarm?.EndAfter ?? 0) // comprobación de null aquí por seguridad adicional. + .DefaultIfEmpty(0) + .Max(); + + foreach (var patient in patients) + { + //Nos quedamos con los pacientes que tienen cama + + if (patient.Location.UnitName == nameof(VirtualPointOfCare.Moved) || + patient.Location.UnitName == nameof(VirtualPointOfCare.Deleted) || + patient.Location.UnitName == nameof(VirtualPointOfCare.Pushed) || + patient.Location.UnitName == nameof(VirtualPointOfCare.Unknown)) + continue; + + _ = CheckBeacon(patient, configObs, endAfterDefaultValue); + + _ = CheckRelay(patient, configObs, endAfterDefaultValue); + + _ = CheckRecordings(patient, pointOfCares, configObs, endAfterDefaultValue); + } + } + catch (Exception ex) + { + _logger.LogError("ERROR expire observations And recalculate {exMessage} trace: {exStackTrace}", + ex.Message, ex.StackTrace); + } + + GlobalData.AddData("isCheckingAlertsExpiration", false); + } + + public async Task> GetPaginatedObservations(PaginationFilter filter) + { + var result = _observationRepository.GetPaginatedObservations(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(dataList, filter.PageNumber, filter.PageSize, count); + } + + + private async Task DoRetentionActions(PatientObservation obs) + { + try + { + var result = await _configObservationService.RetentionActions(obs); + if (result == null || !result.RetentionPolicyValue.HasValue || string.IsNullOrEmpty(obs.Name)) return; + var deletedObs = new List(); + switch (result.RetentionPolicy) + { + case RetentionPolicy.DeleteOlderDays: + deletedObs = + await _observationRepository.DeleteOlderDaysAsync(obs.Name, result.RetentionPolicyValue.Value); + break; + case RetentionPolicy.DeleteOlderSeconds: + deletedObs = await _observationRepository.DeleteOlderSecondsAsync(obs.Name, + result.RetentionPolicyValue.Value); + break; + case RetentionPolicy.DeleteOlderNumber: + deletedObs = await _observationRepository.DeleteOlderNumberAsync(obs.Name, + result.RetentionPolicyValue.Value); + break; + case RetentionPolicy.NoDelete: + default: + break; + } + + foreach (var observation in deletedObs) + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, observation, null); + + await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ByPatient("patients:latestObs", obs.PatientId.ToString())); + + } + catch (Exception ex) + { + _logger.LogError("Error doing retention actions. Exception: {ex}", ex); + } + } + + private async void CheckForGroupedObs(PatientObservation obs) + { + try + { + //Removed ignore obs to future, we accept them and recalculate and front decide what to do + foreach (var wsg in _subscriberGroupedService.GetGrouped().Where(g => + g.PatientId == obs.PatientId && obs.Name != null && g.Names.Contains(obs.Name) && + g.IsNewObservationRelevantForGroup(obs))) + { + wsg.Timer.Stop(); + + var groupedField = new GroupedField + { + Names = wsg.Names, + Name = obs.Name ?? string.Empty, + Max = wsg.Max, + Group = wsg.Group.FirstOrDefault().Value, + Regularity = wsg.Regularity, + Result = wsg.Result, + Since = wsg.Since, + StartTimeShift = wsg.StartTimeShift + }; + + + var newGroupedObservation = await _groupedObservationService + .GenerateGroupedObservation(obs.PatientId, groupedField, wsg.LastGroupedObservationObs, obs, + wsg.TimeZoneId); + + _subscriberGroupedService.UpdateLastGroupedObsInGroup(wsg.HashCode, newGroupedObservation); + + foreach (var gr in wsg.Group) + { + var grName = wsg.Group.GetValue(gr.Key); + if (grName == null) continue; + + newGroupedObservation.Group = grName; + _ = Task.Run(() => _clientMessageService.SendAsync(gr.Key, OperationType.GroupedObservation, + newGroupedObservation)); + } + + //wsg.TimerReestart(); + + await _cacheService.DeleteByPatternAsync(CacheKeys.GroupedObs(obs.PatientId, obs.Name ?? string.Empty)); + + } + } + catch (Exception e) + { + _logger.LogError("error checking grouped observations: exception: {eMessage} trace:{eStackTrace}", + e.Message, e.StackTrace); + } + } + + private async Task ProcessIntravenousLinesObservation(ApiRequest apiRequest, Patient patient) + { + var isInsertable = false; + _logger.LogDebug("INSERT IntraVenousLineObservation"); + + if (apiRequest.ObsertationData == null || !apiRequest.ObsertationData.Time.HasValue) + { + _logger.LogError("Error INSERTING IntraVenousLineObservation. "); + return; + } + + var obs = new PatientObservation + { + Time = apiRequest.ObsertationData.Time.Value, + Code = apiRequest.ObsertationData.Code, + CodingSystem = apiRequest.ObsertationData.CodingSystem, + PatientId = patient.Id, + ParentData = new ParentDataClass + { + Code = apiRequest.ObservationData?.Code, CodingSystem = apiRequest.ObservationData?.CodingSystem + }, + MessageTime = apiRequest.MessageTime + }; + + + //Todos los formatos de texto de vías son Catéter X: + localizacion + var typeLocation = apiRequest.ObservationData?.Text?.Split(':'); + obs.Value = new PatientIntravenousLinesValue + { + Type = typeLocation != null ? typeLocation[0] : string.Empty + }; + if (typeLocation is { Length: > 1 }) + if (obs.Value is PatientIntravenousLinesValue obsValue) + obsValue.Location = typeLocation[1]; + + if (apiRequest.Observations != null) + { + if (obs.Value is not PatientIntravenousLinesValue obsValue) + return; + + foreach (var observation in apiRequest.Observations) + { + var obsValueStr = observation.Value.ToString(); + if (obsValueStr == null) continue; + + switch (observation.Code) + { + case "273248003": + //In ICCA an intravenouse Line OBS should come with Insertado and Retirado but can come with more, ignore them. + if (obsValueStr.Equals("Insertado") || obsValueStr.Equals("Retirado")) + { + obsValue.Action = observation.Value.ToString() ?? "NULL"; + isInsertable = true; + } + else + { + return; + } + + break; + case "397898000": + obsValue.RemoveTime = (DateTime?)observation.Value; + break; + case "439272007": + obsValue.InsertTime = (DateTime?)observation.Value; + break; + case "228864003": + obsValue.Duration = observation.Value.ToString() ?? "NULL"; + break; + } + } + } + + if (!isInsertable) return; + + await InsertObservation(obs); + } + + private async Task ProcessAllergiesObservation(ApiRequest apiRequest, Patient patient) + { + _logger.LogDebug("INSERT AllergiesObservation"); + + if (apiRequest.ObservationData == null) + { + _logger.LogError("Error Processing Allergies Observation. ObservationData is null"); + return; + } + + if (!apiRequest.ObservationData.Time.HasValue) + { + _logger.LogError("Error Processing Allergies Observation. ObservationData time null"); + return; + } + + var obs = new PatientObservation + { + Time = apiRequest.ObservationData.Time.Value, + Name = apiRequest.ObsertationData?.Text ?? string.Empty, + Code = apiRequest.ObservationData.Code, + CodingSystem = apiRequest.ObservationData.CodingSystem, + PatientId = patient.Id, + ParentData = new ParentDataClass + { + Code = apiRequest.ObservationData.Code, + CodingSystem = apiRequest.ObservationData.CodingSystem, + Name = apiRequest.ObservationData.Text + }, + MessageTime = apiRequest.MessageTime + }; + + List patientAllergiesValues = []; + + PatientAllergiesValue allergiesValues = new(); + + if (apiRequest.Observations == null) + { + _logger.LogError("Error Processing Allergies Observation. Observations null"); + return; + } + + for (var i = 0; i <= apiRequest.Observations.Count - 1; i++) + { + switch (apiRequest.Observations[i].Code) + { + case "263490005": + if (apiRequest.Observations[i].Value.ToString() == "Sin alergias conocidas") return; + + continue; + + case "300916003": //Latex + if (apiRequest.Observations[i].Value.ToString() == "Si") + { + allergiesValues.Type = "Latex"; + allergiesValues.Value = "Si"; + //patientAllergiesValues.Add(allergiesValues); + //allergiesValues = new PatientAllergiesValue { }; + } + + break; + + case "419199007": + allergiesValues.Type = apiRequest.Observations[i].Value.ToString(); + break; + + case "277054007": + case "416098002": + allergiesValues.Value = apiRequest.Observations[i].Value.ToString(); + break; + + case "281296001": + allergiesValues.Notes = apiRequest.Observations[i].Value.ToString(); + break; + } + + if (i == apiRequest.Observations.Count - 1) + { + if (allergiesValues is { Type: not null }) + patientAllergiesValues.Add(allergiesValues); + //allergiesValues = new PatientAllergiesValue { value = new List(), notes = new List() }; + allergiesValues = new PatientAllergiesValue(); + } + else + { + i++; + if (apiRequest.Observations[i].Code == "300916003" || + apiRequest.Observations[i].Code == "419199007") + { + if (allergiesValues is { Type: not null }) + patientAllergiesValues.Add(allergiesValues); + //allergiesValues = new PatientAllergiesValue { value = new List(), notes = new List() }; + allergiesValues = new PatientAllergiesValue(); + } + + i--; + } + } + + obs.Value = patientAllergiesValues; + await InsertObservation(obs); + } + + + private async Task ProcessIsolationObservation(ApiRequest apiRequest, Patient patient) + { + if (apiRequest.ObservationData is { Text: "Aislamiento", Time: not null }) + { + _logger.LogDebug("INSERT Isolation OBS"); + var value = apiRequest.ObservationData.Value?.ToString()?.Replace(";", ","); + if (value == null) + { + _logger.LogError("Error Processing Isolation Observation. Value is null"); + return; + } + + var obs = new PatientObservation + { + Time = apiRequest.ObservationData.Time.Value, + Name = "Isolation", + CodingSystem = "ADAS", + PatientId = patient.Id, + Value = value, + MessageTime = apiRequest.MessageTime + }; + + await InsertObservation(obs); + } + } + + private async Task ProcessPositionObservation(ApiRequest apiRequest, Patient patient) + { + if (apiRequest.ObservationData != null && + apiRequest.ObservationData?.Text?.ToUpper() == "CAMBIOS POSTURALES") + { + _logger.LogDebug("INSERT Position OBS"); + var strValue = apiRequest.ObservationData.Value?.ToString(); + + if (string.IsNullOrEmpty(strValue)) + { + var ob = apiRequest.Observation ?? apiRequest.Observations?.FirstOrDefault(); + if (ob != null) + strValue = ob.Value.ToString(); + } + + if (string.IsNullOrEmpty(strValue)) + { + _logger.LogWarning("Position Observation value is null or empty."); + return; + } + + if (!apiRequest.ObservationData.Time.HasValue) + { + _logger.LogWarning("Position Observation time is null. "); + return; + } + + var obs = new PatientObservation + { + Time = apiRequest.ObservationData.Time.Value, + Name = "Patient_Position", + CodingSystem = "ADAS", + PatientId = patient.Id, + Value = strValue.Replace(";", ","), + MessageTime = apiRequest.MessageTime + }; + + await InsertObservation(obs); + } + } + + + private async Task ProcessDrainageObservation(ApiRequest apiRequest, Patient patient) + { + _logger.LogDebug("INSERT DrainageObservation"); + + if (apiRequest.ObservationData == null || !apiRequest.ObservationData.Time.HasValue || + apiRequest.Observations == null) + { + _logger.LogWarning("Error Processing Drainage Observation."); + return; + } + + var obs = new PatientObservation + { + Time = apiRequest.ObservationData.Time.Value, + + Code = apiRequest.ObservationData.Code, + CodingSystem = apiRequest.ObservationData.CodingSystem, + PatientId = patient.Id, + ParentData = new ParentDataClass + { + Code = apiRequest.ObservationData.Code, + CodingSystem = apiRequest.ObservationData.CodingSystem, + Name = apiRequest.ObservationData.Text + }, + MessageTime = apiRequest.MessageTime + }; + + if (apiRequest.Observation != null) obs.Time = apiRequest.Observation.Time; + + obs.Value = new PatientDrainagesValue(); + + if (obs.Value is not PatientDrainagesValue obsVal) + { + _logger.LogDebug("Error Processing Drainage Observation. Value is null. "); + return; + } + + foreach (var observation in apiRequest.Observations) + switch (observation.Code) + { + case "138875005": + + switch (observation.Name ?? "") + { + case "Tipo de drenaje": + obsVal.Type = observation.Value.ToString(); + break; + + case "Altura columna(cmH2O)": + if (int.TryParse(observation.Value.ToString(), out var valueParsed)) + obsVal.Height = valueParsed; + break; + } + + break; + + case "10546003": + obsVal.Location = observation.Value.ToString(); + break; + + case "56868008": + if (int.TryParse(observation.Value.ToString(), out var obsValue)) obsVal.Volume = obsValue; + + break; + } + + await InsertObservation(obs); + } + + /** + * Las obs que se insertan de forma manual desde nurse deben seguir la logica contraria a las obs + * recibidas desde el censo + */ + public async Task SaveRequestNurseObs(ApiRequest apiRequest) + { + var patient = await _patientService.FindPatientByApiRequest(apiRequest); + + if (patient == null) + { + // NO PATIENTS OR LOCATIONS WERE FOUND + _logger.LogWarning( + "Observation for patient: {apiRequestpatientNumber} with location: {apiRequestlocation} not found, ignoring", + apiRequest.PatientNumber, apiRequest.Location); + return; + } + + if (apiRequest.Observations != null) + ProcessObservations(apiRequest.Observations, patient, apiRequest.MessageTime, + apiRequest.ObservationData); + } + + + private async Task CheckRecordings(Patient patient, List configs, + IEnumerable obsConfigList, int defaultValue) + { + //paramos grabación si no hay alarmas activas y hay una grabación + var recordingEndAfter = obsConfigList + .Where(c => c.Alarm is { Recording: not null }) + .Select(c => c.Alarm is { Recording: not null } ? c.Alarm.Recording.EndAfter : defaultValue) + .DefaultIfEmpty(defaultValue) + .Max(); + + var obsListWithRecordingAlarms = + await _observationRepository.FindLastNotExpiredObservatonsByPatient(patient.Id, "ADAS_ALARM", + recordingEndAfter); + + if (obsListWithRecordingAlarms.Any()) + return; + + + var boxCfg = configs.FirstOrDefault(box => box.Id == patient.PointOfCareId); + + var roomId = boxCfg?.Configuration?.Id; + if (roomId == null || !int.TryParse(roomId.ToString(), out var roomIdParsed)) + return; + + var currentRecordings = await _recordingService.GetRecordings(roomIdParsed); + + + if (currentRecordings == null || !currentRecordings.Any()) + return; + + var manualRecordings = + currentRecordings.Where(r => r.AlarmType == AlarmEnum.Type.Manual); + + foreach (var rec in manualRecordings) + { + _logger.LogDebug("PatientId: {obsid}. Send Stop Manual Recording Power Off", patient.Id); + + var manualRecording = new ManualRecording + { + Recording = new MRecording + { + StartRecordingTime = rec.StartRecordingTime, + StopRecordingTime = rec.StopRecordingTime ?? DateTime.Now + } + }; + + if (boxCfg != null) + await _recordingService.SendRecordingData(patient, boxCfg, manualRecording, false); + } + } + + private async Task CheckRelay(Patient patient, + IEnumerable obsConfigList, int defaultValue) + { + var relayEndAfter = obsConfigList + .Where(c => c.Alarm is { OpenDoor: not null }) + .Select(c => c.Alarm is { OpenDoor: not null } ? c.Alarm.OpenDoor.EndAfter : defaultValue) + .DefaultIfEmpty(defaultValue) + .Max(); + + var obsListWithRelayAlarms = + await _observationRepository.FindLastNotExpiredObservatonsByPatient(patient.Id, "ADAS_ALARM", + relayEndAfter); + + if (!patient.PointOfCareId.HasValue) + { + _logger.LogError("PatientId: {PatId} has no points of Care on check relay", patient.Id); + return; + } + + //Si no hay activas apagamos Relay + var relayConfig = await _pointOfCareService.FindById(patient.PointOfCareId.Value); + if (relayConfig is { Configuration.RelayIdList: not null } && !obsListWithRelayAlarms.Any()) + { + _logger.LogDebug("PatientId: {PatId}. Send relay Power Off", patient.Id); + var relays = _relayService.GetRelayInList(relayConfig.Configuration.RelayIdList); + relays.ForEach(async void (r) => + { + try + { + await _relayService.PowerOff(r); + } + catch (Exception e) + { + _logger.LogError("PatientId: {PatId}. Exception Power Off Relay {exMessage} trace: {exStackTrace}", + patient.Id, e.Message, e.StackTrace); + } + }); + } + } + + private async Task CheckBeacon(Patient patient, + IEnumerable obsConfigList, int defaultValue) + { + var beaconEndAfter = obsConfigList + .Where(c => c.Alarm is { Enabled: true, Beacon.Enabled: true }) + .Select(c => c.Alarm is { Beacon: not null } ? c.Alarm.Beacon.EndAfter : defaultValue) + .DefaultIfEmpty(defaultValue) + .Max(); + + var obsListWithBeaconAlarms = + await _observationRepository.FindLastNotExpiredObservatonsByPatient(patient.Id, "ADAS_ALARM", + beaconEndAfter); + + + //TODO: Heredar de la observación el endAfter si el de la baliza está 0 + + //Si no tiene alarmas activas y no está ya apagada lo hacemos + if (!obsListWithBeaconAlarms.Any() && patient.PointOfCareId.HasValue) + { + _logger.LogDebug("PatientId: {patientId}. Send Beacon code Power Off", patient.Id); + await _lightBeaconService.PowerOffLed(patient.PointOfCareId.Value); + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/PatientCarePlanService.cs b/adas-core.Application/Services/PatientCarePlanService.cs new file mode 100644 index 00000000..732af498 --- /dev/null +++ b/adas-core.Application/Services/PatientCarePlanService.cs @@ -0,0 +1,129 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; + +namespace adas_core.Application.Services; + +public class PatientCarePlanService : IPatientCarePlanService +{ + private readonly IArchivePatientCarePlanService _archivePatientCarePlanService; + private readonly ILocalAuditService _auditService; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly ILogger _logger; + private readonly IPatientCarePlanRepository _patientCarePlanRepository; + private readonly IUserRepository _userRepository; + + public PatientCarePlanService( + ILogger logger, + IPatientCarePlanRepository patientCarePlanRepository, + IUserRepository userRepository, + IArchivePatientCarePlanService archivePatientCarePlanService, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService) + { + _logger = logger; + _patientCarePlanRepository = patientCarePlanRepository; + _userRepository = userRepository; + _archivePatientCarePlanService = archivePatientCarePlanService; + _httpContextAccessor = httpContextAccessor; + _auditService = auditService; + _httpContextAccessor = httpContextAccessor; + } + + public async Task> FindByPatientId(ObjectId patientId) + { + try + { + return await _patientCarePlanRepository.FindByPatientId(patientId); + } + catch (Exception e) + { + _logger.LogError("Error finding Patient care plan by patientId: {PatientId} message: {Message}", patientId, + e.Message); + return []; + } + } + + public async Task> FindByUserId(ObjectId userId) + { + try + { + return await _patientCarePlanRepository.FindByUserId(userId); + } + catch (Exception e) + { + _logger.LogError("Error finding Patient care plan by userId: {UserId} message: {Message}", userId, + e.Message); + return []; + } + } + + public async Task> FindAll() + { + try + { + return await _patientCarePlanRepository.FindAll(); + } + catch (Exception e) + { + _logger.LogError("Error finding all Patient care plan message: {Message}", e.Message); + return []; + } + } + + public async Task InsertOneAsync(PatientCarePlan patientCarePlan) + { + try + { + patientCarePlan.Time = DateTime.UtcNow; + await _patientCarePlanRepository.InsertOneAsync(patientCarePlan); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, patientCarePlan); + } + catch (Exception e) + { + _logger.LogError("Error InsertOneAsync Patient care plan message: {Message} trace: {Trace}", e.Message, + e.StackTrace); + } + } + + public async Task ArchiveCarePlanFromJob(Patient patientWithFinishedProcedure, List itemsToArchive) + { + var systemUser = await _userRepository.GetOrCreateSystemUser(); + foreach (var item in itemsToArchive) + { + var itemToArchive = new PatientCarePlan + { + PatientId = patientWithFinishedProcedure.Id, + PatientNumber = patientWithFinishedProcedure.PatientNumber, + PointOfCareId = patientWithFinishedProcedure.PointOfCareId, + UserId = systemUser.Id, + CarePlan = item, + Description = "Archive item expired from job", + Action = ActionsEnum.CrudAction.Archive, + Time = DateTime.UtcNow + }; + await _patientCarePlanRepository.InsertOneAsync(itemToArchive); + } + } + + public async Task ArchiveByPatientId(ObjectId patientid) + { + var carePlansToArchive = await FindByPatientId(patientid); + await _archivePatientCarePlanService.InsertManyAsync(carePlansToArchive); + foreach (var item in carePlansToArchive) + { + await _patientCarePlanRepository.DeleteAsync(item.Id); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, item, null); + } + } + + public async Task UpdateManyObjectId(string patientid, ObjectId patientId, ObjectId oldId) + { + await _patientCarePlanRepository.UpdateManyObjectId(patientid, patientId, oldId); + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/PatientService.cs b/adas-core.Application/Services/PatientService.cs new file mode 100644 index 00000000..d46585ba --- /dev/null +++ b/adas-core.Application/Services/PatientService.cs @@ -0,0 +1,3223 @@ +using System.Diagnostics; +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.DTO; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.GroupedObservations; +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; +using Serilog; +using Patient = adas_core.Domain.Models.MongoModels.Patient; + +namespace adas_core.Application.Services; + +public class PatientService : IPatientService +{ + private readonly Lazy _admissionService; + private readonly IAppointmentService _appointmentService; + private readonly ILocalAuditService _auditService; + private readonly IClientMessageService _clientMessageService; + private readonly bool _createPatientWithAdtA08; + private readonly bool _createPatientWithLocation; + + private readonly bool _createPatientWithOru; + private readonly bool _cretatePatientWithoutPatientNumber; + private readonly IDiagnosisService _diagnosisService; + private readonly IDischargeService _dischargeService; + private readonly IDisplayConfigService _displayConfigService; + private readonly IDisplayService _displayService; + private readonly bool _findPatientByLocationWithoutId; + private readonly IGroupedObservationService _groupedObservationService; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly string _iccaFacility; + private readonly ListSettings _listSettings; + private readonly ILogger _logger; + private readonly IMasterListServiceFactory _masterListServiceFactory; + private readonly Lazy _observationService; + + private readonly OnArchiveAction _onArchiveAction; + private readonly IPatientArchiveRepository _patientArchiveRepository; + private readonly IPatientCarePlanService _patientCarePlanService; + + private readonly IPatientRepository _patientRepository; + private readonly IPoCMappingService _pocMappingService; + private readonly IPointOfCareService _pointOfCareService; + private readonly Lazy _pumpService; + private readonly bool _pushPatientWithOru; + private readonly IRecordingAlertService _recordingAlertService; + private readonly List _sendingFacility; + private readonly ISubscriberGroupedService _subscriberGroupedService; + private readonly ISubscribersService _subscribersService; + private readonly Lazy _treatmentService; + private readonly IUnitService _unitService; + private readonly bool _updatePatientDataWithAdtA02; + private readonly bool _updatePatientDataWithOru; + private readonly bool _updatePatientLocationWithOru; + + public PatientService( + IPatientRepository patientRepository, + IPatientArchiveRepository patientArchiveRepository, + Lazy observationService, + Lazy treatmentService, + IPoCMappingService pocMappingService, + IDiagnosisService diagnosisService, + IAppointmentService appointmentService, + Lazy pumpService, + IRecordingAlertService recordingAlertService, + IDischargeService dischargeService, + IOptions apiSettings, + IOptions listSettings, + ILogger logger, + IClientMessageService clientMessageService, + ISubscribersService subscribersService, + ISubscriberGroupedService subscriberGroupedService, + IUnitService unitService, + IDisplayService displayService, + IPointOfCareService pointOfCareService, + Lazy admissionService, + IDisplayConfigService displayConfigService, + IGroupedObservationService groupedObservationService, + IPatientCarePlanService patientCarePlanService, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService, IMasterListServiceFactory masterListServiceFactory) + { + _patientRepository = patientRepository; + _patientArchiveRepository = patientArchiveRepository; + _observationService = observationService; + _treatmentService = treatmentService; + _diagnosisService = diagnosisService; + _appointmentService = appointmentService; + _pumpService = pumpService; + _recordingAlertService = recordingAlertService; + _logger = logger; + _clientMessageService = clientMessageService; + _subscribersService = subscribersService; + _subscriberGroupedService = subscriberGroupedService; + _pocMappingService = pocMappingService; + _dischargeService = dischargeService; + _unitService = unitService; + _displayService = displayService; + _pointOfCareService = pointOfCareService; + _admissionService = admissionService; + _displayConfigService = displayConfigService; + _groupedObservationService = groupedObservationService; + _patientCarePlanService = patientCarePlanService; + _httpContextAccessor = httpContextAccessor; + _auditService = auditService; + _masterListServiceFactory = masterListServiceFactory; + + _ = Enum.TryParse(apiSettings.Value.OnArchivePatientAction, out _onArchiveAction); + _createPatientWithOru = apiSettings.Value.CreatePatientWithOru; + _pushPatientWithOru = apiSettings.Value.PushPatientWithOru; + _createPatientWithLocation = apiSettings.Value.CreatePatientWithLocation; + _updatePatientLocationWithOru = apiSettings.Value.UpdatePatientLocationWithOru; + _updatePatientDataWithOru = apiSettings.Value.UpdatePatientDataWithOru; + _findPatientByLocationWithoutId = apiSettings.Value.FindPatientByLocationWithoutId; + _iccaFacility = apiSettings.Value.IccaFacility ?? string.Empty; + _sendingFacility = apiSettings.Value.SendingFacility ?? []; + _cretatePatientWithoutPatientNumber = apiSettings.Value.CreatePatientWithoutPatientNumber; + _createPatientWithAdtA08 = apiSettings.Value.CreatePatientWithAdtA08; + _updatePatientDataWithAdtA02 = apiSettings.Value.UpdatePatientDataWithAdtA02; + _listSettings = listSettings.Value; + } + + public async Task Insert(Patient patient) + { + if (!CheckPatient(patient)) + return; + + await _patientRepository.InsertOneAsync(patient); + + + _ = SendNewPatientBroadcast(patient); + } + + public async Task FindByPatientIdWithLocale(string patientId, LocaleEnum localeEnum) + { + ObjectId.TryParse(patientId, out var id); + var patient = await FindById(id); + if (patient?.UnitId == null) return null; + var unit = await _unitService.FindById(patient.UnitId); + if (unit == null) return patient; + var patientWithLocale = await _masterListServiceFactory.GetPatientTraslated(unit, localeEnum, patient); + return patientWithLocale; + } + + public async Task InsertAsync(Patient patient) + { + if (!CheckPatient(patient)) + return; + + await _patientRepository.InsertOneAsync(patient); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, patient); + _ = SendNewPatientBroadcast(patient); + } + + public async Task Move(Patient patient, ObjectId newPocId, ObjectId oldPocId) + { + var newPoc = await _pointOfCareService.GetInfo(newPocId); + if (newPoc == null) + { + _logger.LogError("Unable to move patient {PatientId} to new poc {NewPocId} is null", patient.Id, newPocId); + return false; + } + + if (newPoc.Status != StatusEnum.PointOfCare.InUse && + newPoc.Status != StatusEnum.PointOfCare.Locked) + { + var oldPoc = await _pointOfCareService.GetInfo(oldPocId); + if (oldPoc == null) + { + _logger.LogError("Unable to move patient {PatientId} from old poc {OldPocId} is null", patient.Id, + oldPocId); + return false; + } + + patient.Location = new PatientLocation(newPoc.UnitName, newPoc.Bed, newPoc.Room); + patient.Bed = newPoc.Bed; + patient.UnitString = newPoc.UnitName; + patient.Room = newPoc.Room; + + patient.PointOfCareId = newPoc.Id; + patient.UnitId = newPoc.UnitId; + newPoc.Patient = patient; + await Update(patient); + await _pointOfCareService.SetPointOfCareStatus(newPoc.Id, StatusEnum.PointOfCare.InUse); + await _pointOfCareService.SetPointOfCareStatus(oldPoc.Id, StatusEnum.PointOfCare.Available); + _pointOfCareService.CheckNextAdmission(oldPoc.Id); + var currentDischarge = await _dischargeService.GetDischargeByPatientId(patient.Id); + if (currentDischarge != null) + { + _dischargeService.SendDischargeBroadcast(currentDischarge, OperationType.DeleteDischarge); + + currentDischarge.PatientLocation = patient.Location; + currentDischarge.PointOfCareId = newPoc.Id; + currentDischarge.UnitId = newPoc.UnitId; + currentDischarge.Patient = patient; + await _dischargeService.UpdateDischargeAsync(currentDischarge); + } + + // Enviar en base a la config de display que tenga cada subscriptor + _ = SendLastObsToSubscriberByPoc(newPoc, patient); + // Enviar las grupadas dependiendo del config del subscriptor + _ = SendLastGroupedObsToSubscriberByPoc(newPoc.Id, oldPoc.Id, patient); + + return true; + } + + _logger.LogError("Unable to move patient {PatientId} to new poc {NewPocId} is in use or locked", patient.Id, + newPocId); + return false; + } + + public async Task FindById(ObjectId id, bool withLocation = false) + { + if (id == ObjectId.Empty) + return null; + + var patient = await _patientRepository.FindById(id); + + if (withLocation && patient != null) + { + if (patient.PointOfCareId.HasValue) + { + var poc = await _pointOfCareService.FindById(patient.PointOfCareId.Value) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + patient.Bed = poc.Bed; + patient.Room = poc.Room; + } + + if (patient.UnitId.HasValue) + { + var unit = await _unitService.FindById(patient.UnitId.Value); + patient.UnitString = unit?.Name; + } + } + + // var lastObs = await _observationService.Value.FindLastObservations(patient.Id,1); + // if (lastObs.Any()) + // { + // patient.LastObservationDate = lastObs.First().Time; + // } + + // TODO: check if it can be cached or served faster + + return patient; + } + + public async Task FindByLocation(PatientLocation? location) + { + if (location?.UnitName == null || location.Bed == null) + return null; + var unit = await _unitService.FindByName(location.UnitName); + if (unit == null) return null; + var poc = await _pointOfCareService.FindByBedAndUnitId(location.Bed, unit.Id); + if (poc == null) return null; + return await FindByUnitAndPocId(unit.Id, poc.Id); + } + + public async Task FindByPointOfCareId(ObjectId pocId) + { + return await _patientRepository.FindByPointOfCareId(pocId); + } + + public async Task CountPatientsByUnitId(ObjectId unitId) + { + return await _patientRepository.CountByUnitId(unitId); + } + + public async Task ArchivePatient(Patient patient) + { + patient.DisTime ??= DateTime.UtcNow; + //var sections = sectionService.FindByPatient(patient.id); + var pocId = patient.PointOfCareId; + _logger.LogDebug("{OnArchiveAction} patient {Patient}", _onArchiveAction, patient); + switch (_onArchiveAction) + { + case OnArchiveAction.Archive: + default: + patient.ArchiveDate = DateTime.UtcNow; + await _patientArchiveRepository.InsertOneAsync(patient); + break; + + case OnArchiveAction.Delete: + //No hay archivado + break; + } + + await _patientRepository.Delete(patient.Id); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, patient, null); + + await ArchivePatientData(patient.Id); + + if (pocId.HasValue) + { + _ = SendDeletePatientBroadcast(patient.Id, pocId.Value); + //cambiamos el estado del punto de cuidado a disponible + await _pointOfCareService.SetPointOfCareStatus(pocId.Value, StatusEnum.PointOfCare.Available); + } + } + + public async Task ArchivePatientData(ObjectId patientid) + { + _logger.LogDebug("Archiving patient data action: {onArchiveAction} : {patientid}", _onArchiveAction, + patientid); + switch (_onArchiveAction) + { + case OnArchiveAction.Archive: + default: + await _observationService.Value.ArchiveByPatientId(patientid); + await _treatmentService.Value.ArchiveByPatientId(patientid); + await _diagnosisService.ArchiveByPatientId(patientid); + await _appointmentService.ArchiveByPatientId(patientid); + await _pumpService.Value.ArchiveByPatientId(patientid); + await _recordingAlertService.ArchiveByPatientId(patientid); + await _patientCarePlanService.ArchiveByPatientId(patientid); + break; + + case OnArchiveAction.Delete: + await _observationService.Value.DeleteByPatientId(patientid); + await _treatmentService.Value.DeleteByPatientId(patientid); + await _diagnosisService.DeleteByPatientId(patientid); + await _appointmentService.DeleteByPatientId(patientid); + await _pumpService.Value.DeleteByPatientId(patientid); + await _recordingAlertService.DeleteByPatientId(patientid); + break; + } + } + + public async Task Update(Patient patient) + { + var oldPatient = await _patientRepository.FindById(patient.Id); + try + { + _logger.LogDebug("Update {patient}", patient); + //To solve multi "sin cama" from ICCADB making duplicated keys error on mongodb. + if (patient.Location is { Bed: "Sin cama" }) patient.Location.Bed = ObjectId.GenerateNewId().ToString(); + + if (string.IsNullOrEmpty(patient.PatientNumber)) + //si permite crear pacientes sin patientNumber le asignamos el patient.Id + patient.PatientNumber = !_cretatePatientWithoutPatientNumber ? patient.Id.ToString() : string.Empty; + + await _patientRepository.Update(patient); + if (oldPatient != null) + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, patient); + _ = SendPatientUpdateBroadcast(patient); + } + catch (Exception e) + { + _logger.LogError("error updating patient: {patient} exception: {e.Message}", patient, e.Message); + } + } + + public async Task UpdateLocation(ObjectId id, PatientLocation? location) + { + try + { + if (location == null) + { + _logger.LogError("PointOfCare is null. Not updated location for patient id: {id} ", id); + return; + } + + _logger.LogDebug("Update location id: {id} data: {location}", id, location); + + var patient = await _patientRepository.FindById(id); + var auxPatient = await _auditService.DeepCopyAsync(patient); + if (patient == null) + { + _logger.LogError("person is null. Not updated location for patient id: {id} ", id); + return; + } + + if (patient.PointOfCareId == null) + { + _logger.LogError("person pocId or unitId is null. Not updated location for patient id: {id} ", id); + return; + } + + var oldPoc = await _pointOfCareService.FindById(patient.PointOfCareId.Value); + if (oldPoc == null) + { + _logger.LogError("person old poc not found {pocId}. Not updated location for patient id: {id} ", + patient.PointOfCareId.Value, id); + return; + } + + var isRecovered = Enum.TryParse(oldPoc.Bed, out _); + //"DELETED".Equals(patient.Location.UnitName); + + //var oldLocation =new PatientLocation(oldUnit?.Name, oldPoc.Bed, oldPoc.Room); + if (isRecovered) + patient.DisTime = null; + + var newUnit = await _unitService.FindByName(location.UnitName); + var newPoc = await _pointOfCareService.FindByBedAndUnitId(location.Bed, newUnit?.Id); + if (newUnit == null || newPoc == null) + { + _logger.LogError("person new poc not found {pocId}. Not updated location for patient id: {id} ", + patient.PointOfCareId.Value, id); + return; + } + + patient.Location = new PatientLocation(newUnit.Name, newPoc.Bed, newPoc.Room); + patient.PointOfCareId = newPoc.Id; + patient.UnitId = newUnit.Id; + + await _patientRepository.UpdateOneAsync(patient.Id, patient); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, auxPatient, patient); + if (isRecovered) + await SendNewPatientBroadcast(patient); + + var pocListToNotify = new List { newPoc.Id }; + + await _pointOfCareService.SetPointOfCareStatus(newPoc.Id, StatusEnum.PointOfCare.InUse); + + + pocListToNotify.Add(oldPoc.Id); + _pointOfCareService.CheckNextAdmission(oldPoc.Id); + + + //Prio location to advertise both locations of movement between sections + _logger.LogDebug( + "SendPatientLocationBroadcast id: {id} location: {location} newSection: {newSection}, oldSection: {oldSection}", + id, location, newPoc.Location.ToString(), oldPoc.Location.ToString()); + + if (!isRecovered) + _ = SendPatientLocationBroadcast(id, location, pocListToNotify); + if (oldPoc.Id != newPoc.Id) + CheckLocationForWsSubscriber(location, id); + } + catch (Exception ex) + { + _logger.LogError("error updating location for patient id: {id} exception: {exMessage}", id, ex.Message); + } + } + + public async Task UpdateAttendingDoctor(ObjectId id, Person doctor) + { + var oldPatient = await _patientRepository.FindById(id) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictOperationIncompleted); + _logger.LogDebug("Update attending doctor id: {id} data: {doctor}", id, doctor); + await _patientRepository.UpdateAttendingDoctor(id, doctor); + var newPatient = await _patientRepository.FindById(id) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictOperationIncompleted); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, newPatient); + _ = SendPatientAttendingDoctorBroadcast(id, doctor); + } + + public async Task UpdatePatientData(ObjectId id, string patientNumber, Person data, + bool updatePatientNumber = true) + { + var oldPatient = await _patientRepository.FindById(id) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictOperationIncompleted); + _logger.LogDebug("Update person data id: {id} number: {patientNumber} data: {data}", id, patientNumber, + data); + await _patientRepository.UpdatePatientData(id, patientNumber, data, updatePatientNumber); + var newPatient = await _patientRepository.FindById(id) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictOperationIncompleted); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, newPatient); + _ = SendPatientDataBroadcast(id, data); + } + + public async Task UpdatePatientData(ObjectId id, string patientNumber, Patient patient, + bool updatePatientNumber = true) + { + _logger.LogDebug("Update person data id: {id} number: {patientNumber} patient: {patient}", id, + patientNumber, patient); + if (patient.Person != null) + { + var oldPatient = await _patientRepository.FindById(id) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictOperationIncompleted); + await _patientRepository.UpdatePatientData(id, patientNumber, patient.Person, updatePatientNumber); + var newPatient = await _patientRepository.FindById(id) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictOperationIncompleted); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, newPatient); + } + + _ = SendPatientBroadcast(patient); + } + + public async Task FindByPatientId(string patientId, bool withLocation = false) + { + + var patient = await _patientRepository.FindByPatientId(patientId); + if (withLocation && patient != null) + { + if (patient.PointOfCareId.HasValue) + { + var poc = await _pointOfCareService.FindById(patient.PointOfCareId.Value); + patient.Bed = poc?.Bed; + patient.Room = poc?.Room; + } + + if (patient.UnitId.HasValue) + { + var unit = await _unitService.FindById(patient.UnitId.Value); + patient.UnitString = unit?.Name; + } + } + + return patient; + } + + public async Task FindByPatientNumber(string patientNumber, bool withLocation = false) + { + var patient = await _patientRepository.FindByPatientNumber(patientNumber); + if (withLocation && patient != null) + { + if (patient.PointOfCareId.HasValue) + { + var poc = await _pointOfCareService.FindById(patient.PointOfCareId.Value); + patient.Bed = poc?.Bed; + patient.Room = poc?.Room; + } + + if (patient.UnitId.HasValue) + { + var unit = await _unitService.FindById(patient.UnitId.Value); + patient.UnitString = unit?.Name; + } + } + + return patient; + } + + public async Task FindByPatientNumberArchived(string patientNumber) + { + return await _patientArchiveRepository.FindByPatientNumber(patientNumber); + } + + public async Task ArchivePatientWithoutObservationsSinceDate(DateTime date) + { + try + { + // Observations not updated since date, archive their patients and data + + var ptimes = (await _observationService.Value.FindAllLastPatientObservationTime()) + .Where(o => date > o.Value).ToDictionary(o => o.Key, o => o.Value); + (await _pumpService.Value.FindAllLastPatientObservationTime()).Where(o => date > o.Value) + .Where(o => !ptimes.ContainsKey(o.Key) || ptimes[o.Key] > o.Value).ToList() + .ForEach(o => ptimes[o.Key] = o.Value); + + //ptimes.ToList().Sort( (o1, o2) => o1.Value.CompareTo(o2.Value)); + var foundPatients = new HashSet(); + var notFoundPatients = new HashSet(); + foreach (var o in ptimes) + { + var patient = await FindById(o.Key); + if (patient != null) foundPatients.Add(patient); + else notFoundPatients.Add(o.Key); + } + + var notUpdatedPatients = await _patientRepository.FindPatientsNotUpdatedSince(date); + foreach (var notUpdatedPatient in notUpdatedPatients.Where(p => p.IsInActivePoC())) + { + var obsPatient = await _observationService.Value.FindLastObservations(notUpdatedPatient.Id); + var pumpPatient = await _pumpService.Value.FindLastPumpObservations(notUpdatedPatient.Id); + if (!foundPatients.Contains(notUpdatedPatient) && obsPatient.Count == 0 && pumpPatient.Count == 0) + foundPatients.Add(notUpdatedPatient); + } + + if (foundPatients.Count > 0) + { + _logger.LogDebug("Patients found for archive: {foundPatientsCount}", foundPatients.Count); + foreach (var patient in foundPatients) + try + { + await ArchivePatient(patient); + } + catch (Exception ex) + { + _logger.LogError( + "ERROR trying to ArchivePatient {patientId} because {exMessage}", patient.Id, + ex.Message); + } + } + + if (notFoundPatients.Count > 0) + { + _logger.LogWarning("Patients not found in patient collection: {notFoundPatientsCount}", + notFoundPatients.Count); + foreach (var patientId in notFoundPatients) + { + _logger.LogWarning("- Deleting orphan data for patient: {patientId}", patientId); + try + { + await ArchivePatientData(patientId); + } + catch (Exception ex) + { + _logger.LogError( + "ERROR trying to ArchivePatientData {patientId} because {exMessage}", patientId, + ex.Message); + } + } + } + } + catch (Exception ex) + { + _logger.LogError( + "ERROR trying to retrieve patients without observations in date range because {exMessage}", + ex.Message); + throw; + } + } + + public async Task ArchiveDischargedPatients(int hoursBeforeArchive) + { + var disBeforeDate = DateTime.Now.AddHours(-hoursBeforeArchive); + try + { + var dischargedPatients = (await _patientRepository.FindDischargedPatients()) + .Where(patient => + patient.DisTime != null && + patient.DisTime.Value < disBeforeDate + ).ToList(); + _logger.LogDebug( + "ArchiveDischargedPatients older than {HoursBeforeArchive} hours ({DisBeforeDate}). Found {Count} patients for archiving", + hoursBeforeArchive, disBeforeDate, dischargedPatients.Count); + foreach (var patient in dischargedPatients) await ArchivePatient(patient); + } + catch (Exception ex) + { + _logger.LogError("Error archiving discharged patients {Message}", ex.Message); + } + } + + public async Task FindPatient(string? patientId, string? patientNumber, PatientLocation? location, + bool findPatientByLocation = false) + { + Patient? patient = null; + if (!string.IsNullOrEmpty(patientNumber)) + patient = await FindByPatientNumber(patientNumber, true); + + if (patient != null) return patient; + + if (!string.IsNullOrEmpty(patientId)) + patient = await FindByPatientId(patientId, true); + if (patient != null) return patient; + + + // No buscamos por localización por defecto + if (!_findPatientByLocationWithoutId && !findPatientByLocation) return patient; + //if location pointOfCare and bed != null + if (location is { UnitName: not null, Bed: not null }) + { + var loc = await _pocMappingService.Map(location); + patient = await FindByLocation(loc); + if(patient is not null && loc is not null) + patient.Location = loc; + } + + + return patient; + } + + public async Task GetByPointOfCare(PointOfCare item, bool observations = false, + List? filterObservations = null) + { + try + { + return await _patientRepository.FindByPointOfCareId(item.Id); + } + catch (Exception ex) + { + Debug.WriteLine("ERROR: " + ex.Message); + return null; + } + } + + public async Task GetByPointOfCareAndLocale(PointOfCare item, Unit? unit, LocaleEnum? localeEnum) + { + try + { + var patient = await _patientRepository.FindByPointOfCareId(item.Id); + if (unit == null || localeEnum == null) return patient; + return await _masterListServiceFactory.GetPatientTraslated(unit, localeEnum.Value, patient); + } + catch (Exception ex) + { + Debug.WriteLine("ERROR: " + ex.Message); + return null; + } + } + + public async Task GetBox(PointOfCare poc, bool observations = false, + List? filterObservations = null) + { + Box response = new(); + Patient? patient = null; + try + { + patient = await _patientRepository.FindByPointOfCareId(poc.Id); + } + catch (Exception ex) + { + Debug.WriteLine("ERROR: " + ex.Message); + } + + response.PointOfCare = poc.UnitName; + response.Bed = poc.Bed; + response.IsVisible = true; + if (patient == null) + { + response.HasPatient = false; + return response; + } + + response.HasPatient = true; + response.Patientid = patient.Id; + response.Patient = patient; + response.AttendingDoctor = patient.AttendingDoctor; + if (!observations) return response; + var obs = await _observationService.Value.FindLastObservations(patient.Id, 2, filterObservations); + obs.ForEach(Action); + response.Observations = obs; + + return response; + + void Action(PatientObservation o) + { + _observationService.Value.MapObservationsByName(o); + } + } + + + public Task SendNewPatientBroadcast(Patient patient) + { + var subscribers = new List(); + // var section = await _sectionService.FindByPointOfCare(patient.UnitString); + // if (section != null) + // { + // subscribers.AddRange(_subscribersService.GetSubscribers().Where(s => + // (s.SubscriptionType == SubscriptionType.Box && s.Box == patient.Bed && s.Section == section.Id) || + // (s.SubscriptionType == SubscriptionType.Section && s.Section == section.Id)).ToList()); + // } + + subscribers.AddRange(_subscribersService.GetSubscribers().Where(s => + !s.LocationIds.IsNullOrEmpty() && s.LocationIds.Any(c => + c == patient.PointOfCareId + )).ToList()); + + foreach (var subscriber in subscribers) + _ = _clientMessageService.SendAsync(subscriber.Id, OperationType.NewPatient, patient); + + return Task.CompletedTask; + } + + + public async Task SendPatientUpdateBroadcast(Patient patient) + { + if (!patient.PointOfCareId.HasValue) + { + _logger.LogError("patient pointOfCareId is null. Not sent patient id: {id} ", patient.Id); + return; + } + + var subscribersGroup = _subscribersService.GetSubscribers().Where(s => + s.LocationIds.Any(c => c == patient.PointOfCareId.Value)).GroupBy(h => h.Locale); + var unit = await _unitService.FindById(patient.UnitId); + foreach (var group in subscribersGroup) + { + var locale = group.Key ?? LocaleEnum.Default; + IEnumerable subscribers = group; + + foreach (var subscriber in subscribers) + { + var patientWithLocale = await _masterListServiceFactory.GetPatientTraslated(unit, locale, patient); + _ = _clientMessageService.SendAsync(subscriber.Id, OperationType.UpdatePatient, patientWithLocale); + } + } + } + + + public Task SaveRequestAsync(ApiRequest apiRequest) + { + return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); })); + } + + public async Task SaveRequest(ApiRequest apiRequest) + { + if (string.IsNullOrEmpty(apiRequest.PatientNumber) && + string.IsNullOrEmpty(apiRequest.Location?.UnitName) && apiRequest.Type != "ICCA") + { + _logger.LogDebug("person and PointOfCare are nulls"); + throw new ApiRequestException("person and PointOfCare are nulls"); + } + + _logger.LogDebug("patientNumber: {apiRequestpatientNumber} location: {apiRequestlocation}", + apiRequest.PatientNumber, apiRequest.Location); + + if (apiRequest.Location != null) + { + var originalPointOfCare = apiRequest.Location; + apiRequest.Location = await _pocMappingService.Map(originalPointOfCare); + if (apiRequest.Location == null) + { + _logger.LogWarning("PointOfCare not found in Mapping list: {originalPointOfCare}", + originalPointOfCare.ToString()); + return; + } + } + + if (apiRequest.OldLocation != null) + { + var originalOldPointOfCare = apiRequest.OldLocation; + apiRequest.OldLocation = await _pocMappingService.Map(originalOldPointOfCare); + } + + _logger.LogDebug("RequestType: {apiRequesttype}", apiRequest.Type); + + + var unitConfig = await _unitService.FindByName(apiRequest.Location?.UnitName); + Unit? oldUnitConfig = null; + if (apiRequest.OldLocation != null) + { + var originalOldLocation = await _pocMappingService.Map(apiRequest.OldLocation); + if (originalOldLocation != null) + oldUnitConfig = await _unitService.FindByName(apiRequest.OldLocation?.UnitName); + } + + if (apiRequest.Type != "ICCA") + { + if (apiRequest.PatientNumber != null && unitConfig == null && oldUnitConfig == null) + { + var pat = await FindByPatientNumber(apiRequest.PatientNumber); + if (pat is { UnitId: not null }) unitConfig = await _unitService.FindById(pat.UnitId); + } + + if (!Hl7Utils.ManageAutoAdt(unitConfig, oldUnitConfig, _logger, "ADT")) return; + } + //TODO Crear para el adt5 + + try + { + Patient? patient = null; + + switch (apiRequest.Type) + { + //* ADT EVENTS + + case "ADT_A01": + case "ADT_A05": + { + //* A01 Admit/visit notification + //* A05 Pre-admit a patient + + _logger.LogDebug("NEW ADT {apiRequestType} PATIENT NUMBER: {apiRequestPatientNumber}", + apiRequest.Type, apiRequest.PatientNumber ?? "NULL"); + patient = await ProcessAdtPatientAdmit(apiRequest); + break; + } + case "ADT_A02": + { + //* A02 Transfer a patient + + if (apiRequest.Location != null) + apiRequest.Location = await _pocMappingService.Map(apiRequest.Location); + + patient = await FindPatient(apiRequest.PatientId, apiRequest.PatientNumber, + apiRequest.OldLocation); + + await ProcessAdtMovePatient(apiRequest, patient); + + break; + } + case "ADT_A03": + { + // * A03 Discharge/end visit + // *Ya no archiva, archiva el job del Scheduler service pasado el tiempo establecido por el hospital en SinceDischargeTimeToArchive + // se le mueve a una cama temporal. + + _logger.LogDebug("NEW ADT {apiRequestType} PATIENT NUMBER: {apiRequestPatientNumber}", + apiRequest.Type, apiRequest.PatientNumber); + if (apiRequest.Location != null) + apiRequest.Location = await _pocMappingService.Map(apiRequest.Location); + + patient = await FindPatient(apiRequest.PatientId, apiRequest.PatientNumber, + apiRequest.Location); + if (patient != null) + await RemovePatientAndSendToVirtualPoc(apiRequest, patient, VirtualPointOfCare.Deleted); + else + _logger.LogDebug("ADT A03 not found patient number {number}", apiRequest.PatientNumber); + + break; + } + case "ADT_A04": + { + // * A04 Register a patient + + if (apiRequest.Location != null) + apiRequest.Location = await _pocMappingService.Map(apiRequest.Location); + + patient = await FindPatient(apiRequest.PatientId, apiRequest.PatientNumber, + apiRequest.Location); + if (patient == null) + patient = await CreatePatientFromRequest(apiRequest); + else + _logger.LogDebug("ADT A04 not found patient number {number}", apiRequest.PatientNumber); + + break; + } + case "ADT_A08": + { + //* A08 Update patient information + + patient = await FindPatient(apiRequest.PatientId, apiRequest.PatientNumber, + apiRequest.Location); + if (patient != null) + { + await UpdatePatientFromRequest(patient, apiRequest, true); + await Update(patient); + } + else + { + _logger.LogDebug("ADT A08 not found by patient number {number}", apiRequest.PatientNumber); + if (_createPatientWithAdtA08) + { + _logger.LogDebug( + "ProcessAdtPatientAdmit ADT_A08 Because CreatePatientWithADT_A08 is true {apiRequestpatientNumber}", + apiRequest.PatientNumber); + + await ProcessAdtPatientAdmit(apiRequest); + } + } + + break; + } + + // A11 Cancel admit/ visit notification/ + // Remove patient + + case "ADT_A11": + { + var patientNumber = apiRequest.PatientNumber; + if (patientNumber == null) + { + _logger.LogError("ADT A11 Without patient number {number}", apiRequest.PatientNumber); + } + else + { + patient = await _patientRepository.FindByPatientNumber(patientNumber); + if (patient != null) + await RemovePatientAndSendToVirtualPoc(apiRequest, patient, + VirtualPointOfCare.Cancelled); + else + _logger.LogDebug("ADT A11 not found patient number {number}", apiRequest.PatientNumber); + } + + break; + } + + //* A12 Cancel patient transfer. + //* Se mueve el patientNumber que viene a la cama que indica, si hay ya un paciente en esa cama ese paciente se mueve a una temporal + + case "ADT_A12": + { + var patientNumber = apiRequest.PatientNumber; + + if (apiRequest.Location != null) + apiRequest.Location = await _pocMappingService.Map(apiRequest.Location); + + + if (patientNumber == null) + { + _logger.LogError("ADT A12 Without patient number {number}", apiRequest.PatientNumber); + } + + else + { + patient = await _patientRepository.FindByPatientNumber(patientNumber); + await ProcessAdtMovePatient(apiRequest, patient); + } + + break; + } + + //* A13 Cancel discharge/end visit + //* Recuperamos el paciente, seteamos ArchiveDate a null.Lo movemos a la cama que nos indique. + + case "ADT_A13": + { + if (apiRequest.Location != null) + apiRequest.Location = await _pocMappingService.Map(apiRequest.Location); + var patientNumber = apiRequest.PatientNumber; + if (patientNumber == null) + { + _logger.LogError("ADT A13 Without patient number {number}", apiRequest.PatientNumber); + } + else + { + patient = await _patientRepository.FindByPatientNumber(patientNumber); + if (patient != null) + { + if ("".Equals(apiRequest.Location?.Bed?.Replace("\"\"", ""))) + { + var unit = await _unitService.FindById(patient.UnitId); + apiRequest.Location = new PatientLocation(unit?.Name, + VirtualPointOfCare.Recovered.ToString(), VirtualPointOfCare.Recovered.ToString()); + } + + await ProcessAdtMovePatient(apiRequest, patient); + } + else + { + _logger.LogDebug("ADT A13 not found patient number {number}", apiRequest.PatientNumber); + } + } + + break; + } + + case "ADT_A31": + { + //* ADT_A31 - Update Person Information + + patient = await FindPatient(apiRequest.PatientId, apiRequest.PatientNumber, + apiRequest.Location); + if (patient != null) + { + await UpdatePatientFromRequest(patient, apiRequest, true); + await Update(patient); + } + else + { + _logger.LogDebug("ADT A31 not found patient number {number}", apiRequest.PatientNumber); + } + + break; + } + + //ADT_A39 - Merge Person - Patient Id + //ADT_A40 - Merge patient - Patient Identifier List + case "ADT_A39": + case "ADT_A40": + { + if (apiRequest.Location != null) + apiRequest.Location = await _pocMappingService.Map(apiRequest.Location); + var patientNumber = apiRequest.PatientNumber; + if (patientNumber == null) + { + _logger.LogError("ADT A39 Without patient number {number}", apiRequest.PatientNumber); + } + else + { + patient = await _patientRepository.FindByPatientNumber(patientNumber); + if (patient != null && !string.IsNullOrEmpty(apiRequest.OldPatientNumber)) + await MergePatient(patient, apiRequest.OldPatientNumber); + else + _logger.LogDebug("ADT A39 not found patient number {number}", apiRequest.PatientNumber); + } + + break; + } + + //ADT A_47: actualización de lista de ids + case "ADT_A47": + { + //Para el A47 no envía necesariamente el el location + //Buscamos el paciente por patientNumber + if (!string.IsNullOrEmpty(apiRequest.PatientNumber)) + patient = await _patientRepository.FindByPatientNumber(apiRequest.PatientNumber); + + if (patient == null) + { + _logger.LogError("{type}: Patient not found in apiRequest: {apirequest}", apiRequest.Type, + apiRequest.ToString()); + break; + } + + _logger.LogDebug("Updating patient from request. Patient: {patient} ", patient.ToString()); + UpdatePatientFromRequestToIdList(patient, apiRequest); + + _logger.LogDebug("Updating patient. Patient: {patient}", patient.ToString()); + await Update(patient); + _logger.LogDebug("updated patient: {patientId}", patient.Id); + break; + } + //ADT A_44: actualización de patient number + case "ADT_A44": + { + //Cambiar el patientNumber + var oldPatientNumber = apiRequest.OldPatientNumber; + if (oldPatientNumber == null) + { + if (apiRequest.Location == null) + { + //A44 no tiene ni oldPatientNumber ni ubicación + //No se procesa + _logger.LogError("{type} Without old Patient Number and location ", apiRequest.Type); + break; + } + + //Buscamos al paciente por su localización + patient = await _patientRepository.FindByLocation(apiRequest.Location); + } + else + { + patient = await _patientRepository.FindByPatientNumber(oldPatientNumber); + } + + if (patient == null) + { + _logger.LogDebug("ADT A44 not found patient"); + break; + } + + + _logger.LogDebug("Updating patient from request. Patient: {patient} ", patient.ToString()); + await UpdatePatientFromRequest(patient, apiRequest); + + _logger.LogDebug("Updating patient. Patient: {patient}", patient.ToString()); + await Update(patient); + _logger.LogDebug("updated patient: {patientId}", patient.Id); + break; + } + + //* A06 Change an outpatient to an inpatient + //* A07 Change an inpatient to an outpatient + //* A08 Update patient information + //* A09 Patient departing - tracking + //* A10 Patient arriving - tracking + //* A12 Cancel transfer + //* A14 Pending admit + //* A15 Pending transfer + //* A16 Pending discharge + //* A17 Swap patients + //* A18 Merge patient information + //* A19 QRY/ADR - Patient query + //* A20 Bed status update + + case "ADT_A06": + case "ADT_A07": + case "ADT_A09": + case "ADT_A10": + case "ADT_A14": + case "ADT_A15": + case "ADT_A16": + case "ADT_A17": + case "ADT_A18": + case "ADT_A19": + case "ADT_A20": + _logger.LogWarning("{apiRequesttype} is not valid for Patients", apiRequest.Type); + break; + //For icca db sync + case "ICCA": + _ = ProcessIccaSync(apiRequest); + break; + + default: + throw new ApiRequestException("ApiRequest type " + apiRequest.Type + + " is not valid for Patients"); + } + + if (patient != null) + { + if (apiRequest.Observations != null && apiRequest.Observations.Any()) + _observationService.Value.ProcessObservations(apiRequest.Observations, patient, + apiRequest.MessageTime, apiRequest.ObservationData); + + if (apiRequest.Diagnosis != null && apiRequest.Diagnosis.Any()) + _ = _diagnosisService.ProcessDiagnosis(apiRequest.Diagnosis, patient, apiRequest.MessageTime); + + if (apiRequest.Appointments != null && apiRequest.Appointments.Any()) + _ = _appointmentService.ProcessApiRequest(apiRequest, patient); + } + } + catch (Exception ex) + { + _logger.LogError("ERROR saving patient: {exMessage} trace: {exStackTrace}", ex.Message, ex.StackTrace); + } + } + + public async Task CreatePatientFromRequest(ApiRequest apiRequest, bool ignoreLocation = false) + { + var patient = new Patient + { + Id = ObjectId.GenerateNewId(), + CreationDate = DateTime.UtcNow + }; + await UpdatePatientFromRequest(patient, apiRequest, ignoreLocation); + await _patientRepository.InsertOneAsync(patient); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, patient); + return patient; + } + + public async Task FindPatientByApiRequest(ApiRequest apiRequest) + { + Patient? patient = null; + if (apiRequest.Location != null) + { + var originalPointOfCare = apiRequest.Location; + apiRequest.Location = await _pocMappingService.Map(originalPointOfCare); + if (apiRequest.Location == null) + { + _logger.LogDebug("PointOfCare not found in Mapping list: {OriginalPointOfCare} ", + originalPointOfCare); + return patient; + } + } + + _logger.LogDebug("RequestType: {apiRequest}", apiRequest.Type); + + try + { + switch (apiRequest.Type) + { + //* ORU_R01 - Unsolicited transmission of an observation message + //* ORU_R40 - Unsolicited transmission of an alert observation message + //* AÑADIDO. SI EL FACILITY DEL API REQUEST NO ES EL DE ICCA BUSCA SOLO POR POINT OF CARE Y BED NO POR PATIENT ID. + case "AlarisPump": + case "ORU_R01": + case "ORU_R42": + case "ORU_R40": + _logger.LogDebug("patientNumber: {apiRequestpatientNumber} location: {apiRequestlocation}", + apiRequest.PatientNumber, apiRequest.Location); + // SEARCH PATIENT || SendingFacility.Contains(admRequest.facility) + if (!string.IsNullOrWhiteSpace(apiRequest.PatientNumber) && + !string.IsNullOrWhiteSpace(apiRequest.Facility) && + (apiRequest.Facility.Equals(_iccaFacility) || apiRequest.Facility.Equals("obs"))) + { + // SEARCH PATIENT BY PATIENT NUMBER + _logger.LogDebug("SEARCH PATIENT BY PATIENT NUMBER"); + patient = await FindByPatientNumber(apiRequest.PatientNumber, true); + if (patient == null) + // PATIENT NOT FOUND OR ARCHIVED + _logger.LogDebug("PATIENT NOT FOUND OR ARCHIVED"); + } + + if (patient == null) + { + _logger.LogDebug("PATIENT {apiRequestpatient}", apiRequest.Patient); + _logger.LogDebug("PATIENT NUMBER {apiRequestpatientNumber}", apiRequest.PatientNumber); + _logger.LogDebug("PATIENT LOCATION {apiRequestlocation} EMPTY {apiRequestlocationIsEmpty}", + apiRequest.Location, apiRequest.Location?.ToStr() ?? "null"); + if (!string.IsNullOrEmpty(apiRequest.PatientNumber) && + !string.IsNullOrWhiteSpace(apiRequest.Facility) && + (apiRequest.Facility.Equals(_iccaFacility) || _sendingFacility.Contains(apiRequest + .Facility))) + { + _logger.LogDebug("PATIENT LOCATION Find person by PatientNumber"); + var patientExists = + await _patientRepository.FindByPatientNumber(apiRequest.PatientNumber); + if (patientExists != null) + { + patient = patientExists; + } + else + { + _logger.LogDebug("PATIENT LOCATION IS EMPTY.Not found Patient by PatientNumber"); + + if (_createPatientWithOru) + { + if (apiRequest.Location == null || apiRequest.Location.IsEmpty()) + { + _logger.LogDebug( + "PATIENT LOCATION IS EMPTY. Generate temporal bed because createPatientWithoutLocation is TRUE"); + apiRequest.Location = new PatientLocation( + VirtualPointOfCare.Unknown.ToString(), + VirtualPointOfCare.Unknown.ToString()); + } + + if (!apiRequest.Location.IsEmpty() && + string.IsNullOrEmpty(apiRequest.Location.Bed)) + apiRequest.Location.Bed = VirtualPointOfCare.Unknown.ToString(); + + patient = new Patient + { + Id = ObjectId.GenerateNewId(), + PatientNumber = apiRequest.PatientNumber, + Person = apiRequest.Patient, + AttendingDoctor = apiRequest.AttendingDoctor, + Location = apiRequest.Location + }; + + var patientLocation = await FindPatient(null, null, apiRequest.Location, true); + + if (patientLocation != null) + { + _logger.LogDebug( + "PATIENT LOCATION IS OCUPED, Created UNKNOW location {patient}.", + patient); + + + var pocUnknown = + await _pointOfCareService.FindByBedAndUnitId( + VirtualPointOfCare.Unknown.ToString(), + patientLocation.UnitId); + patient.UnitId = patientLocation.UnitId; + patient.PointOfCareId = pocUnknown?.Id; + var un = await _unitService.FindById(patientLocation.UnitId); + patient.Location = new PatientLocation( + un?.Name ?? VirtualPointOfCare.Unknown.ToString(), + pocUnknown?.Bed ?? VirtualPointOfCare.Unknown.ToString()); + } + else + { + var unit = await _unitService.FindByName(apiRequest.Location.UnitName); + var poc = + await _pointOfCareService.FindByBedAndUnitId(apiRequest.Location.Bed, + unit?.Id); + if (unit != null && poc != null) + { + patient.UnitId = poc.UnitId; + patient.PointOfCareId = poc.Id; + patient.Location = new PatientLocation( + unit.Name, + poc.Bed); + } + else + { + patient.Location = new PatientLocation( + VirtualPointOfCare.Unknown.ToString(), + VirtualPointOfCare.Unknown.ToString()); + } + } + + _logger.LogDebug( + "CREATE PATIENT BECAUSE createPatientWithORU CONFIG IS TRUE {patient}", + patient); + await Insert(patient); + } + } + } + else + { + patient = await FindPatient(null, apiRequest.PatientNumber, apiRequest.Location, true); + + if (!string.IsNullOrWhiteSpace(apiRequest.PatientNumber) || + (Enum.TryParse(apiRequest.Location?.Bed, out var dataPoC) && + dataPoC == VirtualPointOfCare.UnitData)) + { + if (patient == null) + { + if (_createPatientWithLocation) + { + _logger.LogDebug( + "PATIENT LOCATION NOT IS EMPTY. Generate patient because createPatientWithLocation is TRUE"); + + patient = new Patient + { + Id = ObjectId.GenerateNewId() + }; + + await UpdatePatientFromRequest(patient, apiRequest); + + _logger.LogDebug( + "CREATE PATIENT BECAUSE CreatePatientWithLocation CONFIG IS TRUE {patient}", + patient); + await Insert(patient); + } + } + else + { + if (_pushPatientWithOru && apiRequest.PatientNumber != patient.PatientNumber) + { + var admTime = patient.AdmTime; + var msgTime = apiRequest.MessageTime; + //Si la fecha de admisión es anterior < a la fecha del mensaje (con un patient number distinto) + // estamos recibiendo orus para un paciente que no tenemos aun insertado en esa localización entonces lo creamos desplazando al que está actualmente + if (admTime < msgTime) + { + _logger.LogDebug( + "PATIENT LOCATION IS NOT EMPTY AND PATIENT NUMBER IS NOT THE SAME. Generate patient because CreatePatientWithORU is TRUE AND ADMTIME IS OLDER THAN MSGTIME AND PUSH OLD PATIENT {patient}", + patient); + var unit = await _unitService.FindById(patient.UnitId); + await UpdateLocation(patient.Id, + new PatientLocation(unit?.Name, + VirtualPointOfCare.Pushed.ToString())); + + patient = new Patient + { + Id = ObjectId.GenerateNewId() + }; + + await UpdatePatientFromRequest(patient, apiRequest); + + _logger.LogDebug( + "CREATE PATIENT BECAUSE CreatePatientWithORU CONFIG IS TRUE {patient}", + patient); + await Insert(patient); + } + } + } + } + } + } + + else + { + // PATIENT FOUND BY PATIENT NUMBER + _logger.LogDebug("PATIENT FOUND BY PATIENT NUMBER"); + + + if (apiRequest.Location is { Bed: not null } && !apiRequest.Location.Equals(patient.Location)) + { + //Al cambiar de localizacion set active nueva localizacion set innactive old localizacion + + // PATIENT LOCATION HAS CHANGED + _logger.LogDebug("PATIENT LOCATION HAS CHANGED"); + if (_updatePatientLocationWithOru) + { + // UPDATE PATIENT LOCATION BECAUSE updatePatientLocationWithORU CONFIG IS TRUE + _logger.LogDebug( + "UPDATE PATIENT LOCATION BECAUSE updatePatientLocationWithORU CONFIG IS TRUE"); + + patient.Location = apiRequest.Location; + var patientExistsInLocation = await FindByLocation(apiRequest.Location); + if (patientExistsInLocation != null && + patient.PatientNumber != patientExistsInLocation.PatientNumber) + { + var unit = await _unitService.FindById(patientExistsInLocation.UnitId); + var newLocation = new PatientLocation(unit?.Name, + VirtualPointOfCare.Unknown.ToString()); + await UpdateLocation(patientExistsInLocation.Id, newLocation); + } + + // ONLY ADTS archive if (patientExistsInLocation != null) patientService.ArchivePatient(patientExistsInLocation); + await ProcessUpdatePatientLocationWithOru(patient, apiRequest); + } + } + } + + if (_updatePatientDataWithOru && patient != null) + { + if (apiRequest.AttendingDoctor != null && + !apiRequest.AttendingDoctor.Equals(patient.AttendingDoctor)) + { + // PATIENT DOCTOR CHANGED + _logger.LogDebug("PATIENT FOUND BY LOCATION"); + patient.AttendingDoctor = apiRequest.AttendingDoctor; + await UpdateAttendingDoctor(patient.Id, apiRequest.AttendingDoctor); + } + + if (apiRequest.Patient != null && !apiRequest.Patient.Equals(patient.Person)) + { + // PATIENT DATA CHANGED + _logger.LogDebug("PATIENT DATA CHANGED"); + + if (apiRequest.Patient?.BirthDate != null) + { + patient.Person ??= new Person(); + patient.Person.BirthDate = apiRequest.Patient.BirthDate; + } + + if (apiRequest.Patient?.Language != null) + { + patient.Person ??= new Person(); + patient.Person.Language = apiRequest.Patient.Language; + } + + if (apiRequest.Patient?.FirstName != null) + { + patient.Person ??= new Person(); + patient.Person.FirstName = apiRequest.Patient.FirstName; + } + + if (apiRequest.Patient?.SecondName != null) + { + patient.Person ??= new Person(); + patient.Person.SecondName = apiRequest.Patient.SecondName; + } + + if (apiRequest.Patient?.LastName != null) + { + patient.Person ??= new Person(); + patient.Person.LastName = apiRequest.Patient.LastName; + } + + if (apiRequest.Patient?.Ids is { Count: > 0 }) + { + patient.Person ??= new Person(); + patient.Person.SetIds(apiRequest.Patient.Ids, apiRequest.MessageTime); + } + + //bool updatePatientNumber = false; + //patient.patient = admRequest.patient; + //Solo actualizamos patientNumber de paciente si es de ICCA el mensaje. En central puede haber patient numbers incorrectos. + if (apiRequest.PatientNumber is { Length: > 0 } && + !string.IsNullOrWhiteSpace(apiRequest.Facility) && + apiRequest.Facility.Equals(_iccaFacility)) + patient.PatientNumber = apiRequest.PatientNumber; + + //UpdatePatientData(patient.id, patient.patientNumber, patient.patient); + if (patient.PatientNumber != null) + await UpdatePatientData(patient.Id, patient.PatientNumber, patient); + } + } + + break; + } + } + catch (Exception ex) + { + _logger.LogError("ERROR SAVING REQUEST: Exception: {exMessage} ", ex.Message); + return null; + } + + return patient; + } + + public async Task> FindAll(bool withLocation = false) + { + var patients = await _patientRepository.FindAll(); + + if (!withLocation) return patients; + + foreach (var patient in patients) + { + if (patient.PointOfCareId.HasValue) + { + var poc = await _pointOfCareService.FindById(patient.PointOfCareId.Value); + patient.Bed = poc?.Bed; + patient.Room = poc?.Room; + } + + if (patient.UnitId.HasValue) + { + var unit = await _unitService.FindById(patient.UnitId); + patient.UnitString = unit?.Name; + } + } + + return patients; + } + + + public async Task> FindInActivePoC() + { + return await _patientRepository.FindInActivePoC(); + } + + public async Task> FindInInactivePoC() + { + return await _patientRepository.FindInInactivePoC(); + } + + + public async Task> GetPaginatedPatients(PaginationFilter filter) + { + var result = _patientRepository.GetPaginatedPatients(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(dataList, filter.PageNumber, filter.PageSize, count); + } + + public async Task FindByUnitAndPocId(ObjectId unit, ObjectId pointOfCare) + { + return await _patientRepository.FindByUnitAndPocId(unit, pointOfCare); + } + + public async Task> FindByPointOfCare(string pointOfCare) + { + return await _patientRepository.FindByPointOfCare(pointOfCare); + } + + public async Task MergePatient(Patient patient, string oldPatientNumber) + { + var oldLocation = patient.PointOfCareId; // sectionService.FindByPatient(patient.id); + var oldPatient = await FindByPatientNumber(oldPatientNumber); + + if (oldPatient == null) + { + _logger.LogDebug( + "the patient has not been merged: {oldPatientNumber} in patient {patientpatientId} , old patient not found ", + oldPatientNumber, patient.PatientId); + return; + } + + var oldId = oldPatient.Id; + + + _logger.LogDebug("merge patient action: {oldPatientNumber} in {patientpatientId}", oldPatientNumber, + patient.PatientId); + //TODO: TO CHECK + _ = _observationService.Value.UpdateManyObjectId("patientid", patient.Id, oldId); + _ = _treatmentService.Value.UpdateManyObjectId("patientid", patient.Id, oldId); + _ = _diagnosisService.UpdateManyObjectId("patientid", patient.Id, oldId); + _ = _appointmentService.UpdateManyObjectId("patientid", patient.Id, oldId); + _ = _pumpService.Value.UpdateManyObjectId("patientid", patient.Id, oldId); + _ = _recordingAlertService.UpdateManyObjectId("patientid", patient.Id, oldId); + _ = _patientCarePlanService.UpdateManyObjectId("patientId", patient.Id, oldId); + _ = _patientRepository.Delete(oldId); + + if (oldLocation.HasValue) + { + await UpdatePatientFromMerge(patient, oldPatient); + await SendDeletePatientBroadcast(patient.Id, oldLocation.Value); + _pointOfCareService.CheckNextAdmission(oldPatient.PointOfCareId); + } + } + + public async Task DischargeInactivePatients(DateTime sinceDate, int hoursBeforeArchive) + { + try + { + GlobalData.AddData("isCheckingInactivePatients", true); + await ArchiveDischargedPatients(hoursBeforeArchive); + await ArchiveNotUpdatedPatientsInInactivePoCSince(hoursBeforeArchive); + await ArchivePatientWithoutObservationsSinceDate(sinceDate); + } + catch (Exception ex) + { + _logger.LogError("Error discharging inactive patients, error: {exMessage}, trace: {exStackTrace}", + ex.Message, ex.StackTrace); + } + + GlobalData.AddData("isCheckingInactivePatients", false); + } + + public async Task UpdateOne(Patient updatedPatient) + { + var oldPatient = await _patientRepository.FindById(updatedPatient.Id); + var patient = await _patientRepository.UpdateOne(updatedPatient); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, patient); + return patient; + } + + public async Task UpdatePatientAltable(ObjectId patientId, OptionList altable, User? user) + { + var patient = await _patientRepository.FindByPatientId(patientId); + var oldPatient = await _auditService.DeepCopyAsync(patient); + if (patient == null) + { + _logger.LogError("Patient id: {id} not found", patientId); + return null; + } + + var aux = patient.Altable; + patient.Altable = altable; + await _patientRepository.Update(patient); + if (oldPatient != null) + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, patient); + await SendPatientBroadcast(patient); + await GenerateNurseObsAndInsert(_listSettings.AltableOptionList.ManualObservationName!, + [altable], patient.Id, user); + //si son iguales no actualiza + if (altable.Name.Equals(aux?.Name)) + return patient; + + if (!patient.PointOfCareId.HasValue) + return patient; + + var poc = await _pointOfCareService.FindById(patient.PointOfCareId.Value); + + //Si antes era NotAltable y cambia se crea un registro en discharge + if (aux?.OptionType is null or "NotAltable") + { + var discharge = await _dischargeService.GetDischargeByPatientId(patient.Id); + var unit = await _unitService.FindById(patient.UnitId); + if (discharge == null) + { + var dischargeToInsert = new Discharge + { + DischargeDate = DateTime.UtcNow, + Patient = patient, + PatientId = patientId, + PatientLocation = new PatientLocation(unit?.Name, poc?.Bed, poc?.Room), + PointOfCareId = poc?.Id, + UnitId = poc?.UnitId + }; + + await _dischargeService.InsertDischarge(dischargeToInsert); + } + else + { + discharge.PatientId = patientId; + discharge.Patient = patient; + discharge.PatientLocation = new PatientLocation(unit?.Name, poc?.Bed, poc?.Room); + discharge.PointOfCareId = poc?.Id; + discharge.UnitId = poc?.UnitId; + await _dischargeService.UpdateDischargeAsync(discharge); + } + } + + //Si pasa a ser NotAltable borramos registro en discharge + if ("NotAltable".Equals(altable.OptionType)) + { + var discharge = await _dischargeService.GetDischargeByPatientId(patient.Id); + if (discharge != null) + await _dischargeService.DeleteDischargeAsync(discharge); + } + + return patient; + } + + /// + /// Delete discharge and archive patient by id. + /// + /// + /// + /// + public async Task ExitPatientById(ObjectId id, bool archivePatient = true) + { + try + { + var patient = await FindById(id); + if (patient == null) + { + _logger.LogError("Patient not found by id: {id}", id); + return; + } + + var discharge = await _dischargeService.GetDischargeByPatientId(patient.Id); + if (discharge != null) + { + await _dischargeService.DeleteDischargeAsync(discharge); + _logger.LogInformation("Discharge deleted. {discharge}", discharge); + } + + if (archivePatient) + await ArchivePatient(patient); + _logger.LogInformation("ExitPatientById: {id}", id); + _pointOfCareService.CheckNextAdmission(patient.PointOfCareId); + } + catch (Exception ex) + { + _logger.LogError("Exception exiting patient. Exception:{ex}", ex); + throw; + } + } + + public async Task UpdatePatientMasterList(ObjectId patientId, MasterListType typeName, + List updatedOptions, User? user, List? carePlanLog) + { + try + { + var patient = await FindById(patientId); + if (patient == null) + { + _logger.LogError("Error updating Patient Master List. Patient not found by Id:{id} ", patientId); + return null; + } + + switch (typeName) + { + case MasterListType.TreatmentList: + await GenerateNurseCarePlanAndInsert(MasterListType.TreatmentList, carePlanLog, patient, user); + patient.Treatment = updatedOptions; + break; + case MasterListType.AllergyList: + patient.Allergies = updatedOptions; + await GenerateNurseObsAndInsert(_listSettings.AllergyList.ManualObservationName!, + updatedOptions, patient.Id, user); + break; + case MasterListType.ProcedureList: + await GenerateNurseCarePlanAndInsert(MasterListType.ProcedureList, carePlanLog, patient, user); + patient.Procedures = updatedOptions; + break; + case MasterListType.TestList: + await GenerateNurseCarePlanAndInsert(MasterListType.TestList, carePlanLog, patient, user); + patient.Tests = updatedOptions; + break; + case MasterListType.DoctorList: + patient.Doctors = updatedOptions; + await GenerateNurseObsAndInsert(_listSettings.DoctorList.ManualObservationName!, + updatedOptions, patient.Id, user); + break; + case MasterListType.LanguageBarrierList: + patient.LanguageBarrier = updatedOptions; + await GenerateNurseObsAndInsert(_listSettings.LanguageBarrierList.ManualObservationName!, + updatedOptions, patient.Id, user); + break; + case MasterListType.MobilityOptionList: + patient.Mobility = updatedOptions.FirstOrDefault(); + await GenerateNurseObsAndInsert(_listSettings.MobilityOptionList.ManualObservationName!, + updatedOptions, patient.Id, user); + break; + case MasterListType.OriginList: + patient.Origin = updatedOptions.FirstOrDefault(); + await GenerateNurseObsAndInsert(_listSettings.OriginList.ManualObservationName!, + updatedOptions, patient.Id, user); + break; + case MasterListType.PatientStatusList: + patient.PatientStatus = updatedOptions.FirstOrDefault(); + await GenerateNurseObsAndInsert(_listSettings.PatientStatusList.ManualObservationName!, + updatedOptions, patient.Id, user); + break; + case MasterListType.TherapeuticCeilingList: + patient.TherapeuticCeiling = updatedOptions.FirstOrDefault(); + await GenerateNurseObsAndInsert(_listSettings.TherapeuticCeilingList.ManualObservationName!, + updatedOptions, patient.Id, user); + break; + case MasterListType.VisitOptionList: + patient.Visits = updatedOptions.FirstOrDefault(); + await GenerateNurseObsAndInsert(_listSettings.VisitOptionList.ManualObservationName!, + updatedOptions, patient.Id, user); + break; + case MasterListType.AccessControlList: + patient.AccessControl = updatedOptions.FirstOrDefault(); + await GenerateNurseObsAndInsert(_listSettings.AccessControlList.ManualObservationName!, + updatedOptions, patient.Id, user); + break; + case MasterListType.AltableOptionList: + patient.Altable = updatedOptions.FirstOrDefault(); + await GenerateNurseObsAndInsert(_listSettings.AltableOptionList.ManualObservationName!, + updatedOptions, patient.Id, user); + break; + case MasterListType.DiagnosisList: + patient.Diagnosis = updatedOptions.FirstOrDefault(); + await GenerateNurseObsAndInsert(_listSettings.DiagnosisList.ManualObservationName!, + updatedOptions, patient.Id, user); + break; + case MasterListType.DischargeStatusList: + patient.DischargeStatus = updatedOptions.FirstOrDefault(); + await GenerateNurseObsAndInsert(_listSettings.DischargeStatusList.ManualObservationName!, + updatedOptions, patient.Id, user); + break; + case MasterListType.InsulationList: + patient.Insulation = updatedOptions.FirstOrDefault(); + await GenerateNurseObsAndInsert(_listSettings.InsulationList.ManualObservationName!, + updatedOptions, patient.Id, user); + break; + case MasterListType.PassiveSittingList: + patient.PassiveSitting = updatedOptions.FirstOrDefault(); + await GenerateNurseObsAndInsert(_listSettings.PassiveSittingList.ManualObservationName!, + updatedOptions, patient.Id, user); + break; + case MasterListType.GenericList: + await GenerateNurseObsAndInsert(_listSettings.GenericList.ManualObservationName!, + updatedOptions, patient.Id, user); + break; + default: + await GenerateNurseObsAndInsert(updatedOptions.FirstOrDefault()?.Name ?? "UNKNOWNOBS", + updatedOptions, patient.Id, user); + break; + } + + await Update(patient); + + return patient; + } + catch (Exception ex) + { + _logger.LogError("Exception updating patient id:{id}. Data: {type} . Exception: {ex}", patientId, + typeName, ex); + return null; + } + } + + public async Task GenerateNurseCarePlanAndInsert(MasterListType carePlanType, List? options, + Patient patient, User? user) + { + OptionList? addedItems = null; + OptionList? removedItems = null; + OptionList? updatedItems = null; + OptionList? option = null; + switch (carePlanType) + { + case MasterListType.ProcedureList: + var patientProcedures = patient.Procedures ?? []; + + // Encuentra elementos agregados: están en optionsList pero no en patientProcedures + addedItems = options?.FirstOrDefault(opt => patientProcedures.All(proc => proc.Name != opt.Name)); + if (addedItems != null) + { + option = addedItems; + break; + } + + // Encuentra elementos actualizados: están en ambas listas pero tienen diferencias en sus propiedades + updatedItems = options? + .FirstOrDefault(opt => + patientProcedures.Any(proc => + proc.Name == opt.Name && !proc.AreEqualExcludeDescription(proc, opt))); + if (updatedItems != null) + { + option = updatedItems; + option.OptionType = "procedure"; + } + + // Encuentra elementos eliminados: están en patientProcedures y en optionsList pero tienen una descripción + if (options?.FirstOrDefault()?.Description != null) + { + removedItems = options.FirstOrDefault(); + if (removedItems != null) removedItems.OptionType = "procedure"; + } + + break; + case MasterListType.TestList: + var patientTests = patient.Tests ?? []; + + // Encuentra elementos agregados: están en optionsList pero no en patientProcedures + addedItems = options?.FirstOrDefault(opt => patientTests.All(test => test.Name != opt.Name)); + if (addedItems != null) + { + option = addedItems; + break; + } + + // Encuentra elementos actualizados: están en ambas listas pero tienen diferencias en sus propiedades + updatedItems = options? + .FirstOrDefault(opt => + patientTests.Any(test => test.Name == opt.Name && !test.AreEqualExcludeDescription(test, opt))); + if (updatedItems != null) + { + option = updatedItems; + option.OptionType = "test"; + } + + // Encuentra elementos eliminados: están en patientProcedures y en optionsList pero tienen una descripción + if (options?.FirstOrDefault()?.Description != null) + { + removedItems = options.FirstOrDefault(); + if (removedItems != null) removedItems.OptionType = "test"; + } + + break; + case MasterListType.TreatmentList: + var patientTreatment = patient.Treatment ?? []; + + // Encuentra elementos agregados: están en optionsList pero no en patientProcedures + addedItems = options?.FirstOrDefault(opt => patientTreatment.All(proc => proc.Name != opt.Name)); + if (addedItems != null) + { + option = addedItems; + option.OptionType = "treatment"; + break; + } + + // Encuentra elementos actualizados: están en ambas listas pero tienen diferencias en sus propiedades + updatedItems = options? + .FirstOrDefault(opt => + patientTreatment.Any(proc => + proc.Name == opt.Name && !proc.AreEqualExcludeDescription(proc, opt))); + if (updatedItems != null) + { + option = updatedItems; + option.OptionType = "treatment"; + } + + // Encuentra elementos eliminados: están en patientProcedures y en optionsList pero tienen una descripción + if (options?.FirstOrDefault()?.Description != null) + { + removedItems = options.FirstOrDefault(); + if (removedItems != null) removedItems.OptionType = "treatment"; + } + + break; + } + + ActionsEnum.CrudAction? action = addedItems != null ? ActionsEnum.CrudAction.Create : + updatedItems != null ? ActionsEnum.CrudAction.Update : + removedItems?.Description is "Archive" ? ActionsEnum.CrudAction.Archive : ActionsEnum.CrudAction.Delete; + var patietCarePlan = new PatientCarePlan + { + PatientId = patient.Id, + PatientNumber = patient.PatientNumber, + PointOfCareId = patient.PointOfCareId, + Action = action, + CarePlan = + action is ActionsEnum.CrudAction.Delete or ActionsEnum.CrudAction.Archive ? removedItems : option, + UserId = user?.Id, + Description = action == ActionsEnum.CrudAction.Delete && string.IsNullOrEmpty(removedItems?.Description) + ? "No reason given" + : removedItems?.Description + }; + await _patientCarePlanService.InsertOneAsync(patietCarePlan); + if (action != ActionsEnum.CrudAction.Delete && action != ActionsEnum.CrudAction.Archive && removedItems != null) + { + var secondaryAction = removedItems.Description is "Archive" + ? ActionsEnum.CrudAction.Archive + : ActionsEnum.CrudAction.Delete; + var patietCarePlanSecondaryAction = new PatientCarePlan + { + PatientId = patient.Id, + PatientNumber = patient.PatientNumber, + PointOfCareId = patient.PointOfCareId, + Action = secondaryAction, + CarePlan = removedItems, + UserId = user?.Id, + Description = + secondaryAction == ActionsEnum.CrudAction.Delete && string.IsNullOrEmpty(option?.Description) + ? "No reason given" + : option?.Description + }; + await _patientCarePlanService.InsertOneAsync(patietCarePlanSecondaryAction); + } + } + + public async Task UpdatePatientIncomingData(ObjectId patientId, Patient person) + { + var oldPatient = await _patientRepository.FindByPatientId(patientId) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + var pat = await _patientRepository.UpdatePatientIncomingData(patientId, person) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, pat); + + await SendPatientBroadcast(pat); + } + + public async Task UpdatePatientIncomingData(ObjectId patientId, Patient person, + PatientIncomeData patientIncomeData, User user) + { + await UpdatePatientIncomingData(patientId, person); + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + Name = "PatientIncomingData", + Value = patientIncomeData, + PatientId = patientId, + Time = DateTime.Now, + UserId = user.Id + }; + await _observationService.Value.InsertNurseObservation(obs); + } + + public async Task UpdatePatientMasterListItemChange(UpdateOptionMasterListDto opt, IEnumerable unitList, + string typeName) + { + // Filtrar paciente por unidad y valor del item de la lista + var unitIds = unitList.Select(x => x.Id).ToList(); + var oldPatientList = await _patientRepository.GetPatientsByUnitIds(unitIds, typeName); + var patientUpdatedList = await _patientRepository.UpdateMasterListOption(unitIds, opt, typeName); + foreach (var patient in patientUpdatedList) + { + var oldPatient = oldPatientList.Find(p => p.Id == patient.Id); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, patient); + await SendPatientBroadcast(patient); + } + } + + public async Task DeletePatientMasterListItem(OptionList opt, IEnumerable unitList, string typeName) + { + var unitIds = unitList.Select(x => x.Id).ToList(); + var patientUpdatedList = await _patientRepository.DeleteMasterListOption(unitIds, opt, typeName); + + foreach (var patient in patientUpdatedList) + { + var patientUpdated = await FindById(patient.Id); + if (patientUpdated == null) continue; + await SendPatientBroadcast(patientUpdated); + var obsName = GetManualObservationName(typeName); + var obs = await _observationService.Value.FindLastObservationsByField(patientUpdated.Id, + new List { new() { Name = obsName } }); + if (obs.FirstOrDefault() != null) + { + var newObs = obs.First().Value as List; + newObs?.RemoveAll(c => c.Id == opt.Id); + await GenerateNurseObsAndInsert(obsName, newObs, patient.Id, null); + } + } + } + + public async Task UpdatePatientDemographicData(ObjectId patientId, Patient person, User? user) + { + var oldPatient = await _patientRepository.FindByPatientId(patientId) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + var pat = await _patientRepository.UpdatePatientDemographicData(patientId, person); + if (pat != null) + { + await UpdatePatientMasterList(pat.Id, MasterListType.AllergyList, pat.Allergies??[], user, null); + var lbl = pat.LanguageBarrier != null ? pat.LanguageBarrier : null; + var dl = pat.Diagnosis != null ? new List { pat.Diagnosis } : null; + await UpdatePatientMasterList(pat.Id, MasterListType.LanguageBarrierList, lbl??[], user, null); + await UpdatePatientMasterList(pat.Id, MasterListType.DiagnosisList, dl??[], user, null); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, pat); + await SendPatientBroadcast(pat); + } + } + + public Task SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId) + { + return _patientRepository.SearchByPatientNumberAndDistinctUnit(patientNumber, unitId); + } + + public async Task> FindAllPatientWithFinishedProcedures(int archiveProcedureEndDateAfterMinutes) + { + return await _patientRepository.FindAllPatientWithFinishedProcedures(archiveProcedureEndDateAfterMinutes); + } + + public async Task> FindAllPatientWithFinishedTest(int archiveTestEndDateAfterMinutes) + { + return await _patientRepository.FindAllPatientWithFinishedTests(archiveTestEndDateAfterMinutes); + } + + public async Task> FindAllPatientWithFinishedTreatment(int archiveTreatmentEndDateAfterMinutes) + { + return await _patientRepository.FindAllPatientWithFinishedTreatment(archiveTreatmentEndDateAfterMinutes); + } + + private bool CheckPatient(Patient patient) + { + //patient.AdmTime ??= DateTime.UtcNow; + _logger.LogDebug("Inserting patient with patientNumber: {PatientpatientNumber} {Patient}", + patient.PatientNumber, patient); + + if (patient.PointOfCareId == null && patient.UnitId == null) + { + _logger.LogWarning( + "Not Inserted patient with patientNumber: {PatientpatientNumber}. PointOfCare is null {Patient}", + patient.PatientNumber, patient); + return false; + } + + if (string.IsNullOrEmpty(patient.PatientNumber)) + { + _logger.LogWarning("person with patientNumber null or empty: {Patient}", patient); + if (!_cretatePatientWithoutPatientNumber) + { + _logger.LogWarning( + "CretatePatientWithoutPatientNumber: {CretatePatientWithoutPatientNumber}: person not inserted: {Patient} ", + _cretatePatientWithoutPatientNumber, patient.Id); + return false; + } + } + + return true; + } + + private async Task SendLastGroupedObsToSubscriberByPoc(ObjectId newPoc, ObjectId? oldPoc, + Patient patient) + { + // Enviar y rellenar los subscriptores que necesitan las observaciones agrupadas para este paciente + // Obtén la lista de suscriptores + var relevantSubscribers = _subscribersService.GetByPocId(newPoc); + // Filtra y selecciona los elementos con DisplayId único + var filteredSubscribers = relevantSubscribers + .GroupBy(subscriber => subscriber.DisplayId) // Agrupa por DisplayId + .Where(group => group.Key != null) // Asegurar de que DisplayId no sea null + .Select(group => group.First()) // Selecciona el primer elemento de cada grupo + .ToList(); // Convierte el resultado a una lista + foreach (var sub in filteredSubscribers) + if (sub.DisplayId.HasValue) + { + var display = await _displayService.GetById(sub.DisplayId.Value); + if (display is { DisplayConfigId: not null }) + { + var config = await _displayConfigService.GetById(display.DisplayConfigId.Value); + var configGroupField = config.GroupedFieldList; + if (configGroupField != null) + foreach (var groupedField in configGroupField) + { + var groupedObservation = await _groupedObservationService.GenerateGroupedObservation( + patient.Id, + groupedField); + _ = _clientMessageService.SendAsync(sub.Id, OperationType.GroupedObservation, + groupedObservation); + + _subscriberGroupedService.CheckOnSubscriptionGroup(groupedField, patient.Id, + "Romance Standard Time", + sub.Id, groupedObservation); + } + } + } + + // En este punto los subscriptores que necesitan las obs agrupadas para este paciente ya las tienen + // Y la lista de subscriptorGrouped ya tiene la id del nuevo subscriptor + // Ahora habria que ver que subscriptores ya no necesitan las agrupadas para este paciente + if (oldPoc.HasValue) + { + var relevantSubscribersToDelete = _subscribersService.GetByPocId(oldPoc.Value); + var relevantSubscribersToDeleteFiltered = + relevantSubscribersToDelete.Where(sub => !sub.LocationIds.Contains(newPoc)); + foreach (var wsSubscriber in relevantSubscribersToDeleteFiltered) + _subscriberGroupedService.RemoveWsSubscriberPatientIdAndWsId(patient.Id.ToString(), wsSubscriber.Id); + } + } + + private async Task SendLastObsToSubscriberByPoc(PointOfCare newPoc, Patient patient) + { + // Obtén la lista de suscriptores + var relevantSubscribers = _subscribersService.GetByPocId(newPoc.Id); + + // Filtra y selecciona los elementos con DisplayId único + // var filteredSubscribers = relevantSubscribers + // .GroupBy(subscriber => subscriber.DisplayId) // Agrupa por DisplayId + // .Where(group => group.Key != null) // Asegúrate de que DisplayId no sea null + // .Select(group => group.First()) // Selecciona el primer elemento de cada grupo + // .ToList(); // Convierte el resultado a una lista + foreach (var sub in relevantSubscribers) + if (sub.DisplayId.HasValue) + { + var display = await _displayService.GetById(sub.DisplayId.Value); + if (display is { DisplayConfigId: not null }) + { + var config = await _displayConfigService.GetById(display.DisplayConfigId.Value); + + var observations = + await _observationService.Value.FindLastObservationsByField(patient.Id, config.FieldList); + if (observations.Count > 0) + { + observations = observations.OrderBy(p => p.Time).ToList(); + foreach (var observation in observations) + { + var obs = await _observationService.Value.MapObservationsByName(observation); + if (obs != null) + _ = _clientMessageService.SendAsync(sub.Id, OperationType.Observation, obs); + } + } + } + } + } + + private async Task UpdateLocation(ObjectId id, ObjectId unitId, ObjectId pocId) + { + try + { + _logger.LogDebug("Update location id: {id}", id); + + var patient = await _patientRepository.FindById(id); + var oldPatient = await _auditService.DeepCopyAsync(patient); + if (patient is not { PointOfCareId: not null }) + { + _logger.LogError("person is null. Not updated location for patient id: {id} ", id); + return; + } + + var newPoc = await _pointOfCareService.FindById(pocId); + var newUnit = await _unitService.FindById(unitId); + // El valor de bed: 04 daria como resultado NO_BED por eso comprobamos que no sea un int !int.TryParse(newPoc.Bed, out _) + var isMoveToVirtualPoc = newPoc != null && + !int.TryParse(newPoc.Bed, out _) && + Enum.TryParse(newPoc.Bed, out _); + var oldLocation = await _pointOfCareService.FindById(patient.PointOfCareId.Value); + var virtualPocOldLocation = VirtualPointOfCare.Unknown; + var comesFromVirtualPoc = oldLocation != null && + !int.TryParse(oldLocation.Bed, out _) && + Enum.TryParse(oldLocation.Bed, out virtualPocOldLocation); + if (comesFromVirtualPoc) + { + var isRecovered = VirtualPointOfCare.Deleted.Equals(virtualPocOldLocation); + // Añadir log warning + if (isRecovered) + { + _logger.LogWarning( + "Patient is recovered on UpdateLocation patientId: {id}, oldLocation: {oldLocation}, newLocation: {newPoc}", + patient.Id, oldLocation?.Location.ToString(), newPoc?.Location.ToString()); + patient.DisTime = null; + } + } + + + patient.PointOfCareId = pocId; + patient.UnitId = unitId; + var bed = newPoc?.Bed; + var room = newPoc?.Room; + var location = new PatientLocation(newUnit?.Name, bed, room); + patient.Location = location; + + if (isMoveToVirtualPoc && !comesFromVirtualPoc) + patient.Altable = new OptionList + { + IconDefault = "icNotAltable", + OptionType = "NotAltable", + Name = "NotAltable" + }; + + await _patientRepository.UpdateOneAsync(patient.Id, patient); + if (oldPatient != null) + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, patient); + if (isMoveToVirtualPoc && !comesFromVirtualPoc) + { + await ExitPatientById(patient.Id, false); + _pointOfCareService.CheckNextAdmission(oldLocation?.Id); + } + + if (!isMoveToVirtualPoc && comesFromVirtualPoc) + await _pointOfCareService.SetPointOfCareStatus(pocId, StatusEnum.PointOfCare.InUse); + + //Prio location to advertise both locations of movement between sections + _logger.LogDebug( + "Move patient: {id} from oldPocId: {oldPocId}, to newPocId: {newPocId}", + id, oldLocation != null ? oldLocation.Id : "NULL", pocId); + + if (!isMoveToVirtualPoc && !comesFromVirtualPoc && oldLocation != null) + { + await _pointOfCareService.SetPointOfCareStatus(pocId, StatusEnum.PointOfCare.Available); + await Move(patient, pocId, oldLocation.Id); + } + else + { + _logger.LogDebug( + "Patient: {id} not moved because isMoveToVirtualPoc: {isMoveToVirtualPoc}, comesFromVirtualPoc: {comesFromVirtualPoc}, oldLocationIsNotNull: {oldLocation}", + id, isMoveToVirtualPoc, comesFromVirtualPoc, oldLocation != null); + } + //TODO: Revisar + // CheckLocationForWsSubscriber(location, id); + } + catch (Exception ex) + { + _logger.LogError("error updating location for patient id: {id} exception: {exMessage}", id, ex.Message); + } + } + + /// + /// If location is updated and pointOfCare are DELETED or PUSHED + /// Ws subscriber for GropedObs dont need to keep notificated + /// Else could be that patient from Section A move to Section B + /// clients from the section that is moved dont need to keep notified + /// and other client will ask for grouped obs once receive SendPatientLocationBroadcast + /// + /// New location where patient is getting updated + /// Object Id for patient + private void CheckLocationForWsSubscriber(PatientLocation? newLocation, ObjectId? patientId) + { + if (patientId == null || newLocation == null) return; + try + { + if (newLocation.Bed == VirtualPointOfCare.Pushed.ToString() || + newLocation.Bed == VirtualPointOfCare.Deleted.ToString() || + newLocation.Bed == VirtualPointOfCare.Moved.ToString()) + _subscriberGroupedService.RemoveGroupedObsByPatientId(patientId.Value.ToString()); + else + _subscriberGroupedService.RemoveWsSubscriberByLocation(patientId.Value.ToString(), newLocation); + } + catch (Exception ex) + { + Log.Error( + $"ERROR DELETING PATIENT MOVEMENT. person Id: {patientId} New location: {newLocation.ToStr()}. Exception message: {ex.Message} ex: {ex}"); + } + } + + /// method ArchiveNotUpdatedPatientsSince archive all patients who have not updated since in + /// hoursBeforeArchive + /// . + /// + public async Task ArchiveNotUpdatedPatientsInInactivePoCSince(int hoursBeforeArchive) + { + _logger.LogDebug( + "Archiving not updated and in inactive PoC patients since older than {hoursBeforeArchive} hours", + hoursBeforeArchive); + try + { + // Patients not updated since date + var dischargedPatients = + await _patientRepository.FindPatientsNotUpdatedSince(DateTime.Now.AddHours(-hoursBeforeArchive)); + foreach (var patient in dischargedPatients.Where(p => !p.IsInActivePoC())) await ArchivePatient(patient); + } + catch (Exception ex) + { + _logger.LogError("Error archiving not updated and in inactive PoC patients {exMessage}", ex); + } + } + + public Task SendPatientLocationBroadcast(object patient, PatientLocation location, + List? pocs) + { + if (pocs == null || pocs.Count == 0) return Task.CompletedTask; + + var subscribers = _subscribersService.GetSubscribers().Where + (s => + s.LocationIds.Any(pocs.Contains) + ).ToList(); + subscribers.ForEach(Action); + return Task.CompletedTask; + + void Action(WsSubscriber subscriber) + { + _clientMessageService.SendAsync(subscriber.Id, + OperationType.PatientLocation, new Dictionary + { + { "patient", patient }, + { "location", location } + }); + } + } + + private async Task SendPatientAttendingDoctorBroadcast(ObjectId patientid, Person attendingDoctor) + { + var patient = await FindById(patientid); + if (patient == null) return; + + var poc = await _pointOfCareService.FindPoCByPatientId(patient.Id); + if (poc == null) return; + + var subscribers = _subscribersService.GetSubscribers().Where(s => s.LocationIds.Any(sb => sb == poc.Id)) + .ToList(); + + subscribers.ForEach(Action); + return; + + void Action(WsSubscriber subscriber) + { + _clientMessageService.SendAsync(subscriber.Id, + OperationType.PatientAttendingDoctor, new Dictionary + { + { "id", patientid.ToString() }, + { "attendingDoctor", attendingDoctor } + }); + } + } + + private async Task SendPatientDataBroadcast(ObjectId patientid, Person data) + { + var patient = await FindById(patientid); + if (patient == null) return; + + //var section = await _sectionService.FindByPointOfCare(patient.UnitString); + //if (section == null) return; + + var subscribers = _subscribersService.GetSubscribers().Where(s => + s.LocationIds.Any(c => c == patient.PointOfCareId)); + foreach (var subscriber in subscribers) + _ = _clientMessageService.SendAsync(subscriber.Id, OperationType.PatientData, new Dictionary + { + { "id", patientid.ToString() }, + { "data", data } + }); + } + + private async Task SendPatientBroadcast(Patient patient) + { + var subscribersGroup = + _subscribersService.GetSubscribers() + .Where(s => patient.PointOfCareId != null && + s.LocationIds.Any(c => c == patient.PointOfCareId.Value)).GroupBy(h => h.Locale); + + var unit = await _unitService.FindById(patient.UnitId); + foreach (var group in subscribersGroup) + { + var locale = group.Key ?? LocaleEnum.Default; + IEnumerable subscribers = group; + + foreach (var subscriber in subscribers) + { + var patientWithLocale = await _masterListServiceFactory.GetPatientTraslated(unit, locale, patient); + _ = _clientMessageService.SendAsync(subscriber.Id, OperationType.UpdatePatient, patientWithLocale); + } + } + } + + + private Task SendDeletePatientBroadcast(ObjectId patientid, ObjectId pocId) + { + var subscribers = _subscribersService.GetSubscribers().Where(s => s.LocationIds.Any(x => x == pocId)).ToList(); + _logger.LogDebug("Remove grouped obs for patientId: {patientid} on location: {location}", patientid, + pocId); + _subscriberGroupedService.RemoveGroupedObsByPatientId(patientid.ToString()); + subscribers.ForEach(Action); + return Task.CompletedTask; + + void Action(WsSubscriber subscriber) + { + _clientMessageService.SendAsync(subscriber.Id, + OperationType.DeletePatient, new Dictionary + { + { "id", patientid.ToString() } + }); + } + } + + private void UpdatePatientFromRequestToIdList(Patient patient, ApiRequest apiRequest) + { + if (apiRequest.AdmTime.HasValue) patient.AdmTime = apiRequest.AdmTime; + if (!string.IsNullOrEmpty(apiRequest.PatientId)) patient.PatientId = apiRequest.PatientId; + + if (apiRequest.Patient != null && !apiRequest.Patient.IsEmpty()) + { + // Log action to describe + + patient.Person ??= new Person(); + + patient.Person.BirthDate = apiRequest.Patient.BirthDate; + patient.Person.Language = apiRequest.Patient.Language; + patient.Person.FirstName = apiRequest.Patient.FirstName; + patient.Person.Gender = apiRequest.Patient.Gender; + patient.Person.LastName = apiRequest.Patient.LastName; + patient.Person.SecondName = apiRequest.Patient.SecondName; + } + + if (apiRequest.Patient?.Ids != null) + { + patient.Person ??= new Person(); + patient.Person.SetIds(apiRequest.Patient.Ids, + apiRequest.MessageTime == DateTime.MinValue ? DateTime.UtcNow : apiRequest.MessageTime); + } + + if (apiRequest.DisTime.HasValue) patient.DisTime = apiRequest.DisTime; + + patient.UpdateDate = DateTime.UtcNow; + } + + private async Task RemovePatientAndSendToVirtualPoc(ApiRequest apiRequest, Patient patient, + VirtualPointOfCare virtualPocEnum) + { + var virtualPoc = await _pointOfCareService.FindByBedAndUnitId(virtualPocEnum.ToString(), + patient.UnitId); + if (virtualPoc == null) + _logger.LogError( + "Error getting virtual poc {virtualPocEnum} PATIENT: {patientId} on unit: {patientUnitId} continue with autogenerate pocId", + virtualPocEnum.ToString(), patient.Id, patient.UnitId); + + + var uni = await _unitService.FindById(patient.UnitId); + var oldLocation = patient.PointOfCareId; + patient.DisTime = apiRequest.DisTime ?? DateTime.UtcNow; + patient.PointOfCareId = virtualPoc?.Id ?? new ObjectId(); + patient.UnitId = virtualPoc?.UnitId ?? patient.UnitId; + patient.Location = new PatientLocation(uni?.Name, + virtualPoc?.Bed ?? $"Error on {virtualPocEnum.ToString()}", + virtualPoc?.Room ?? $"Error on {virtualPocEnum.ToString()}"); + + await _patientRepository.UpdateOneAsync(patient.Id, patient); + + _logger.LogDebug("{virtualPocEnum} PATIENT: {patientId} ", virtualPoc?.ToString(), patient.Id); + + await ExitPatientById(patient.Id, false); + + if (virtualPoc != null && oldLocation.HasValue) + _ = SendLastGroupedObsToSubscriberByPoc(virtualPoc.Id, oldLocation.Value, patient); + + _pointOfCareService.CheckNextAdmission(oldLocation); + } + + + // * To ensure db sync with ICCA we receive their patients data table and sync with our patients. + // * GetByCodeSysAndCode all boxes from all sections and match with iccaPatients + + private async Task ProcessIccaSync(ApiRequest apiRequest) + { + //var sections = await _sectionService.GetAll(); + // var boxes = sections.Where(box => box.Items != null).SelectMany(s => s.Items.SelectMany(d => d.Boxes)) + // ?.ToList(); + //var boxes = sections.SelectMany(s => s.items.SelectMany(d => d.boxes)).ToList(); + + var pocList = await _pointOfCareService.GetAllLocationInfo(); + var unitList = await _unitService.GetAll(); + foreach (var pointOfCare in pocList) + pointOfCare.Unit = unitList.FirstOrDefault(u => u.Id == pointOfCare.UnitId); + + var iccaPatientsFromOtherLocations = apiRequest.IccaPatients?.Select(x => x.DeepCopy()).ToList(); + foreach (var poc in pocList) + { + if (poc.Unit == null) + continue; + + var iccaPatientOnBox = apiRequest.IccaPatients?.FirstOrDefault(i => + i.Location.Bed == poc.Bed && i.Location.UnitName == poc.Unit.Name); + + if (iccaPatientOnBox != null) + iccaPatientsFromOtherLocations?.RemoveAll(s => s.PatientNumber == iccaPatientOnBox.PatientNumber); + + //Find actual patient in poc + var actualPatient = await FindByPointOfCareId(poc.Id); + + // 1 Exists in ICCA and not in our DB => create it and if any other patient is on bed move it to Temporal. + if (iccaPatientOnBox != null && actualPatient == null && iccaPatientOnBox.PatientNumber != null) + { + //check if patient exists to only move + var patientInOtherBox = await FindByPatientNumber(iccaPatientOnBox.PatientNumber); + if (patientInOtherBox != null) + await UpdateLocation(patientInOtherBox.Id, + new PatientLocation(poc.Unit.Name, poc.Bed, poc.Room)); + else + await Insert(new Patient + { + AdmTime = iccaPatientOnBox.AdmitTime, + PatientId = iccaPatientOnBox.PatientId, + PatientNumber = iccaPatientOnBox.PatientNumber, + Location = new PatientLocation(poc.Unit.Name, poc.Bed, poc.Room), + UnitId = poc.UnitId, + PointOfCareId = poc.Id + }); + } + // 2 Exists in ICCA and in our DB => equal patientNumber same person, continue else move actual and create new from icca + else if (iccaPatientOnBox != null && actualPatient != null) + { + if (iccaPatientOnBox.PatientNumber == actualPatient.PatientNumber) + { + //They are the same do anything. + } + else + { + await UpdateLocation(actualPatient.Id, + new PatientLocation(poc.Unit.Name, + VirtualPointOfCare.Moved.ToString(), VirtualPointOfCare.Moved.ToString())); + await Insert(new Patient + { + AdmTime = iccaPatientOnBox.AdmitTime, + PatientId = iccaPatientOnBox.PatientId, + PatientNumber = iccaPatientOnBox.PatientNumber, + Location = new PatientLocation(poc.Unit.Name, poc.Bed, poc.Room), + UnitId = poc.UnitId, + PointOfCareId = poc.Id + }); + } + } + // 3 Exists in our DB and not in ICCA => move it to temporal bed to match patients on layout boxes. + else if (iccaPatientOnBox == null && actualPatient != null) + { + //move it to another bed + await UpdateLocation(actualPatient.Id, + new PatientLocation(poc.Unit.Name, VirtualPointOfCare.Unknown.ToString(), + VirtualPointOfCare.Unknown.ToString())); + } + } + + if (iccaPatientsFromOtherLocations == null) + return; + + foreach (var patientOutOfConfigBoxes in iccaPatientsFromOtherLocations) + { + //Patients from others PointsOfCares or box not in our configured sections. we save them + if (patientOutOfConfigBoxes.PatientNumber == null) + continue; + + //check exists + var patientExists = await FindByPatientNumber(patientOutOfConfigBoxes.PatientNumber); + if (patientExists != null) continue; + var patientLocation = await FindByLocation(patientOutOfConfigBoxes.Location); + + //move it to another bed + if (patientLocation != null) + { + var unitName = unitList.FirstOrDefault(u => u.Id == patientLocation.UnitId); + await UpdateLocation(patientLocation.Id, + new PatientLocation(unitName?.Name ?? "UNKNOWN", VirtualPointOfCare.Unknown.ToString(), + VirtualPointOfCare.Unknown.ToString())); + } + + var pocUnknown = pocList.FirstOrDefault(u => u.Bed == VirtualPointOfCare.Unknown.ToString()); + var locationFromRequest = pocList.FirstOrDefault(u => + u.Bed == patientOutOfConfigBoxes.Location.Bed && + u.Unit?.Name == patientOutOfConfigBoxes.Location.UnitName); + await Insert(new Patient + { + AdmTime = patientOutOfConfigBoxes.AdmitTime, + PatientId = patientOutOfConfigBoxes.PatientId, + PatientNumber = patientOutOfConfigBoxes.PatientNumber, + PointOfCareId = locationFromRequest?.Id ?? pocUnknown?.Id ?? ObjectId.GenerateNewId(), + UnitId = locationFromRequest?.UnitId ?? pocUnknown?.UnitId ?? ObjectId.GenerateNewId() + }); + } + } + + /*For adt_a02 || adt_a12 + */ + private async Task ProcessAdtMovePatient(ApiRequest apiRequest, Patient? patient) + { + try + { + var patientExistsInLocation = await FindByLocation(apiRequest.Location); + + if (patient is { PatientNumber: not null } && apiRequest.Patient != null && + patient.PatientNumber == apiRequest.PatientNumber && _updatePatientDataWithAdtA02) + { + _logger.LogDebug( + "Debg MovePatient, UpdatePatientDataWithADT_A02 is enabled, patient number found {pn} and apirequest patient number {pnApirequest} are the same update demographic data", + patient.PatientNumber, apiRequest.PatientNumber); + //await UpdatePatientFromRequest(patient, apiRequest, true); + await UpdatePatientData(patient.Id, patient.PatientNumber, apiRequest.Patient, false); + } + + if (patientExistsInLocation != null) + { + if (patientExistsInLocation.PatientNumber != apiRequest.PatientNumber) + { + _logger.LogDebug( + "Debug MovePatient, patient to move is null but patientExistsInLocation is not null moving patient: {patientExistsInLocation}, to pointOfCare MOVED", + patientExistsInLocation); + var movedLocationPoc = await _pointOfCareService.FindByBedAndUnitId( + VirtualPointOfCare.Moved.ToString(), + patientExistsInLocation.UnitId); + + if (movedLocationPoc != null) + await UpdateLocation(patientExistsInLocation.Id, movedLocationPoc.UnitId, movedLocationPoc.Id); + } + else + { + // Demographic data only + return; + } + } + + if (patient != null) + { + //to solve problems when empty beds with not scapped "" + if (apiRequest.Location != null) + { + var bed = apiRequest.Location.Bed?.Replace("\"", ""); + apiRequest.Location.Bed = string.IsNullOrEmpty(bed) + ? VirtualPointOfCare.Unknown.ToString() + : bed; + + if (apiRequest.Location.Bed == VirtualPointOfCare.Unknown.ToString()) + { + var unknownLocationPoc = await _pointOfCareService.FindByBedAndUnitId( + VirtualPointOfCare.Unknown.ToString(), + patient.UnitId); + if (unknownLocationPoc != null) + await UpdateLocation(patient.Id, unknownLocationPoc.UnitId, unknownLocationPoc.Id); + } + else + { + var mapLocation = await _pocMappingService.Map(apiRequest.Location); + var newLocationUnit = await _unitService.FindByName(apiRequest.Location.UnitName); + var newLocationPoc = await _pointOfCareService.FindByBedAndUnitId(mapLocation?.Bed, + newLocationUnit?.Id); + if (newLocationPoc != null) + { + await UpdateLocation(patient.Id, newLocationPoc.UnitId, newLocationPoc.Id); + } + else + { + var unknownLocationPoc = await _pointOfCareService.FindByBedAndUnitId( + VirtualPointOfCare.Unknown.ToString(), + patient.UnitId); + if (unknownLocationPoc != null) + { + await UpdateLocation(patient.Id, unknownLocationPoc.UnitId, unknownLocationPoc.Id); + _logger.LogError( + "Unable to find new location on ProcessAdtMovePatient location: {Unit} {Bed}", + apiRequest.Location.UnitName, apiRequest.Location?.Bed); + } + } + } + } + else + { + _logger.LogDebug("Move patient to null location transfering patient to UNKNOWN bed"); + var unknownLocationPoc = await _pointOfCareService.FindByBedAndUnitId( + VirtualPointOfCare.Unknown.ToString(), + patient.UnitId); + var unitUnknowPoc = await _unitService.FindById(patient.UnitId); + if (unknownLocationPoc == null || unitUnknowPoc == null) + { + _logger.LogError( + "Unable to find new location on ProcessAdtMovePatient location: Unit: {Unit}, Bed: {Bed}", + unitUnknowPoc?.Name ?? "UNKNOWN", VirtualPointOfCare.Unknown.ToString()); + return; + } + + apiRequest.Location = new PatientLocation(unitUnknowPoc.Name, unknownLocationPoc.Bed, + unknownLocationPoc.Room); + await UpdateLocation(patient.Id, unitUnknowPoc.Id, unknownLocationPoc.Id); + } + + _logger.LogDebug( + "Debug MovePatient, patient not null to move: {patient} Bed: {apiRequestLocationBed}, PointOfCare: {apiRequestLocationPointOfCare}", + patient, apiRequest.Location?.Bed, apiRequest.Location?.UnitName); + } + else + { + // if (patientExistsInLocation != null) + // { + // if (patientExistsInLocation.PatientNumber != apiRequest.PatientNumber) + // { + // _logger.LogDebug( + // "Debug MovePatient, patient to move is null but patientExistsInLocation is not null moving patient: {patientExistsInLocation}, to pointOfCare MOVED", + // patientExistsInLocation); + // + // await UpdateLocation(patientExistsInLocation.Id,movedLocationPoc.UnitId, movedLocationPoc.Id); + // } + // } + + patient = await CreatePatientFromRequest(apiRequest); + if (patient != null) + { + var newLocationUnit = await _unitService.FindByName(apiRequest.Location?.UnitName); + var newLocationPoc = await _pointOfCareService.FindByBedAndUnitId(apiRequest.Location?.Bed, + newLocationUnit?.Id); + if (newLocationPoc == null) + { + _logger.LogError("Unable to find new location on ProcessAdtMovePatient location: {Unit} {Bed}", + apiRequest.Location?.UnitName, apiRequest.Location?.Bed); + return; + } + + _logger.LogDebug( + "person to move was null on ADT_A02 newPatient: {patient}, proceed to send notification to front", + patient); + await SendNewPatientBroadcast(patient); + await _pointOfCareService.SetPointOfCareStatus(newLocationPoc.Id, StatusEnum.PointOfCare.InUse); + //_pointOfCareService.CheckNextAdmission(newLocationPoc.Id); + } + } + } + catch (Exception e) + { + _logger.LogError("Error on ProcessAdtMovePatient trace: {eStackTrace}, message: {eMessage}", + e.StackTrace, e.Message); + } + } + + /// + /// Process new patient. try to find him by + /// + /// or + /// + /// if exits and was not discharged use him. + /// If not insert new one in patients table + /// + /// + /// The Inserted patient or null. + private async Task ProcessAdtPatientAdmit(ApiRequest apiRequest) + { + Patient? patient; + try + { + if (apiRequest.Location != null) + apiRequest.Location = await _pocMappingService.Map(apiRequest.Location); + + patient = await FindPatient(apiRequest.PatientId, apiRequest.PatientNumber, + apiRequest.Location); //Null ADTA01 + var unit = await _unitService.FindByName(apiRequest.Location?.UnitName); + if (unit == null) + { + _logger.LogDebug("Process admit patient to unit name: {UnitName} NOT FOUND", + apiRequest.Location?.UnitName); + return null; + } + + var poc = await _pointOfCareService.FindByBedAndUnitId(apiRequest.Location?.Bed, unit.Id); + if (poc == null) + { + _logger.LogDebug("Process admit patient to bed name: {BedName} NOT FOUND", + apiRequest.Location?.Bed); + return null; + } + + var patientExistsInLocation = await FindByUnitAndPocId(unit.Id, poc.Id); + + + //if previous patient exists is moved to PUSHED + + if (patientExistsInLocation != null) + { + _logger.LogDebug("person Exists InLocation: {PatientExistsInLocation}", + patientExistsInLocation.ToStr()); + + if (patient == null || patientExistsInLocation.PatientNumber != patient.PatientNumber) + { + _logger.LogDebug("previous patient: {PatientExistsInLocationId} moved to PUSHED ", + patientExistsInLocation.Id); + _logger.LogDebug( + "Remove grouped obs for patientId: {ApiRequestPatientId} patient exist on location for new patient location: {PatientExistsInLocationLocation}, traceId: {ApiRequestTraceId}, traceMessageId: {ApiRequestTraceMessageId}", + apiRequest.PatientId ?? "NULL", patientExistsInLocation.Location, + apiRequest.TraceId ?? "NULL", apiRequest.TraceMessageId ?? "NULL"); + var pocPushed = + await _pointOfCareService.FindByBedAndUnitId(VirtualPointOfCare.Pushed.ToString(), unit.Id); + if (pocPushed != null) + { + await UpdateLocation(patientExistsInLocation.Id, + unit.Id, pocPushed.Id); + _ = SendLastGroupedObsToSubscriberByPoc(pocPushed.Id, poc.Id, patientExistsInLocation); + } + else + { + _logger.LogError( + "Unable to update location for existing patient: {Patient} on poc: {VirtualPoc} and unit: {Unit}", + patientExistsInLocation.Id.ToString(), VirtualPointOfCare.Pushed.ToString(), + unit.Id.ToString()); + } + } //patient already exists and is the same again, do nothing + else if (patient.PatientNumber == patientExistsInLocation.PatientNumber && + patient.Location.Bed == patientExistsInLocation.Location.Bed && + patient.Location.UnitName == patientExistsInLocation.Location.UnitName) + { + _logger.LogDebug("patient: {PatientId} already exist, ignoring ADT", patient.Id); + return null; + } + } + + + _logger.LogDebug("person dsTime:{PatientDisTime}", patient?.DisTime?.ToString() ?? "NULL"); + + + if (patient is not + { + DisTime: null + }) //If new patient, it returns true because patient not contains a field disTime == null + { + var creationDate = patient?.CreationDate ?? DateTime.UtcNow; + if (patient != null) + { + patient.CreationDate = creationDate; + patient.UnitId = unit.Id; + patient.DisTime = null; + patient.PointOfCareId = poc.Id; + patient.Altable = new OptionList + { IconDefault = "icNotAltable", Name = "NotAltable", OptionType = "NotAltable" }; + } + else + { + patient = new Patient + { + Id = ObjectId.GenerateNewId(), + CreationDate = creationDate, + UnitId = unit.Id, + DisTime = null, + PointOfCareId = poc.Id, + Altable = new OptionList + { IconDefault = "icNotAltable", Name = "NotAltable", OptionType = "NotAltable" } + }; + } + } + + _logger.LogDebug("Updating patient from request. person: {Patient} ", patient.ToStr()); + await UpdatePatientFromRequest(patient, apiRequest); + if (patient.PatientNumber != null) + { + var patientInAdmission = + await _admissionService.Value.GetAdmissionByPatientNumber(patient.PatientNumber); + if (patientInAdmission != null) + { + await MergePatientWithAdmission(patient, patientInAdmission); + await _admissionService.Value.DeleteAdmissionByIdAsync(patientInAdmission.Id); + } + } + + _logger.LogDebug("Inserting patient. person: {Patient}", patient.ToStr()); + await Update(patient); + _ = SendNewPatientBroadcast(patient); + await _pointOfCareService.SetPointOfCareStatus(poc.Id, StatusEnum.PointOfCare.InUse); + // Enviar en base a la config de display que tenga cada subscriptor + _ = SendLastObsToSubscriberByPoc(poc, patient); + // Enviar las grupadas dependiendo del config del subscriptor + _ = SendLastGroupedObsToSubscriberByPoc(poc.Id, null, patient); + _logger.LogDebug("Updated patient: {PatientId}", patient.Id); + } + catch (Exception e) + { + _logger.LogError("Exception processing patient admission : {EMessage}", e); + return null; + } + + return patient; + } + + private async Task MergePatientWithAdmission(Patient patient, Admission patientInAdmission) + { + patient.Insulation = patientInAdmission.Insulation; + if (patient.Insulation != null) + await GenerateNurseObsAndInsert(_listSettings.InsulationList.ManualObservationName!, [patient.Insulation], + patient.Id, null); + + patient.Allergies = patientInAdmission.Allergies; + if (patient.Allergies != null) + await GenerateNurseObsAndInsert(_listSettings.AllergyList.ManualObservationName!, patient.Allergies, + patient.Id, null); + + patient.Diagnosis = patientInAdmission.Diagnosis; + if (patient.Diagnosis != null) + await GenerateNurseObsAndInsert(_listSettings.DiagnosisList.ManualObservationName!, [patient.Diagnosis], + patient.Id, null); + + patient.DiagnosisAux = patientInAdmission.DiagnosisAux; + + patient.LanguageBarrier = patientInAdmission.LanguageBarrier; + if (patient.LanguageBarrier != null) + await GenerateNurseObsAndInsert(_listSettings.LanguageBarrierList.ManualObservationName!, + patient.LanguageBarrier, patient.Id, null); + + patient.Origin = patientInAdmission.Origin; + if (patient.Origin != null) + await GenerateNurseObsAndInsert(_listSettings.OriginList.ManualObservationName!, [patient.Origin], + patient.Id, null); + + patient.OriginAux = patientInAdmission.OriginAux; + } + + private async Task UpdatePatientFromRequest(Patient patient, ApiRequest apiRequest, + bool ignoreLocation = false) + { + if (apiRequest.AdmTime.HasValue) patient.AdmTime = apiRequest.AdmTime; + if (!string.IsNullOrEmpty(apiRequest.PatientId)) patient.PatientId = apiRequest.PatientId; + + if (apiRequest.Patient != null && !apiRequest.Patient.IsEmpty()) + { + // Log action to describe + + patient.Person ??= new Person(); + + patient.Person.BirthDate = apiRequest.Patient.BirthDate; + patient.Person.Language = apiRequest.Patient.Language; + patient.Person.FirstName = apiRequest.Patient.FirstName; + patient.Person.Gender = apiRequest.Patient.Gender; + patient.Person.LastName = apiRequest.Patient.LastName; + patient.Person.SecondName = apiRequest.Patient.SecondName; + } + + var patientNumberChanged = true; + + if (!string.IsNullOrEmpty(apiRequest.PatientNumber)) + { + // Log action to describe + + patientNumberChanged = patient.PatientNumber != apiRequest.PatientNumber; + patient.PatientNumber = apiRequest.PatientNumber; + } + + + if (patientNumberChanged && apiRequest.Patient?.Ids != null) + { + patient.Person ??= new Person(); + patient.Person.SetIds(apiRequest.Patient.Ids, apiRequest.MessageTime); + } + + var unit = await _unitService.FindByName(apiRequest.Location?.UnitName); + if (unit == null && !ignoreLocation) + _logger.LogWarning( + "WARNING updating patient from request, unit not found by name: {UnitName} for patientId: {PatientId} apiRequest: {Request}", + apiRequest.Location?.UnitName ?? "", patient.Id, apiRequest.ToString()); + if (apiRequest.AttendingDoctor != null && !apiRequest.AttendingDoctor.IsEmpty()) + patient.AttendingDoctor = apiRequest.AttendingDoctor; + if (apiRequest.Location != null && !apiRequest.Location.IsEmpty() && apiRequest.Location.Bed != null && + apiRequest.Location.UnitName != null && !ignoreLocation && unit != null) + { + var poc = await _pointOfCareService.FindByBedAndUnitId(apiRequest.Location.Bed, unit.Id); + if (poc != null) + { + patient.Location = new PatientLocation(unit.Name, poc.Bed, poc.Room); + patient.UnitId = unit.Id; + patient.PointOfCareId = poc.Id; + } + else + { + _logger.LogError( + "Error updating patient from request finding poc by bed name: {BedName} and unitId: {UnitId} not found request: {ApiRequest}", + apiRequest.Location?.Bed, unit.Id, apiRequest); + } + } + + //To solve multi "sin cama" from ICCADB making duplicated keys error on mongodb. + if (apiRequest.Location != null && patient.Location.Bed != null && + (patient.Location.Bed.ToLower().Equals("sin cama") || patient.Location.Bed == string.Empty) && unit != null) + { + // Log action to describe + var poc = await _pointOfCareService.FindByBedAndUnitId(VirtualPointOfCare.NoBed.ToString(), unit.Id); + if (poc != null) + { + patient.Location = new PatientLocation(unit.Name, poc.Bed, poc.Room); + patient.UnitId = unit.Id; + patient.PointOfCareId = poc.Id; + } + else + { + _logger.LogError( + "Error updating patient from request with bed empty finding poc by bed name: {BedName} and unitId: {UnitId} not found request: {ApiRequest}", + apiRequest.Location.Bed, unit.Id, apiRequest); + } + } + + if (apiRequest.DisTime.HasValue) patient.DisTime = apiRequest.DisTime; + + patient.UpdateDate = DateTime.UtcNow; + } + + private async Task ProcessUpdatePatientLocationWithOru(Patient patient, ApiRequest apiRequest) + { + await UpdateLocation(patient.Id, apiRequest.Location); + _ = SendPatientUpdateBroadcast(patient); + } + + public async Task UpdatePatientFromMerge(Patient patient, Patient oldPatient) + { + var listObsName = _masterListServiceFactory.StringNurseObs(); + var obs = await _observationService.Value.FindLastObservations(patient.Id, 1, listObsName); + patient.Visits = + obs.FirstOrDefault(o => o.Name == _listSettings.VisitOptionList.ManualObservationName)?.Value is + List visitList + ? visitList.FirstOrDefault() + : null; + patient.AccessControl = + obs.FirstOrDefault(o => o.Name == _listSettings.AccessControlList.ManualObservationName)?.Value is + List accessControList + ? accessControList.FirstOrDefault() + : null; + patient.TherapeuticCeiling = + obs.FirstOrDefault(o => o.Name == _listSettings.TherapeuticCeilingList.ManualObservationName)?.Value is + List ceilingList + ? ceilingList.FirstOrDefault() + : null; + patient.Mobility = + obs.FirstOrDefault(o => o.Name == _listSettings.MobilityOptionList.ManualObservationName)?.Value is + List mobilityList + ? mobilityList.FirstOrDefault() + : null; + patient.PassiveSitting = + obs.FirstOrDefault(o => o.Name == _listSettings.PassiveSittingList.ManualObservationName)?.Value is + List passiveList + ? passiveList.FirstOrDefault() + : null; + patient.LanguageBarrier = + obs.FirstOrDefault(o => o.Name == _listSettings.LanguageBarrierList.ManualObservationName)?.Value as List; + patient.Insulation = + obs.FirstOrDefault(o => o.Name == _listSettings.InsulationList.ManualObservationName)?.Value is + List insulationList + ? insulationList.FirstOrDefault() + : null; + patient.Doctors = + obs.FirstOrDefault(o => o.Name == _listSettings.DoctorList.ManualObservationName) + ?.Value as List; + patient.Allergies = + obs.FirstOrDefault(o => o.Name == _listSettings.AllergyList.ManualObservationName)?.Value as + List; + patient.Altable = + obs.FirstOrDefault(o => o.Name == _listSettings.AltableOptionList.ManualObservationName)?.Value is + List altableList + ? altableList.FirstOrDefault() + : null; + var obsPatientIncomingData = + (PatientIncomeData?)obs.FirstOrDefault(o => o.Name == "PatientIncomingData")?.Value; + if (obsPatientIncomingData != null) + { + patient.AdmTime = obsPatientIncomingData.AdmTime; + patient.Origin ??= obsPatientIncomingData.Origin; + patient.OriginAux ??= obsPatientIncomingData.OriginAux; + patient.Diagnosis ??= obsPatientIncomingData.Diagnosis; + patient.DiagnosisAux ??= obsPatientIncomingData.DiagnosisAux; + } + + if (patient.Treatment != null) + { + var missingTreatments = oldPatient.Treatment? + .Where(t => patient.Treatment.All(p => p.Name != t.Name)) + .ToList(); + if (missingTreatments != null) + patient.Treatment.AddRange(missingTreatments); + } + else if (oldPatient.Treatment != null) + { + patient.Treatment = oldPatient.Treatment; + } + + if (patient.Procedures != null) + { + var missingProcedures = oldPatient.Procedures? + .Where(t => patient.Procedures.All(p => p.Name != t.Name)) + .ToList(); + if (missingProcedures != null) + patient.Procedures.AddRange(missingProcedures); + } + else if (oldPatient.Procedures != null) + { + patient.Procedures = oldPatient.Procedures; + } + + if (patient.Tests != null) + { + var missingTests = oldPatient.Tests? + .Where(t => patient.Tests.All(p => p.Name != t.Name)) + .ToList(); + if (missingTests != null) + patient.Tests.AddRange(missingTests); + } + else if (oldPatient.Tests != null) + { + patient.Tests = oldPatient.Tests; + } + + await _patientRepository.Update(patient); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, patient); + await SendPatientBroadcast(patient); + } + + private async Task GenerateNurseObsAndInsert(string name, List? value, ObjectId patientId, User? user) + { + var obs = GenerateObs( + name, + value ?? [], + patientId); + obs.UserId = user?.Id; + await _observationService.Value.InsertNurseObservation(obs); + } + + private PatientObservation GenerateObs(string name, List? value, ObjectId patientId) + { + var valueList = value ?? []; + + return new PatientObservation + { + Name = name, + Value = valueList, + PatientId = patientId, + Time = DateTime.Now, + InsertMode = ObservationEnum.InsertMode.Manual + }; + } + + public string GetManualObservationName(string masterListNameString) + { + // 1. Obtener el tipo de la clase ListSettings. + var settingsType = typeof(ListSettings); + + // 2. Obtener la propiedad con el nombre que coincide con el string del enum. + // El 'masterListNameString' debe coincidir exactamente con el nombre de la propiedad. + var propertyInfo = settingsType.GetProperty(masterListNameString); + + if (propertyInfo == null) + // La propiedad no se encontró (el string no coincide con un nombre de propiedad). + return $"Error: Propiedad '{masterListNameString}' no encontrada en ListSettings."; + + // 3. Obtener la instancia de ListSettingItem (el valor) de esa propiedad, + // usando la instancia 'settings' proporcionada. + + if (propertyInfo.GetValue(_listSettings) is not ListSettingItem listSettingItem) + // Esto no debería suceder si la clase ListSettings está bien definida, + // pero es una buena práctica comprobarlo. + return "Error: El valor de la propiedad no es un ListSettingItem."; + + // 4. Devolver el valor de ManualObservationName. + return listSettingItem.ManualObservationName!; + } + + private enum OnArchiveAction + { + Delete, + Archive + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/PermissionService.cs b/adas-core.Application/Services/PermissionService.cs new file mode 100644 index 00000000..ee23f311 --- /dev/null +++ b/adas-core.Application/Services/PermissionService.cs @@ -0,0 +1,264 @@ +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.AppSettings; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; + +namespace adas_core.Application.Services; + +public class PermissionService( + IOptions permissionsConfig, + ILogger logger, + Lazy displayService, + Lazy userRepository, + Lazy authorityRepository) + : IPermissionService +{ + private readonly ILogger _logger = logger; + private readonly PermissionSettings _permissionsConfig = permissionsConfig.Value; + + public async Task GetPermissionsForDisplay(Display display, User user) + { + var authorities = user.Authorization; + if (authorities == null) + { + var a = await userRepository.Value.GetByUserAndAuthoritesName(user.UserName); + authorities = a?.Authorization; + user.Authorization = authorities; + } + + if (authorities == null) throw new ForbbidenException(HttpEnum.ErrorMessage.ForbiddenNoPermission); + + foreach (var auth in authorities) + if (display.Id.ToString().Equals(auth.DisplayId) || display.UnitId.ToString().Equals(auth.UnitId)) + { + _logger.LogInformation("User {user} has role {role} for display {displayId} or unit {unitId}", + user.UserName, auth.Rol, auth.DisplayId, auth.UnitId); + + //var unitPerms = await GetPermissionsForUnit(display.UnitId.ToString(), user); + switch ((PermissionEnum.RolesType?)Enum.Parse(typeof(PermissionEnum.RolesType), + auth.Rol ?? string.Empty)) + { + case PermissionEnum.RolesType.AuthGuest: + // return (await CheckPermissions(_permissionsConfig.Guest.Display, display.UnitId.ToString())); + return _permissionsConfig.Guest.Display; + case PermissionEnum.RolesType.AuthAdmin: + // return await CheckPermissions(_permissionsConfig.Admin.Display, display.UnitId.ToString()); + return _permissionsConfig.Admin.Display; + case PermissionEnum.RolesType.AuthDeveloper: + // return await CheckPermissions(_permissionsConfig.Developer.Display, display.UnitId.ToString()); + return _permissionsConfig.Developer.Display; + case PermissionEnum.RolesType.AuthDoctorchief: + // return await CheckPermissions(_permissionsConfig.DoctorChief.Display, display.UnitId.ToString()); + return _permissionsConfig.DoctorChief.Display; + case PermissionEnum.RolesType.AuthDoctor: + // return await CheckPermissions(_permissionsConfig.Doctor.Display, display.UnitId.ToString()); + return _permissionsConfig.Doctor.Display; + case PermissionEnum.RolesType.AuthNursesupervisor: + // return await CheckPermissions(_permissionsConfig.NursingSupervisor.Display, display.UnitId.ToString()); + return _permissionsConfig.NursingSupervisor.Display; + case PermissionEnum.RolesType.AuthNurse: + // return await CheckPermissions(_permissionsConfig.Nurse.Display, display.UnitId.ToString()); + return _permissionsConfig.Nurse.Display; + } + } + + //return _permissionsConfig.NoPermissions.Display; + throw new ForbbidenException(HttpEnum.ErrorMessage.ForbiddenNoPermission); + } + + public Task GetPermissionsForUnit(string unitId, User user) + { + var authorities = user.Authorization; + if (authorities == null) throw new ForbbidenException(HttpEnum.ErrorMessage.ForbiddenNoPermission); + foreach (var auth in authorities) + if (auth.UnitId != null && auth.UnitId.Equals(unitId)) + { + _logger.LogInformation("User {user} has role {role} for unit {unitId}", user.UserName, auth.Rol, + auth.UnitId); + + switch ((PermissionEnum.RolesType?)Enum.Parse(typeof(PermissionEnum.RolesType), + auth.Rol ?? string.Empty)) + { + case PermissionEnum.RolesType.AuthGuest: + // return await CheckPermissions(_permissionsConfig.Guest.Unit, auth.UnitId); + return Task.FromResult(_permissionsConfig.Guest.Unit); + case PermissionEnum.RolesType.AuthAdmin: + // return await CheckPermissions(_permissionsConfig.Admin.Unit, auth.UnitId); + return Task.FromResult(_permissionsConfig.Admin.Unit); + case PermissionEnum.RolesType.AuthDeveloper: + // return await CheckPermissions(_permissionsConfig.Developer.Unit, auth.UnitId); + return Task.FromResult(_permissionsConfig.Developer.Unit); + case PermissionEnum.RolesType.AuthDoctorchief: + // return await CheckPermissions(_permissionsConfig.DoctorChief.Unit, auth.UnitId); + return Task.FromResult(_permissionsConfig.DoctorChief.Unit); + case PermissionEnum.RolesType.AuthDoctor: + // return await CheckPermissions(_permissionsConfig.Doctor.Unit, auth.UnitId); + return Task.FromResult(_permissionsConfig.Doctor.Unit); + case PermissionEnum.RolesType.AuthNursesupervisor: + // return await CheckPermissions(_permissionsConfig.NursingSupervisor.Unit, auth.UnitId); + return Task.FromResult(_permissionsConfig.NursingSupervisor.Unit); + case PermissionEnum.RolesType.AuthNurse: + // return await CheckPermissions(_permissionsConfig.Nurse.Unit, auth.UnitId); + return Task.FromResult(_permissionsConfig.Nurse.Unit); + } + } + + throw new ForbbidenException(HttpEnum.ErrorMessage.ForbiddenNoPermission); + } + +/* + private async Task CheckPermissions(DisplayPermissionTypes guestUnit, string authUnitId) + { + var isParsed = ObjectId.TryParse(authUnitId, out var unitId); + if(isParsed) + { + var unit = await uniService.Value.FindById(unitId); + var unitConfiguration = unit?.Configuration; + if (unitConfiguration != null) + { + guestUnit.Admissions.Execute = unitConfiguration.ManualAdmit; + + guestUnit.Discharges.Execute = unitConfiguration.ManualDischarge; + + guestUnit.DemographicData.Create = unitConfiguration.ManualEdit; + guestUnit.DemographicData.Delete = unitConfiguration.ManualEdit; + guestUnit.DemographicData.Update = unitConfiguration.ManualEdit; + guestUnit.DemographicData.Execute = unitConfiguration.ManualEdit; + } + } + + _logger.LogInformation("Permissions for unit {unitId}: {@permissions}", authUnitId, guestUnit); + + return guestUnit; + } +*/ + + public async Task GetPermissionsForPanel(string user) + { + var userFound = await userRepository.Value.GetByUserName(user); + if(userFound == null) throw new ForbbidenException(HttpEnum.ErrorMessage.ForbiddenNoPermission); + userFound.Authorization = await authorityRepository.Value.GetUserAuthorities(userFound.Id); + return GetPermissionsForPanel(userFound); + } + public PanelPermissionTypes GetPermissionsForPanel(User user) + { + var authorities = user.Authorization; + if (authorities == null) throw new ForbbidenException(HttpEnum.ErrorMessage.ForbiddenNoPermission); + + foreach (var auth in authorities) + if (auth.PanelAuthorization) + { + _logger.LogInformation("User {user} has role {role} for panel", user.UserName, auth.Rol); + + switch ((PermissionEnum.RolesType?)Enum.Parse(typeof(PermissionEnum.RolesType), + auth.Rol ?? string.Empty)) + { + case PermissionEnum.RolesType.AuthGuest: + return _permissionsConfig.Guest.Panel; + case PermissionEnum.RolesType.AuthAdmin: + return _permissionsConfig.Admin.Panel; + case PermissionEnum.RolesType.AuthDeveloper: + return _permissionsConfig.Developer.Panel; + case PermissionEnum.RolesType.AuthDoctorchief: + return _permissionsConfig.DoctorChief.Panel; + case PermissionEnum.RolesType.AuthDoctor: + return _permissionsConfig.Doctor.Panel; + case PermissionEnum.RolesType.AuthNursesupervisor: + return _permissionsConfig.NursingSupervisor.Panel; + case PermissionEnum.RolesType.AuthNurse: + return _permissionsConfig.Nurse.Panel; + } + } + + throw new ForbbidenException(HttpEnum.ErrorMessage.ForbiddenNoPermission); + } + + public async Task HasAccessToDisplay(string username, PermissionEnum.RolesType userRole, + PermissionEnum.SourcePermissionsEnum source, string displayId) + { + if (!ObjectId.TryParse(displayId, out var displayObjectId)) return false; + + var display = await displayService.Value.GetById(displayObjectId); + var user = await userRepository.Value.GetByUserName(username); + + if (user == null || display == null) return false; + var authorities = await authorityRepository.Value.GetUserAuthorities(user.Id); + user.Authorization = authorities; + + return SearchRoleInDisplay(display, authorities, userRole); + } + + public async Task HasAccessToUnit(string username, PermissionEnum.RolesType userRole, + PermissionEnum.SourcePermissionsEnum source, string unitId) + { + var user = await userRepository.Value.GetByUserName(username); + + if (user == null) return false; + var authorities = await authorityRepository.Value.GetUserAuthorities(user.Id); + user.Authorization = authorities; + + return SearchRoleInUnit(unitId, authorities, userRole); + } + + public async Task HasAccessToPanel(string username, PermissionEnum.RolesType userRole, + PermissionEnum.SourcePermissionsEnum source) + { + var user = await userRepository.Value.GetByUserName(username); + + if (user == null) return false; + var authorities = await authorityRepository.Value.GetUserAuthorities(user.Id); + user.Authorization = authorities; + + return SearchRoleInPanel(authorities, userRole); + } + + + private static bool SearchRoleInDisplay(Display display, List? authorities, + PermissionEnum.RolesType role) + { + if (authorities is not { Count: > 0 }) return false; + + return ( + from auth in authorities + let authRole = + (PermissionEnum.RolesType?)Enum.Parse(typeof(PermissionEnum.RolesType), auth.Rol ?? string.Empty) + where display.Id.ToString().Equals(auth.DisplayId) || display.UnitId.ToString().Equals(auth.UnitId) + let result = SearchRoleInUnit(display.UnitId.ToString(), authorities, role) + where result || authRole == role + select authRole + ).Any(); + } + + private static bool SearchRoleInUnit(string unitId, List? authorities, PermissionEnum.RolesType role) + { + if (authorities == null) + return false; + foreach (var auth in authorities) + { + var authRole = + (PermissionEnum.RolesType?)Enum.Parse(typeof(PermissionEnum.RolesType), auth.Rol ?? string.Empty); + if (auth.UnitId != null && auth.UnitId.Equals(unitId) && authRole == role) return true; + } + + return false; + } + + private static bool SearchRoleInPanel(List? authorities, PermissionEnum.RolesType role) + { + if (authorities == null) + return false; + + return ( + from auth in authorities + let authRole = + (PermissionEnum.RolesType?)Enum.Parse(typeof(PermissionEnum.RolesType), auth.Rol ?? string.Empty) + where auth.PanelAuthorization && authRole == role + select auth + ).Any(); + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/PoCMappingService.cs b/adas-core.Application/Services/PoCMappingService.cs new file mode 100644 index 00000000..849fadc8 --- /dev/null +++ b/adas-core.Application/Services/PoCMappingService.cs @@ -0,0 +1,60 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; + +namespace adas_core.Application.Services; + +public class PoCMappingService : IPoCMappingService +{ + private readonly string _key; + private readonly bool _mappingRequired; + private readonly IPoCMappingRepository _pocMappingRepository; + private readonly int? _refreshTimeout; + + private PoCMapping? _mapping; + private DateTime _nextRefresh = DateTime.MinValue; + + public PoCMappingService(IPoCMappingRepository pocMappingRepository, IOptions apiSettings) + { + _pocMappingRepository = pocMappingRepository; + _refreshTimeout = apiSettings.Value.PointOfCareMapping?.Refresh; + _mappingRequired = apiSettings.Value.PointOfCareMapping?.Required ?? _mappingRequired; + _key = apiSettings.Value.PointOfCareMapping?.Key ?? "PV1"; + } + + /// + /// Maps a received location with bed and pointOfCare to the equivalence in the PocMapping table if it finds the value, + /// otherwise it returns null + /// + /// location + /// mapped location + public async Task Map(PatientLocation original) + { + var pocMapping = await GetMapping(); + var poc = pocMapping?.PointOfCares.FindAll(p => p.OriginalPoC == original.UnitName); + var pocWithBed = poc?.FirstOrDefault(p => p.Beds.Any(bed => bed[0] == original.Bed)); + var newBed = pocWithBed?.Beds.Where(bed => bed[0] == original.Bed).Select(bed => bed[1]).FirstOrDefault(); + if (string.IsNullOrEmpty(newBed)) return _mappingRequired ? null : original; + return new PatientLocation(pocWithBed!.NewPoC, newBed); + } + + /// + /// Return mapping from cache of database based on refreshTime + /// + /// + private async Task GetMapping() + { + if (_mapping == null || DateTime.Now > _nextRefresh) + { + _mapping = await _pocMappingRepository.FindByKey(_key); + _nextRefresh = _refreshTimeout.HasValue + ? DateTime.Now.AddSeconds(_refreshTimeout.Value) + : DateTime.MaxValue; + } + + return _mapping; + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/PointOfCareService.cs b/adas-core.Application/Services/PointOfCareService.cs new file mode 100644 index 00000000..9a63e3a7 --- /dev/null +++ b/adas-core.Application/Services/PointOfCareService.cs @@ -0,0 +1,398 @@ +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 logger, + IPointOfCareRepository pointOfCareRepository, + Lazy patientService, + Lazy unitService, + ISubscribersService subscribersService, + Lazy clientMessageService, + Lazy admissionService, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService, + ICacheService cacheService, + IOptions cacheSettings) + : IPointOfCareService +{ + private readonly CacheSettings? _cacheSettings = cacheSettings.Value ; + + public async Task 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; + } + } + + 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); + + } + + public async Task DeletePoCsByUnitId(ObjectId unitId) + { + await pointOfCareRepository.DeleteManyByUnitId(unitId); + + await cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.PontOfCare)); + + } + + public async Task> FindAllIdCamerasInUse() + { + return await pointOfCareRepository.FindAllIdCamerasInUse(); + } + public async Task> FindAllIdRelaysInUse() + { + return await pointOfCareRepository.FindAllIdRelaysInUse(); + } + public async Task> FindAllIdBeaconsInUse() + { + return await pointOfCareRepository.FindAllIdBeaconsInUse(); + } + + public async Task?> FindAllByUnitIdWithDevices(ObjectId unitId) + { + var result = await pointOfCareRepository.FindAllByUnitIdWithDevices(unitId); + + return result ?? []; + } + + + public async Task 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; + + } + + 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); + } + + public async Task> GetAll() + { + var c = await pointOfCareRepository.GetAll(); + return c ?? []; + } + + public async Task> GetAllConfigs() + { + var c = await pointOfCareRepository.GetAllConfigs(); + return c ?? []; + } + + public async Task> GetAllLocationInfo() + { + var c = await pointOfCareRepository.GetAllLocationInfo(); + return c ?? []; + } + + public async Task> 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(dataList, filter.PageNumber, filter.PageSize, count); + } + + 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); + } + + public async Task FindById(ObjectId id) + { + return await pointOfCareRepository.FindByIdAllConfig(id); + } + public async Task FindByIdAllConfig(ObjectId id) + { + return await pointOfCareRepository.FindByIdAllConfig(id); + } + + public async Task?> FindAllByUnitId(ObjectId unit) + { + var result = await pointOfCareRepository.FindAllByUnitId(unit); + + return result ?? []; + } + + public async Task> FindByUnitAndStatus(ObjectId unitId, StatusEnum.PointOfCare poc, + bool excludeVirtual = false) + { + return await pointOfCareRepository.FindByUnitAndStatus(unitId, poc, excludeVirtual); + } + + /// + /// Check nex admission when patient has exit from poc + /// + /// PoC id + 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); + } + } + + public async Task FindByBedAndUnitId(string? bed, ObjectId? unitId) + { + if (unitId == null || string.IsNullOrEmpty(bed)) return null; + return await pointOfCareRepository.FindByBedAndUnitId(bed, unitId.Value); + } + + 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); + } + } + + public async Task?> FindByRoom(string room) + { + return await pointOfCareRepository.FindByRoom(room); + } + + public async Task?> FindByBed(string bed) + { + return await pointOfCareRepository.FindByBed(bed); + } + + public async Task> FindAllByUnitIds(List unitIds) + { + var filter = Builders.Filter.In(p => p.UnitId, unitIds); + return await pointOfCareRepository.FindByFilter(filter); + } + + public async Task 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; + } + + public async Task 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; + } + + + + public async Task FindPoCByPatientId(ObjectId patientId) + { + var patient = await patientService.Value.FindById(patientId); + if (patient is { PointOfCareId: not null }) + return await FindById(patient.PointOfCareId.Value); + return null; + } + + public async Task FindPoCByPatientNumber(string patientNumber) + { + var patient = await patientService.Value.FindByPatientNumber(patientNumber); + if (patient is { PointOfCareId: not null }) + return await FindById(patient.PointOfCareId.Value); + return null; + } + + public async Task CountPoCsByUnitId(ObjectId unitId) + { + return await pointOfCareRepository.CountByUnitId(unitId); + } + + public async Task CountVirtualPoCsByUnitId(ObjectId unitId) + { + return await pointOfCareRepository.CountVirtualsByUnitId(unitId); + } + + 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); + } + + public async Task FindPoCByPatientLocation(PatientLocation patientLocation) + { + return await pointOfCareRepository.FindByPatientLocation(patientLocation); + } + + 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); + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/PumpService.cs b/adas-core.Application/Services/PumpService.cs new file mode 100644 index 00000000..94750b22 --- /dev/null +++ b/adas-core.Application/Services/PumpService.cs @@ -0,0 +1,610 @@ +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 +{ + /// + /// 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) + /// + public class PumpService( + IPumpObservationRepository pumpObservationRepository, + IPumpStateRepository pumpStateRepo, + IPumpAlarmEventRepository alarmEventRepo, + IPumpAlarmStateRepository alarmStateRepo, + IPumpArchiveRepository pumpArchiveRepo, + IPatientService patientService, + IConfigPumpsService configPumpsService, + IOptions apiSettings, + ILogger logger, + ISubscribersService subscribersService, + IClientMessageService clientMessageService, + Lazy 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 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 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> FindLastPumpObservations(ObjectId patientId, int num = 1) + { + var list = await pumpObservationRepository.FindByPatientId(patientId); + if (list is List 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> 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?> GetItemsById(string id) + => await configPumpsService.GetConfigItems(id); + + public async Task?> GetAllPumpConfig() + => await configPumpsService.GetAllPumpConfigs() + ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + + public async Task GetPumpConfigsById(string id) + => await configPumpsService.GetPumpConfigById(id) + ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + + public async Task 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 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 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?> 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([], page, size, 0); + + List all; + + if (!string.IsNullOrWhiteSpace(fr.PatientId)) + { + if (!ObjectId.TryParse(fr.PatientId, out var patientId)) + return new PaginationResponse([], 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(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 activeAlarms, + ObjectId? patientId, + ApiRequest? req) + { + var subscribers = new List(); + + 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; + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/RecordingAlertService.cs b/adas-core.Application/Services/RecordingAlertService.cs new file mode 100644 index 00000000..7fb6fdd2 --- /dev/null +++ b/adas-core.Application/Services/RecordingAlertService.cs @@ -0,0 +1,170 @@ +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.MongoModels; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; + +namespace adas_core.Application.Services; + +public class RecordingAlertService( + Lazy patientService, + IConfigObservationService configObservationService, + IRecordingAlertRepository recordingAlertRepository, + IRecordingAlertArchiveRepository recordingAlertArchiveRepository, + ILogger logger, + IClientMessageService clientMessageService, + ISubscribersService subscribersService, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService) + : IRecordingAlertService +{ + public async Task Archive(Patient patient) + { + await ArchiveByPatientId(patient.Id); + } + + public async Task ArchiveByPatientId(ObjectId id) + { + logger.LogDebug("Archive Recording Alerts by Patient Id {id}", id); + using (var cursor = await recordingAlertRepository.FindByPatientIdAsync(id)) + { + while (await cursor.MoveNextAsync()) + foreach (var current in cursor.Current) + await recordingAlertArchiveRepository.InsertOneAsync(current); + } + + await DeleteByPatientId(id); + } + + public async Task DeleteByPatientId(ObjectId id) + { + logger.LogDebug("Delete Recording Alerts by Patient Id {id}", id); + await recordingAlertRepository.DeleteByPatientId(id); + } + + public async Task> FindLastRecordingAlert(ObjectId patientId, int num = 2) + { + return await recordingAlertRepository.AggregatedPatientLastObservations(patientId, num); + } + + + public Task SaveRequestAsync(ApiRequest apiRequest) + { + return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); })); + } + + public async Task SaveRequest(ApiRequest apiRequest) + { + if (string.IsNullOrEmpty(apiRequest.PatientNumber) && string.IsNullOrEmpty(apiRequest.Location?.UnitName) && + string.IsNullOrEmpty(apiRequest.Location?.Bed)) + { + logger.LogDebug("Patient and Location are nulls in apiRequest RecordingAlerts"); + return; + } + + logger.LogDebug("patientNumber: {apiRequestpatientNumber} location: {apiRequestlocation}", + apiRequest.PatientNumber, apiRequest.Location); + logger.LogDebug("RequestType: {apiRequesttype}", apiRequest.Type); + + switch (apiRequest.Type) + { + /* + * ORU_R01 - Unsolicited transmission of an observation message + * ORU_R40 - Unsolicited transmission of an alert observation message + */ + case "ORU_R01": + case "ORU_R40": + case "RecordingAlert": + + + var patient = await patientService.Value.FindPatientByApiRequest(apiRequest); + + + if (patient == null) + { + // NO PATIENTS OR LOCATIONS WERE FOUND + logger.LogWarning( + "Observation for patient: {apiRequestpatientNumber} with location: {apiRequestlocation} not found, ignoring", + apiRequest.PatientNumber, apiRequest.Location); + return; + } + + + // Recording Alerts + + if (apiRequest.RecordingAlert != null && apiRequest.RecordingAlerts?.FirstOrDefault() == null) + apiRequest.RecordingAlerts = [apiRequest.RecordingAlert]; + + + if (apiRequest.RecordingAlerts != null) + { + logger.LogDebug("INSERT {apiRequestrecordingAlerts} OBSERVATIONS", + apiRequest.RecordingAlerts.Count); + foreach (var obs in apiRequest.RecordingAlerts) + { + obs.PatientId = patient.Id; + obs.Id = ObjectId.GenerateNewId(); + if (obs.Time == DateTime.MinValue) obs.Time = DateTime.UtcNow; + await InsertRecordingAlert(obs); + } + } + + break; + default: + logger.LogWarning("ApiRequest type {apiRequesttype} sis not valid for Observations", apiRequest.Type); + throw new ApiRequestException("ApiRequest type " + apiRequest.Type + " is not valid for Observations"); + } + } + + public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId) + { + await recordingAlertRepository.UpdateManyObjectId(nameId, id, oldId); + } + + private async Task InsertRecordingAlert(PatientRecordingAlert recAlert) + { + logger.LogDebug("Insert {recAlert}", recAlert); + await recordingAlertRepository.InsertOneAsync(recAlert); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, recAlert); + await SendObsBroadcast(recAlert); + await DoRetentionActions(recAlert); + } + + private async Task SendObsBroadcast(BasePatientObservation recAlert) + { + if (recAlert.Name == null) return; + + var type = recAlert is PatientRecordingAlert ? OperationType.RecordingAlert : OperationType.Observation; + + + var patient = await patientService.Value.FindById(recAlert.PatientId); + + if (patient?.PointOfCareId == null) return; + + var subscribers = subscribersService.GetSubscribers() + .Where(s => s.LocationIds.Contains(patient.PointOfCareId.Value)).ToList(); + + foreach (var subscriber in subscribers) await clientMessageService.SendAsync(subscriber.Id, type, recAlert); + } + + private async Task DoRetentionActions(PatientRecordingAlert recAlert) + { + var result = await configObservationService.RetentionActions(recAlert); + if (result is not { RetentionPolicyValue: not null } || recAlert.Name == null) return; + switch (result.RetentionPolicy) + { + case RetentionPolicy.DeleteOlderDays: + await recordingAlertRepository.DeleteOlderDaysAsync(recAlert.Name, result.RetentionPolicyValue.Value); + break; + case RetentionPolicy.DeleteOlderNumber: + await recordingAlertRepository.DeleteOlderNumberAsync(recAlert.Name, result.RetentionPolicyValue.Value); + break; + default: + throw new ArgumentOutOfRangeException(); + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/RecordingService.cs b/adas-core.Application/Services/RecordingService.cs new file mode 100644 index 00000000..b65330d6 --- /dev/null +++ b/adas-core.Application/Services/RecordingService.cs @@ -0,0 +1,441 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Text; +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.DTO; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Recording; +using adas_core.Domain.Models.Responses; +using adas_core.Domain.Utils; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; +using Serilog; +using Patient = adas_core.Domain.Models.MongoModels.Patient; + +namespace adas_core.Application.Services; + +public class RecordingService : IRecordingService +{ + private readonly IAuthService _authService; + private readonly IClientMessageService _clientMessageService; + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + private readonly Lazy _patientService; + private readonly IPublisherService _publisherService; + private readonly RabbitMqSettings _rabbitMqSettings; + private readonly RecordingSettings _recordingSettings; + + private readonly bool _startRecordingWithoutPatientNumber; + private readonly ISubscribersService _subscribersService; + private readonly string? _url; + + private AccessGrant? _accessGrant; + + public RecordingService(IOptions rabbitMqSettings, + IOptions recordingSettings, + ILogger logger, + IHttpClientFactory httpClientFactory, + IPublisherService publisherService, + IAuthService authService, + IOptions apiSettings, IClientMessageService clientMessageService, + ISubscribersService subscribersService, + Lazy patientService) + { + _logger = logger; + _httpClient = httpClientFactory.CreateClient(); + + _accessGrant ??= new AccessGrant(); + _rabbitMqSettings = rabbitMqSettings.Value; + _recordingSettings = recordingSettings.Value ?? + throw new Exception("RecordingSettings must be defined on appSettings"); + _httpClient.Timeout = new TimeSpan(0, 0, _recordingSettings.HttpClientTimeout); + _url = _recordingSettings.RecordingApiUrl; + _publisherService = publisherService; + _authService = authService; + _clientMessageService = clientMessageService; + _subscribersService = subscribersService; + _patientService = patientService; + RecordingQueueName = _rabbitMqSettings.RecordingQueue; + ErrorRecordingQueueName = $"{RecordingQueueName}_Error"; + + _startRecordingWithoutPatientNumber = apiSettings.Value.StartRecordingWithoutPatientNumber; + } + + private string RecordingQueueName { get; } + private string ErrorRecordingQueueName { get; } + + + public async Task SendCancelRecordingToRecordingApi(Patient patient, PointOfCare poc) + { + var token = await _authService.GetToken(); + if (string.IsNullOrEmpty(_url) || string.IsNullOrEmpty(token)) + throw new Exception( + "send recording data to recording api require RecordingOrApiUrl defined and valid token"); + var body = JsonConvert.SerializeObject(GenerateRecordingData(patient, poc, DateTime.MinValue, + null, null, null, null, AlarmEnum.Severity.None, "")); + var request = + new HttpRequestMessage(HttpMethod.Post, + $"{_url}/videos/delete-video-in-progress") //las fechas no se mandan + { + Content = new StringContent(body, Encoding.UTF8) + }; + request.Headers.TryAddWithoutValidation("Authorization", $"Bearer {token}"); + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + var response = await _httpClient.SendAsync(request); + _logger.LogDebug( + "send cancel to recording api status code:{responseStatusCode} url: {url} body: {body}", + response.StatusCode, _url, body); + return response.IsSuccessStatusCode; + } + + public async Task SendRecordingData(Patient patient, PointOfCare poc, ManualRecording manualRecording, + bool start) + { + await SendRecordingDataToQueue(patient, poc, manualRecording.Recording?.StartRecordingTime, + manualRecording.Recording?.StopRecordingTime, null, null, AlarmEnum.Severity.None, null, start); + } + + public async Task SendAutoRecordingData(Patient patient, PointOfCare poc, RecordingData automaticRecording) + { + try + { + if (!Enum.TryParse(automaticRecording.AlarmName, out var alarmName)) + return false; + + await SendRecordingDataToQueue(patient, poc, automaticRecording.StartRecordingTime, + automaticRecording.StopRecordingTime, automaticRecording.EventTime, + alarmName, automaticRecording.Severity, automaticRecording.AlarmDescription, + type: AlarmEnum.Type.Auto); + + return true; + } + catch (Exception ex) + { + Log.Error("Exception sending automatic recording data. Exception: {ex}", ex); + return false; + } + } + + public async Task SendRecordingDataToQueue(Patient patient, PointOfCare poc, DateTime? date, DateTime? endDate, + DateTime? eventDate, AlarmEnum.Name? alarmName, AlarmEnum.Severity severity, string? alarmDescription, + bool start = true, AlarmEnum.Type type = AlarmEnum.Type.Manual) + { + if (!_startRecordingWithoutPatientNumber && string.IsNullOrEmpty(patient.PatientNumber)) + { + _logger.LogError( + "Error sending recording data to queue. Patient number is null or empty. PatientId: {patientId}", + patient.PatientId); + return; + } + + if (poc.Status != StatusEnum.PointOfCare.InUse) + throw new Exception($"Box does not contain patients:->{JsonConvert.SerializeObject(poc)}"); + + if (string.IsNullOrEmpty(alarmDescription)) alarmDescription = "UNKNOWN"; + + var retryCount = 1; + + while (retryCount <= _rabbitMqSettings.MaxRetries) + { + try + { + ApiRequest recordingRequest = new() + { + Recording = GenerateRecordingData(patient, poc, date, endDate, eventDate, null, alarmName, + severity, alarmDescription, type) + }; + + _logger.LogDebug("Send recording data patient {patient}", patient); + + await _publisherService.SendMessage(recordingRequest, RecordingQueueName); + + + if (recordingRequest.Recording != null) + _ = SendRecordingBroadcast([recordingRequest.Recording]); + + + return; + } + catch (HttpRequestException ce) + { + retryCount++; + _logger.LogError("Connection error sending recording data to queue. {ce} Retrying...", ce.Message); + } + catch (Exception e) + { + _logger.LogError("Error sending recording data to queue.{eError} {eMessage} ", e, e.Message); + await _publisherService.SendMessage(e.Message, ErrorRecordingQueueName); + throw; + } + + if (retryCount == _rabbitMqSettings.MaxRetries) + await _publisherService.SendMessage("Error: Connection retries exceeded.", ErrorRecordingQueueName); + } + } + + + public async Task?> GetRecordings(int roomId) + { + var retryCount = 1; + while (retryCount <= _rabbitMqSettings.MaxRetries) + try + { + var token = await _authService.GetToken(); + if (string.IsNullOrEmpty(_recordingSettings.RecordingApiUrl) || string.IsNullOrEmpty(token)) + { + Log.Warning( + "Not recording url defined at recordingSettings or empty token, ignoring recordings"); + return null; + } + + var request = new HttpRequestMessage(HttpMethod.Get, + $"{_recordingSettings.RecordingApiUrl}/videos/allInProgress/{roomId}"); + + request.Headers.TryAddWithoutValidation("Authorization", $"Bearer {token}"); + + var response = await _httpClient.SendAsync(request); + _logger.LogDebug( + "GetByCodeSysAndCode recordings to recording api status code:{responseStatusCode} url: {url} body", + response.StatusCode, _url); + var responseContent = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + if (response.StatusCode == HttpStatusCode.Unauthorized) + { + _accessGrant = null; + _logger.LogError( + "error connecting to RecordingOrApi url:{requestRequestUri} errorCode: {responseStatusCode} error {responseContent} token: {}", + request.RequestUri, response.StatusCode, response.Content, + _accessGrant?.AccessToken ?? "not exist"); + } + + else if (response.StatusCode == HttpStatusCode.NotFound) + { + return []; + } + + else + { + _logger.LogError( + "error connecting to RecordingOrApi url:{requestRequestUri} errorCode: {responseStatusCode} error {responseContent}", + request.RequestUri, response.StatusCode, response.Content); + return []; + } + + if (retryCount == _rabbitMqSettings.MaxRetries) + await _publisherService.SendMessage("Error: Connection retries exceeded.", + ErrorRecordingQueueName); + + return []; + } + + var records = JsonConvert.DeserializeObject>(responseContent); + + return records; + } + catch (HttpRequestException ce) + { + retryCount++; + _logger.LogError( + "Connection error sending recording data to queue. {ce} Retrying... {retryCount} of {maxRetrys}", + ce.Message, retryCount, _rabbitMqSettings.MaxRetries); + //return new List(); + } + catch (Exception e) + { + _logger.LogError("Error getting recordings room: {roomId} error: {eMessage} ", roomId, e.Message); + await _publisherService.SendMessage(e.Message, ErrorRecordingQueueName); + return []; + } + + if (retryCount == _rabbitMqSettings.MaxRetries) + { + await _publisherService.SendMessage("Error: Connection retries exceeded.", ErrorRecordingQueueName); + return null; + } + + return []; + } + + public async Task SaveRequestAsync(ApiRequest apiRequest) + { + var retryCount = 1; + + while (retryCount <= _rabbitMqSettings.MaxRetries) + { + try + { + var token = await _authService.GetToken(); + + _logger.LogDebug("sending recording to Recording or api"); + var recordingRequest = apiRequest.Recording; + var request = new HttpRequestMessage(HttpMethod.Post, + $"{_recordingSettings.RecordingApiUrl}/videos/save-recording-data") + { + Content = new StringContent(JsonConvert.SerializeObject(recordingRequest), Encoding.UTF8) + }; + request.Headers.TryAddWithoutValidation("Authorization", $"Bearer {token}"); + + request.Content.Headers.ContentType = new MediaTypeHeaderValue("application/json"); + var response = await _httpClient.SendAsync(request); + _logger.LogDebug("Save Request to recording api status code:{responseStatusCode}", + response.StatusCode); + var responseContent = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + if (response.StatusCode == HttpStatusCode.Unauthorized) _accessGrant = null; + + _logger.LogError( + "error connecting to RecordingOrApi url:{requestRequestUri} errorCode: {responseStatusCode} error {responseContent}", + request.RequestUri, response.StatusCode, response.Content); + retryCount++; + } + else + { + // Response y en Data esta el videoDTO + var resp = JsonConvert.DeserializeObject>(responseContent); + var video = resp?.Data; + if (video == null) + return; + + RecordingData recording = new() + { + StartRecordingTime = video.StartDate, + StopRecordingTime = video.EndDate, + EventTime = video.Date, + RoomId = video.RoomId, + AlarmType = video.Alarm, + Store = video.Store, + Status = video.VideoStatus.ToString(), + Patient = new Domain.Models.Recording.Patient + { + FirstName = apiRequest.Patient?.FirstName ?? + apiRequest.Recording?.Patient?.FirstName ?? string.Empty, + Id = apiRequest.PatientNumber ?? apiRequest.Recording?.Patient?.Id ?? string.Empty, + LastName = apiRequest.Patient?.LastName ?? + apiRequest.Recording?.Patient?.LastName ?? string.Empty + } + }; + + _ = SendRecordingBroadcast([recording]); + return; + } + } + catch (HttpRequestException ce) + { + retryCount++; + _logger.LogError("Connection error sending recording data to queue. {ce} Retrying...", ce.Message); + await _publisherService.SendMessage(apiRequest, ErrorRecordingQueueName); + } + catch (Exception e) + { + _logger.LogError("Error sending recording request to Recording or {eMessage}", e.Message); + await _publisherService.SendMessage(apiRequest, ErrorRecordingQueueName); + await Task.FromException(e); + } + + if (retryCount > _rabbitMqSettings.MaxRetries) + await _publisherService.SendMessage( + $"Error: Connection retries exceeded. for api request: {apiRequest}", ErrorRecordingQueueName); + } + } + + public Task SaveRequest(ApiRequest apiRequest) + { + //return Task.CompletedTask; + throw new NotImplementedException(); + } + + private RecordingData? GenerateRecordingData(Patient patient, PointOfCare poc, DateTime? startDate, + DateTime? endDate, DateTime? eventTime, int? minToExpired, AlarmEnum.Name? alarmName, + AlarmEnum.Severity severity, + string alarmDescription, AlarmEnum.Type alarm = AlarmEnum.Type.Manual) + { + if (patient.PatientNumber == null) + { + _logger.LogError("Error Generating Recording Data . Patient number is null: {patient}", patient); + return null; + } + + + var roomId = poc.Configuration?.Id; + if (roomId == null) + { + _logger.LogError("Error Generating Recording Data on roomId"); + return null; + } + + + var patientRecording = new Domain.Models.Recording.Patient + { + Id = patient.PatientNumber, + FirstName = patient.Person?.FirstName ?? "X", + LastName = $"{patient.Person?.SecondName} {patient.Person?.LastName}".TrimEnd() + }; + + + if (!int.TryParse(roomId.ToString(), out var roomIdParsed)) + { + _logger.LogError("Error Generating Recording Data on roomId parse: {roomId}", roomId); + return null; + } + + var recordingData = new RecordingData + { + Patient = patientRecording, + RoomId = roomIdParsed, + StartRecordingTime = startDate, + StopRecordingTime = + endDate ?? (minToExpired != null ? DateTime.UtcNow.AddMinutes(minToExpired.Value) : null), + EventTime = eventTime, + AlarmType = alarm, + Severity = severity, + AlarmName = alarmName.HasValue ? EnumUtils.GetDescription(alarmName) : null, + AlarmDescription = alarmDescription, + Retry = 0 + }; + + recordingData.GenerateStore(); + return recordingData; + } + + + private async Task SendRecordingBroadcast(List recording) + { + if (!recording.IsNullOrEmpty()) + { + var patientNumber = recording[0].Patient?.Id; + if (string.IsNullOrEmpty(patientNumber)) + { + _logger.LogError("Box not found by RoomId {roomId}", recording[0].RoomId); + return; + } + + var patient = await _patientService.Value.FindByPatientNumber(patientNumber); + if (patient == null) + return; + + + var type = OperationType.RecordingStatus; + + var subscribers = _subscribersService.GetSubscribers().Where(s => + s.SubscriptionType == SubscriptionEnum.WsType.Box && s.Box == patient.Location.Bed && + s.Section == patient.Location.UnitName).ToList(); + + foreach (var subscriber in subscribers) + await _clientMessageService.SendAsync(subscriber.Id, type, recording); + } + } + + protected static string Base64Encode(string plainText) + { + var plainTextBytes = Encoding.UTF8.GetBytes(plainText); + return Convert.ToBase64String(plainTextBytes); + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/SchedulerService.cs b/adas-core.Application/Services/SchedulerService.cs new file mode 100644 index 00000000..90fe211f --- /dev/null +++ b/adas-core.Application/Services/SchedulerService.cs @@ -0,0 +1,1006 @@ +using System.Reflection; +using System.Text; +using adas_core.Application.Providers; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Providers; +using adas_core.Domain.Utils; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Quartz; +using Quartz.Impl; +using Serilog; +using ILogger = Serilog.ILogger; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Application.Services; + +public class SchedulerService +{ + private readonly bool _activateCheckExpiredAlerts; + private readonly bool _activateCheckExpiredObservations; + private readonly bool _activateCheckInactivePatients; + private readonly bool _activateSchedulerCheckActiveTreatmentsJob; + private readonly bool _activateSchedulerCheckActiveAppointmentsJob; + private readonly bool _activateSchedulerCheckHydricBalance; + private readonly bool _activateSchedulerCheckNewsJob; + private readonly bool _activateSchedulerCheckOpiateBoluses; + private readonly bool _activateSchedulerGetProvidersObservations; + + private readonly ArchiveNurseData _archiveNurseData; + private readonly int _archivePatientsWithoutObservationsSinceHours; + private readonly Lazy _calculatedObservationsService; + private readonly int _checkActiveTreatmentsSchedulerIntervalMinutes; + private readonly int _checkActiveAppointmentsSchedulerIntervalMinutes; + private readonly int _checkExpiredAlertsIntervalSeconds; + private readonly int _checkExpiredObservationsIntervalMinutes; + private readonly int _checkInactivePatientSchedulerIntervalHours; + + private readonly int _checkNewsJobIntervalMinutes; + private readonly Lazy _configObservationService; + + private readonly int _getProviderObservationsIntervalMinutes; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + private readonly Lazy _medicineService; + private readonly Lazy _observationService; + private readonly Lazy _patientProcedureService; + private readonly Lazy _patientService; + private readonly IOptions> _providersSettings; + private readonly int _sinceDischargeTimeToArchive; + private readonly Lazy _treatmentService; + private readonly Lazy _appointmentService; + private readonly Lazy _diagnosisService; + + + public SchedulerService(IOptions apiSettings, + IOptions> providersSettings, + Lazy patientService, + Lazy treatmentService, + Lazy appointmentService, + Lazy diagnosisService, + Lazy medicineService, + Lazy observationService, + Lazy configObservationService, + ILogger logger, + IHttpClientFactory httpClientFactory, + Lazy calculatedObservationsService, + Lazy patientProcedureService) + { + _patientService = patientService; + _treatmentService = treatmentService; + _appointmentService = appointmentService; + _diagnosisService = diagnosisService; + _medicineService = medicineService; + _observationService = observationService; + _configObservationService = configObservationService; + _providersSettings = providersSettings; + _httpClientFactory = httpClientFactory; + _calculatedObservationsService = calculatedObservationsService; + _patientProcedureService = patientProcedureService; + _logger = logger; + + //LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + _archivePatientsWithoutObservationsSinceHours = + apiSettings.Value.ArchivePatientsWithoutObservationsSinceHours ?? 24; + + var checkInactivePatientSchedulerIntervalHours = + apiSettings.Value.CheckInactivePatientSchedulerIntervalHours; + _checkInactivePatientSchedulerIntervalHours = checkInactivePatientSchedulerIntervalHours ?? 12; + + var checkActiveTreatmentsSchedulerIntervalMinutes = + apiSettings.Value.CheckActiveTreatmentsSchedulerIntervalMinutes; + _checkActiveTreatmentsSchedulerIntervalMinutes = checkActiveTreatmentsSchedulerIntervalMinutes ?? 5; + + var checkActiveAppointmentsSchedulerIntervalMinutes = + apiSettings.Value.CheckActiveAppointmentsSchedulerIntervalMinutes; + _checkActiveAppointmentsSchedulerIntervalMinutes = checkActiveAppointmentsSchedulerIntervalMinutes ?? 60; + + var getProvidersObservationsIntervalMinutes = + apiSettings.Value.GetProvidersObservationsSchedulerIntervalMinutes; + _getProviderObservationsIntervalMinutes = getProvidersObservationsIntervalMinutes ?? 5; + + + var sinceDischargeTimeToArchive = apiSettings.Value.SinceDischargeTimeToArchive; + _sinceDischargeTimeToArchive = sinceDischargeTimeToArchive ?? 48; + + var activateSchedulerCheckExpiredObservations = apiSettings.Value.ActivateSchedulerCheckExpiredObservation; + _activateCheckExpiredObservations = activateSchedulerCheckExpiredObservations; + + _activateCheckExpiredAlerts = apiSettings.Value.ActivateCheckExpiredAlerts; + + _activateCheckInactivePatients = apiSettings.Value.ActivateCheckInactivePatients; + + _activateSchedulerCheckActiveTreatmentsJob = apiSettings.Value.ActivateSchedulerCheckActiveTreatmentsJob; + + _activateSchedulerCheckActiveAppointmentsJob = apiSettings.Value.ActivateSchedulerCheckActiveAppointmentsJob; + + _activateSchedulerCheckOpiateBoluses = apiSettings.Value.ActivateSchedulerCheckOpiateBoluses; + + _activateSchedulerCheckHydricBalance = apiSettings.Value.ActivateSchedulerCheckHydricBalance; + + _activateSchedulerGetProvidersObservations = apiSettings.Value.ActivateSchedulerGetProvidersObservations; + + _archiveNurseData = apiSettings.Value.ArchiveNurseData; + + var schedulerCheckExpiredObservationsIntervalMinutes = + apiSettings.Value.SchedulerCheckExpiredObservationsIntervalMinutes; + _checkExpiredObservationsIntervalMinutes = schedulerCheckExpiredObservationsIntervalMinutes ?? 3; + + var schedulerCheckExpiredAlertsIntervalSeconds = + apiSettings.Value.SchedulerCheckExpiredAlertsIntervalSeconds; + _checkExpiredAlertsIntervalSeconds = schedulerCheckExpiredAlertsIntervalSeconds ?? 6; + + + _activateSchedulerCheckNewsJob = apiSettings.Value.ActivateSchedulerCheckNewsJob; + + + _checkNewsJobIntervalMinutes = + apiSettings.Value.CheckNewsJobIntervalMinutes; + + + Task.Run(InitScheduler); + } + + + private async Task InitScheduler() + { + //TODO con el cambio a .net core y el repaso a la inyección de dependencias de estructure map + //probar a inyectar la dependencia así https://dev.to/bohdanstupak1/dependency-injection-for-quartz-net-in-net-core-3oh7 + CheckInactivePatientsJob.PatientService = _patientService.Value; + CheckActiveTreatmentsJob.MedicineService = _medicineService.Value; + CheckActiveTreatmentsJob.PatientService = _patientService.Value; + CheckActiveTreatmentsJob.TreatmentService = _treatmentService.Value; + CheckActiveTreatmentsJob.CalculatedObservationsService = _calculatedObservationsService.Value; + + CheckActiveAppointmentsJob.AppointmentService = _appointmentService.Value; + CheckActiveAppointmentsJob.DiagnosisService = _diagnosisService.Value; + CheckActiveAppointmentsJob.PatientService = _patientService.Value; + + ArchiveNurseDataJob.PatientCarePlanService = _patientProcedureService.Value; + ArchiveNurseDataJob.PatientService = _patientService.Value; + ArchiveNurseDataJob.ArchiveNurseDataSettings = _archiveNurseData; + + CheckOpiateBolusesJob.PatientService = _patientService.Value; + CheckOpiateBolusesJob.CalculatedObservationsService = _calculatedObservationsService.Value; + + CheckHydricBalanceRyCJob.ObservationService = _observationService.Value; + CheckHydricBalanceRyCJob.PatientService = _patientService.Value; + CheckHydricBalanceRyCJob.CalculatedObservationsService = _calculatedObservationsService.Value; + + CheckExpiredObservationsJob.ObservationService = _observationService.Value; + + CheckExpiredAlertsJob.ObservationService = _observationService.Value; + + GetProvidersObservationsJob.ObservationService = _observationService.Value; + GetProvidersObservationsJob.PatientService = _patientService.Value; + GetProvidersObservationsJob.Providers = _providersSettings.Value; + GetProvidersObservationsJob.HttpClientFactory = _httpClientFactory; + + CalculateNewsJob.ObservationService = _observationService.Value; + CalculateNewsJob.ConfigObservationService = _configObservationService.Value; + CalculateNewsJob.PatientService = _patientService.Value; + CalculateNewsJob.ExpiresIn = _checkNewsJobIntervalMinutes; + + var jobDataMap = new JobDataMap + { + { "archivePatientsWithoutObservationsSinceHours", _archivePatientsWithoutObservationsSinceHours }, + { "sinceDischargeTime", _sinceDischargeTimeToArchive } + }; + + // Grab the Scheduler instance from the Factory + var factory = new StdSchedulerFactory(); + var scheduler = await factory.GetScheduler(); + // and start it off + await scheduler.Start(); + + var checkInactivePatientsJob = JobBuilder.Create() + .UsingJobData(jobDataMap) + .Build(); + + // Trigger the job to run now, and then repeat every 10 seconds + var checkInactivePatientsTrigger = TriggerBuilder.Create() + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInHours(_checkInactivePatientSchedulerIntervalHours) + .RepeatForever()) + .Build(); + + + var treatmentsJob = JobBuilder.Create() + .Build(); + + // Trigger the job to run now, and then repeat every 10 seconds + var treatmentsTrigger = TriggerBuilder.Create() + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInMinutes(_checkActiveTreatmentsSchedulerIntervalMinutes) + .RepeatForever()) + .Build(); + + + var appointmentsJob = JobBuilder.Create() + .Build(); + var appointmentsTrigger = TriggerBuilder.Create() + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInMinutes(_checkActiveAppointmentsSchedulerIntervalMinutes) + .RepeatForever()) + .Build(); + + //Job only for RYC hydric balance. + //Check every hour if hydric balance of that hour exists and recalculate. + var hydricBalanceJob = JobBuilder.Create() + .UsingJobData(jobDataMap) + .Build(); + + var datetimePlusOneHour = DateTime.UtcNow.AddHours(1); + + var hydricBalanceTrigger = TriggerBuilder.Create() + .StartAt(new DateTime(datetimePlusOneHour.Year, datetimePlusOneHour.Month, datetimePlusOneHour.Day, + datetimePlusOneHour.Hour, 0, 0)) + .WithSimpleSchedule(x => x + .WithIntervalInHours(1) + .RepeatForever()) + .Build(); + + + //Check_error_queues + var opiateBolusesJob = JobBuilder.Create() + .Build(); + + + var opiateBolusesTrigger = TriggerBuilder.Create() + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInMinutes(5) + .RepeatForever()) + .Build(); + + + //Job to expire expired observations + //Check every minute if observations are expired and recalculate. + var checkExpiredObservationsJob = JobBuilder.Create() + .Build(); + + + var checkExpiredObservationsTrigger = TriggerBuilder.Create() + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInMinutes(_checkExpiredObservationsIntervalMinutes) + .RepeatForever()) + .Build(); + + + //Job to expire Alert + //Check every minute if Alerts are expired and power off. + var checkExpiredAlertsJob = JobBuilder.Create() + .Build(); + + + var checkExpiredAlertsTrigger = TriggerBuilder.Create() + .StartAt(DateBuilder.FutureDate(30, IntervalUnit.Second)) // Comienza 30segundos después del inicio + .WithSimpleSchedule(x => x + //.WithIntervalInMinutes(_checkExpiredAlertsIntervalMinutes) + .WithIntervalInSeconds(_checkExpiredAlertsIntervalSeconds) + .RepeatForever()) + .Build(); + + + //Job to get Providers (ADAS) observations + //Check every minute if Alerts are expired and power off. + var getProvidersObservationsJob = JobBuilder.Create() + .Build(); + + + var getProvidersObservationsTrigger = TriggerBuilder.Create() + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInMinutes(_getProviderObservationsIntervalMinutes) + .RepeatForever()) + .Build(); + + + //Job to get Providers (ADAS) observations + //Check every minute if Alerts are expired and power off. + var calculateNewsJob = JobBuilder.Create() + .Build(); + + + var calculateNewsJobTrigger = TriggerBuilder.Create() + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInMinutes(_checkNewsJobIntervalMinutes) + .RepeatForever()) + .StartNow() + .Build(); + + //Job to ArchiveNurseDataConfig + //Check every 15 minute if Patients has data to archive. + var archiveNurseDataJob = JobBuilder.Create() + .Build(); + + + var archiveNurseDataJobTrigger = TriggerBuilder.Create() + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInMinutes(_archiveNurseData.IntervalMinutes) + .RepeatForever()) + .StartNow() + .Build(); + + var jobsDictionary = new Dictionary>(); + + + if (_activateCheckExpiredObservations) + jobsDictionary.Add(checkExpiredObservationsJob, new List { checkExpiredObservationsTrigger }); + + if (_activateCheckExpiredAlerts) + jobsDictionary.Add(checkExpiredAlertsJob, new List { checkExpiredAlertsTrigger }); + + if (_activateCheckInactivePatients) + jobsDictionary.Add(checkInactivePatientsJob, new List { checkInactivePatientsTrigger }); + + if (_activateSchedulerCheckActiveTreatmentsJob) + jobsDictionary.Add(treatmentsJob, new List { treatmentsTrigger }); + + if (_activateSchedulerCheckOpiateBoluses) + jobsDictionary.Add(opiateBolusesJob, new List { opiateBolusesTrigger }); + + if (_activateSchedulerCheckHydricBalance) + jobsDictionary.Add(hydricBalanceJob, new List { hydricBalanceTrigger }); + + if (_activateSchedulerGetProvidersObservations && _providersSettings.Value.Any()) + jobsDictionary.Add(getProvidersObservationsJob, new List { getProvidersObservationsTrigger }); + + if (_activateSchedulerCheckNewsJob) + jobsDictionary.Add(calculateNewsJob, new List { calculateNewsJobTrigger }); + + if (_archiveNurseData.Active) + jobsDictionary.Add(archiveNurseDataJob, new List { archiveNurseDataJobTrigger }); + + _logger.LogDebug("Tell to schedule the job using our trigger. {jobsDictionary}", jobsDictionary); + // Tell to schedule the job using our trigger + await scheduler.ScheduleJobs(jobsDictionary, true); + + // and last shut down the scheduler when you are ready to close your program + //await scheduler.Shutdown(); + } +} + +[DisallowConcurrentExecution] +public class CheckHydricBalanceRyCJob : IJob +{ + private readonly ILogger _logger = Log.ForContext(); + public static IObservationService ObservationService { get; set; } = null!; + public static IPatientService PatientService { get; set; } = null!; + public static ICalculatedObservationsService CalculatedObservationsService { get; set; } = null!; + + + public async Task Execute(IJobExecutionContext context) + { + var start = DateTime.Now; + + //LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + _logger.Debug("Start check hydric balance job"); + + try + { + var patients = await PatientService.FindAll(); + + foreach (var patient in patients) + { + var lastHydricBalance = await ObservationService.FindLastBeforeDate(patient.Id, + DateTime.UtcNow.AddMinutes(50), "Hydric_Balance"); + + if (lastHydricBalance != null && + DateTime.Compare( + new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, DateTime.UtcNow.Day, + DateTime.UtcNow.Hour, 0, 0), + new DateTime(lastHydricBalance.Time.Year, lastHydricBalance.Time.Month, + lastHydricBalance.Time.Day, lastHydricBalance.Time.Hour, 0, 0)) + == 0 + ) + await CalculatedObservationsService.Map(lastHydricBalance); + } + } + catch (Exception ex) + { + _logger.Error("ERROR executing check hydric balance job {exMessage}", ex.Message); + } + + var end = DateTime.Now; + _logger.Debug("Finished check hydric balance job in {TotalMilliseconds:F1} ms", + (end - start).TotalMilliseconds); + } +} + +[DisallowConcurrentExecution] +public class CheckActiveTreatmentsJob : IJob +{ + private readonly ILogger _logger = Log.ForContext(); + public static ITreatmentService TreatmentService { get; set; } = null!; + + public static IMedicineService MedicineService { get; set; } = null!; + + public static IPatientService PatientService { get; set; } = null!; + + public static ICalculatedObservationsService CalculatedObservationsService { get; set; } = null!; + + + public async Task Execute(IJobExecutionContext context) + { + var start = DateTime.Now; + + //LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + _logger.Debug("Executing check active treatments job"); + + + try + { + var patients = await PatientService.FindAll(); + + foreach (var patient in patients) + { + var activeMedicines = new List(); + //var treatments = treatmentService.GetActiveTreatmentsByPatient(patient.id); + + var treatments = + await CalculatedObservationsService.GetActiveTreatmentsByPatient(patient.Id); + + + var medicines = await MedicineService.GetMedicinesOfTreatments(treatments); + + var medicinesList = medicines.ToList(); + var medicinesWithoutNutrition = medicinesList.Where(m => !m.Type.Contains("Nutrition")) + .ToList(); + + activeMedicines.AddRange(medicinesWithoutNutrition); + + + await CalculatedObservationsService.CalculateMedicineObservation(activeMedicines, + patient.Id); + } + } + catch (Exception ex) + { + _logger.Error("ERROR executing check active treatments job {exMessage}", ex.Message); + } + + var end = DateTime.Now; + _logger.Debug("Finished check active treatments job in {TotalMilliseconds:F1} ms", + (end - start).TotalMilliseconds); + } +} + +[DisallowConcurrentExecution] +public class CheckActiveAppointmentsJob : IJob +{ + private readonly ILogger _logger = Log.ForContext(); + public static IAppointmentService AppointmentService { get; set; } = null!; + + public static IDiagnosisService DiagnosisService { get; set; } = null!; + + public static IPatientService PatientService { get; set; } = null!; + + + public async Task Execute(IJobExecutionContext context) + { + var start = DateTime.Now; + + _logger.Debug("Executing check active appointments job"); + + + try + { + //TODO falta definir + } + catch (Exception ex) + { + _logger.Error("ERROR executing check active treatments job {exMessage}", ex.Message); + } + + var end = DateTime.Now; + _logger.Debug("Finished check active treatments job in {TotalMilliseconds:F1} ms", + (end - start).TotalMilliseconds); + } +} +[DisallowConcurrentExecution] +public class CheckInactivePatientsJob : IJob +{ + private readonly ILogger _logger = Log.ForContext(); + + public static IPatientService PatientService { get; set; } = null!; + + public async Task Execute(IJobExecutionContext context) + { + //LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + var start = DateTime.Now; + + var dataMap = context.JobDetail.JobDataMap; + var archivePatientsWithoutObservationsSinceHours = + dataMap.GetInt("archivePatientsWithoutObservationsSinceHours"); + var sinceDischargeTime = dataMap.GetInt("sinceDischargeTime"); + var sinceDate = DateTime.Now.AddHours(-archivePatientsWithoutObservationsSinceHours); + + _logger.Debug( + "Executing Check inactive patients job archivePatientsWithoutObservationsSinceHours: {archivePatientsWithoutObservationsSinceHours} sinceDate: {sinceDate} sinceDischargeTime: {sinceDischargeTime}", + archivePatientsWithoutObservationsSinceHours, sinceDate, sinceDischargeTime); + try + { + var exists = GlobalData.Data.TryGetValue("isCheckingInactivePatients", out var isChecking); + if (!exists || isChecking is false) + await PatientService.DischargeInactivePatients(sinceDate, sinceDischargeTime); + } + catch (Exception ex) + { + _logger.Error("ERROR executing check inactive patients job {ExMessage}", ex.Message); + } + + var end = DateTime.Now; + _logger.Debug("Finished Check inactive patients job in {TotalMilliseconds:F1} ms", + (end - start).TotalMilliseconds); + } +} + +[DisallowConcurrentExecution] +public class CheckOpiateBolusesJob : IJob +{ + private readonly ILogger _logger = Log.ForContext(); + public static IPatientService PatientService { get; set; } = null!; + public static ICalculatedObservationsService CalculatedObservationsService { get; set; } = null!; + + public async Task Execute(IJobExecutionContext context) + { + var start = DateTime.Now; + + //LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + _logger.Debug("Executing check opiate boluses job"); + + try + { + var patients = await PatientService.FindAll(); + foreach (var patient in patients) await CalculatedObservationsService.CalculateBolusOpiates(patient.Id); + } + catch (Exception ex) + { + _logger.Error("ERROR executing check opiate boluses job {ExMessage} trace: {ExStackTrace}", ex.Message, + ex.StackTrace); + } + + var end = DateTime.Now; + + _logger.Debug("Finished Check opiate boluses job in {TotalSeconds:F1} seconds", (end - start).TotalSeconds); + } +} + +[DisallowConcurrentExecution] +public class CheckExpiredObservationsJob : IJob +{ + private readonly ILogger _logger = Log.ForContext(); + + public static IObservationService ObservationService { get; set; } = null!; + + + public async Task Execute(IJobExecutionContext context) + { + var start = DateTime.Now; + + //LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + _logger.Debug("Executing check expired observations job"); + + try + { + var exists = GlobalData.Data.TryGetValue("isCheckingExpiration", out var isChecking); + if (!exists) + { + GlobalData.AddData("isCheckingExpiration", false); + isChecking = false; + } + + if (!exists || isChecking is false) await ObservationService.ExpireObservationsAndRecalculateAsync(); + } + catch (Exception ex) + { + _logger.Error("ERROR executing check expired observations {ExMessage} trace: {ExStackTrace}", + ex.Message, ex.StackTrace); + } + + var end = DateTime.Now; + _logger.Debug("Finished Check expired observations job in {TotalSeconds:F1} seconds", + (end - start).TotalSeconds); + } +} + +/// +/// Job for calculate NEWS based on FR, SPo2, Temperature, Tas and FC +/// +[DisallowConcurrentExecution] +public class CalculateNewsJob : IJob +{ + private readonly ILogger _logger = Log.ForContext(); + + public static IObservationService ObservationService { get; set; } = null!; + + public static IConfigObservationService ConfigObservationService { get; set; } = null!; + + public static IPatientService PatientService { get; set; } = null!; + + public static int ExpiresIn { get; set; } = 15; + + + public async Task Execute(IJobExecutionContext context) + { + //Filter patients in real locations + //LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + var patients = await PatientService.FindInActivePoC(); + var observationList = new List { "Resp_Rate_Calculated", "SpO2", "Temperature", "TAs", "FC" }; + + foreach (var patient in patients) + { + var observations = + await CheckExpiredAlertsJob.ObservationService.FindLastObservations(patient.Id, 1, observationList); + if (!observations.Any()) continue; + var newsScore = 0; + var msg = new StringBuilder(); + _logger.Debug("CalculateNewsJob - patient ({patientPointOfCare} {patientBed})", patient.UnitString, + patient.Bed); + foreach (var obs in observations) + { + var conf = await ConfigObservationService.Get(obs); + var expires = conf?.Expires; + if (obs.Expired || (expires != null && + obs.Time.AddSeconds(expires.Value).ToUniversalTime() < DateTime.UtcNow)) + { + _logger.Verbose( + "CalculateNewsJob - patient ({patientPointOfCare} {patientBed}) obs ({obsName} value {obsValue} time {obsTime} expired {obsExpired}) confexpires: {expires} EXPIRED", + patient.UnitString, patient.Bed, obs.Name, obs.Value, obs.Time, obs.Expired, expires); + continue; + } + + _logger.Verbose( + "CalculateNewsJob - patient ({patientPointOfCare} {patientBed}) obs ({obsName} value {obsValue} time {obsTime} expired {obsExpired}) confexpires: {expires} NOT EXPIRED", + patient.UnitString, patient.Bed, obs.Name, obs.Value, obs.Time, obs.Expired, expires); + var partialNewsScore = 0; + switch (obs.Name) + { + case "Resp_Rate_Calculated": + if (!double.TryParse(obs.Value.ToString(), out var frValue)) continue; + switch (Math.Round(frValue, 0)) + { + case >= 12 and <= 20: + break; + case >= 25: + case <= 8: + partialNewsScore = 3; + break; + case >= 9 and <= 11: + partialNewsScore = 1; + break; + case >= 21 and <= 24: + partialNewsScore = 2; + break; + } + + break; + case "SpO2": + if (!double.TryParse(obs.Value.ToString(), out var spo2Value)) continue; + switch (Math.Round(spo2Value, 0)) + { + case <= 91: + partialNewsScore = 3; + break; + case >= 92 and <= 93: + partialNewsScore = 2; + break; + case >= 94 and <= 95: + partialNewsScore = 1; + break; + } + + break; + case "Temperature": + if (!double.TryParse(obs.Value.ToString(), out var tempValue)) continue; + switch (Math.Round(tempValue, 1)) + { + case <= 35: + partialNewsScore = 3; + break; + case >= 35.1 and <= 36.0: + case >= 38.1 and <= 39.0: + partialNewsScore = 1; + break; + case >= 39.1: + partialNewsScore = 2; + break; + } + + break; + case "TAs": + if (!double.TryParse(obs.Value.ToString(), out var tasValue)) continue; + switch (Math.Round(tasValue, 0)) + { + case <= 90: + case >= 220: + partialNewsScore = 3; + break; + case >= 91 and <= 100: + partialNewsScore = 2; + break; + case >= 101 and <= 110: + partialNewsScore = 1; + break; + } + + break; + case "FC": + if (!double.TryParse(obs.Value.ToString(), out var fcValue)) continue; + switch (Math.Round(fcValue, 0)) + { + case <= 40: + case >= 131: + partialNewsScore = 3; + break; + case >= 111 and <= 130: + partialNewsScore = 2; + break; + case >= 41 and <= 50: + case >= 91 and <= 110: + partialNewsScore = 1; + break; + } + + break; + } + + msg.Append($"[{obs.Name} {obs.Value} {partialNewsScore}] "); + newsScore += partialNewsScore; + } + + var lastObs = + await CheckExpiredAlertsJob.ObservationService.FindLastObservations(patient.Id, 1, observationList); + + if (lastObs.FirstOrDefault()?.Expired != true) + { + var newObs = new PatientObservation + { + Name = "NEWS", + CodingSystem = "ADAS", + Expires = ExpiresIn, + Value = newsScore, + PatientId = patient.Id, + Time = DateTime.Now + }; + await CheckExpiredAlertsJob.ObservationService.InsertObservation(newObs); + _logger.Debug( + $"CalculateNewsJob - patient ({patient.UnitString} {patient.Bed}) newsScore: {newsScore} from {msg}"); + } + } + } +} + +[DisallowConcurrentExecution] +public class CheckExpiredAlertsJob : IJob +{ + private readonly ILogger _logger = Log.ForContext(); + + public static IObservationService ObservationService { get; set; } = null!; + + + public async Task Execute(IJobExecutionContext context) + { + var start = DateTime.Now; + + //LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + _logger.Debug("Executing check expired alerts and power off job"); + + try + { + var exists = GlobalData.Data.TryGetValue("isCheckingAlertsExpiration", out var isChecking); + if (!exists || isChecking is false) await ObservationService.ExpireAlertsAndPowerOffAsync(); + } + catch (Exception ex) + { + _logger.Error("ERROR executing check expired alerts and power off {ExMessage} trace: {ExStackTrace}", + ex.Message, + ex.StackTrace); + } + + var end = DateTime.Now; + _logger.Debug("Finished Check check expired alerts and power off job in {TotalSeconds:F1} seconds", + (end - start).TotalSeconds); + } +} + +[DisallowConcurrentExecution] +public class ArchiveNurseDataJob : IJob +{ + private readonly ILogger _logger = Log.ForContext(); + + //private readonly ArchiveNurseData _archiveNurseData; + public static ArchiveNurseData ArchiveNurseDataSettings { get; set; } = new(); + + public static IPatientService PatientService { get; set; } = null!; + + public static IPatientCarePlanService PatientCarePlanService { get; set; } = null!; + + public async Task Execute(IJobExecutionContext context) + { + var start = DateTime.Now; + _logger.Debug("Executing ArchiveNurseDataJob job"); + + try + { + if (ArchiveNurseDataSettings.ArchiveProcedure.Active) + { + var patientWithFinishedProcedures = + await PatientService.FindAllPatientWithFinishedProcedures(ArchiveNurseDataSettings.ArchiveProcedure + .EndDateAfterMinutes); + foreach (var patientWithFinishedProcedure in patientWithFinishedProcedures) + { + var itemsToArchive = patientWithFinishedProcedure.Procedures? + .Where(c => + c is + { + //OptionType: "procedure", + EndDate: not null + } && + c.EndDate.Value.AddMinutes(ArchiveNurseDataSettings.ArchiveProcedure.EndDateAfterMinutes) < + DateTime.UtcNow) + .ToList(); + + if (itemsToArchive != null) + { + _ = PatientCarePlanService.ArchiveCarePlanFromJob(patientWithFinishedProcedure, + itemsToArchive); + patientWithFinishedProcedure.Procedures = patientWithFinishedProcedure.Procedures? + .Where(c => !itemsToArchive.Contains(c)).ToList(); + await PatientService.Update(patientWithFinishedProcedure); + } + } + } + + if (ArchiveNurseDataSettings.ArchiveTest.Active) + { + var patientWithFinishedTests = + await PatientService.FindAllPatientWithFinishedTest(ArchiveNurseDataSettings.ArchiveTest + .EndDateAfterMinutes); + foreach (var patientWithFinishedTest in patientWithFinishedTests) + { + var itemsToArchive = patientWithFinishedTest.Tests? + .Where(c => + c is + { + //OptionType: "test", + EndDate: not null + } && + c.EndDate.Value.AddMinutes(ArchiveNurseDataSettings.ArchiveTest.EndDateAfterMinutes) < + DateTime.UtcNow) + .ToList(); + + if (itemsToArchive != null) + { + _ = PatientCarePlanService.ArchiveCarePlanFromJob(patientWithFinishedTest, + itemsToArchive); + patientWithFinishedTest.Tests = patientWithFinishedTest.Tests? + .Where(c => !itemsToArchive.Contains(c)).ToList(); + await PatientService.Update(patientWithFinishedTest); + } + } + } + + if (ArchiveNurseDataSettings.ArchiveTreatment.Active) + { + var patientWithFinishedTreatments = + await PatientService.FindAllPatientWithFinishedTreatment(ArchiveNurseDataSettings.ArchiveTreatment + .EndDateAfterMinutes); + foreach (var patientWithFinishedTreatment in patientWithFinishedTreatments) + { + var itemsToArchive = patientWithFinishedTreatment.Treatment? + .Where(c => + c.EndDate.HasValue && + c.EndDate.Value.AddMinutes(ArchiveNurseDataSettings.ArchiveTest.EndDateAfterMinutes) < + DateTime.UtcNow) + .ToList(); + + if (itemsToArchive != null) + { + _ = PatientCarePlanService.ArchiveCarePlanFromJob(patientWithFinishedTreatment, + itemsToArchive); + patientWithFinishedTreatment.Treatment = patientWithFinishedTreatment.Treatment? + .Where(c => !itemsToArchive.Contains(c)) + .ToList(); + await PatientService.Update(patientWithFinishedTreatment); + } + } + } + } + catch (Exception ex) + { + _logger.Error("ERROR executing ArchiveNurseDataJob {ExMessage} trace: {ExStackTrace}", + ex.Message, + ex.StackTrace); + } + + var end = DateTime.Now; + _logger.Debug("Finished ArchiveNurseDataJob {TotalSeconds:F1} seconds", + (end - start).TotalSeconds); + } +} + +[DisallowConcurrentExecution] +public class GetProvidersObservationsJob : IJob +{ + private readonly ILogger _logger = Log.ForContext(); + + public static IObservationService ObservationService { get; set; } = null!; + + public static IPatientService PatientService { get; set; } = null!; + + public static List Providers { get; set; } = null!; + + + public static IHttpClientFactory HttpClientFactory { get; set; } = null!; + + public async Task Execute(IJobExecutionContext context) + { + var start = DateTime.Now; + _logger.Debug("Executing get providers job"); + + try + { + if (!Providers.Any()) return; + + foreach (var provider in Providers) + { + //replace - to _ to fit name + var providerType = + Type.GetType( + $"{Assembly.GetExecutingAssembly().GetName().Name?.Replace("-", "_")}.Providers.{provider.Name}Provider"); + if (providerType == null) + { + Log.Error("Provider {ProviderName} not able lo load by reflection", provider.Name); + continue; + } + + + var ctor = providerType.GetConstructor([ + typeof(IOptions), + typeof(IHttpClientFactory) + ]); + if (ctor == null) + { + Log.Error("not able to find constructor for provider: {ProviderName}", provider.Name); + continue; + } + + var customProvider = (BaseProvider)ctor.Invoke([ + Options.Create(provider), + HttpClientFactory + ]); + + var patients = await PatientService.FindAll(); + + foreach (var patient in patients) + { + var observations = await customProvider.GetObservations(patient); + if (observations.Any()) observations.ForEach(o => ObservationService.InsertObservation(o)); + } + } + } + catch (Exception ex) + { + _logger.Error( + "ERROR executing GetByCodeSysAndCode providers observations job error: {ExMessage} trace: {ExStackTrace}", + ex.Message, ex.StackTrace); + } + + var end = DateTime.Now; + _logger.Debug("Finished GetByCodeSysAndCode providers observations job in {TotalMilliseconds:F1} ms", + (end - start).TotalMilliseconds); + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/ServiceConfigService.cs b/adas-core.Application/Services/ServiceConfigService.cs new file mode 100644 index 00000000..bd9407ab --- /dev/null +++ b/adas-core.Application/Services/ServiceConfigService.cs @@ -0,0 +1,30 @@ +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.MongoModels; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; + +namespace adas_core.Application.Services; + +public class ServiceConfigService( + IServiceConfigRepository serviceConfigRepository, + ILogger logger) + : IServiceConfigService +{ + private readonly ILogger _logger = logger; + + + public async Task Get(string id) + { + var result = await serviceConfigRepository.FindById(id); + if (result == null && ObjectId.TryParse(id, out var oid)) + result = await serviceConfigRepository.FindById(oid) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + + _logger.LogDebug("ServiceConfig {id} found: {result}", id, result != null); + + return result; + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/SubscribersService.cs b/adas-core.Application/Services/SubscribersService.cs new file mode 100644 index 00000000..9f46bf2a --- /dev/null +++ b/adas-core.Application/Services/SubscribersService.cs @@ -0,0 +1,52 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Application.Subscriptions; +using MongoDB.Bson; + +namespace adas_core.Application.Services; + +public class SubscribersService : ISubscribersService +{ + private readonly List _subscribers = []; + + + public List GetSubscribers() + { + lock (_subscribers) + { + return _subscribers.ToList(); + } + } + + + public WsSubscriber? GetById(string contextConnectionId) + { + lock (_subscribers) + { + return _subscribers.FirstOrDefault(s => s.Id == contextConnectionId); + } + } + + public List GetByPocId(ObjectId pocId) + { + lock (_subscribers) + { + return _subscribers.Where(s => s.LocationIds.Contains(pocId)).ToList(); + } + } + + public int RemoveConnectionById(string contextConnectionId) + { + lock (_subscribers) + { + return _subscribers.RemoveAll(s => s.Id == contextConnectionId); + } + } + + public void AddSubscriber(WsSubscriber subscriber) + { + lock (_subscribers) + { + _subscribers.Add(subscriber); + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/TreatmentService.cs b/adas-core.Application/Services/TreatmentService.cs new file mode 100644 index 00000000..ae762671 --- /dev/null +++ b/adas-core.Application/Services/TreatmentService.cs @@ -0,0 +1,288 @@ +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.Filter; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Application.Services; + +public class TreatmentService( + ITreatmentRepository treatmentRepository, + ITreatmentArchiveRepository treatmentArchiveRepository, + IPatientService patientService, + IConfigObservationService configObservationService, + ILogger logger, + IClientMessageService clientMessageService, + ISubscribersService subscribersService, + Lazy calculatedObservationsService, + IUnitService unitService, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService) + : ITreatmentService +{ + public async Task> GetTreatmentsByPatientId(ObjectId id) + { + return await treatmentRepository.GetByPatientId(id); + } + + public async Task Insert(PatientTreatment treatment) + { + logger.LogDebug("Insert {treatment}", treatment); + var mappedTreatment = await MapTreatment(treatment); + if (mappedTreatment != null) + { + await treatmentRepository.InsertOneAsync(mappedTreatment); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, mappedTreatment); + _ = SendBroadcast(mappedTreatment, OperationType.Treatment); + } + } + + public async Task DeleteByPatientId(ObjectId id) + { + var treatmentToDelete = await FindByPatientId(id) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); + + if (!treatmentToDelete.Any()) + return true; + + logger.LogDebug("Delete Treatments by Patient Id {id}", id); + var result = await treatmentRepository.DeleteByPatientId(id); + if (!result) throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, treatmentToDelete, null); + return result; + } + + public async Task Archive(Patient patient) + { + await ArchiveByPatientId(patient.Id); + } + + public async Task ArchiveByPatientId(ObjectId id) + { + logger.LogDebug("ArchiveB Treatments by Patient Id PatientId {id}", id); + using (var cursor = await FindByPatientIdAsync(id)) + { + while (await cursor.MoveNextAsync()) + foreach (var current in cursor.Current) + await treatmentArchiveRepository.InsertOneAsync(current); + } + + await DeleteByPatientId(id); + } + + public async Task UpdateTreatment(PatientTreatment patientTreatment) + { + var oldTreatment = await treatmentRepository.GetById(patientTreatment.Id) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + logger.LogDebug("Updating Treatment {treatment}", patientTreatment.ToJson()); + var result = await treatmentRepository.Update(patientTreatment); + if (!result) throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldTreatment, patientTreatment); + + _ = SendBroadcast(patientTreatment, OperationType.UpdateTreatment); + + return result; + } + + public async Task> FindByPatientIdAsync(ObjectId patientId) + { + return await treatmentRepository.FindByPatientIdAsync(patientId); + } + + public async Task> FindByPatientId(ObjectId patientId) + { + return await treatmentRepository.FindByPatientId(patientId); + } + + public async Task> GetBolusTreatments(ObjectId patientId) + { + return await treatmentRepository.FindBolusTreatments(patientId); + } + + public async Task SaveRequestAsync(ApiRequest apiRequest) + { + await SaveRequest(apiRequest); + } + + public async Task SaveRequest(ApiRequest apiRequest) + { + if (string.IsNullOrEmpty(apiRequest.PatientNumber)) + { + logger.LogDebug("Patient is null"); + throw new ApiRequestException("Patient is null"); + } + + logger.LogDebug("patientNumber: {apiRequestpatientNumber}", apiRequest.PatientNumber); + var unitConfig = await unitService.FindByName(apiRequest.Location?.UnitName); + if (unitConfig != null) + { + if (!unitConfig.Configuration.AutoAdt && !apiRequest.RequestFromPanel) + { + logger.LogWarning("Unit: {UnitName} with auto adt set to false can't manage auto ADT", + apiRequest.Location?.UnitName); + return; + } + } + else + { + logger.LogWarning("Unit not found in Mapping list: {UnitName} can't manage auto ADT", + apiRequest.Location?.UnitName); + return; + } + + try + { + switch (apiRequest.Type) + { + /* + * OMP EVENT + * OMP_O09 - Pharmacy/treatment order + * ORM_O01 - Order message + * RAS_O17 - Pharmacy/treatment administration + */ + case "OMP_O09": + case "ORM_O01": + case "RAS_O17": + var patient = await patientService.FindByPatientNumber(apiRequest.PatientNumber); + if (patient == null) + { + logger.LogDebug( + "Ignore treatment: {apiRequest.treatments} for patient: {apiRequest.patientNumber}, patient not found", + apiRequest.Treatment, apiRequest.PatientNumber); + return; + } + + apiRequest.Treatments ??= []; + + if (apiRequest.Treatment != null) apiRequest.Treatments.Add(apiRequest.Treatment); + + foreach (var treatment in apiRequest.Treatments) + { + treatment.PatientId = patient.Id; + treatment.MessageTime = apiRequest.MessageTime; + await Insert(treatment); + } + + break; + default: + throw new ApiRequestException("ApiRequest type " + apiRequest.Type + + " is not valid for Treatments"); + } + } + catch (Exception ex) + { + logger.LogError("ERROR SAVING REQUEST: {exMessage} trace: {exStackTrace}", ex.Message, ex.StackTrace); + throw; + } + } + + /* + * Return active treatments of patient PatientType active = NW, canceled= DC + */ + public async Task> GetActiveTreatmentsByPatient(ObjectId id) + { + var treatments = await treatmentRepository.GetByPatientId(id); + + var activeTreatments = treatments + .Where(IsValidTreatment) + .GroupBy(t => t.PlacerOrder?.EntityIdentifier) + .Select(GetMostRecentActiveTreatment) + .Where(t => t != null) + .AsEnumerable(); + + return activeTreatments; + } + + public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId) + { + await treatmentRepository.UpdateManyObjectId(nameId, id, oldId); + + var treatments = await treatmentRepository.FindByPatientId(id); + foreach (var treat in treatments) _ = SendBroadcast(treat, OperationType.UpdateTreatment); + } + + + public async Task> GetPaginatedTreatments(PaginationFilter filter) + { + var result = treatmentRepository.GetPaginatedTreatments(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(dataList, filter.PageNumber, filter.PageSize, count); + } + + public async Task> GetActiveTreatmentsByPatientIdAndOrder(ObjectId patientId, string order) + { + return await treatmentRepository.GetActiveTreatmentsByPatientIdAndOrder(patientId, order); + } + + + public async Task DeleteById(ObjectId id) + { + var oldTreatment = await treatmentRepository.GetById(id) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldTreatment, null); + await treatmentRepository.DeleteAsync(id); + } + + private async Task MapTreatment(PatientTreatment treatment) + { + var treatment2 = await calculatedObservationsService.Value.Map(treatment); + if (treatment2 == null) return null; + var treatment3 = await configObservationService.Map(treatment2); + if (treatment3 == null) logger.LogDebug("Mapping {treatment}: Ignored", treatment); + + return treatment; + } + + + private async Task SendBroadcast(PatientTreatment treatment, OperationType operationType) + { + var patient = await patientService.FindById(treatment.PatientId); + + if (patient?.PointOfCareId == null) return; + + var subscribers = subscribersService.GetSubscribers() + .Where(s => s.LocationIds.Contains(patient.PointOfCareId.Value)).ToList(); + + + foreach (var subscriber in subscribers) + await clientMessageService.SendAsync(subscriber.Id, operationType, treatment); + } + + private static bool IsValidTreatment(PatientTreatment treatment) + { + if (treatment.PlacerOrder == null) + return false; + + var currentTime = DateTime.UtcNow; + if (treatment.EndTime != null && currentTime.CompareTo(treatment.EndTime) > 0) + return false; + if (treatment.StartTime != null && currentTime.CompareTo(treatment.StartTime) < 0) + return false; + + return treatment.OrderControl != OrderControlType.Dc; + } + + private static PatientTreatment? GetMostRecentActiveTreatment(IGrouping group) + { + return group + .Where(t => t.OrderControl is OrderControlType.Nw or OrderControlType.Xo) + .OrderByDescending(t => t.OrderTime) + .FirstOrDefault(); + } +} \ No newline at end of file diff --git a/adas-core.Application/Services/UnitService.cs b/adas-core.Application/Services/UnitService.cs new file mode 100644 index 00000000..567411fa --- /dev/null +++ b/adas-core.Application/Services/UnitService.cs @@ -0,0 +1,434 @@ +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.DTO; +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 Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; +using MongoDB.Driver; +using Serilog; + +namespace adas_core.Application.Services; + +public class UnitService( + IUnitRepository unitRepository, + Lazy patientService, + ILogger logger, + IMasterListServiceFactory masterListServiceFactory, + ISubscribersService subscribersService, + Lazy clientMessageService, + IPointOfCareService pointOfCareService, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService) + : IUnitService +{ + public async Task> GetAll(bool withPoCs = false) + { + var units = await unitRepository.GetAll(); + + if (withPoCs) + foreach (var unit in units) + { + var pocList = await pointOfCareService.FindAllByUnitId(unit.Id); + unit.PointOfCares = pocList?.ToList(); + } + + return units; + } + + public async Task> GetAllCompact() + { + var units = await unitRepository.GetAll(); + var result = new List(); + foreach (var unit in units) + result.Add(new UnitInfoDto + { + Id = unit.Id, + Name = unit.Name ?? string.Empty, + Title = unit.Title ?? string.Empty + }); + return result; + } + + public async Task GetOneCompact(ObjectId id) + { + var unit = await unitRepository.FindById(id); + var result = new UnitInfoDto + { + Id = unit?.Id, + Name = unit?.Name ?? string.Empty, + Title = unit?.Title ?? string.Empty + }; + return result; + } + + public async Task> GetPaginatedUnits(PaginationFilter filter, bool withPoCs = false) + { + var result = unitRepository.GetPaginatedUnits(filter); + + var count = await result.CountDocumentsAsync(); + + var data = await result.Skip((filter.PageNumber - 1) * filter.PageSize) + .Limit(filter.PageSize) + .ToListAsync(); + + + if (withPoCs && data.Any()) + { + var unitIds = data.Select(u => u.Id).ToList(); + var allPocsForUnits = await pointOfCareService.FindAllByUnitIds(unitIds); + + foreach (var unit in data) unit.PointOfCares = allPocsForUnits.Where(poc => poc.UnitId == unit.Id).ToList(); + } + + return new PaginationResponse(data, filter.PageNumber, filter.PageSize, count); + } + + // public Task GetByPointOfCare(PointOfCare pointOfCare) + // { + // return _unitRepository.FindByPointOfCare(pointOfCare); + // } + + public async Task GetInfo(ObjectId id, LocaleEnum? dataLocale, bool fillLists = true, bool withPoCs = false) + { + var unit = await Get(id.ToString()) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + //if (unit == null )// || unit.PointOfCares == null tiene sentido? + //{ + // _logger.LogError("Section not found: {id}", id); + // return null; + //} + + if (fillLists) + { + if (unit.AltableOptionListId.HasValue) + unit.AltableOptionList = + await masterListServiceFactory.GetMasterListById(MasterListType.AltableOptionList, + unit.AltableOptionListId.Value, dataLocale) as AltableOptionList; + if (unit.AllergyListId.HasValue) + unit.AllergyList = + await masterListServiceFactory.GetMasterListById(MasterListType.AllergyList, + unit.AllergyListId.Value, dataLocale) as AllergyList; + if (unit.DestinationListId.HasValue) + unit.DestinationList = + await masterListServiceFactory.GetMasterListById(MasterListType.DestinationList, + unit.DestinationListId.Value, dataLocale) as DestinationList; + if (unit.InternalDestinationListId.HasValue) + unit.InternalDestinationList = + await masterListServiceFactory.GetMasterListById(MasterListType.InternalDestinationList, + unit.InternalDestinationListId.Value, dataLocale) as InternalDestinationList; + if (unit.DiagnosisListId.HasValue) + unit.DiagnosisList = + await masterListServiceFactory.GetMasterListById(MasterListType.DiagnosisList, + unit.DiagnosisListId.Value, dataLocale) as DiagnosisList; + if (unit.DischargeStatusListId.HasValue) + unit.DischargeStatusList = + await masterListServiceFactory.GetMasterListById(MasterListType.DischargeStatusList, + unit.DischargeStatusListId.Value, dataLocale) as DischargeStatusList; + if (unit.DoctorListId.HasValue) + unit.DoctorList = + await masterListServiceFactory.GetMasterListById(MasterListType.DoctorList, unit.DoctorListId.Value, + dataLocale) as DoctorList; + if (unit.DoctorTypeListId.HasValue) + unit.DoctorTypeList = + await masterListServiceFactory.GetMasterListById(MasterListType.DoctorTypeList, + unit.DoctorTypeListId.Value, dataLocale) as DoctorTypeList; + if (unit.InsulationListId.HasValue) + unit.InsulationList = + await masterListServiceFactory.GetMasterListById(MasterListType.InsulationList, + unit.InsulationListId.Value, dataLocale) as InsulationList; + if (unit.MobilityOptionListId.HasValue) + unit.MobilityOptionList = + await masterListServiceFactory.GetMasterListById(MasterListType.MobilityOptionList, + unit.MobilityOptionListId.Value, dataLocale) as MobilityOptionList; + if (unit.OriginListId.HasValue) + unit.OriginList = + await masterListServiceFactory.GetMasterListById(MasterListType.OriginList, unit.OriginListId.Value, + dataLocale) as OriginList; + if (unit.PatientStatusListId.HasValue) + unit.PatientStatusList = + await masterListServiceFactory.GetMasterListById(MasterListType.PatientStatusList, + unit.PatientStatusListId.Value, dataLocale) as PatientStatusList; + if (unit.ProcedureListId.HasValue) + unit.ProcedureList = + await masterListServiceFactory.GetMasterListById(MasterListType.ProcedureList, + unit.ProcedureListId.Value, dataLocale) as ProcedureList; + if (unit.TestListId.HasValue) + unit.TestList = + await masterListServiceFactory.GetMasterListById(MasterListType.TestList, unit.TestListId.Value, + dataLocale) as TestList; + if (unit.ServiceListId.HasValue) + unit.ServiceList = + await masterListServiceFactory.GetMasterListById(MasterListType.ServiceList, + unit.ServiceListId.Value, dataLocale) as ServiceList; + if (unit.TherapeuticCeilingListId.HasValue) + unit.TherapeuticCeilingList = + await masterListServiceFactory.GetMasterListById(MasterListType.TherapeuticCeilingList, + unit.TherapeuticCeilingListId.Value, dataLocale) as TherapeuticCeilingList; + if (unit.TreatmentListId.HasValue) + unit.TreatmentList = + await masterListServiceFactory.GetMasterListById(MasterListType.TreatmentList, + unit.TreatmentListId.Value, dataLocale) as TreatmentList; + if (unit.VisitOptionListId.HasValue) + unit.VisitOptionList = + await masterListServiceFactory.GetMasterListById(MasterListType.VisitOptionList, + unit.VisitOptionListId.Value, dataLocale) as VisitOptionList; + if (unit.AccessControlListId.HasValue) + unit.AccessControlList = + await masterListServiceFactory.GetMasterListById(MasterListType.AccessControlList, + unit.AccessControlListId.Value, dataLocale) as AccessControlList; + if (unit.LanguageBarrierListId.HasValue) + unit.LanguageBarrierList = + await masterListServiceFactory.GetMasterListById(MasterListType.LanguageBarrierList, + unit.LanguageBarrierListId.Value, dataLocale) as LanguageBarrierList; + if (unit.PassiveSittingListId.HasValue) + unit.PassiveSittingList = + await masterListServiceFactory.GetMasterListById(MasterListType.PassiveSittingList, + unit.PassiveSittingListId.Value, dataLocale) as PassiveSittingList; + if (unit.GenericListId.HasValue) + unit.GenericList = + await masterListServiceFactory.GetMasterListById(MasterListType.GenericList, + unit.GenericListId.Value, dataLocale) as GenericList; + } + + if (withPoCs) + { + var pocList = await pointOfCareService.FindAllByUnitId(unit.Id); + unit.PointOfCares = pocList?.ToList(); + } + + return unit; + } + + public async Task GetInfo(ObjectId id, bool withPoCs = true, bool withDevices = true) + { + var unit = await Get(id.ToString()) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + if (withPoCs) + { + var pocList = await pointOfCareService.FindAllByUnitIdWithDevices(unit.Id); + unit.PointOfCares = pocList?.ToList(); + } + + return unit; + } + + public async Task GetByName(string itemUnitName) + { + return await unitRepository.FindByName(itemUnitName) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + } + + public async Task FindByPatientId(ObjectId patientId) + { + //var sections = await GetAll(); + var patient = await patientService.Value.FindById(patientId); + // if (patient != null && !string.IsNullOrEmpty(patient.Bed) && patient.IsInActivePoC()) + // { + // // Deberia ser una lista? revisar como gestionar varias unidades con eel mismo pointOfCare + // return sections.FirstOrDefault(s => s.PointOfCares.Any(c=>c.Bed==patient.Bed && c.UnitName == patient.UnitString)); + // } + if (patient is { UnitId: not null }) return await FindById(patient.UnitId); + + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + } + + public async Task?> FindUnitsByMasterListId(ObjectId masterListId, MasterListType masterListType) + { + try + { + var result = await unitRepository.FindByMasterListId(masterListId, masterListType); + + return result; + } + catch (Exception ex) + { + Log.Error("Exception finding by MasterId{id} section exception:{e} ", masterListId, ex.Message); + + return null; + } + } + + public async Task CountUnitsByMasterListId(ObjectId masterListId, MasterListType masterListType) + { + return await unitRepository.CountUnitsByMasterListId(masterListId, masterListType); + } + + public async Task> FindUnitsByMasterListId(ObjectId masterListId) + { + try + { + var result = await unitRepository.FindByMasterListId(masterListId); + + return result; + } + catch (Exception ex) + { + Log.Error("Exception finding by MasterId{id} section exception:{e} ", masterListId, ex.Message); + + return new List(); + } + } + + public async Task UpdateUnitMasterList(UpdateUnitIdListDto updateUnitListDto) + { + var unit = await FindById(updateUnitListDto.UnitId) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + var updatedUnit = await unitRepository.UpdateUnitMasterList(updateUnitListDto) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, unit, updatedUnit); + return updatedUnit; + } + + public async Task UpdateConfiguration(ObjectId unitIdParsed, UnitConfiguration unitConfiguration) + { + var oldConfig = await FindById(unitIdParsed) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + var result = await unitRepository.UpdateConfiguration(unitIdParsed, unitConfiguration); + var newConfig = await FindById(unitIdParsed) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + if (!result) throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldConfig, newConfig); + + return result; + } + + public async Task FindById(ObjectId? id) + { + if (id == null) + return null; + return await unitRepository.FindById(id); + } + + public async Task FindByName(string? name) + { + if (string.IsNullOrEmpty(name)) + return null; + return await unitRepository.FindByName(name); + } + + public async Task FindByUnitNameOrPocName(string? name, string? pocName) + { + if (!string.IsNullOrEmpty(name)) + return await unitRepository.FindByName(name); + + if (!string.IsNullOrEmpty(pocName)) + { + var poc = await pointOfCareService.FindByBed(pocName); + if (poc != null) + return await FindById(poc.FirstOrDefault()?.UnitId); + } + + return null; + } + // public async Task FindByPointOfCare(PointOfCare pointOfCare) + // { + // return await _unitRepository.FindByPointOfCare(pointOfCare); + // } + + public async Task InsertOne(Unit unit) + { + var newUnit = await unitRepository.InsertOneUnit(unit) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, newUnit); + return newUnit; + } + + public async Task UpdateUnit(Unit unit) + { + var oldUnit = await FindById(unit.Id) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + var newUnit = await unitRepository.UpdateUnit(unit); + if (newUnit == null) + return null; + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldUnit, newUnit); + + SendUnitBroadcast(newUnit, OperationType.UpdateUnit); + return newUnit; + } + + public async Task UpdateUnitInfo(ObjectId unitId, string name, string title, string? configObsId = null) + { + var oldUnit = await FindById(unitId) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + var newUnit = await unitRepository.UpdateUnitInfo(unitId, name, title) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldUnit, newUnit); + + if (newUnit != null) SendUnitBroadcast(newUnit, OperationType.UpdateUnit); + return newUnit; + } + + public async Task DeleteUnitById(Unit unit) + { + //if (unit is not { Status: null }) throw new ConflictException(ErrorMessage.Conflict_ResourceInUse); + _ = await unitRepository.DeleteAsync(unit.Id) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, unit, null); + return true; + } + + public async Task Get(string id) + { + Unit? section = null; + + if (ObjectId.TryParse(id, out var oid)) section = await FindById(oid); + + var sections = await GetAll(); + + section ??= sections.FirstOrDefault(s => s.Title == id); + + section ??= sections.FirstOrDefault(s => s.Name == id); + + return section; + } + + public async Task?> FindByLocation(PatientLocation location) + { + try + { + var sections = await GetAll(); + // Cambiar por comparacion con Location? + return sections.Where(section => section.PointOfCares != null && + section.PointOfCares.Any(c => + c.Bed == location.Bed && c.UnitName == location.UnitName)).ToList(); + } + catch (Exception e) + { + Log.Error("Exception finding by location section exception:{e} location: {location} ", e.Message, + location); + return null; + } + } + + private async void SendUnitBroadcast(Unit unit, OperationType operation) + { + try + { + var locations = new List(); + var pocs = await pointOfCareService.FindAllByUnitId(unit.Id); + if (pocs == null) + return; + + locations.AddRange(pocs.Select(c => c.Id)); + var subscribers = subscribersService.GetSubscribers() + .Where(s => s.LocationIds.Any(id => locations.Contains(id))) + .ToList(); + + foreach (var subscriber in subscribers) + _ = clientMessageService.Value.SendAsync(subscriber.Id, operation, unit); + } + catch (Exception e) + { + logger.LogError( + "Error sending Unit Broadcast {unit} with operation type {operationToString()} message: {eMessage}", + unit, operation.ToString(), e.Message); + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Subscriptions/SubscriberGroupedService.cs b/adas-core.Application/Subscriptions/SubscriberGroupedService.cs new file mode 100644 index 00000000..9b51b70f --- /dev/null +++ b/adas-core.Application/Subscriptions/SubscriberGroupedService.cs @@ -0,0 +1,224 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Utils; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; + +namespace adas_core.Application.Subscriptions; + +public class SubscriberGroupedService( + ICacheService? cacheService, + IGroupedObservationService groupedObservationService, + ILogger logger, + Lazy clientMessageService) + : ISubscriberGroupedService +{ + private List _subscriberGrouped = []; + + + //private static readonly ILogger _logger = Log.ForContext(); + + public List SubscriberGroupedList + { + get + { + lock (_subscriberGrouped) + { + return _subscriberGrouped; + } + } + set + { + lock (_subscriberGrouped) + { + _subscriberGrouped = value; + } + } + } + + public List GetGrouped() + { + lock (_subscriberGrouped) + { + return _subscriberGrouped.ToList(); + } + } + + public void RemoveGroupedObsByPatientId(string patientId) + { + lock (_subscriberGrouped) + { + var itemToRemove = _subscriberGrouped.Where(c => c.PatientId.ToString() == patientId).ToList(); + itemToRemove.ForEach(item => _subscriberGrouped.Remove(item)); + } + } + + /// + /// Check if what clients dont need to get updated with new grouped observations and remove them + /// diference between section and box if location is updated doesn't matter if is section or box + /// BoxSubscribers don't need to get updated otherwise SectionSubscribers may still need to get updated + /// + /// objectId on db for Patient + /// + public void RemoveWsSubscriberByLocation(string patientId, PatientLocation? newLocation) + { + lock (_subscriberGrouped) + { + var itemToRemove = _subscriberGrouped.Where(c => c.PatientId.ToString() == patientId).ToList(); + foreach (var wsSubscriberGrouped in itemToRemove) + { + // Make a separate list for wsClients to be removed + var wsClientsToRemove = new List(); + foreach (var wsClient in wsSubscriberGrouped.WsSubscriber) wsClientsToRemove.Add(wsClient); + + // Remove the wsClients + foreach (var wsClient in wsClientsToRemove) wsSubscriberGrouped.WsSubscriber.Remove(wsClient); + + CheckEmptySubscriberGroup(wsSubscriberGrouped); + } + } + } + + public void RemoveWsSubscriberPatientIdAndWsId(string patientId, string wsIdToRemove) + { + lock (_subscriberGrouped) + { + var itemToRemove = _subscriberGrouped + .Where(c => c.PatientId.ToString() == patientId && c.WsSubscriber.Contains(wsIdToRemove)).ToList(); + foreach (var wsSubscriberGrouped in itemToRemove) + { + // // Make a separate list for wsClients to be removed + // var wsClientsToRemove = new List(); + // foreach (var wsClient in wsSubscriberGrouped.WsSubscriber) + // { + // if(wsClient == wsIdToRemove) + // wsClientsToRemove.Add(wsClient); + // } + // + // // Remove the wsClients + // foreach (var wsClient in wsClientsToRemove) + // { + // wsSubscriberGrouped.WsSubscriber.Remove(wsClient); + // wsSubscriberGrouped.Group.Remove(wsClient); + // } + wsSubscriberGrouped.WsSubscriber.Remove(wsIdToRemove); + wsSubscriberGrouped.Group.Remove(wsIdToRemove); + + CheckEmptySubscriberGroup(wsSubscriberGrouped); + } + } + } + + /// + /// Check clients using generated grouped observations on disconections + /// removing the wole object in case of 0 or + /// only the group if any other client is using it + /// + /// + /// Object with properties to generate new grouped observations, clients using those + /// grouped obsercations and the generated list of grouped observations + /// + public List CheckEmptySubscriberGroup(WsSubscriberGrouped? wsSubscriberGrouped) + { + lock (_subscriberGrouped) + { + var removed = new List(); + if (wsSubscriberGrouped == null) return []; + + if (wsSubscriberGrouped.WsSubscriber.Count == 0) + { + //_logger.Debug($"Removing wsSubscriberGrouped from list because client subscriber are empty"); + wsSubscriberGrouped.Timer.Stop(); + wsSubscriberGrouped.Timer.Dispose(); + _subscriberGrouped.Remove(wsSubscriberGrouped); + removed.Add( + $"ws:{wsSubscriberGrouped.PatientId}-{string.Join(";", wsSubscriberGrouped.Names)}-{wsSubscriberGrouped.Regularity}-{wsSubscriberGrouped.Group.Count}"); + cacheService?.DeleteObjectAsync(wsSubscriberGrouped.HashCode); + } + else + { + foreach (var kvp in wsSubscriberGrouped.Group.ToList()) + if (!wsSubscriberGrouped.WsSubscriber.Any(subscriber => + subscriber.Contains(kvp.Key))) + { + //_logger.Debug($"Removing group {kvp.Key}:{kvp.Value} from list because client subscriber got disconnected but wsSubscriberGrouped still in use with {wsSubscriberGrouped.WsSubscriber.Count()} clients"); + wsSubscriberGrouped.Group.Remove(kvp.Key); + wsSubscriberGrouped.WsSubscriber.Remove(kvp.Key); + removed.Add($"group:{kvp.Key}"); + } + } + + + return removed; + } + } + + public void AddSubscriberGrouped(WsSubscriberGrouped wsSubscriberGrouped) + { + lock (_subscriberGrouped) + { + _subscriberGrouped.Add(wsSubscriberGrouped); + } + } + + public void CheckOnSubscriptionGroup(GroupedField groupedField, ObjectId patientId, string? timeZoneId, + string connectionId, GroupedObservation lastObsInGroup) + { + var wsSubscriberGrouped = GetGrouped().FirstOrDefault(s => s.Compare(groupedField, patientId)); + if (wsSubscriberGrouped != null) + { + wsSubscriberGrouped.WsSubscriber.Add(connectionId); + + // Verifica si el diccionario contiene la clave y el valor deseado + if (!wsSubscriberGrouped.Group.ContainsKey(connectionId) || + wsSubscriberGrouped.Group[connectionId] != connectionId) + // Añade el par clave-valor al diccionario + wsSubscriberGrouped.Group[connectionId] = groupedField.Group ?? string.Empty; + } + else + { + AddSubscriberGrouped( + // ReSharper disable once AsyncVoidLambda + new WsSubscriberGrouped(patientId, connectionId, timeZoneId, groupedField, lastObsInGroup, + async delegate(object? sender, string _) + { + try + { + if (sender is WsSubscriberGrouped ws) await AddEmptyObs(ws); + } + catch (Exception ex) + { + logger.LogError(ex, "Ocurrió un error al procesar el evento del suscriptor."); + throw; + } + })); + } + } + + public void UpdateLastGroupedObsInGroup(string wsgHashCode, GroupedObservation newGroupedObservation) + { + lock (_subscriberGrouped) + { + _subscriberGrouped.FirstOrDefault(c => c.HashCode == wsgHashCode)?.UpdateLastGo(newGroupedObservation); + } + } + + private async Task AddEmptyObs(WsSubscriberGrouped ws) + { + var gobs = await groupedObservationService.CreateNextEmptyObs(ws); + + UpdateLastGroupedObsInGroup(ws.HashCode, gobs); + lock (_subscriberGrouped) + { + foreach (var sub in ws.WsSubscriber) + { + gobs.Group = ws.Group.GetValue(sub)!; + logger.LogInformation("Sending empty obs on gruped for: {Sub}, with group: {Group}", sub, gobs.Group); + + _ = clientMessageService.Value.SendAsync(sub, OperationType.GroupedObservation, gobs); + } + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Subscriptions/WsSubscriberGrouped.cs b/adas-core.Application/Subscriptions/WsSubscriberGrouped.cs new file mode 100644 index 00000000..b79189d4 --- /dev/null +++ b/adas-core.Application/Subscriptions/WsSubscriberGrouped.cs @@ -0,0 +1,299 @@ +using System.Timers; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Utils; +using MongoDB.Bson; +using Serilog; +using static adas_core.Domain.Models.GroupedObservation; +using Timer = System.Timers.Timer; + +namespace adas_core.Application.Subscriptions; + +/** + * This class is used to group the subscribers by the same group + */ +public class WsSubscriberGrouped +{ + private readonly EventHandler _sendEvent; + + public WsSubscriberGrouped(ObjectId patientId, string wsId, string? timeZoneId, + GroupedField gf, GroupedObservation lastGroupedObservationObs, EventHandler sendEvent) + { + _sendEvent = sendEvent; + if (WsSubscriber.ToList().FirstOrDefault(s => s == wsId) == null) WsSubscriber.Add(wsId); + + TimeZoneId = timeZoneId ?? "Romance Standard Time"; + PatientId = patientId; + Names = CollectionsUtils.IfEmptyOrNull(gf.Names, [gf.Name ?? string.Empty]); + Max = gf.Max; + Since = gf.Since; + StartTimeShift = gf.StartTimeShift; + Regularity = gf.Regularity; + Result = gf.Result; + Group.Add(wsId, gf.Group ?? string.Empty); + UpdateLastGo(lastGroupedObservationObs); + HashCode = CryptoAdas.CreateMd5GroupedObs(gf, patientId); + + Timer = new Timer + { + Interval = SetUpTimerInterval() + }; + Timer.Elapsed += Timer_Elapsed; + if (Regularity != GroupedObservationEnum.Regularity.Times) Timer.Start(); + } + + public List WsSubscriber { get; set; } = []; + public ObjectId PatientId { get; set; } + public List Names { get; set; } + public int Max { get; set; } + public GroupedObservationEnum.Regularity? Regularity { get; set; } + public List Result { get; set; } + public GroupedObservationEnum.Since Since { get; set; } + public List StartTimeShift { get; set; } + public string HashCode { get; set; } + public string TimeZoneId { get; set; } + public List LastGroupedObservationObs { get; set; } = []; + public Dictionary Group { get; set; } = new(); + public Timer Timer { get; set; } + + public void UpdateLastGo(GroupedObservation lastGroupedObservationObs) + { + LastGroupedObservationObs.Clear(); + foreach (var name in Names) + { + var lastObs = lastGroupedObservationObs.Observations.LastOrDefault(c => c.Name == name); + if (lastObs != null) + { + LastGroupedObservationObs.Add(lastObs); + } + else + { + lastObs = lastGroupedObservationObs.Observations.LastOrDefault(); + if (lastObs != null) LastGroupedObservationObs.Add(lastObs); + } + } + } + + private void Timer_Elapsed(object? sender, ElapsedEventArgs e) + { + var currentDateTime = DateTime.Now; + Timer.Stop(); + try + { + bool hasCurrentObs; + + switch (Regularity) + { + case GroupedObservationEnum.Regularity.Second: + // Filtra observaciones con el mismo segundo actual + hasCurrentObs = LastGroupedObservationObs.Any(obs => obs.Time.Second == currentDateTime.Second + && obs.Time.Minute == currentDateTime.Minute + && obs.Time.Hour == currentDateTime.Hour + && obs.Time.Date == currentDateTime.Date); + break; + + case GroupedObservationEnum.Regularity.Minute: + // Filtra observaciones con el mismo minuto actual + hasCurrentObs = LastGroupedObservationObs.Any(obs => obs.Time.Minute == currentDateTime.Minute + && obs.Time.Hour == currentDateTime.Hour + && obs.Time.Date == currentDateTime.Date); + break; + + case GroupedObservationEnum.Regularity.Day: + // Filtra observaciones con la misma fecha actual (día completo) + hasCurrentObs = LastGroupedObservationObs.Any(obs => obs.Time.Date == currentDateTime.Date); + break; + + default: + // Filtra observaciones con la misma hora actual + hasCurrentObs = LastGroupedObservationObs.Any(obs => obs.Time.Hour == currentDateTime.Hour + && obs.Time.Date == currentDateTime.Date); + break; + } + + // Si no hay al menos una observación que coincide, invoca el evento + if (!hasCurrentObs) _sendEvent(this, HashCode); + } + catch (Exception ex) + { + Log.Warning( + $"Exception on WsSubsciptor for patient: {PatientId} message: {ex.Message} exception: {ex}"); + } + + TimerReestart(); + } + + private void TimerReestart() + { + Timer.Interval = SetUpTimerInterval(); + Timer.Start(); + } + + private int SetUpTimerInterval() + { + var currentDateTime = DateTime.Now; + int graceTime; + DateTime nextIntervalTime; + + switch (Regularity) + { + case GroupedObservationEnum.Regularity.Second: + nextIntervalTime = new DateTime( + currentDateTime.Year, + currentDateTime.Month, + currentDateTime.Day, + currentDateTime.Hour, + currentDateTime.Minute, + currentDateTime.Second + ).AddSeconds(1); + graceTime = 500; + break; + + case GroupedObservationEnum.Regularity.Minute: + nextIntervalTime = new DateTime( + currentDateTime.Year, + currentDateTime.Month, + currentDateTime.Day, + currentDateTime.Hour, + currentDateTime.Minute, + 0 + ).AddMinutes(1); + graceTime = 30000; + break; + + case GroupedObservationEnum.Regularity.Day: + nextIntervalTime = new DateTime( + currentDateTime.Year, + currentDateTime.Month, + currentDateTime.Day, + 0, + 0, + 0 + ).AddDays(1); + graceTime = 60000; + break; + + default: + nextIntervalTime = new DateTime( + currentDateTime.Year, + currentDateTime.Month, + currentDateTime.Day, + currentDateTime.Hour, + 0, + 0 + ).AddHours(1); + graceTime = 30000; + break; + } + + var timeSpanToNextInterval = nextIntervalTime - currentDateTime; + Log.Information( + "Obs {Obs}, currentDateTime: {CurrentDateTime}, nextIntervalTime: {NextIntervalTime}, timeSpanToNextInterval: {TimeSpanToNextInterval}, timeSpanToNextInterval.TotalMilliseconds+graceTime: {Tt}", + Group, currentDateTime, nextIntervalTime, timeSpanToNextInterval, + (int)timeSpanToNextInterval.TotalMilliseconds + graceTime); + + return (int)timeSpanToNextInterval.TotalMilliseconds + graceTime; + } +} + +public static class WsSubscriberExtension +{ + public static bool Compare(this WsSubscriberGrouped source, GroupedField r, ObjectId patientId) + { + return r.Regularity == source.Regularity && + r.Since == source.Since && + CompareStringShiftList(r.StartTimeShift, source.StartTimeShift) && + r.Max == source.Max && + r.Result.SequenceEqual(source.Result) && + patientId.ToString() == source.PatientId.ToString() && + CompareNamesStringList(r.Names, [r.Name ?? string.Empty], source.Names); + } + + private static bool CompareStringShiftList(List? shift, List? sourceShift) + { + if (shift != null && sourceShift != null) return sourceShift.SequenceEqual(shift); + if (shift == null && sourceShift == null) return true; + return false; + } + + private static bool CompareNamesStringList(List names, List name, List sourceNames) + { + if (!CollectionsUtils.IsEmptyOrNull(names) && sourceNames.SequenceEqual(names)) return true; + return !CollectionsUtils.IsEmptyOrNull(name) && sourceNames.SequenceEqual(name); + } + + /// + /// Check if the incoming observation affect to the group and is new info or is irrelevant + /// + /// + public static bool IsNewObservationRelevantForGroup(this WsSubscriberGrouped group, PatientObservation obs) + { + double.TryParse(obs.Value.ToString(), out var pobsValue); + switch (group.Regularity) + { + case GroupedObservationEnum.Regularity.Shift: + case GroupedObservationEnum.Regularity.Hour: + var obsInThisHour = group.LastGroupedObservationObs.Where(c => + new DateTime(c.Time.Year, c.Time.Month, c.Time.Day, c.Time.Hour, 0, 0) == + new DateTime(obs.Time.Year, obs.Time.Month, obs.Time.Day, obs.Time.Hour, 0, 0) && !c.IsFilled); + //Obs is in grouped time, don't generate group for observation outside of range + + + //Change to don't cut is obs time is a future + var obsInTime = DateTime.UtcNow.AddHours(group.Max) > obs.Time && + DateTime.UtcNow.AddHours(group.Max * -1) < obs.Time; + //future data is relevant because maybe nothing is inserted in the same hour after it and we cant lose it + /* if (DateTime.UtcNow.AddHours(group.Max) > obs.time) + { + + } + */ + + if (!obsInTime) return false; + var groupedObservationObsEnumerable = obsInThisHour.ToList(); + if (!groupedObservationObsEnumerable.Any() && obsInTime) return true; + + //If obs in this hour check if is relevant by Result + if (groupedObservationObsEnumerable.Any()) + foreach (var result in group.Result) + switch (result) + { + case GroupedObservationEnum.Result.Max: + var value = groupedObservationObsEnumerable.Max(s => s.Max?.Value); + var parsed = double.TryParse(value?.ToString(), out var valueParsed); + //actual value is higher, is relevant + if (parsed && pobsValue > valueParsed) return true; + break; + case GroupedObservationEnum.Result.Min: + var minValue = groupedObservationObsEnumerable.Min(s => s.Min?.Value); + var isParsed = double.TryParse(minValue?.ToString(), out var minValueParsed); + //actual value is higher, is relevant + if (isParsed && pobsValue < minValueParsed) return true; + break; + case GroupedObservationEnum.Result.Last: + var lastObsInTime = groupedObservationObsEnumerable.OrderBy(t => t.Last?.Time).First(); + //is the new latest in the hour, relevant + if (lastObsInTime.Time < obs.Time) return true; + break; + case GroupedObservationEnum.Result.LastFilled: + var lastFilledObsInTime = groupedObservationObsEnumerable + .OrderBy(t => t.LastFilled?.Time).First(); + //is the new latest in the hour, relevant + if (lastFilledObsInTime.Time < obs.Time) return true; + break; + //always is relevant because SUM everything in the hour + //TODO halfHour should check if any obs already in the halfHour + case GroupedObservationEnum.Result.Sum: + case GroupedObservationEnum.Result.Average: + case GroupedObservationEnum.Result.HalfHour: + return true; + } + + return false; + default: + + return true; + } + } +} \ No newline at end of file diff --git a/adas-core.Application/Subscriptions/WsSuscriber.cs b/adas-core.Application/Subscriptions/WsSuscriber.cs new file mode 100644 index 00000000..ad44e0c1 --- /dev/null +++ b/adas-core.Application/Subscriptions/WsSuscriber.cs @@ -0,0 +1,22 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Models.Masters; +using MongoDB.Bson; + +namespace adas_core.Application.Subscriptions; + +public class WsSubscriber(string id) +{ + public string? UserName; + public string Id { get; set; } = id; + public ObjectId? DisplayId { get; set; } + public List Locations { get; set; } = []; + public List LocationIds { get; set; } = []; + public string? Box { get; set; } + public string? Section { get; set; } + public string? Version { get; set; } + public List? GroupedFields { get; set; } + public LocaleEnum? Locale { get; set; } + public SubscriptionEnum.WsType? SubscriptionType { get; set; } +} \ No newline at end of file diff --git a/adas-core.Application/adas-core.Application.csproj b/adas-core.Application/adas-core.Application.csproj new file mode 100644 index 00000000..ede1a4b1 --- /dev/null +++ b/adas-core.Application/adas-core.Application.csproj @@ -0,0 +1,43 @@ + + + + net8.0 + adas_core.Application + enable + enable + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ..\..\..\..\..\..\..\Program Files\dotnet\shared\Microsoft.AspNetCore.App\7.0.10\Microsoft.AspNetCore.Http.Features.dll + + + + diff --git a/adas-core.Authentication/Attributes/AuthorizeRolesAttribute.cs b/adas-core.Authentication/Attributes/AuthorizeRolesAttribute.cs new file mode 100644 index 00000000..fc0ee05b --- /dev/null +++ b/adas-core.Authentication/Attributes/AuthorizeRolesAttribute.cs @@ -0,0 +1,28 @@ +using System.Security.Claims; +using adas_core.Domain.Enums; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace adas_core.Authentication.Attributes; + +[AttributeUsage(AttributeTargets.Method)] +public class AuthorizeRolesAttribute(PermissionEnum.RolesType type) : Attribute, IAuthorizationFilter +{ + public void OnAuthorization(AuthorizationFilterContext context) + { + var user = context.HttpContext.User; + + if (user.Identity?.IsAuthenticated != true) + { + context.Result = new UnauthorizedResult(); + return; + } + + var roles = user.Claims.Where(c => c.Type == ClaimTypes.Role); + + if (type == PermissionEnum.RolesType.AuthSome) return; + + var hasRequiredRole = roles.Any(r => r.Value.StartsWith(type.ToString())); + if (!hasRequiredRole) context.Result = new ForbidResult(); + } +} \ No newline at end of file diff --git a/adas-core.Authentication/Attributes/AuthorizeUserByService.cs b/adas-core.Authentication/Attributes/AuthorizeUserByService.cs new file mode 100644 index 00000000..d47f4c15 --- /dev/null +++ b/adas-core.Authentication/Attributes/AuthorizeUserByService.cs @@ -0,0 +1,29 @@ +using adas_core.Authentication.Interfaces; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace adas_core.Authentication.Attributes; + +public class AuthorizePermissionsAttribute( + IUserService userService) + : Attribute, IAuthorizationFilter +{ + public void OnAuthorization(AuthorizationFilterContext context) + { + var user = context.HttpContext.User; + + var userName = user.Identity?.Name ?? string.Empty; + var displayIdHeader = context.HttpContext.Request.Headers["Displayid"]; + + if (string.IsNullOrEmpty(displayIdHeader.ToString())) + { + context.Result = new NotFoundObjectResult("displayIdHeader not found reference id doesn't exist on header"); + return; + } + + var userDao = userService.GetUserByUserName(userName).Result; + + if (userDao == null) + context.Result = new NotFoundObjectResult("User not found on AuthorizePermissionsAttribute"); + } +} \ No newline at end of file diff --git a/adas-core.Authentication/Attributes/PermissionAuthorizeAttribute.cs b/adas-core.Authentication/Attributes/PermissionAuthorizeAttribute.cs new file mode 100644 index 00000000..b77e287c --- /dev/null +++ b/adas-core.Authentication/Attributes/PermissionAuthorizeAttribute.cs @@ -0,0 +1,98 @@ +using System.Security.Claims; +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; + +namespace adas_core.Authentication.Attributes; + +public class PermissionAuthorizeAttribute(string source, string? resourceIdHeader = null) + : AuthorizeAttribute, IAsyncAuthorizationFilter +{ + public async Task OnAuthorizationAsync(AuthorizationFilterContext context) + { + // Obtener el servicio de permisos. + var permissionService = context.HttpContext.RequestServices.GetService() + ?? throw new ForbbidenException(HttpEnum.ErrorMessage.ForbiddenNoPermission); + + var user = context.HttpContext.User; + + // Obtener el header que indica la fuente. + var headerSource = context.HttpContext.Request.Headers[source].FirstOrDefault(); + + // Obtener el identificador del usuario + var username = user.Claims.FirstOrDefault(c => c.Type == ClaimTypes.NameIdentifier)?.Value; + + // Obtiene la lista de roles (valores) del usuario. + var userRoles = user.Claims + .Where(c => c.Type == ClaimTypes.Role) + .Select(c => c.Value) + .ToList(); + + // Procesa cada rol: si contiene 2 guiones bajos, elimina el tercer segmento. + var processedRoles = userRoles.Select(role => + { + var parts = role.Split('_'); + return parts.Length == 2 ? parts[0] : role; + }).ToList(); + + // Validar que se tenga al menos un rol, username y headerSource. + if (!processedRoles.Any() || string.IsNullOrEmpty(username) || string.IsNullOrEmpty(headerSource)) + { + context.Result = new ForbidResult(); + return; + } + + // Parseamos el header para obtener la fuente. + var source1 = + (PermissionEnum.SourcePermissionsEnum)Enum.Parse(typeof(PermissionEnum.SourcePermissionsEnum), + headerSource); + + var hasPermission = false; + + if (source1 == PermissionEnum.SourcePermissionsEnum.Panel) + { + // Si la fuente es PANEL, verificamos que alguno de los roles tenga acceso. + foreach (var roleStr in processedRoles) + { + if (!Enum.TryParse(typeof(PermissionEnum.RolesType), roleStr, out var roleObj)) continue; + var role = (PermissionEnum.RolesType)roleObj; + if (!await permissionService.HasAccessToPanel(username, role, source1)) continue; + hasPermission = true; + break; + } + + if (!hasPermission) context.Result = new ForbidResult(); + } + else if (resourceIdHeader != null) + { + // Para otras fuentes, se requiere un header que indique el resourceId. + var resourceId = context.HttpContext.Request.Headers[resourceIdHeader].FirstOrDefault(); + if (string.IsNullOrEmpty(resourceId)) + { + context.Result = new ForbidResult(); + return; + } + + // Verificar que al menos uno de los roles tenga acceso al recurso. + foreach (var roleStr in processedRoles) + if (Enum.TryParse(typeof(PermissionEnum.RolesType), roleStr, out var roleObj)) + { + var role = (PermissionEnum.RolesType)roleObj; + hasPermission = resourceIdHeader switch + { + "DisplayId" => await permissionService.HasAccessToDisplay(username, role, source1, resourceId), + "UnitId" => await permissionService.HasAccessToUnit(username, role, source1, resourceId), + _ => false + }; + if (hasPermission) + break; + } + + if (!hasPermission) context.Result = new ForbidResult(); + } + } +} \ No newline at end of file diff --git a/adas-core.Authentication/AuthorityService.cs b/adas-core.Authentication/AuthorityService.cs new file mode 100644 index 00000000..2ef4f6a0 --- /dev/null +++ b/adas-core.Authentication/AuthorityService.cs @@ -0,0 +1,108 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Interfaces; +using adas_core.Domain.Models.MongoModels; +using Microsoft.AspNetCore.Http; +using MongoDB.Bson; +using Serilog; + +namespace adas_core.Authentication; + +public class AuthorityService( + IAuthorityRepository authorityRepository, + IHttpContextAccessor httpContextAccessor, + ILocalAuditService auditService) + : IAuthorityService +{ + public void CreateNew(string roleName, ObjectId userId) + { + authorityRepository.CreateNewAuthority(roleName, userId); + } + + public async Task InsertOne(Authorization authorization) + { + await authorityRepository.InsertOneAsync(authorization); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, authorization); + } + + public async Task updateOne(Authorization authorization) + { + var oldAuth = await authorityRepository.GetById(authorization.Id); + await authorityRepository.UpdateOneAsync(authorization.Id, authorization); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldAuth, authorization); + } + + /// + /// Retrieves the authorities of the specified user. + /// + /// + /// A list of the user's authorities. + public Task> GetUserAuthorities(ObjectId userId) + { + return authorityRepository.GetUserAuthorities(userId); + } + + public Task> GetAllAuthorities() + { + return authorityRepository.GetAllAuthorities(); + } + + public async Task DeleteAuthoritiesForUser(ObjectId id) + { + try + { + var list = await authorityRepository.GetUserAuthorities(id); + var result = await authorityRepository.DeleteAllAuthoritiesByUser(id); + if (!result) return result; + + foreach (var auth in list) + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, auth, null); + return result; + } + catch (Exception e) + { + Log.Error("Exception deleting authorities for user {id}. Exception: {e}", id, e.Message); + return false; + } + } + + public async Task CreateNewUserAuthority(Authorization authorization) + { + await authorityRepository.InsertOneAsync(authorization); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, authorization); + return authorization; + } + + public async Task DeleteUserAuthority(ObjectId userAuthorityIdParsed) + { + try + { + var result = await authorityRepository.DeleteAsync(userAuthorityIdParsed); + if (result != null) + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, result, null); + return result != null; + } + catch (Exception e) + { + Log.Error("Exception deleting user authority {userAuthorityIdParsed}. Exception: {e}", + userAuthorityIdParsed, e.Message); + return false; + } + } + + public async Task EditUserAuthority(Authorization authorization) + { + try + { + var oldAuth = await authorityRepository.GetById(authorization.Id); + await authorityRepository.UpdateOneAsync(authorization.Id, authorization); + await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldAuth, authorization); + return authorization; + } + catch (Exception e) + { + Log.Error("Exception editing user authority {authorization}. Exception: {e}", authorization, e.Message); + return null; + } + } +} \ No newline at end of file diff --git a/adas-core.Authentication/Interfaces/IAuthorityService.cs b/adas-core.Authentication/Interfaces/IAuthorityService.cs new file mode 100644 index 00000000..7913a780 --- /dev/null +++ b/adas-core.Authentication/Interfaces/IAuthorityService.cs @@ -0,0 +1,19 @@ +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Authentication.Interfaces; + +public interface IAuthorityService +{ + public Task> GetUserAuthorities(ObjectId userId); + public void CreateNew(string roleName, ObjectId userId); + public Task InsertOne(Authorization authorization); + public Task updateOne(Authorization authorization); + + public Task> GetAllAuthorities(); + + Task DeleteAuthoritiesForUser(ObjectId id); + Task CreateNewUserAuthority(Authorization authorization); + Task DeleteUserAuthority(ObjectId userAuthorityIdParsed); + Task EditUserAuthority(Authorization authorization); +} \ No newline at end of file diff --git a/adas-core.Authentication/Interfaces/ILoginService.cs b/adas-core.Authentication/Interfaces/ILoginService.cs new file mode 100644 index 00000000..6319f977 --- /dev/null +++ b/adas-core.Authentication/Interfaces/ILoginService.cs @@ -0,0 +1,21 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using Microsoft.AspNetCore.Http; +using MongoDB.Bson; + +namespace adas_core.Authentication.Interfaces; + +public interface ILoginService +{ + bool AllowPassword { get; } + UserEnum.LoginMethod Method { get; } + Task Login(string username, string password); + Task Login(HttpContext context); + + Task Authenticate(string username, string password); + Task GetById(ObjectId id); + Task GetByEmail(string email); + Task GetByUsername(string username); + + Task> GetAllUsers(); +} \ No newline at end of file diff --git a/adas-core.Authentication/Interfaces/ITokenService.cs b/adas-core.Authentication/Interfaces/ITokenService.cs new file mode 100644 index 00000000..ae7c84e1 --- /dev/null +++ b/adas-core.Authentication/Interfaces/ITokenService.cs @@ -0,0 +1,14 @@ +using System.Security.Claims; +using Microsoft.IdentityModel.Tokens; + +namespace adas_core.Authentication.Interfaces; + +public interface ITokenService +{ + int RefreshTokenValidityInDays { get; } + SecurityToken? GetToken(string username, List claims); + + string GetRefreshToken(); + + string Serialize(SecurityToken token); +} \ No newline at end of file diff --git a/adas-core.Authentication/Interfaces/IUserService.cs b/adas-core.Authentication/Interfaces/IUserService.cs new file mode 100644 index 00000000..64ec7e2d --- /dev/null +++ b/adas-core.Authentication/Interfaces/IUserService.cs @@ -0,0 +1,43 @@ +using System.IdentityModel.Tokens.Jwt; +using adas_core.Authentication.Models; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using Microsoft.IdentityModel.Tokens; +using MongoDB.Bson; + +namespace adas_core.Authentication.Interfaces; + +public interface IUserService +{ + static readonly string TokenTypeUser = "user"; + static readonly string TokenTypeRefresh = "refresh"; + + Task Login(string username, string password); + Task GetUser(string username, string password); + Task RefreshToken(string refreshToken); + + Task LoginWithAccessToken(string token); + void ValidateToken(string token, string tokenType, out SecurityToken securityToken); + Task GetUserByToken(JwtSecurityToken? jwtToken); + Task GetUserByCasTicket(string service, string ticket); + Task GenerateJwt(User user); + Task> GetAll(); + Task GetUserById(ObjectId id); + Task GetUserByUserName(string name); + Task GetUserByName(string name); + Task CreateUser(User userEntryLdap); + Task CreateNewUserByRequest(User user); + + Task CreateNewUserWithAuthorities(CreateUserWithAuthDto createUserWithAuthDto); + Task UpdateUserWithAuthorities(UpdateUserWithAuthDto createUserWithAuthDto, bool updatePass); + Task UpdateUsersByRequest(User user, bool updatePass); + Task UpdateUserPassword(ObjectId id, string oldPassword, string newPassword); + Task DeleteUser(ObjectId id); + Task> GetPaginatedUsers(PaginationFilter config); + Task CreateNewAuthority(Authorization auth); + Task DeleteAuthority(string id); + Task UpdateAuthority(Authorization authorization); + Task LoginWithGivenAccessToken(string token); +} \ No newline at end of file diff --git a/adas-core.Authentication/Models/LoginInfo.cs b/adas-core.Authentication/Models/LoginInfo.cs new file mode 100644 index 00000000..7ca1bce9 --- /dev/null +++ b/adas-core.Authentication/Models/LoginInfo.cs @@ -0,0 +1,7 @@ +namespace adas_core.Authentication.Models; + +public class LoginInfo +{ + public string Username { get; set; } = null!; + public string Password { get; set; } = null!; +} \ No newline at end of file diff --git a/adas-core.Authentication/Models/Token.cs b/adas-core.Authentication/Models/Token.cs new file mode 100644 index 00000000..995ad849 --- /dev/null +++ b/adas-core.Authentication/Models/Token.cs @@ -0,0 +1,19 @@ +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Authentication.Models; + +public class TokenResult +{ + public string AccessToken { get; set; } = null!; + public DateTime AccessTokenExpiryTime { get; set; } + public string RefreshToken { get; set; } = null!; + public DateTime RefreshTokenExpiryTime { get; set; } + public User User { get; set; } = new(); +} + +public class TokenResultPanel +{ + public TokenResult TokenResult { get; set; } = null!; + public PanelPermissionTypes Permissions { get; set; } = null!; +} \ No newline at end of file diff --git a/adas-core.Authentication/Models/UciResponse.cs b/adas-core.Authentication/Models/UciResponse.cs new file mode 100644 index 00000000..06dfe7f2 --- /dev/null +++ b/adas-core.Authentication/Models/UciResponse.cs @@ -0,0 +1,47 @@ +namespace adas_core.Authentication.Models; + +public class UciResponse +{ + protected UciResponse() + { + } + + protected UciResponse(T entity) + { + Success = true; + Data = entity; + Message = null; + Error = null; + } + + public string? Error { get; set; } + public DateTime Date { get; set; } = DateTime.Now; + public bool Success { get; protected set; } + public string? Message { get; protected set; } + public T? Data { get; protected set; } + + + public static UciResponse FromSuccess(T entity) + { + return new UciResponse(entity); + } + + + public static UciResponse FromError(Exception ex) + { + var error = ex.GetType().Name; + var index = error.LastIndexOf("Exception", StringComparison.Ordinal); + if (index > -1) error = error[..index]; + return FromError(error, ex.Message); + } + + public static UciResponse FromError(string error, string? message = null) + { + return new UciResponse + { + Success = false, + Error = error, + Message = message + }; + } +} \ No newline at end of file diff --git a/adas-core.Authentication/RegistrationExtensions/AuthRegistration.cs b/adas-core.Authentication/RegistrationExtensions/AuthRegistration.cs new file mode 100644 index 00000000..49e16481 --- /dev/null +++ b/adas-core.Authentication/RegistrationExtensions/AuthRegistration.cs @@ -0,0 +1,14 @@ +using adas_core.Authentication.Interfaces; +using Microsoft.Extensions.DependencyInjection; + +namespace adas_core.Authentication.RegistrationExtensions; + +public static class AuthRegistration +{ + public static void AddAuth(this IServiceCollection serviceCollection) + { + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); + } +} \ No newline at end of file diff --git a/adas-core.Authentication/UserService.cs b/adas-core.Authentication/UserService.cs new file mode 100644 index 00000000..c7727967 --- /dev/null +++ b/adas-core.Authentication/UserService.cs @@ -0,0 +1,647 @@ +using System.Diagnostics; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; +using adas_core.Application.Exceptions; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Interfaces; +using adas_core.Authentication.Models; +using adas_core.Domain.Enums; +using adas_core.Domain.Exceptions; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Filter; +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 Microsoft.IdentityModel.Tokens; +using MongoDB.Bson; +using MongoDB.Driver; +using static System.Enum; + +namespace adas_core.Authentication; + +public class UserService : IUserService +{ + private readonly ILocalAuditService _auditService; + private readonly IAuthorityService _authorityService; + private readonly IEnumerable _availableLoginServices; + private readonly Lazy _displayService; + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly JwtConfig _jwtConfig; + private readonly ILogger _logger; + private readonly IUserRepository _userRepository; + private readonly UsersWhiteListConfig _usersWhiteList; + private readonly ValidGroupsConfig _validGroups; + private readonly List _validLoginMethods; + private readonly ISubscribersService _subscribersService; + private readonly IClientMessageService _clientMessageService; + private readonly Lazy _permissionService; + public UserService( + IEnumerable loginServices, + IOptions configuration, + IOptions validGroups, + IOptions usersWhiteList, + IOptions jwt, + IHttpContextAccessor httpContextAccessor, + IUserRepository userRepository, + IAuthorityService authorityService, + ILocalAuditService auditService, + Lazy displayService, + ILogger logger, + ISubscribersService subscribersService, + IClientMessageService clientMessageService, Lazy permissionService) + { + _availableLoginServices = loginServices; + _httpContextAccessor = httpContextAccessor; + _validGroups = validGroups.Value; + _usersWhiteList = usersWhiteList.Value.UsersWhiteListConfig; + _jwtConfig = jwt.Value.JwtConfig ?? throw new Exception("JWT NOT CONFIGURED"); + var loginMethods = configuration.Value.LoginMethods; + _validLoginMethods = loginMethods; + _userRepository = userRepository; + _authorityService = authorityService; + _auditService = auditService; + _displayService = displayService; + _logger = logger; + _subscribersService = subscribersService; + _clientMessageService = clientMessageService; + _permissionService = permissionService; + } + + + public async Task Login(string username, string password) + { + var user = await GetUser(username, password); + _logger.LogInformation("[UserService] Login user {u}", user.ToString()); + + if (user == null) throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedInvalidCredentials); + if (!user.IsEnabled || (user.LockExpirationDate != null && user.LockExpirationDate > DateTime.UtcNow)) + throw new ForbbidenException(HttpEnum.ErrorMessage.ForbiddenUserBlocked); + if (!_usersWhiteList.IsValid(user) && !_validGroups.IsValid(user)) + throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedNoPermission); + + if (user.LockExpirationDate < DateTime.UtcNow) user.LockExpirationDate = null; + user.LastLogin = DateTime.UtcNow; + await UpdateUsersByRequest(user, false); + return await GenerateJwt(user); + } + + public void ValidateToken(string token, string tokenType, out SecurityToken validatedToken) + { + try + { + var tokenValidationParameters = new TokenValidationParameters + { + ValidateAudience = _jwtConfig.ValidateAudience, + ValidateIssuer = _jwtConfig.ValidateIssuer, + ValidAudience = _jwtConfig.Audience, + ValidIssuer = _jwtConfig.Issuer, + ValidateIssuerSigningKey = true, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtConfig.Key ?? string.Empty)), + ValidateLifetime = false + }; + + var tokenHandler = new JwtSecurityTokenHandler(); + tokenHandler.ValidateToken(token, tokenValidationParameters, out var securityToken); + if (securityToken is not JwtSecurityToken jwtSecurityToken || + !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, + StringComparison.InvariantCultureIgnoreCase)) + throw new SecurityTokenException("Invalid token"); + validatedToken = securityToken; + } + catch (SecurityTokenException a) + { + _logger.LogError("Error validating token, {msg}", a.Message); + throw new TokenException(a.Message); + } + catch (Exception e) + { + _logger.LogError("Error validating token, {msg}", e.Message); + throw new TokenException("Error validating token"); + } + } + + public async Task GetUserByToken(JwtSecurityToken? jwtToken) + { + User? user = null; + if (jwtToken == null) + return user; + + var userName = jwtToken.Claims.First(x => x.Type == ClaimTypes.Name).Value; + + user = await _userRepository.GetByUserName(userName); + if (user == null) + return user; + + //Eliminamos la contraseña + user.Password = string.Empty; + if (user.Authorization == null || user.Authorization.Count == 0) + user.Authorization = await _authorityService.GetUserAuthorities(user.Id); + return user; + } + + public async Task GetUserByCasTicket(string serviceUrl, string ticket) + { + var service = _availableLoginServices.FirstOrDefault(s => s.Method == UserEnum.LoginMethod.Cas); + if (service == null) throw new LoginServicesException("CAS login method not available"); + + try + { + var user = await service.Login(_httpContextAccessor.HttpContext!); + if (!_usersWhiteList.IsValid(user) && !_validGroups.IsValid(user)) + throw new UnauthorizedAccessException("You don't have sufficient permissions"); + return user; + } + catch (BusinessException) + { + throw; + } + catch (UnauthorizedAccessException) + { + throw; + } + catch (Exception e) + { + var message = e.Message; + if (e.InnerException != null && e.Message != e.InnerException?.Message) + message += $", {e.InnerException?.Message}"; + Debug.WriteLine($"Error while login: {message} "); + throw new LoginServicesException(message, e); + } + //////////////////////// + /* + if (!_validLoginMethods.Contains(LoginMethod.CAS.ToString())) + { + Log.Warning("CAS login method not enabled"); + return null; + } + _casLoginService = new + Log.Information("CAS ticket received: '{CasTicket}'", ticket); + return string.IsNullOrEmpty(ticket) ? null : _casLoginService.GetUser(serviceUrl, ticket, context); + */ + } + + public async Task RefreshToken(string refreshToken) + { + ValidateToken(refreshToken, IUserService.TokenTypeRefresh, out var validatedToken); + var user = await GetUserByToken((JwtSecurityToken)validatedToken); + if (user == null) + return new TokenResult(); + return await GenerateJwt(user); + } + + + public async Task GetUser(string username, string password) + { + //BusinessException? loginException = null; + foreach (var m in _validLoginMethods) + { + if (!TryParse(m, out UserEnum.LoginMethod method)) continue; + var service = _availableLoginServices.FirstOrDefault(s => s.Method == method && s.AllowPassword); + if (service == null) continue; + try + { + _logger.LogInformation("[UserService] Trying login with method {method}", method); + return await service.Login(username, password); + } + catch (Exception e) + { + _logger.LogError("[UserService] Error while trying method {method}, err {err}", method, e.Message); + } + } + + throw new LoginServicesNotFoundException(); + } + + public async Task GetUserById(ObjectId id) + { + try + { + var u = await _userRepository.GetById(id); + if (u != null) u.Authorization = await _authorityService.GetUserAuthorities(u.Id); + return u; + } + catch (Exception) + { + return null; + } + } + + public async Task GetUserByUserName(string name) + { + var u = await _userRepository.GetByUserName(name); + if (u == null) return u; + u.Authorization = await _authorityService.GetUserAuthorities(u.Id); + return u; + } + + public async Task GetUserByName(string name) + { + try + { + var u = await _userRepository.GetByName(name); + if (u != null) u.Authorization = await _authorityService.GetUserAuthorities(u.Id); + return u; + } + catch (Exception) + { + return null; + } + } + + public async Task CreateUser(User userEntryLdap) + { + CheckIfUserNameExists(userEntryLdap.UserName); + CheckIfEmailExists(userEntryLdap.Email); + await _userRepository.InsertOneAsync(userEntryLdap); + var result = await GetUserByUserName(userEntryLdap.UserName); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User, null, result); + return result; + } + + public async Task CreateNewUserByRequest(User user) + { + CheckIfEmailExists(user.Email); + CheckIfUserNameExists(user.UserName); + if (user.Type == UserEnum.Type.Local) + { + if (!CryptoAdas.IsStrongPassword(user.Password)) + throw new UnprocessableEntityException(HttpEnum.ErrorMessage.UnprocessableEntityInvalidPassword); + user.Password = CryptoAdas.CreateBCrypt(user.Password); + } + + await _userRepository.InsertOneAsync(user); + var result = await GetUserByUserName(user.UserName) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User, null, result); + return result; + } + + public async Task CreateNewUserWithAuthorities(CreateUserWithAuthDto createUserWithAuthDto) + { + var user = createUserWithAuthDto.User; + var newUser = await CreateNewUserByRequest(user) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + var auths = createUserWithAuthDto.Authorizations; + foreach (var auth in auths) + { + auth.UserId = newUser.Id; + await CreateNewAuthority(auth); + } + + return newUser; + } + + public async Task UpdateUsersByRequest(User user, bool updatePass) + { + var oldUser = await _userRepository.GetByUserName(user.UserName); + var logoutHasBeenSent = false; + if (updatePass && !CryptoAdas.IsStrongPassword(user.Password)) + throw new UnprocessableEntityException(HttpEnum.ErrorMessage.UnprocessableEntityInvalidPassword); + if (updatePass) + { + user.Password = CryptoAdas.CreateBCrypt(user.Password); + await SendBroadcastUser(user, user.UserName, OperationType.UpdatePassword); + logoutHasBeenSent = true; + } + + var result = await _userRepository.UpdateUser(user, updatePass); + if(!logoutHasBeenSent && oldUser != null && result != null && oldUser.IsEnabled && !result.IsEnabled) + { + await SendBroadcastUser(result, user.UserName, OperationType.DisableUser); + logoutHasBeenSent = true; + } + if(!logoutHasBeenSent && result != null && result.LockExpirationDate != null && result.LockExpirationDate>DateTime.UtcNow) await SendBroadcastUser(result, user.UserName, OperationType.LockUser); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User, oldUser, result); + return result; + } + + private async Task SendBroadcastUser(object elementUpdated, string userName, OperationType operation) + { + var subscribers = _subscribersService.GetSubscribers().Where(s => + s.UserName == userName).ToList(); + foreach (var subscriber in subscribers) + { + await _clientMessageService.SendAsync(subscriber.Id, operation, elementUpdated); + } + } + private async Task SendBroadcastPermissions(object elementUpdated, string userName, OperationType operation) + { + var subscribers = _subscribersService.GetSubscribers().Where(s => + s.UserName == userName).ToList(); + foreach (var subscriber in subscribers) + { + if (subscriber.SubscriptionType == SubscriptionEnum.WsType.Admin) + { + try + { + var permissionsForPanel = await _permissionService.Value.GetPermissionsForPanel(userName); + _ = _clientMessageService.SendAsync(subscriber.Id, operation, permissionsForPanel); + } + catch (Exception) + { + _ = _clientMessageService.SendAsync(subscriber.Id, operation, null); + } + + } + else + { + _ = _clientMessageService.SendAsync(subscriber.Id, operation, elementUpdated); + } + } + } + public async Task UpdateUserWithAuthorities(UpdateUserWithAuthDto updateUserWithAuthDto, bool updatePass) + { + var u = updateUserWithAuthDto.User; + CheckIfEmailExists(u.Email, u.Id); + CheckIfUserNameExists(u.UserName, u.Id); + var userUpdated = await UpdateUsersByRequest(u, updatePass) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + + foreach (var id in updateUserWithAuthDto.AuthsToDelete) await DeleteAuthority(id); + foreach (var auth in updateUserWithAuthDto.Authorizations) await UpdateAuthority(auth); + foreach (var auth in updateUserWithAuthDto.NewAuthorizations) + { + auth.UserId = userUpdated.Id; + await CreateNewAuthority(auth); + } + if(updateUserWithAuthDto.AuthsToDelete.Any() || updateUserWithAuthDto.NewAuthorizations.Any() || updateUserWithAuthDto.Authorizations.Any()) + { + var allPermissionsForDisplay = await _displayService.Value.GetAllByUser(u.UserName); + + await SendBroadcastPermissions(allPermissionsForDisplay, u.UserName, OperationType.UpdateAuthorities); + } + return userUpdated; + } + + public async Task UpdateUserPassword(ObjectId id, string oldPassword, string newPassword) + { + if (oldPassword == newPassword) + throw new BadRequestException("New password must be different"); + + var user = await _userRepository.GetById(id) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + + + if (!CryptoAdas.VerifyPassword(oldPassword, user.Password)) + throw new UnprocessableEntityException("Invalid current password"); + + if (!CryptoAdas.IsStrongPassword(newPassword)) + throw new UnprocessableEntityException("Weak password"); + + user.Password = CryptoAdas.CreateBCrypt(newPassword); + var result = await _userRepository.UpdateUser(user, true); + if (result != null) + { + _logger.LogInformation("Password updated for user {UserId}", id); + + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User, user, result); + return true; + } + + + + return false; + } + + public async Task DeleteUser(ObjectId id) + { + try + { + var user = await _userRepository.GetById(id); + await _userRepository.DeleteAsync(id); + await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User, user, null); + return true; + } + catch (Exception e) + { + _logger.LogError("Error deleting user. Exception: {e}", e); + return false; + } + } + + public async Task> GetPaginatedUsers(PaginationFilter filter) + { + var result = _userRepository.GetPaginatedUsers(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(dataList, filter.PageNumber, filter.PageSize, count); + } + + public async Task GenerateJwt(User user) + { + var claims = new List + { + new(ClaimTypes.Name, user.UserName), + new(ClaimTypes.Email, user.Email), + new("IpAddress", user.IpAddress) + }; + + // Agregar claims de autorizaciones utilizando ClaimsFromAuthorities + if (user.Authorization != null) + claims.AddRange(await ClaimsFromAuthorities(user.Authorization)); + + var accessToken = JwtHelper.GetJwtToken( + user.UserName, + _jwtConfig.Key ?? string.Empty, + _jwtConfig.Issuer ?? string.Empty, + _jwtConfig.Audience ?? string.Empty, + _jwtConfig.TokenValidityInMinutes, + claims.ToArray()); + + var refreshToken = JwtHelper.GetJwtToken( + user.UserName, + _jwtConfig.Key ?? string.Empty, + _jwtConfig.Issuer ?? string.Empty, + _jwtConfig.Audience ?? string.Empty, + _jwtConfig.RefreshTokenValidityInMinutes, + claims.ToArray()); + + return new TokenResult + { + AccessToken = new JwtSecurityTokenHandler().WriteToken(accessToken), + RefreshToken = new JwtSecurityTokenHandler().WriteToken(refreshToken), + AccessTokenExpiryTime = accessToken.ValidTo, + RefreshTokenExpiryTime = refreshToken.ValidTo, + User = user + }; + } + + public async Task> GetAll() + { + BusinessException? loginException = null; + foreach (var m in _validLoginMethods) + { + if (!TryParse(m, out UserEnum.LoginMethod method)) continue; + var service = _availableLoginServices.FirstOrDefault(s => s.Method == method && s.AllowPassword); + if (service == null) continue; + try + { + return await service.GetAllUsers(); + } + catch (BusinessException e) + { + loginException = e; + } + catch (Exception e) + { + var message = e.Message; + if (e.Message != e.InnerException?.Message) message += $", {e.InnerException?.Message}"; + Debug.WriteLine($"Error Getting all users: {message} "); + loginException = new LoginServicesException(message); + } + } + + + if (loginException != null) throw loginException; + + throw new LoginServicesNotFoundException(); + } + + public async Task LoginWithGivenAccessToken(string token) + { + ValidateToken(token, IUserService.TokenTypeUser, out var validatedToken); + var jwtToken = (JwtSecurityToken)validatedToken; + var user = await GetUserByToken(jwtToken) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + user.Authorization ??= await _authorityService.GetUserAuthorities(user.Id); + + if (jwtToken.ValidTo < DateTime.UtcNow) + return await GenerateJwt(user); + + + var claims = new List + { + new(ClaimTypes.Name, user.UserName), + new(ClaimTypes.Email, user.Email), + new("IpAddress", user.IpAddress) + }; + + // Agregar claims de autorizaciones utilizando ClaimsFromAuthorities + claims.AddRange(await ClaimsFromAuthorities(user.Authorization)); + var dif = jwtToken.ValidTo > DateTime.Now ? jwtToken.ValidTo - DateTime.Now : DateTime.Now - jwtToken.ValidTo; + var accessToken = JwtHelper.GetJwtToken( + user.UserName, + _jwtConfig.Key ?? string.Empty, + _jwtConfig.Issuer ?? string.Empty, + _jwtConfig.Audience ?? string.Empty, + (int)dif.TotalMinutes, + claims.ToArray()); + var refreshToken = JwtHelper.GetJwtToken( + user.UserName, + _jwtConfig.Key ?? string.Empty, + _jwtConfig.Issuer ?? string.Empty, + _jwtConfig.Audience ?? string.Empty, + (int)dif.TotalMinutes + _jwtConfig.RefreshTokenValidityInMinutes, + claims.ToArray()); + + return new TokenResult + { + AccessToken = new JwtSecurityTokenHandler().WriteToken(accessToken), + RefreshToken = new JwtSecurityTokenHandler().WriteToken(refreshToken), + AccessTokenExpiryTime = accessToken.ValidTo, + RefreshTokenExpiryTime = refreshToken.ValidTo, + User = user + }; + } + + public async Task LoginWithAccessToken(string token) + { + ValidateToken(token, IUserService.TokenTypeUser, out var validatedToken); + var jwtToken = (JwtSecurityToken)validatedToken; + var user = await GetUserByToken(jwtToken) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + user.Authorization ??= await _authorityService.GetUserAuthorities(user.Id); + + return await GenerateJwt(user); + } + + public async Task CreateNewAuthority(Authorization auth) + { + var newUserAuthority = await _authorityService.CreateNewUserAuthority(auth) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed); + return newUserAuthority; + } + + public async Task DeleteAuthority(string id) + { + if (!ObjectId.TryParse(id, out var userAuthorityIdParsed)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var isDeleted = await _authorityService.DeleteUserAuthority(userAuthorityIdParsed); + + return !isDeleted ? throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed) : isDeleted; + } + + public async Task UpdateAuthority(Authorization authorization) + { + _ = await _authorityService.EditUserAuthority(authorization) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + return true; + } + + public async Task> ClaimsFromAuthorities(List authorities) + { + var claims = new List(); + foreach (var auth in authorities) + if (auth.UnitId != null) + { + var dis = await + _displayService.Value.GetByUnitId(ObjectId.Parse(auth.UnitId)); + foreach (var claim in dis) + { + auth.DisplayId = claim.Id.ToString(); + AddClaim(auth, claims); + } + } + else + { + AddClaim(auth, claims); + } + + return claims; + } + + private static void AddClaim(Authorization auth, List claims) + { + var newClaim = new Claim(ClaimTypes.Role, + auth.Rol + (string.IsNullOrEmpty(auth.DisplayId) ? "" : "_" + auth.DisplayId)); + if (claims.Any(c => c.Value == newClaim.Value)) return; + + //newClaim.Properties.Add("DisplayId", auth.DisplayId ?? ""); + claims.Add(newClaim); + } + + private void CheckIfUserNameExists(string userName, ObjectId? userId = null) + { + var users = GetAll().Result; + var userNameExists = userId.HasValue + ? users.Any(user => user.Id != userId.Value && user.UserName == userName) + : users.Any(user => user.UserName == userName); + + if (userNameExists) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectEmailOrUsername); + } + + private void CheckIfEmailExists(string email, ObjectId? userId = null) + { + var users = GetAll().Result; + var emailExists = userId.HasValue + ? users.Any(user => user.Id != userId.Value && user.Email == email) + : users.Any(user => user.Email == email); + + if (emailExists && !string.IsNullOrEmpty(email)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectEmailOrUsername); + } +} \ No newline at end of file diff --git a/adas-core.Authentication/adas-core.Authentication.csproj b/adas-core.Authentication/adas-core.Authentication.csproj new file mode 100644 index 00000000..8af44874 --- /dev/null +++ b/adas-core.Authentication/adas-core.Authentication.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + adas_core.Authentication + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/adas-core.Domain/Enums/ActionsEnum.cs b/adas-core.Domain/Enums/ActionsEnum.cs new file mode 100644 index 00000000..f910de8c --- /dev/null +++ b/adas-core.Domain/Enums/ActionsEnum.cs @@ -0,0 +1,26 @@ +namespace adas_core.Domain.Enums; + +public static class ActionsEnum +{ + public enum Actions + { + View, + Write + } + + public enum CrudAction + { + Create, + Read, + Update, + Delete, + Archive + } + + public enum ResourceAction + { + Add, + Update, + Delete + } +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/AlarmEnum.cs b/adas-core.Domain/Enums/AlarmEnum.cs new file mode 100644 index 00000000..216ad706 --- /dev/null +++ b/adas-core.Domain/Enums/AlarmEnum.cs @@ -0,0 +1,165 @@ +using System.ComponentModel; + +namespace adas_core.Domain.Enums; + +public static class AlarmEnum +{ + public enum AudioAlarmType + { + Continuous, + Off, + OnStart + } + + public enum AudioVideoState + { + Paused, + Off, + Enabled, + Disabled + } + + public enum BeaconColor + { + Blue, + Yellow, + Red, + None + } + + public enum EventPhase + { + Tpoint, + Start, + Continue, + End, + Update, + Escalate, + Deescalate, + Reset, + Inactivation + } + + public enum Name + { + [Description("CÓDIGO AZUL")] Blue, + [Description("Hemofiltro")] Hemo, + [Description("PEEP baja")] Peep, + + [Description("Límite superior de presión del respirador vía aerea")] + Pico, + [Description("Presión intracraneal")] Picm, + + [Description("Fuga Respirador")] VniLeak, + + [Description("Superación de Límites FR")] + EventFr, + + [Description("Superación de Límites FC")] + EventFc, + + [Description("Superación de Límites PAm")] + EventPam, + + [Description("Superación de Límites ARTm")] + EventArtm, + + [Description("Superación de Límites SpO2")] + EventSpo2, + + [Description("Superación de Límites PEEP")] + EventPeep, + + [Description("Límite FC Alto")] EventFcHi, + [Description("Límite FR Alto")] EventFrHi, + [Description("Límite SPO2 Alto")] EventSpo2Hi, + [Description("Límite PaM Alto")] EventPamHi, + [Description("Límite PICm Alto")] EventPicmHi, + [Description("Límite PEEPt Alto")] EventPeeptHi, + [Description("Límite MVexp Alto")] EventMvexpHi, + [Description("Límite ARTm Alto")] EventArtmHi, + + + [Description("Límite PEEP Bajo")] EventPeepLo, + [Description("Límite FC Bajo")] EventFcLo, + [Description("Límite FR Bajo")] EventFrLo, + [Description("Límite SPO2 Bajo")] EventSpo2Lo, + [Description("Límite PaM Bajo")] EventPamLo, + [Description("Límite PICm Bajo")] EventPicmLo, + [Description("Límite PEEPt Bajo")] EventPeeptLo, + [Description("Límite VMExp Bajo")] EventVmexpLo, + [Description("Límite ARTm Bajo")] EventArtmLo, + + [Description("Parada flujo de sangre Hemofiltro")] + EventAlarmFlow, + + [Description("Evento extremo")] ExtremeEvent, + + [Description("Bradicardia extrema")] ExtremeBradi, + [Description("Taquicardia extrema")] ExtremeTaqui, + [Description("Desaturación extrema")] ExtremeDesat, + + [Description("Alerta de volumen minuto respirador")] + VolMin, + + [Description("TAm alta extrema")] EvtExtrPamHi, + [Description("TAm baja extrema")] EvtExtrPamLo, + [Description("Límite VMExp Bajo")] EvtExtrVmexpLo, + + [Description("Apnea")] Apnea, + [Description("Respiratorio")] Respiration, + + //separar en varias + [Description("Alerta bomba")] Pump, + + //Tipo para hacer pruebas + [Description("Alerta Test")] Test, + + [Description("NEWS RED")] NewsAlert, + + [Description("NEWS YELLOW")] NewsWarning, + + [Description("NEWS OFF")] NewsOff + } + + public enum ObservationAlarmCode + { + AdvisoryAlert, + SoftInop + } + + public enum ObservationAlarmPriority + { + Ph, + Pm, + Pl + } + + public enum ObservationAlarmState + { + Inactive, + Active, + Latched + } + + public enum ObservationAlarmType + { + Sp, + St, + Sa + } + + public enum Severity + { + None, + Blue, + Red, + Yellow + } + + public enum Type + { + Manual, + Auto + } +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/AuthorizeUserByServiceType.cs b/adas-core.Domain/Enums/AuthorizeUserByServiceType.cs new file mode 100644 index 00000000..838a7f38 --- /dev/null +++ b/adas-core.Domain/Enums/AuthorizeUserByServiceType.cs @@ -0,0 +1,14 @@ +namespace adas_core.Domain.Enums; + +public enum AuthorizeUserByServiceType +{ + Display, + Discharge, + Admission, + PointOfCare, + ConfigDisplay, + MasterList, + Notice, + Patient, + Unit +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/CacheEnum.cs b/adas-core.Domain/Enums/CacheEnum.cs new file mode 100644 index 00000000..e825ca07 --- /dev/null +++ b/adas-core.Domain/Enums/CacheEnum.cs @@ -0,0 +1,28 @@ +namespace adas_core.Domain.Enums; + +public static class CacheEnum +{ + + public enum Mode + { + None = 0, // No cachear + Cache = 1, // CacheService (memoria) + Redis = 2 // RedisService + } + + + public enum EntityType + { + Unknown, + Patients, + Displays, + PumpObservations, + Appointments, + PontOfCare, + GroupedObservations, + PatientObservations, + ConfigObservations + } + + +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/ClickType.cs b/adas-core.Domain/Enums/ClickType.cs new file mode 100644 index 00000000..0fad7abd --- /dev/null +++ b/adas-core.Domain/Enums/ClickType.cs @@ -0,0 +1,10 @@ +namespace adas_core.Domain.Enums; + +public enum ClickType +{ + Unknown, + SingleClick, + DoubleClick, + Hold, + Release +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/DeviceActionType.cs b/adas-core.Domain/Enums/DeviceActionType.cs new file mode 100644 index 00000000..91091ae3 --- /dev/null +++ b/adas-core.Domain/Enums/DeviceActionType.cs @@ -0,0 +1,9 @@ +namespace adas_core.Domain.Enums; + +public enum DeviceActionType +{ + Unknown, + SendAlarm, + SendObs, + All +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/DeviceType.cs b/adas-core.Domain/Enums/DeviceType.cs new file mode 100644 index 00000000..61a94376 --- /dev/null +++ b/adas-core.Domain/Enums/DeviceType.cs @@ -0,0 +1,7 @@ +namespace adas_core.Domain.Enums; + +public enum DeviceType +{ + Unknown, + Button +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/DisplayConfigEnums.cs b/adas-core.Domain/Enums/DisplayConfigEnums.cs new file mode 100644 index 00000000..77116bf7 --- /dev/null +++ b/adas-core.Domain/Enums/DisplayConfigEnums.cs @@ -0,0 +1,301 @@ +namespace adas_core.Domain.Enums; + +public static class DisplayConfigEnums +{ + public enum AxisPosition + { + Top, + Bottom, + Left, + Right + } + + public enum AxisType + { + Time, + Value + } + + public enum BannerType + { + Inputs, + InputsFull, + Outputs, + OutputsText, + Slots, + Notices, + Status + } + + public enum CellType + { + BoxNumber, + BoxNumberStatus, + BeaconColor, + Simple, + Alert, + Alarm, + ObsGraphList, + GraphCompose, + GraphTitle, + Lab, + Plus, + ObsText, + Triple, + TripleData, + UciDays, + Predict, + TableBox, + TableDate, + TableCell, + TripleDataFormatted, + Sensors, + SensorAwair, + SensorObs, + Default, + FullIcon, + List, + Text, + TableText, + TableRepeated, + TableInsulation, + TableDropdown, + TableBoxRepeated, + TableAge, + Icon, + TableIcon, + TableGender, + Admission, + Demographic, + DemographicNoDiagnosis, + Intervention, + InterventionSection, + Isolation, + DetailCard, + DetailHeader, + DetailHeaderText, + PreviewCard, + PatientStatus, + Physicians, + Test, + TherapeuticCeiling, + Mobility, + Therapies, + DoctorCell, + CellDivider, + RowDivider, + VisitIcon, + AltableDestinationIcon, + LenguageBarrier, + DemographicCell, + DaysCalendarCell, + ObsCell, + ObsCellIcon, + AdmDateCellIcon, + NurseDateCellIcon, + MedicalDateCellIcon, + AllergyCell, + AltableInfo, + AltableInfoText, + TherapiesSection, + TestSection, + PhysiciansSection, + PatientStatusSection, + WrapperSection, + WrapperSectionCell, + DropDown, + TherapeuticCeilingDropDown, + IncomingDate, + AccessIconComponent, + FullAccessIconComponent, + IncomingSection, + WrapperObsCell, + SubHeader, + PatientInfo, + ObsGa, + TextObs, + ObsIcon, + DynamicText, + ObsMultiIcon, + ObsIconText, + WeightObs, + PlainIconText, + ObsBoolean, + CellWrapper, + Filler, + ObsMultiText, + LifeTime, + ObsIsolation, + BirthObs, + WrapperCell, + Language, + ObsTextIfMatch, + SensorWrapper + } + + public enum ConfigTypes + { + SectionCfg, + ObservationCfg, + PumpCfg + } + + public enum DirectionEnum + { + Row, + Column, + RowReverse, + ColumnReverse + } + + public enum DisplayType + { + DisplayNurse, + SmartDisplay, + StandarDisplay, + PumpDisplay, + Unknown + } + + public enum ELegendIconType + { + Circle, + Square, + Triangle, + Diamond, + Cross, + Star, + HorizontalLine, + VerticalLine, + DashedLine, + DottedLine + } + + public enum HospitalCustomizeEnum + { + Test, + Vdh, + Trueta, + Ryc, + Chuo, + Gregorio, + H12O + } + + public enum LabelFormat + { + Hour, + Second, + Minute, + HourExact, + Default, + FullDateExact, + FixPercentage, + Custom + } + + public enum LineType + { + Solid, + Dashed, + Dotted + } + + public enum MarkerIcon + { + Kangaroo, + Bath, + Hand, + None + } + + public enum RotatingLayoutMode + { + Default, + Equitable, + FixedImageTime + } + + + public enum RotatingLayoutType + { + HomeSection, + Ad + } + + public enum RowType + { + Simple, + General, + GraphLast, + Prediction, + Prevision, + Treatment, + Camera, + SubHeader, + Custom, + BoxNavigationSubheader, + Admission, + Demographic, + Intervention, + Isolation, + TherapiesSection, + TestSection, + InterventionSection, + IncomingSection, + DetailHeaderSection, + PatientStatus, + Physicians, + Test, + Therapies, + AltableInfo, + WrapperSection, + WrapperSectionCell, + RowDivider + } + + public enum SeriesType + { + Line, + Area, + Marker, + MarkArea, + Candlestick, + BarPercentage, + LineMapColor, + LineMark, + Scatter, + Bar + } + + public enum SourceType + { + Obs, + Diagnosis, + Alarm, + Pump, + Treatment, + Sensor + } + + public enum StepType + { + Standar, + Advertising, + Alarms + } + + public enum WebDisplayCellSubtype + { + Default, + Mobility, + Isolation, + Tests, + Therapy, + Intervention, + Visits, + Therapeutic, + Days, + WeeksDays + } +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/FileEnum.cs b/adas-core.Domain/Enums/FileEnum.cs new file mode 100644 index 00000000..ec606060 --- /dev/null +++ b/adas-core.Domain/Enums/FileEnum.cs @@ -0,0 +1,19 @@ +namespace adas_core.Domain.Enums; + +public static class FileEnum +{ + public enum AssetTheme + { + Light, + Dark + } + + public enum FileType + { + Csv, + Excel, + Json, + Xml, + Txt + } +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/GroupedObservationEnum.cs b/adas-core.Domain/Enums/GroupedObservationEnum.cs new file mode 100644 index 00000000..b3e1b44d --- /dev/null +++ b/adas-core.Domain/Enums/GroupedObservationEnum.cs @@ -0,0 +1,56 @@ +namespace adas_core.Domain.Enums; + +public class GroupedObservationEnum +{ + public enum Regularity + { + Times, + Second, + Minute, + Hour, + Day, + Shift + } + + public enum Result + { + First, + Last, + Sum, + Min, + Max, + Average, + Count, + + /* + * Rellena las horas que no tienen valor con la última media hora del anterior en caso de haberla. + * Al pedir las observaciones a la bd, devuelve todas las observaciones de la hora + */ + HalfHour, + + /* + * Devuelve valor para todas las horas puestas en el max, Rellenando con el valor anterior de no haber ninguno en esas horas. + */ + LastFilled, + + /* + * Igual que el last filled, pero no rellena con el valor anterior en caso de no haberlo. ¿Last puede ser así siempre?, creo que si + * (caso diuresis gráfica) + * No devuelve valores?? + */ + LastAll + } + + public enum Since + { + /* + * Does not take into account the current date for the number of groupings (default value). + */ + Now, + + /* + * It takes into account the current date for the number of groupings. + */ + Last + } +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/HttpEnum.cs b/adas-core.Domain/Enums/HttpEnum.cs new file mode 100644 index 00000000..a61ed0f8 --- /dev/null +++ b/adas-core.Domain/Enums/HttpEnum.cs @@ -0,0 +1,242 @@ +namespace adas_core.Domain.Enums; + +public static class HttpEnum +{ + /// + /// Enumeration for HTTP error code messages. + /// + public enum ErrorMessage + { + #region 200 OK + + /// + /// The update could not be completed because the resource has not been modified. + /// + OkResouceNotModified = 2001, + + #endregion + + #region 400 Bad Request + + /// + /// Required parameters are missing from the request. + /// + BadRequestMissingParameters = 4001, + + /// + /// The format of the provided data is invalid. + /// + BadRequestInvalidFormat = 4002, + + /// + /// The submitted data contains duplicate values that are not allowed. + /// + BadRequestDuplicateData = 4003, + + /// + /// Username already exists. + /// + BadRequestInvalidUsername = 4004, + + /// + /// Email already exists. + /// + BadRequestInvalidEmail = 4005, + + /// + /// Incorrect type. + /// + BadRequestIncorrectType = 4006, + + /// + /// Incorrect length. + /// + BadRequestIncorrectLength = 4007, + + /// + /// Invalid username or email. + /// + BadRequestIncorrectEmailOrUsername = 4008, + + #endregion + + #region 401 Unauthorized + + /// + /// The access token has expired. + /// + UnauthorizedTokenExpired = 4011, + + /// + /// The user does not have permission to perform this action. + /// + UnauthorizedNoPermission = 4012, + + /// + /// The authentication token is invalid or has been revoked. + /// + UnauthorizedInvalidToken = 4013, + + /// + /// User not registered. + /// + UnauthorizedUserNotRegistered = 4014, + + /// + /// Invalid user credentials. + /// + UnauthorizedInvalidCredentials = 4015, + + #endregion + + #region 403 Forbidden + + /// + /// User does not have access to this resource. + /// + ForbiddenNoAccess = 4031, + + /// + /// User does not have permission to perform this operation. + /// + ForbiddenNoPermission = 4032, + + /// + /// Access is restricted due to security configurations. + /// + ForbiddenRestrictedAccess = 4033, + + /// + /// User is blocked or disabled. + /// + ForbiddenUserBlocked = 4034, + + #endregion + + #region 404 Not Found + + /// + /// The requested resource does not exist. + /// + NotFoundResourceMissing = 4041, + + /// + /// No items matching the search criteria were found. + /// + NotFoundNoMatches = 4042, + + #endregion + + #region 409 Conflict + + /// + /// The update could not be completed. + /// + ConflictUpdateFailed = 4091, + + /// + /// The deletion could not be completed. + /// + ConflictDeleteFailed = 4092, + + /// + /// The deletion could not be completed because the resource is in use. + /// + ConflictResourceInUse = 4093, + + /// + /// The resource already exists and cannot be duplicated. + /// + ConflictResourceAlreadyExists = 4094, + + /// + /// The current state of the resource prevents this operation. + /// + ConflictInvalidResourceState = 4095, + + /// + /// The creation could not be completed. + /// + ConflictCreationFailed = 4096, + + /// + /// Operation could not be completed. + /// + ConflictOperationIncompleted = 4097, + + #endregion + + #region 422 Unprocessable Entity + + /// + /// One or more fields do not meet the required validations. + /// + UnprocessableEntityInvalidFields = 4221, + + /// + /// The resource cannot be processed. + /// + UnprocessableEntityCannotProcess = 4222, + + /// + /// The provided value is outside the allowed range. + /// + UnprocessableEntityOutOfRange = 4223, + + /// + /// The password is not strong enough. + /// + UnprocessableEntityInvalidPassword = 4224, + + /// + /// The current password is incorrect, or the new password does not meet the required format. Please check and try + /// again. + /// + BadRequestIncorrectPassword = 4225, + + #endregion + + #region 500 Internal Server Error + + /// + /// An unexpected error occurred while processing the request. + /// + InternalServerErrorUnexpected = 5001, + + /// + /// The service could not complete the operation due to an internal failure. + /// + InternalServerErrorInternalFailure = 5002, + + /// + /// Error communicating with an external service. + /// + InternalServerErrorExternalServiceError = 5003, + + #endregion + + #region 503 Service Unavailable + + /// + /// The service is under maintenance. Please try again later. + /// + ServiceUnavailableUnderMaintenance = 5031, + + /// + /// The system is experiencing high load. Please try again later. + /// + ServiceUnavailableHighLoad = 5032, + + /// + /// The data service is temporarily unavailable. + /// + ServiceUnavailableDataServiceUnavailable = 5033, + + /// + /// Action is temporarily unavailable. + /// + ServiceUnavailableCensusInAutomaticMode = 5034 + + #endregion + } +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/LightBeaconColor.cs b/adas-core.Domain/Enums/LightBeaconColor.cs new file mode 100644 index 00000000..64844007 --- /dev/null +++ b/adas-core.Domain/Enums/LightBeaconColor.cs @@ -0,0 +1,13 @@ +namespace adas_core.Domain.Enums; + +public enum LightBeaconColor +{ + Off = 000, + Red = 001, + Yellow = 011, + Green = 010, + Cyan = 110, + Blue = 100, + Magenta = 101, + White = 111 +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/MasterListType.cs b/adas-core.Domain/Enums/MasterListType.cs new file mode 100644 index 00000000..06366ed6 --- /dev/null +++ b/adas-core.Domain/Enums/MasterListType.cs @@ -0,0 +1,27 @@ +namespace adas_core.Domain.Enums; + +public enum MasterListType +{ + AltableOptionList, + AllergyList, + DestinationList, + DiagnosisList, + DischargeStatusList, + DoctorList, + DoctorTypeList, + InternalDestinationList, + InsulationList, + LanguageBarrierList, + PassiveSittingList, + MobilityOptionList, + OriginList, + PatientStatusList, + ProcedureList, + ServiceList, + TherapeuticCeilingList, + TreatmentList, + VisitOptionList, + AccessControlList, + TestList, + GenericList +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/MedicineEnum.cs b/adas-core.Domain/Enums/MedicineEnum.cs new file mode 100644 index 00000000..0861f4e6 --- /dev/null +++ b/adas-core.Domain/Enums/MedicineEnum.cs @@ -0,0 +1,30 @@ +namespace adas_core.Domain.Enums; + +public static class MedicineEnum +{ + public enum Group + { + Metabolic, + DoubleSignature, + Vasoactive + } + + public enum Types + { + MuscleRelaxant, + Vasopressin, + AdrenalineNoradrenaline, + DopamineDobutamine, + Pge1, + FibrinolyticAnticoagulants, + DexmedetomidineMidazolam, + Anticomiciales, + AntiarrhythmicsCardiovascular, + BloodDerivative, + ParacetamolOpiates, + ParenteralNutrition, + ParenteralNutritionLipids, + Insulin, + CrystalloidExpansion + } +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/ObservationEnum.cs b/adas-core.Domain/Enums/ObservationEnum.cs new file mode 100644 index 00000000..779778bc --- /dev/null +++ b/adas-core.Domain/Enums/ObservationEnum.cs @@ -0,0 +1,51 @@ +namespace adas_core.Domain.Enums; + +public static class ObservationEnum +{ + /** + * Default => Devuelve flecha blanca siempre + * DefaultStatus => Devuelve flecha basándonos en el estado de la obs manteniendo la tendencia respecto al último valor + * RegularLastValue => Si baja flecha color rojo Si sube flecha color verde + * RegularInvertLastValue => Si baja flecha color verde Si sube flecha color rojo + * MinMaxValue => Solo se muestra flecha por encima / debajo max / min y siempre en rojo + */ + public enum ArrowType + { + None, + Default, + DefaultStatus, + RegularLastValue, + RegularInvertLastValue, + MinMaxValue + } + + public enum Category + { + Treatment, + Observation, + Intervention + } + + public enum InsertMode + { + Auto, + Manual, + Mix + } + + public enum ValueType + { + Number, + String, + Boolean, + Date + } + + public enum XmlType + { + Observation, + Treatment, + Intervention, + Canbra + } +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/OperationType.cs b/adas-core.Domain/Enums/OperationType.cs new file mode 100644 index 00000000..1eb6b05b --- /dev/null +++ b/adas-core.Domain/Enums/OperationType.cs @@ -0,0 +1,83 @@ +namespace adas_core.Domain.Enums; + +public enum OperationType +{ + Status, + Section, + SectionSensors, + Box, + Observation, + GroupedObservation, + ConfigObservation, + + Pump, + PumpAlarm, + + Treatment, + UpdateTreatment, + + UpdatedPointOfCare, + NewPointOfCare, + PatientLocation, + DeletePatient, + PatientAttendingDoctor, + PatientData, + UpdatePatient, + NewPatient, + + RecordingAlert, + Diagnosis, + + NewAppointment, + UpdatedAppointment, + CanceledAppointment, + + Sensor, + Error, + SurgeryRoomAlert, + ChatMessage, + Relay, + RecordingStatus, + UpdateApplication, + Beacon, + Alarm, + + NewAdmission, + DeleteAdmission, + UpdateAdmission, + + NewDischarge, + DeleteDischarge, + UpdateDischarge, + + Display, + DisplayList, + + NewNotice, + DeleteNotice, + UpdateNotice, + + NewMasterList, + DeleteMasterList, + UpdateMasterList, + + UpdateUnit, + UpdateDisplayConfig, + UpdateCardDisplayConfig, + UpdateSmartDisplayConfig, + UpdateNurseDisplayConfig, + UpdateDisplayPoC, + + NewPoC, + UpdatePoc, + DeletePoc, + UpdateMasterListItem, + DeleteMasterListItem, + AddMasterListItem, + UpdateDetailDisplayConfig, + UpdateChartConfig, + UpdatePassword, + UpdateAuthorities, + DisableUser, + LockUser +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/OrderControlType.cs b/adas-core.Domain/Enums/OrderControlType.cs new file mode 100644 index 00000000..4d6f4760 --- /dev/null +++ b/adas-core.Domain/Enums/OrderControlType.cs @@ -0,0 +1,56 @@ +namespace adas_core.Domain.Enums; + +public enum OrderControlType +{ + Af, //Order/service refill request approval + Ca, //Cancel order/service request + Ch, //Child order/service + Cn, //Combined result + Cr, //Canceled as requested + Dc, //Discontinue order/service request + De, //Data errors + Df, //Order/service refill request denied + Dr, //Discontinued as requested + Fu, //Order/service refilled, unsolicited + Hd, //Hold order request + Hr, //On hold as requested + Li, //Link order/service to patient care problem or goal + Mc, //Miscellaneous Charge - not associated with an order + Na, //Number assigned + Nw, //New order/service + Oc, //Order/service canceled + Od, //Order/service discontinued + Oe, //Order/service released + Of, //Order/service refilled as requested + Oh, //Order/service held + Ok, //Order/service accepted & OK + Op, //Notification of order for outside dispense + Or, //Released as requested + Pa, //Parent order/service + Pr, //Previous Results with new order/service + Py, //Notification of replacement order for outside dispense + Re, //Observations/Performed Service to follow + Rf, //Refill order/service request + Rl, //Release previous hold + Ro, //Replacement order + Rp, //Order/service replace request + Rq, //Replaced as requested + Rr, //Request received + Ru, //Replaced unsolicited + Sc, //Status changed + Sn, //Send order/service number + Sr, //Response to send order/service status request + Ss, //Send order/service status request + Ua, //Unable to accept order/service + Uc, //Unable to cancel + Ud, //Unable to discontinue + Uf, //Unable to refill + Uh, //Unable to put on hold + Um, //Unable to replace + Un, //Unlink order/service from patient care problem or goal + Ur, //Unable to release + Ux, //Unable to change + Xo, //Change order/service request + Xr, //Changed as requested + Xx //Order/service changed, unsol. +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/PatientEnum.cs b/adas-core.Domain/Enums/PatientEnum.cs new file mode 100644 index 00000000..48b20510 --- /dev/null +++ b/adas-core.Domain/Enums/PatientEnum.cs @@ -0,0 +1,12 @@ +namespace adas_core.Domain.Enums; + +public static class PatientEnum +{ + public enum Gender + { + Male, + Female, + Undifferentiated, + Unknown + } +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/PermissionEnum.cs b/adas-core.Domain/Enums/PermissionEnum.cs new file mode 100644 index 00000000..3d8e46fb --- /dev/null +++ b/adas-core.Domain/Enums/PermissionEnum.cs @@ -0,0 +1,55 @@ +namespace adas_core.Domain.Enums; + +public class PermissionEnum +{ + public enum PermissionTypesEnum + { + Unit, + Display, + DisplayConfig, + MasterList, + Treatment, + Patient, + Medicine, + Pump, + User, + ConfigObservation, + Observation, + Audit, + Admission, + Discharge, + PatientIncome, + Obs, + DemographicData, + BoxBlocking, + Default // ?????????????????????????? + } + + public enum RolesType + { + AuthSome = 1, + AuthGuest = 2, + AuthNurse = 3, + AuthNursesupervisor = 4, + AuthDoctor = 5, + AuthDoctorchief = 6, + AuthDeveloper = 5000, + AuthAdmin = 10000 + } + + + public enum SourcePermissionsEnum + { + Display, + Unit, + Panel + } + + public enum UserActionEnum + { + Create, + Read, + Update, + Delete + } +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/PumpEnum.cs b/adas-core.Domain/Enums/PumpEnum.cs new file mode 100644 index 00000000..179e7229 --- /dev/null +++ b/adas-core.Domain/Enums/PumpEnum.cs @@ -0,0 +1,169 @@ +namespace adas_core.Domain.Enums; + +public static class PumpEnum +{ + public enum PumpMessageType + { + Unknown = 0, + Observation = 1, // PCD-01 / PCD-10 + Alarm = 2 // PCD-04 + } + + public enum AlarmMode + { + Hight, + Medium, + Low, + None + } + + public enum AlarmType + { + // ALARMAS DE ESTADO/CONDICIÓN GENERAL + AlarmCondition, + + // ALARMAS ESPECÍFICAS DE INFUSIÓN Y BOMBA + Occlusion, + DoorOpen, + InfusionNearCompletion, + VtbiComplete, + NoPowerSource, + SyringeSizeMismatch, + + + // ALARMAS DE SISTEMA Y TÉCNICAS + BatteryEmpty, + PowerFailure, + CommunicationFailure, + PumpMalfunction, + ProgramError, + SensorError, + PressureSensorError, + TemperatureTooHigh, + SelfTestFailed, + + //NEXUS ALARMS + SystemError, + CheckSyringePlunger, + CheckSyringeClamp, + SyringeInstallError, + DownstreamOcclusion, + DriveHeadPositionErr, + HandOverFailed, + KvoEnd, + SyringeEmpty, + VtbiDone, + PressureFall, + PressureRise, + StandbyEnd, + AcPowerFail, + Attention, + DrugMismatch, + StepCompleted, + NearEndOfInfusion, + SyringeNearEmpty, + AirInLine, + BatteryLow, + NoBattery, + TitrationNotConfirmed, + DropSensorDisconnect, + NoFlow, + TooFewDrops, + TooManyDrops, + UpstreamOcclusion, + KvoAtSeEnd, + InfusionSetNotCorrectlyFitted + } + + public enum Event + { + Start, + Stop, + Complete, + ProgramRelay, + CommStatusChange, + LimitOverrideSoftUpper, + LimitOverrideSoftLower, + LimitExceededSoftUpper, + LimitExceededSoftLower, + VolumeCountersCleared + } + + public enum EventPhase + { + Start, + Continue, + End + } + + public enum InfusingStatus + { + Infusing, + NotInfusing, + Unknown + } + + public enum MedicationMode + { + DrugDosing, + RampTaper, + MultiStep, + MultiDosing, + LoadingDose, + Continuous, + Piggyback, + Concurrent, + ClinicianDose, + Kvo, + Flushing, + PatientDose, + Priming, + Other + } + + public enum Mode + { + Hold, + Infusing, + Primary, + Secondary, + KeepVeinOpen, + EndOfInfusion, + Titration, + Bolus, + Priming, + Setup, + Induction, + BackOff, + MultiDose, + LoadingDose, + Unknown, + DrugDosing, + RampTaper, + MultiDosing, + Continuous, + Piggyback, + Concurrent, + ClinicianDose, + Other, + Flushing, + PatientDose, + MultiStep + } + + public enum Status + { + Alarm, + Warning, + Caution, + Hold, + Infusing, + Unknown, + NotInfusing, + Delivering, + NotDelivering, + Transitioning, + Kvo, + Priming + } +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/RelayEnum.cs b/adas-core.Domain/Enums/RelayEnum.cs new file mode 100644 index 00000000..f95c0523 --- /dev/null +++ b/adas-core.Domain/Enums/RelayEnum.cs @@ -0,0 +1,29 @@ +namespace adas_core.Domain.Enums; + +public static class RelayEnum +{ + public enum Mode + { + OpenedOnClosedOff, + OpenedOffClosedOn + } + + public enum OutletType + { + Rele, + Pdu + } + + public enum Status + { + On, + Off, + Unknown, + NotInitialized + } + + public enum Type + { + Door + } +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/RespirationType.cs b/adas-core.Domain/Enums/RespirationType.cs new file mode 100644 index 00000000..855afc76 --- /dev/null +++ b/adas-core.Domain/Enums/RespirationType.cs @@ -0,0 +1,9 @@ +namespace adas_core.Domain.Enums; + +public enum RespirationType +{ + NonInvasive, + Invasive, + HighFrequencyVentilation, + None +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/RetentionPolicy.cs b/adas-core.Domain/Enums/RetentionPolicy.cs new file mode 100644 index 00000000..35148401 --- /dev/null +++ b/adas-core.Domain/Enums/RetentionPolicy.cs @@ -0,0 +1,9 @@ +namespace adas_core.Domain.Enums; + +public enum RetentionPolicy +{ + NoDelete, + DeleteOlderDays, + DeleteOlderNumber, + DeleteOlderSeconds +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/StatusEnum.cs b/adas-core.Domain/Enums/StatusEnum.cs new file mode 100644 index 00000000..afda1354 --- /dev/null +++ b/adas-core.Domain/Enums/StatusEnum.cs @@ -0,0 +1,53 @@ +namespace adas_core.Domain.Enums; + +public static class StatusEnum +{ + public enum Discharge + { + Altable, + NoAltable + } + + public enum PointOfCare + { + //Bloqueado + Locked, + + //Reservado cuando hay una admisión prevista + Reserved, + + //Disponible + Available, + + //En uso + InUse + } + + public enum Type + { + Ok = 0, + Warning = 1, + Alert = 2 + } + + public enum User + { + Enabled, + EnabledUnlocked, + EnabledLocked, + Disabled + } + + public enum Video + { + Initialized, + Recording, + Paused, + Stopped, + Finished, + Published, + Deleted, + Purged, + Error + } +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/SubscriptionType.cs b/adas-core.Domain/Enums/SubscriptionType.cs new file mode 100644 index 00000000..be694b10 --- /dev/null +++ b/adas-core.Domain/Enums/SubscriptionType.cs @@ -0,0 +1,28 @@ +namespace adas_core.Domain.Enums; + +public static class SubscriptionEnum +{ + public enum Type + { + SectionSubscribe, + PatientData, + Idle, + BoxSubscribe, + ChatMessage, + RecordingSuscribe, + GroupedObservations, + Sensors, + Display, + Demo, + Admin + } + + public enum WsType + { + Section, + Box, + Unit, + Display, + Admin + } +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/UserEnum.cs b/adas-core.Domain/Enums/UserEnum.cs new file mode 100644 index 00000000..29766aa1 --- /dev/null +++ b/adas-core.Domain/Enums/UserEnum.cs @@ -0,0 +1,17 @@ +namespace adas_core.Domain.Enums; + +public static class UserEnum +{ + public enum LoginMethod + { + Local, + Ldap, + Cas + } + + public enum Type + { + Local, + Ldap + } +} \ No newline at end of file diff --git a/adas-core.Domain/Enums/VirtualPointOfCare.cs b/adas-core.Domain/Enums/VirtualPointOfCare.cs new file mode 100644 index 00000000..44343dfc --- /dev/null +++ b/adas-core.Domain/Enums/VirtualPointOfCare.cs @@ -0,0 +1,13 @@ +namespace adas_core.Domain.Enums; + +public enum VirtualPointOfCare +{ + Pushed, + Unknown, + Deleted, + Moved, + NoBed, + Cancelled, + Recovered, + UnitData +} \ No newline at end of file diff --git a/adas-core.Domain/Exceptions/AdasException.cs b/adas-core.Domain/Exceptions/AdasException.cs new file mode 100644 index 00000000..af4b0fab --- /dev/null +++ b/adas-core.Domain/Exceptions/AdasException.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Exceptions; + +public class AdasException(string message) : Exception($"ADAS Exception: {message}"); \ No newline at end of file diff --git a/adas-core.Domain/Exceptions/BusinessException.cs b/adas-core.Domain/Exceptions/BusinessException.cs new file mode 100644 index 00000000..073b5b44 --- /dev/null +++ b/adas-core.Domain/Exceptions/BusinessException.cs @@ -0,0 +1,14 @@ +using System.Net; + +namespace adas_core.Domain.Exceptions; + +public class BusinessException : AggregateException +{ + public BusinessException(HttpStatusCode status, string message) : base($"{status}: {message}") + { + } + + public BusinessException(string message, Exception exception) : base(message, exception) + { + } +} \ No newline at end of file diff --git a/adas-core.Domain/Exceptions/LoginServicesException.cs b/adas-core.Domain/Exceptions/LoginServicesException.cs new file mode 100644 index 00000000..7480b5eb --- /dev/null +++ b/adas-core.Domain/Exceptions/LoginServicesException.cs @@ -0,0 +1,16 @@ +using System.Net; + +namespace adas_core.Domain.Exceptions; + +public class LoginServicesException : BusinessException +{ + public LoginServicesException(string message) : + base(HttpStatusCode.BadRequest, $"Login service error: {message}") + { + } + + public LoginServicesException(string message, Exception exception) : base($"Login service error: {message}", + exception) + { + } +} \ No newline at end of file diff --git a/adas-core.Domain/Exceptions/LoginServicesNotFoundException.cs b/adas-core.Domain/Exceptions/LoginServicesNotFoundException.cs new file mode 100644 index 00000000..175d5428 --- /dev/null +++ b/adas-core.Domain/Exceptions/LoginServicesNotFoundException.cs @@ -0,0 +1,6 @@ +using System.Net; + +namespace adas_core.Domain.Exceptions; + +public class LoginServicesNotFoundException() + : BusinessException(HttpStatusCode.Forbidden, "No login services available"); \ No newline at end of file diff --git a/adas-core.Domain/Exceptions/UserNotFoundException.cs b/adas-core.Domain/Exceptions/UserNotFoundException.cs new file mode 100644 index 00000000..417934bf --- /dev/null +++ b/adas-core.Domain/Exceptions/UserNotFoundException.cs @@ -0,0 +1,16 @@ +using System.Net; + +namespace adas_core.Domain.Exceptions; + +public class UserNotFoundException : BusinessException +{ + public UserNotFoundException(string username) : + base(HttpStatusCode.NotFound, $"USER with username {username} not found") + { + } + + public UserNotFoundException(string username, Exception exception) : base( + $"USER with username {username} not found", exception) + { + } +} \ No newline at end of file diff --git a/adas-core.Domain/IDefaultRepository.cs b/adas-core.Domain/IDefaultRepository.cs new file mode 100644 index 00000000..9c75e402 --- /dev/null +++ b/adas-core.Domain/IDefaultRepository.cs @@ -0,0 +1,33 @@ +using System.Linq.Expressions; +using adas_core.Domain.Models.Filter; +using Microsoft.EntityFrameworkCore; + +namespace adas_core.Domain; + +public interface IDefaultRepository where T : class +{ + DbSet GetSet(); + IQueryable Query(); + IQueryable Query(PaginationFilter? filter); + IQueryable Query(int? currentPage, int? pageSize = null); + + T? GetById(TS id, params Expression>[] includeExpressions); + + T? GetById(TS id, QueryCustomizer queryCustomizer); + + /*[Obsolete("Use GetById( id) instead")] + T? GetById(int id); + [Obsolete("Use GetById( id) instead")] + T? GetById(long id); + //T? GetById(string id);*/ + T? GetByIds(params object[] keyValues); + T Create(T obj); + List Create(List objs); + T Delete(T obj); + List Delete(ICollection objs); + void DeleteAll(); + T Update(T obj); + int Count(); + int Count(string status); + List FindAll(); +} \ No newline at end of file diff --git a/adas-core.Domain/Models/ADMPanelRequest.cs b/adas-core.Domain/Models/ADMPanelRequest.cs new file mode 100644 index 00000000..267ea77f --- /dev/null +++ b/adas-core.Domain/Models/ADMPanelRequest.cs @@ -0,0 +1,75 @@ +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Pumps; +using MongoDB.Bson; +using Newtonsoft.Json; + +namespace adas_core.Domain.Models; + +public class AdmPanelRequest +{ + public string? Type { get; set; } + + public string? PatientNumber { get; set; } + + public Person? Patient { get; set; } + + public string? PatientId { get; set; } + + public string? Id { get; set; } + + public DateTime? AdmTime { get; set; } + + public DateTime? DisTime { get; set; } + + + public PatientLocation? Location { get; set; } + + public ObjectId? PointOfCareId { get; set; } + public ObjectId? UnitId { get; set; } + + // ADT + public PatientLocation? OldLocation { get; set; } + + public Person? AttendingDoctor { get; set; } + + public string? OldPatientNumber { get; set; } + + + // ORU + public ObservationData? ObservationData { get; set; } + + public ObservationData? ObsertationData + { + get => ObservationData; + set => ObservationData = value; + } + + public List? Observations { get; set; } + + public PatientObservation? Observation { get; set; } + + public List? PumpObservations { get; set; } + + public PumpObservation? PumpObservation { get; set; } + + + public string SectionId { get; set; } = string.Empty; + + public string SectionItemGroup { get; set; } = string.Empty; + + public Box? SectionItemBoxUpdated { get; set; } + + public Dictionary? SectionItems { get; set; } + + public override string? ToString() + { + try + { + return JsonConvert.SerializeObject(this, Formatting.None); + } + catch + { + return base.ToString(); + } + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/APIRequest.cs b/adas-core.Domain/Models/APIRequest.cs new file mode 100644 index 00000000..ec199271 --- /dev/null +++ b/adas-core.Domain/Models/APIRequest.cs @@ -0,0 +1,118 @@ +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Pumps; +using adas_core.Domain.Models.Recording; +using Newtonsoft.Json; + +namespace adas_core.Domain.Models; + +public class ApiRequest +{ + // SIU + public PatientAppointment? Appointment; + + // SIU + public List? Appointments; + + // OMP + public PatientTreatment? Treatment; + + public List? Treatments; + public string? TraceId { get; set; } + + public string? TraceMessageId { get; set; } + + public string? Type { get; set; } + + public string? PatientNumber { get; set; } + + public Person? Patient { get; set; } + + public string? PatientId { get; set; } + + public DateTime? AdmTime { get; set; } + + public DateTime? DisTime { get; set; } + + + public PatientLocation? Location { get; set; } + + //Chat + public ChatMessage? ChatMessage { get; set; } + + //Recording + public RecordingData? Recording { get; set; } + + //SurgeryRoomAlert + public PatientLocation? AlertLocationFrom { get; set; } + + public PatientLocation? AlertLocationTo { get; set; } + + // ADT + public PatientLocation? OldLocation { get; set; } + + public Person? AttendingDoctor { get; set; } + + public string? OldPatientNumber { get; set; } + + + // ORU + public ObservationData? ObservationData { get; set; } + + public ObservationData? ObsertationData + { + get => ObservationData; + set => ObservationData = value; + } + + public List? Observations { get; set; } + + public PatientObservation? Observation { get; set; } + + public List? Alarms { get; set; } + + public List? PumpObservations { get; set; } + + public PumpObservation? PumpObservation { get; set; } + + //DG1 + public List? Diagnosis { get; set; } + + // ALERTS + + public List? RecordingAlerts { get; set; } + + public PatientRecordingAlert? RecordingAlert { get; set; } + + + public DateTime MessageTime { get; set; } + + public bool WaitResult { get; set; } + + //ICCA db sync + public List? IccaPatients { get; set; } + + public string? Facility { get; set; } + + //NursePlan + public Admission? Admission { get; set; } + public Discharge? Discharge { get; set; } + + public MasterList? MasterList { get; set; } + + public Notice? Notice { get; set; } + + public bool RequestFromPanel { get; set; } + + public override string? ToString() + { + try + { + return JsonConvert.SerializeObject(this, Formatting.None); + } + catch + { + return base.ToString(); + } + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/AppSettings/ApiSettings.cs b/adas-core.Domain/Models/AppSettings/ApiSettings.cs new file mode 100644 index 00000000..1ba3c4d5 --- /dev/null +++ b/adas-core.Domain/Models/AppSettings/ApiSettings.cs @@ -0,0 +1,489 @@ +using System.Text.Json; +using adas_core.Domain.Enums; + +namespace adas_core.Domain.Models.AppSettings; + +public class ApiSettings +{ + public const string Api = "ApiSettings"; + public string? Customize { get; set; } + public PointOfCareMapping? PointOfCareMapping { get; set; } + public ConfigObservationSettings? ConfigObservation { get; set; } + + public RetentionPolicy RetentionPolicy { get; set; } + public int RetentionPolicyValue { get; set; } + + //public CacheSettings? CacheSettings { get; set; } + public bool CreatePatientWithOru { get; set; } + public bool PushPatientWithOru { get; set; } + public bool CreatePatientWithSiu { get; set; } + public bool CreatePatientWithoutLocation { get; set; } + public bool CreatePatientWithoutPatientNumber { get; set; } + public bool CreatePatientWithLocation { get; set; } + public bool CreatePatientWithAdtA08 { get; set; } + public bool UpdatePatientDataWithOru { get; set; } + public bool UpdatePatientDataWithAdtA02 { get; set; } + public bool UpdatePatientLocationWithOru { get; set; } + public bool UpdatePatientNumberWithOru { get; set; } + + public bool ActivateSchedulerCheckNewsJob { get; set; } + + public int CheckNewsJobIntervalMinutes { get; set; } = 15; + + public string? PatientsAppointments { get; set; } + public string? PatientsDiagnosis { get; set; } + public string? PatientsObservations { get; set; } + public string? PatientsAlarms { get; set; } + public string? Patients { get; set; } + public string? PoCSettings { get; set; } + public string? PumpObservations { get; set; } + public string? PumpStates { get; set; } + public string? PumpAlarmEvent { get; set; } + public string? PumpAlarmState { get; set; } + + public string? PatientsRecordingAlerts { get; set; } + public string? PatientsTreatments { get; set; } + public string Mappings { get; set; } = "mappings"; + public string? ConfigSections { get; set; } + public string? Units { get; set; } = "units"; + public string? ConfigObservations { get; set; } + public string? HistoricalConfigChanges { get; set; } + + public string Admissions { get; set; } = "admissions"; + public string Discharges { get; set; } = "discharges"; + public string Notices { get; set; } = "notices"; + public string Locations { get; set; } = "pointOfCares"; + public string Relays { get; set; } = "relays"; + public string Cameras { get; set; } = "cameras"; + public string LightBeacons { get; set; } = "light_beacons"; + public string Displays { get; set; } = "displays"; + public string DisplaysConfig { get; set; } = "config_displays"; + public string DisplayCardConfig { get; set; } = "config_displays_card"; + public string DisplayDetailConfig { get; set; } = "config_displays_detail"; + public string DisplayChartConfig { get; set; } = "config_displays_chart"; + public string Authorizations { get; set; } = "authorizations"; + + + public string AltableOptionList { get; set; } = "list_altable"; + public string AllergyList { get; set; } = "list_allergy"; + public string DestinationList { get; set; } = "list_destinations"; + public string DiagnosisList { get; set; } = "list_diagnosis"; + public string DischargeStatusList { get; set; } = "list_dischargeStatus"; + public string DoctorList { get; set; } = "list_doctor"; + public string DoctorTypeList { get; set; } = "list_doctorType"; + public string InternalDestinationList { get; set; } = "list_internalDestination"; + public string InsulationList { get; set; } = "list_insulation"; + public string LanguageBarrierList { get; set; } = "list_languageBarrier"; + public string MobilityOptionList { get; set; } = "list_mobility"; + public string OriginList { get; set; } = "list_origin"; + public string PatientStatusList { get; set; } = "list_patientStatus"; + public string ProcedureList { get; set; } = "list_procedure"; + public string TestList { get; set; } = "list_test"; + public string ServiceList { get; set; } = "list_service"; + public string TherapeuticCeilingList { get; set; } = "list_therapeuticCeiling"; + public string TreatmentList { get; set; } = "list_treatment"; + public string VisitOptionList { get; set; } = "list_visitOption"; + public string AccessControlList { get; set; } = "list_accessControl"; + public string PassiveSittingList { get; set; } = "list_passiveSitting"; + public string GenericList { get; set; } = "list_generic"; + + + public string? ConfigUnits { get; set; } + public bool ConfigUnitsRequired { get; set; } = true; + public string? ConfigUnitsKey { get; set; } + + public string? ConfigPumps { get; set; } + public bool ConfigPumpsRequired { get; set; } = true; + public string? ConfigPumpsKey { get; set; } + + public string? ServiceConfig { get; set; } + public string Users { get; set; } = "users"; + public string? Medicines { get; set; } + + + public string? Devices { get; set; } + public string? ArchivePatientsAppointments { get; set; } + public string? ArchivePatientsDiagnosis { get; set; } + public string? ArchivePatientsObservations { get; set; } + public string? ArchivePatient { get; set; } + public string? ArchivePatientsPumpobservations { get; set; } + public string? ArchivePatientsRecordingalerts { get; set; } + public string? ArchivePatientsTreatments { get; set; } + public string? ArchivePatientProcedure { get; set; } + public string? PatientCarePlan { get; set; } + public bool CretatePatientWithoutPatientNumber { get; set; } + public bool ArchivePatientWithOru { get; set; } + public string? OnArchivePatientAction { get; set; } + public int? ArchivePatientsWithoutObservationsSinceHours { get; set; } + public int? CheckInactivePatientSchedulerIntervalHours { get; set; } + public int? CheckActiveTreatmentsSchedulerIntervalMinutes { get; set; } + public int? CheckActiveAppointmentsSchedulerIntervalMinutes { get; set; } + + public int? GetProvidersObservationsSchedulerIntervalMinutes { get; set; } + public int? SinceDischargeTimeToArchive { get; set; } + public int? CheckErrorQueues { get; set; } + + public bool ActivateCheckExpiredAlerts { get; set; } = true; + public bool ActivateSchedulerCheckExpiredObservation { get; set; } + public bool ActivateSchedulerGetProvidersObservations { get; set; } + public bool ActivateCheckInactivePatients { get; set; } + public bool ActivateSchedulerCheckOpiateBoluses { get; set; } + public bool ActivateSchedulerCheckActiveTreatmentsJob { get; set; } + public bool ActivateSchedulerCheckActiveAppointmentsJob { get; set; } + + public bool ActivateSchedulerCheckHydricBalance { get; set; } + + + public int? SchedulerCheckExpiredObservationsIntervalMinutes { get; set; } + public int? SchedulerCheckExpiredAlertsIntervalSeconds { get; set; } + + public bool StartRecordingWithoutPatientNumber { get; set; } = true; + + public List? Shift { get; set; } + public bool FindPatientByLocationWithoutId { get; set; } + public bool PersistObservationCodes { get; set; } + public string? IccaFacility { get; set; } + public List? SendingFacility { get; set; } + public List AllergiesCode { get; set; } = []; + public List DrainageCode { get; set; } = []; + public List PositionCode { get; set; } = []; + public List IsolationCode { get; set; } = []; + public List? HighFrequencyVentilation { get; set; } + public List? InvasiveVentilation { get; set; } + public List? NonInvasiveVentilation { get; set; } + public List? Ecmo { get; set; } + public List? Surgery { get; set; } + public List? Sedation { get; set; } + public List? AntibioticList { get; set; } + public List? AnxiolyticList { get; set; } + public List? AntipsicoticList { get; set; } + public List? AntidepressantsList { get; set; } + public List? NeuroMedicationList { get; set; } + public List? SerumCrystalloidList { get; set; } + public List? SerumColloidList { get; set; } + public List? AntihypertensivesList { get; set; } + public List? SurgeryText { get; set; } + public List? Transcutaneous { get; set; } + public List? RegionalBrainSaturation { get; set; } + public List? MedicationBolus { get; set; } + public List? NotesIndicatingMedication { get; set; } + public List? IntravenousLinesCode { get; set; } + public List? Electroencephalogram { get; set; } + public List? Respiratory { get; set; } + public List? Oni { get; set; } + public List? PumPressureAlarm { get; set; } + public List? VolumetricAirOcclusionAlarm { get; set; } + public List? VolumetricAirInLineAlarm { get; set; } + + public List? EndContinuousInfusionAlarm { get; set; } + public List? InotropicEndInfusionAlarm { get; set; } + + public List? InotropicMedicines { get; set; } + public int IgnoreCalcObservationsOlderInMinutesThan { get; set; } + public int PumpExpiresSeconds { get; set; } = 60; + public bool SendPumpsZero { get; set; } = true; + + public string? BalizaUrl { get; set; } + public string? BalizaPassword { get; set; } + public string? DiagnosisSystem { get; set; } + public List DiagnosisCode { get; set; } = []; + + + public string[]? PathUpdateFiles { get; set; } + public int? TimeToSendGroupedObs { get; set; } + public string[]? PathToDisplayAssets { get; set; } + public List? DefaultIdRecord { get; set; } + + public bool CalculateExtremeEvents { get; set; } = true; + public double CalculateRespRateVentExpires { get; set; } = 2; + + public bool ActivateExpireAlertsTimer { get; set; } + public int ExpireAlertIntervalSeconds { get; set; } = 5; + + public ArchiveNurseData ArchiveNurseData { get; set; } = new(); + public List? MappingInterventions { get; set; } + public Dictionary? ComplexityMappingObservation { get; set; } + + + public string? NoCalculateStatusWithCodingSystem { get; set; } +} + +public class ArchiveNurseData +{ + public bool Active { get; set; } = false; + public ArchiveNurseDataConfig ArchiveTest { get; set; } = new(); + public ArchiveNurseDataConfig ArchiveProcedure { get; set; } = new(); + public ArchiveNurseDataConfig ArchiveTreatment { get; set; } = new(); + public int IntervalMinutes { get; set; } = 15; +} + +public class ArchiveNurseDataConfig +{ + public bool Active { get; set; } = false; + public int EndDateAfterMinutes { get; set; } = 60; +} + +public class PointOfCareMapping +{ + public string? Key { get; set; } + public bool Required { get; set; } + public int? Refresh { get; set; } = 600; //10min +} + +public class ConfigObservationSettings +{ + public bool IgnoreUnknownObservation { get; set; } + public bool IgnoreUnknownTreatment { get; set; } + public int? Refresh { get; set; } +} + +#region CCC mapping config + +public class InterventionsCodes +{ + private object? _finalValue; + private object? _initialValue; + + public object? InitialValue + { + get => _initialValue switch + { + double d => d, + string s => s, + int i => (double)i, + _ => null + }; + set + { + switch (value) + { + case double: + case string: + _initialValue = value; + break; + case int intValue: + _initialValue = (double)intValue; + break; + case JsonElement element: + switch (element.ValueKind) + { + case JsonValueKind.Number: + _initialValue = element.GetDouble(); + return; + case JsonValueKind.String: + _initialValue = element.GetString(); + break; + default: + throw new ArgumentOutOfRangeException(); + } + + break; + default: + { + if (value != null) + try + { + _initialValue = Convert.ToDouble(value); + } + catch (FormatException) + { + try + { + _initialValue = value.ToString(); + } + catch (Exception ex) + { + throw new ArgumentException( + $"The value must be a double or a string. Received type: {value.GetType()}, Value: {value}, Error: {ex.Message}"); + } + } + catch (Exception ex) + { + throw new ArgumentException( + $"The value must be a double or a string. Received type: {value.GetType()}, Value: {value}, Error: {ex.Message}"); + } + else + _initialValue = null; + + break; + } + } + } + } + + public object? FinalValue + { + get => _finalValue switch + { + double d => d, + string s => s, + _ => null + }; + set + { + // Apply the same JsonElement handling logic here as well + if (value is double) + _finalValue = value; + else if (value is string) + _finalValue = value; + else if (value is JsonElement element) + switch (element.ValueKind) + { + case JsonValueKind.Number: + _finalValue = element.GetDouble(); + return; + case JsonValueKind.String: + _finalValue = element.GetString(); + break; + default: + throw new ArgumentOutOfRangeException(); + } + else if (value != null) + try + { + _finalValue = Convert.ToDouble(value); + } + catch (FormatException) + { + try + { + _finalValue = value.ToString(); + } + catch (Exception ex) + { + throw new ArgumentException( + $"The value must be a double or a string. Received type: {value.GetType()}, Value: {value}, Error: {ex.Message}"); + } + } + catch (Exception ex) + { + throw new ArgumentException( + $"The value must be a double or a string. Received type: {value.GetType()}, Value: {value}, Error: {ex.Message}"); + } + else + _finalValue = null; + } + } +} + +public class InterventionsValues +{ + private object? _finalValue; + private object? _initialValue; + + public object? InitialValue + { + get => _initialValue switch + { + double d => d, + string s => s, + _ => null + }; + set + { + if (value is double) + _initialValue = value; + else if (value is string) + _initialValue = value; + else if (value is JsonElement element) + switch (element.ValueKind) + { + case JsonValueKind.Number: + _initialValue = element.GetDouble(); + return; + case JsonValueKind.String: + _initialValue = element.GetString(); + break; + } + else if (value != null) + try + { + _initialValue = Convert.ToDouble(value); + } + catch (FormatException) + { + try + { + _initialValue = value.ToString(); + } + catch (Exception ex) + { + throw new ArgumentException( + $"The value must be a double or a string. Received type: {value.GetType()}, Value: {value}, Error: {ex.Message}"); + } + } + catch (Exception ex) + { + throw new ArgumentException( + $"The value must be a double or a string. Received type: {value.GetType()}, Value: {value}, Error: {ex.Message}"); + } + else + _initialValue = null; + } + } + + public object? FinalValue + { + get => _finalValue switch + { + double d => d, + string s => s, + _ => null + }; + set + { + if (value is double) + _finalValue = value; + else if (value is string) + _finalValue = value; + else if (value is JsonElement element) + switch (element.ValueKind) + { + case JsonValueKind.Number: + _finalValue = element.GetDouble(); + return; + case JsonValueKind.String: + _finalValue = element.GetString(); + break; + } + else if (value != null) + try + { + _finalValue = Convert.ToDouble(value); + } + catch (FormatException) + { + try + { + _finalValue = value.ToString(); + } + catch (Exception ex) + { + throw new ArgumentException( + $"The value must be a double or a string. Received type: {value.GetType()}, Value: {value}, Error: {ex.Message}"); + } + } + catch (Exception ex) + { + throw new ArgumentException( + $"The value must be a double or a string. Received type: {value.GetType()}, Value: {value}, Error: {ex.Message}"); + } + else + _finalValue = null; + } + } + + public int ValueContributed { get; set; } = 0; +} + +public class MappingInterventions +{ + public string? Type { get; set; } + public string? Group { get; set; } + public string? Name { get; set; } + public ObservationEnum.Category? Category { get; set; } + public List Codes { get; set; } = []; + public List? Value { get; set; } +} + +#endregion \ No newline at end of file diff --git a/adas-core.Domain/Models/AppSettings/AuthSettings.cs b/adas-core.Domain/Models/AppSettings/AuthSettings.cs new file mode 100644 index 00000000..163779e2 --- /dev/null +++ b/adas-core.Domain/Models/AppSettings/AuthSettings.cs @@ -0,0 +1,50 @@ +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Domain.Models.AppSettings; + +public class AuthSettings +{ + public JwtConfig? JwtConfig { get; set; } + public UsersWhiteListConfig UsersWhiteListConfig { get; set; } = []; + public List LoginMethods { get; set; } = []; +} + +public class JwtConfig : JwtExtConfig +{ + public string? Issuer { get; set; } + public bool ValidateIssuer { get; set; } = true; + + public int TokenValidityInMinutes { get; set; } = 10; + public int RefreshTokenValidityInMinutes { get; set; } = 1440; + + public Dictionary? ExternalProviders { get; set; } = null; +} + +public class JwtExtConfig +{ + public string? Key { get; set; } + public string? Audience { get; set; } + public bool ValidateAudience { get; set; } = true; +} + +public class UsersWhiteListConfig : List +{ + public bool IsValid(User user) + { + return this.Any(username => username.ToLowerInvariant().Equals(user.UserName.ToLowerInvariant())); + } +} + +public class ValidGroupsConfig : List +{ + public bool IsValid(User? user) + { + return user != null; + } +} + +public class ValidGroup +{ + public string? Name { get; set; } + public bool IsRegex { get; set; } = false; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/AppSettings/AutoRecordingSettings.cs b/adas-core.Domain/Models/AppSettings/AutoRecordingSettings.cs new file mode 100644 index 00000000..e13f2cb8 --- /dev/null +++ b/adas-core.Domain/Models/AppSettings/AutoRecordingSettings.cs @@ -0,0 +1,29 @@ +namespace adas_core.Domain.Models.AppSettings; + +public class AutoRecordingSettings +{ + public AutoStart? AutoStart { get; set; } + public AutoStop? AutoStop { get; set; } + public MustSendAlert? MustSendAlert { get; set; } +} + +public class AutoStart +{ + public int BlueCode { get; set; } + public int HemoFilterAlert { get; set; } + public int RespirationAlert { get; set; } +} + +public class AutoStop +{ + public int BlueCode { get; set; } + public int HemoFilterAlert { get; set; } + public int RespirationAlert { get; set; } +} + +public class MustSendAlert +{ + public bool BlueCode { get; set; } + public bool HemoFilterAlert { get; set; } + public bool RespirationAlert { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/AppSettings/CacheSettings.cs b/adas-core.Domain/Models/AppSettings/CacheSettings.cs new file mode 100644 index 00000000..d956ee34 --- /dev/null +++ b/adas-core.Domain/Models/AppSettings/CacheSettings.cs @@ -0,0 +1,68 @@ +using adas_core.Domain.Enums; +namespace adas_core.Domain.Models.AppSettings; + + +public class CacheSettings +{ + + /// + /// Activa o desactiva por completo el sistema de caché. + /// Si es false, se usará NoCacheService para todas las entidades. + /// + public bool IsEnabled { get; set; } = true; + + // Cada propiedad define cómo debe cachearse la entidad correspondiente: + // CacheMode None → no cachea nunca + // CacheMode Cache → in-memory (CacheService) + // CacheMode Redis → RedisService + + public CacheEnum.Mode Patients { get; set; } = CacheEnum.Mode.Cache; + public CacheEnum.Mode Displays { get; set; } = CacheEnum.Mode.Cache; + public CacheEnum.Mode PumpObservations { get; set; } = CacheEnum.Mode.Cache; + public CacheEnum.Mode Appointments { get; set; } = CacheEnum.Mode.Cache; + public CacheEnum.Mode PointOfCares { get; set; } = CacheEnum.Mode.Cache; + public CacheEnum.Mode GroupedObservations { get; set; } = CacheEnum.Mode.Cache; + public CacheEnum.Mode PatientObservations { get; set; } = CacheEnum.Mode.Cache; + public CacheEnum.Mode ConfigObservations { get; set; } = CacheEnum.Mode.Cache; + + + +// Configuración específica del backend InMemory + public InMemorySettings InMemory { get; set; } = new(); + + // Configuración específica del backend Redis + public RedisSettings Redis { get; set; } = new(); + +} + + + +// -------------------- InMemory Cache -------------------- + +public class InMemorySettings +{ + public TtlSettings Ttl { get; set; } = new(); +} + + + +// -------------------- TTL por entidad -------------------- + +public class TtlSettings +{ + public int? GlobalSeconds { get; set; } + + public int? PatientsSeconds { get; set; } + public int? DisplaysSeconds { get; set; } + public int? PumpObservationsSeconds { get; set; } + public int? AppointmentsSeconds { get; set; } + public int? ConfigObservationsSeconds { get; set; } + public int? PointOfCaresSeconds { get; set; } + public int? GroupedObservationsSeconds { get; set; } + public int? PatientObservationsSeconds { get; set; } +} + + + + + diff --git a/adas-core.Domain/Models/AppSettings/DatabaseSettings.cs b/adas-core.Domain/Models/AppSettings/DatabaseSettings.cs new file mode 100644 index 00000000..e23f1a70 --- /dev/null +++ b/adas-core.Domain/Models/AppSettings/DatabaseSettings.cs @@ -0,0 +1,12 @@ +namespace adas_core.Domain.Models.AppSettings; + +public class DatabaseSettings +{ + public const string Database = "DatabaseSettings"; + + public string? ConnectionString { get; set; } + public string? DatabaseName { get; set; } + public int MaxConnectionPoolSize { get; set; } = 200; + public int MinConnectionPoolSize { get; set; } = 20; + public int MaxConnectionIdleTime { get; set; } = 60; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/AppSettings/ListSettings.cs b/adas-core.Domain/Models/AppSettings/ListSettings.cs new file mode 100644 index 00000000..eb154200 --- /dev/null +++ b/adas-core.Domain/Models/AppSettings/ListSettings.cs @@ -0,0 +1,141 @@ +namespace adas_core.Domain.Models.AppSettings; + +public class ListSettings +{ + public ListSettingItem AltableOptionList { get; set; } = new() + { + ManualObservationName = "AltableOptionObs", + AutoObservationName = [] + }; + + public ListSettingItem AllergyList { get; set; } = new() + { + ManualObservationName = "AllergyObs", + AutoObservationName = [] + }; + + public ListSettingItem DestinationList { get; set; } = new() + { + ManualObservationName = "DestinationObs", + AutoObservationName = [] + }; + + public ListSettingItem DiagnosisList { get; set; } = new() + { + ManualObservationName = "DiagnosisObs", + AutoObservationName = [] + }; + + public ListSettingItem DischargeStatusList { get; set; } = new() + { + ManualObservationName = "DischargeStatusObs", + AutoObservationName = [] + }; + + public ListSettingItem DoctorList { get; set; } = new() + { + ManualObservationName = "DoctorObs", + AutoObservationName = [] + }; + + public ListSettingItem DoctorTypeList { get; set; } = new() + { + ManualObservationName = "DoctorTypeObs", + AutoObservationName = [] + }; + + public ListSettingItem InternalDestinationList { get; set; } = new() + { + ManualObservationName = "InternalDestinationObs", + AutoObservationName = [] + }; + + public ListSettingItem InsulationList { get; set; } = new() + { + ManualObservationName = "InsulationObs", + AutoObservationName = [] + }; + + public ListSettingItem LanguageBarrierList { get; set; } = new() + { + ManualObservationName = "LanguageBarrierObs", + AutoObservationName = [] + }; + + public ListSettingItem PassiveSittingList { get; set; } = new() + { + ManualObservationName = "PassiveSittingObs", + AutoObservationName = [] + }; + public ListSettingItem GenericList { get; set; } = new() + { + ManualObservationName = "GenericObs", + AutoObservationName = [] + }; + + public ListSettingItem MobilityOptionList { get; set; } = new() + { + ManualObservationName = "MobilityOptionObs", + AutoObservationName = [] + }; + + public ListSettingItem OriginList { get; set; } = new() + { + ManualObservationName = "OriginObs", + AutoObservationName = [] + }; + + public ListSettingItem PatientStatusList { get; set; } = new() + { + ManualObservationName = "PatientStatusObs", + AutoObservationName = [] + }; + + public ListSettingItem ProcedureList { get; set; } = new() + { + ManualObservationName = "ProcedureObs", + AutoObservationName = [] + }; + + public ListSettingItem ServiceList { get; set; } = new() + { + ManualObservationName = "ServiceObs", + AutoObservationName = [] + }; + + public ListSettingItem TherapeuticCeilingList { get; set; } = new() + { + ManualObservationName = "TherapeuticCeilingObs", + AutoObservationName = [] + }; + + public ListSettingItem TreatmentList { get; set; } = new() + { + ManualObservationName = "TreatmentObs", + AutoObservationName = [] + }; + + public ListSettingItem VisitOptionList { get; set; } = new() + { + ManualObservationName = "VisitOptionObs", + AutoObservationName = [] + }; + + public ListSettingItem AccessControlList { get; set; } = new() + { + ManualObservationName = "AccessControlObs", + AutoObservationName = [] + }; + + public ListSettingItem TestList { get; set; } = new() + { + ManualObservationName = "TestObs", + AutoObservationName = [] + }; +} + +public class ListSettingItem +{ + public string? ManualObservationName { get; set; } + public List AutoObservationName { get; set; } = []; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/AppSettings/PermissionSettings.cs b/adas-core.Domain/Models/AppSettings/PermissionSettings.cs new file mode 100644 index 00000000..42886a80 --- /dev/null +++ b/adas-core.Domain/Models/AppSettings/PermissionSettings.cs @@ -0,0 +1,339 @@ +namespace adas_core.Domain.Models.AppSettings; + +public class PermissionSettings +{ + public SourcePermissions Admin { get; set; } = new( + new DisplayPermissionTypes( + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + true + ), + new DisplayPermissionTypes( + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + true + ), + new PanelPermissionTypes( + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + true + ) + ); + + public SourcePermissions Developer { get; set; } = new( + new DisplayPermissionTypes( + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + true + ), + new DisplayPermissionTypes( + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + true + ), + new PanelPermissionTypes( + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + new UserActions(true, true, true, true, true), + true + ) + ); + + public SourcePermissions DoctorChief { get; set; } = new( + new DisplayPermissionTypes( + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, false, false, true, false), + false + ), + new DisplayPermissionTypes( + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + false + ), + new PanelPermissionTypes( + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + false + ) + ); + + public SourcePermissions Doctor { get; set; } = new( + new DisplayPermissionTypes( + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + false + ), + new DisplayPermissionTypes( + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + false + ), + new PanelPermissionTypes( + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + false + ) + ); + + public SourcePermissions NursingSupervisor { get; set; } = new( + new DisplayPermissionTypes( + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, false, false, true, false), + false + ), + new DisplayPermissionTypes( + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + false + ), + new PanelPermissionTypes( + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + new UserActions(false, true, false, true, false), + false + ) + ); + + public SourcePermissions Nurse { get; set; } = new( + new DisplayPermissionTypes( + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + false + ), + new DisplayPermissionTypes( + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + false + ), + new PanelPermissionTypes( + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + false + ) + ); + + public SourcePermissions Guest { get; set; } = new( + new DisplayPermissionTypes( + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + false + ), + new DisplayPermissionTypes( + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + new UserActions(false, false, false, true, false), + false + ), + new PanelPermissionTypes( + new UserActions(false, false, false, true, true), + new UserActions(false, false, false, true, true), + new UserActions(false, false, false, true, true), + new UserActions(false, false, false, true, true), + new UserActions(false, false, false, true, true), + new UserActions(false, false, false, true, true), + new UserActions(false, false, false, true, true), + new UserActions(false, false, false, true, true), + new UserActions(false, false, false, true, true), + new UserActions(false, false, false, true, true), + new UserActions(false, false, false, true, true), + new UserActions(false, false, false, true, true), + false + ) + ); +} + +public class SourcePermissions( + DisplayPermissionTypes unit, + DisplayPermissionTypes display, + PanelPermissionTypes panel) +{ + public DisplayPermissionTypes Unit { get; set; } = unit; + public DisplayPermissionTypes Display { get; set; } = display; + public PanelPermissionTypes Panel { get; set; } = panel; +} + +public class DisplayPermissionTypes( + UserActions admissions, + UserActions discharges, + UserActions obs, + UserActions demographicData, + UserActions boxBlocking, + UserActions notices, + UserActions cell, + bool useDemoMode) +{ + public UserActions Admissions { get; set; } = admissions; + public UserActions Discharges { get; set; } = discharges; + public UserActions Obs { get; set; } = obs; + public UserActions Cell { get; set; } = cell; + public UserActions DemographicData { get; set; } = demographicData; + public UserActions BoxBlocking { get; set; } = boxBlocking; + public UserActions Notices { get; set; } = notices; + public bool UseDemoMode { get; set; } = useDemoMode; +} + +public class PanelPermissionTypes( + UserActions units, + UserActions displays, + UserActions displayConfigs, + UserActions masterLists, + UserActions treatments, + UserActions patients, + UserActions medicines, + UserActions pumps, + UserActions users, + UserActions configObservations, + UserActions observations, + UserActions audits, + bool useDemoMode) +{ + public UserActions Units { get; set; } = units; + public UserActions Displays { get; set; } = displays; + public UserActions DisplayConfigs { get; set; } = displayConfigs; + public UserActions MasterLists { get; set; } = masterLists; + public UserActions Treatments { get; set; } = treatments; + public UserActions Patients { get; set; } = patients; + public UserActions Medicines { get; set; } = medicines; + public UserActions Pumps { get; set; } = pumps; + public UserActions Users { get; set; } = users; + public UserActions ConfigObservations { get; set; } = configObservations; + public UserActions Observations { get; set; } = observations; + public UserActions Audits { get; set; } = audits; + public bool UseDemoMode { get; set; } = useDemoMode; +} + +public class UserActions(bool create, bool update, bool delete, bool read, bool execute) +{ + public bool Create { get; set; } = create; + public bool Update { get; set; } = update; + public bool Delete { get; set; } = delete; + public bool Read { get; set; } = read; + public bool Execute { get; set; } = execute; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/AppSettings/RabbitMQSettings.cs b/adas-core.Domain/Models/AppSettings/RabbitMQSettings.cs new file mode 100644 index 00000000..84cf65e4 --- /dev/null +++ b/adas-core.Domain/Models/AppSettings/RabbitMQSettings.cs @@ -0,0 +1,15 @@ +namespace adas_core.Domain.Models.AppSettings; + +public class RabbitMqSettings +{ + public string ConnectionString { get; set; } = string.Empty; + public string PatientsQueue { get; set; } = "patients"; + public string TreatmentsQueue { get; set; } = "treatments"; + public string ObservationsQueue { get; set; } = "observations"; + public string PumpsQueue { get; set; } = "pumps"; + public string RecordingQueue { get; set; } = "recordings"; + public string AppointmentsQueue { get; set; } = "appointments"; + public int MaxRetries { get; set; } = 2; + public string RecordingAlertQueue { get; set; } = "recordingAlert"; + public string AlarmObservationQueue { get; set; } = "alarms"; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/AppSettings/RecordingSettings.cs b/adas-core.Domain/Models/AppSettings/RecordingSettings.cs new file mode 100644 index 00000000..5642ef41 --- /dev/null +++ b/adas-core.Domain/Models/AppSettings/RecordingSettings.cs @@ -0,0 +1,11 @@ +namespace adas_core.Domain.Models.AppSettings; + +public class RecordingSettings +{ + public const string Recording = "RecordingSettings"; + + public string RecordingApiUrl { get; set; } = ""; + public string RecordingOrApiClientId { get; set; } = ""; + public string RecordingOrApiClientSecret { get; set; } = ""; + public int HttpClientTimeout { get; set; } = 6; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/AppSettings/RedisSettings.cs b/adas-core.Domain/Models/AppSettings/RedisSettings.cs new file mode 100644 index 00000000..12934e41 --- /dev/null +++ b/adas-core.Domain/Models/AppSettings/RedisSettings.cs @@ -0,0 +1,83 @@ +namespace adas_core.Domain.Models.AppSettings +{ + /// + /// Configuración Redis. + /// No contiene lógica de qué entidades usan Redis (eso está en CacheSettings). + /// Esta clase solo define: + /// - Conexión + /// - Cluster/Scalability + /// - SSL/password + /// - TTL global y por entidad + /// - Generación de claves CacheKeys + /// + public class RedisSettings + { + // Conexión y networking + public string? ConnectionString { get; set; } + public bool EnableScalable { get; set; } + public bool Ssl { get; set; } + public string? Password { get; set; } + public IEnumerable Endpoints { get; set; } = []; + public string ChannelPrefix { get; set; } = "MedDisCore"; + + // TTL global y TTL específico por entidad + // (no determina si la entidad se cachea o no) + public int? ExpireKeyInSeconds { get; set; } + + public TtlSettings Ttl { get; set; } = new(); + + // Submodelos + public class RedisEndpoint + { + public string Host { get; set; } = null!; + public int Port { get; set; } + } + + // CacheKeys: claves estandarizadas y unificadas para todas las entidades + // Estas claves son fundamentales para: + + public static class CacheKeys + { + // ----------------- Patients ----------------- + public static string Patient(string patientId) + => $"patients:{patientId}"; + + // ----------------- ConfigDisplays ----------------- + public static string ConfigDisplay(string id) + => $"configDisplays:{id}"; + + public static string ConfigDisplaysAll() + => "configDisplays:all"; + + // ----------------- PumpObservations ----------------- + public static string PumpObservation(string obsId) + => $"pumpObs:{obsId}"; + + public static string PumpDaily(string pumpId, DateTime date) + => $"pumpObs:pump:{pumpId}:{date:yyyyMMdd}"; + + // ----------------- Appointments ----------------- + public static string Appointment(string apptId) + => $"appointments:{apptId}"; + + public static string AppointmentsByPatientDay(string patientId, DateTime date) + => $"appointments:patient:{patientId}:{date:yyyyMMdd}"; + + public static string AppointmentsByPatientMonth(string patientId, DateTime date) + => $"appointments:patient:{patientId}:{date:yyyyMM}"; + + // ----------------- GroupedObservations ----------------- + public static string GroupedObservationsByPatient(string patientId, string? groupedField) + => $"groupedObs:{groupedField}:patient:{patientId}"; + + public static string GroupedObservationsByPatientDay(string patientId, string groupedField, DateTime date) + => $"groupedObs:{groupedField}:patient:{patientId}:{date:yyyyMMdd}"; + + public static string GroupedObservationsByPatientMonth(string patientId, string groupedField, DateTime date) + => $"groupedObs:{groupedField}:patient:{patientId}:{date:yyyyMM}"; + + public static string GroupedObservationsLatest(string patientId, string groupedField, int take) + => $"groupedObs:{groupedField}:patient:{patientId}:latest:{take}"; + } + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/BasePatientObservation.cs b/adas-core.Domain/Models/BasePatientObservation.cs new file mode 100644 index 00000000..00d20695 --- /dev/null +++ b/adas-core.Domain/Models/BasePatientObservation.cs @@ -0,0 +1,119 @@ +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace adas_core.Domain.Models; + +public class BasePatientObservation : JsonConverter +{ + public ObjectId Id { get; set; } + public ObjectId PatientId { get; set; } + public ObjectId? UserId { get; set; } + + public string? ClinicalEpisode { get; set; } + + public Patient? Patient { get; set; } + + public string? SystemId { get; set; } + + + public string? Code { get; set; } + + + public string? CodingSystem { get; set; } + + public ParentDataClass? ParentData { get; set; } + + + public DateTime Time { get; set; } + public DateTime? EndTime { get; set; } + + + public string? Name { get; set; } + + + public string? Units { get; set; } + + public bool CheckObservations { get; set; } + + public List? CreateObservation { get; set; } + + public string? ToJsonString() + { + try + { + return JsonConvert.SerializeObject(this, Formatting.None); + } + catch + { + return base.ToString(); + } + } + + public override string ToString() + { + List items = + [ + $"id: {Id}", + $"patientid: {PatientId}" + ]; + if (!string.IsNullOrEmpty(SystemId)) items.Add($", systemId: {SystemId}"); + if (!string.IsNullOrEmpty(Code)) items.Add($"code: {Code}"); + if (!string.IsNullOrEmpty(CodingSystem)) items.Add($"codingSystem: {CodingSystem}"); + items.Add($"time: {Time}"); + if (EndTime.HasValue) items.Add($"endTime: {EndTime.Value}"); + if (!string.IsNullOrEmpty(Name)) items.Add($"name: {Name}"); + if (!string.IsNullOrEmpty(Units)) items.Add($"unit: {Units}"); + items.Add($"checkObservations: {CheckObservations}"); + CreateObservation?.ForEach(o => items.Add(o.ToString())); + + return "BasePatientObservation[" + string.Join(", ", items) + "]"; + } + + public List ToListString() + { + List items = + [ + $"id: {Id}", + $"patientid: {PatientId}" + ]; + if (!string.IsNullOrEmpty(SystemId)) items.Add($"systemId: {SystemId}"); + if (!string.IsNullOrEmpty(Code)) items.Add($"code: {Code}"); + if (!string.IsNullOrEmpty(CodingSystem)) items.Add($"codingSystem: {CodingSystem}"); + items.Add($"time: {Time}"); + if (!string.IsNullOrEmpty(Name)) items.Add($"name: {Name}"); + return items; + } + + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(BasePatientObservation); + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value == null) return; + + var jo = JObject.FromObject(value); + jo.Property("Id")?.Remove(); + jo.Property("Patient")?.Remove(); + jo.WriteTo(writer); + } + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, + JsonSerializer serializer) + { + throw new NotImplementedException(); + } +} + +public class ParentDataClass +{ + public string? Code { get; set; } + + public string? CodingSystem { get; set; } + + public string? Name { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/BasePatientObservationValue.cs b/adas-core.Domain/Models/BasePatientObservationValue.cs new file mode 100644 index 00000000..ca916465 --- /dev/null +++ b/adas-core.Domain/Models/BasePatientObservationValue.cs @@ -0,0 +1,6 @@ +namespace adas_core.Domain.Models; + +public abstract class BasePatientObservationValue : BasePatientObservation +{ + public object Value { get; set; } = new(); +} \ No newline at end of file diff --git a/adas-core.Domain/Models/BsonConverters/DictionaryBsonConverter.cs b/adas-core.Domain/Models/BsonConverters/DictionaryBsonConverter.cs new file mode 100644 index 00000000..eca856cb --- /dev/null +++ b/adas-core.Domain/Models/BsonConverters/DictionaryBsonConverter.cs @@ -0,0 +1,273 @@ +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; +using MongoDB.Bson.IO; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using Newtonsoft.Json.Linq; + +namespace adas_core.Domain.Models.BsonConverters; + +public class DictionaryBsonConverter : SerializerBase> +{ + public override Dictionary Deserialize(BsonDeserializationContext context, + BsonDeserializationArgs args) + { + var reader = context.Reader; + var stepsDictionary = new Dictionary(); + + if (reader.CurrentBsonType == BsonType.Document) + { + reader.ReadStartDocument(); + + while (reader.ReadBsonType() != BsonType.EndOfDocument) + { + var key = reader.ReadName(); + + switch (key.ToUpper()) + { + case "STEPS": + var stepList = BsonSerializer.Deserialize>(reader); + stepsDictionary[key] = stepList; + break; + case "BEACONS": + var beaconList = BsonSerializer.Deserialize>(reader); + stepsDictionary[key] = beaconList; + break; + case "CAMERAS": + var cameraList = BsonSerializer.Deserialize>(reader); + stepsDictionary[key] = cameraList; + break; + case "RELAY": + var relayList = BsonSerializer.Deserialize(reader); + stepsDictionary[key] = relayList; + break; + + default: + switch (reader.CurrentBsonType) + { + case BsonType.Int32: + case BsonType.Int64: + stepsDictionary[key] = BsonSerializer.Deserialize(reader); + break; + case BsonType.Double: + stepsDictionary[key] = BsonSerializer.Deserialize(reader); + break; + case BsonType.String: + stepsDictionary[key] = BsonSerializer.Deserialize(reader); + break; + case BsonType.Boolean: + stepsDictionary[key] = BsonSerializer.Deserialize(reader); + break; + case BsonType.Array: + reader.ReadStartArray(); + var arrayItems = new List(); + while (reader.ReadBsonType() != BsonType.EndOfDocument) + switch (reader.CurrentBsonType) + { + case BsonType.String: + arrayItems.Add(BsonSerializer.Deserialize(reader)); + break; + case BsonType.Int32: + arrayItems.Add(BsonSerializer.Deserialize(reader)); + break; + case BsonType.Int64: + arrayItems.Add(BsonSerializer.Deserialize(reader)); + break; + case BsonType.Boolean: + arrayItems.Add(BsonSerializer.Deserialize(reader)); + break; + case BsonType.Double: + arrayItems.Add(BsonSerializer.Deserialize(reader)); + break; + case BsonType.ObjectId: + arrayItems.Add(BsonSerializer.Deserialize(reader)); + break; + case BsonType.DateTime: + arrayItems.Add(BsonSerializer.Deserialize(reader)); + break; + + default: + reader.SkipValue(); + break; + } + + reader.ReadEndArray(); + stepsDictionary[key] = arrayItems; + break; + default: + var bsonDoc = BsonSerializer.Deserialize(reader); + stepsDictionary[key] = bsonDoc.ToDictionary(); + break; + } + + break; + } + } + + reader.ReadEndDocument(); + } + + return stepsDictionary; + } + + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, + Dictionary value) + { + var writer = context.Writer; + writer.WriteStartDocument(); + + foreach (var item in value) + { + if (TrySerializeKeyValuePair(item, writer)) continue; + + // Si llegamos a este punto, encontramos un tipo no soportado. + writer.WriteName(item.Key); + writer.WriteNull(); // Escribe un valor null si el tipo no es soportado. + } + + + writer.WriteEndDocument(); + } + + private static bool TrySerializeKeyValuePair(KeyValuePair item, IBsonWriter writer) + { + writer.WriteName(item.Key); + + switch (item.Key) + { + case "steps": + if (item.Value is JArray jSteps) + { + var steps = jSteps.ToObject>(); + BsonSerializer.Serialize(writer, typeof(List), steps); + return true; + } + + break; + + case "beacons": + if (item.Value is JArray jBeaconConfigs) + { + var beaconConfigs = jBeaconConfigs.ToObject>(); + BsonSerializer.Serialize(writer, typeof(List), beaconConfigs); + return true; + } + + break; + case "cameras": + if (item.Value is JArray jCamerasConfigs) + { + var cameraConfigs = jCamerasConfigs.ToObject>(); + BsonSerializer.Serialize(writer, typeof(List), cameraConfigs); + return true; + } + + break; + case "relay": + if (item.Value is JObject jRelayConfigs) + { + var relayConfigs = jRelayConfigs.ToObject(); + BsonSerializer.Serialize(writer, relayConfigs); + return true; + } + + break; + case "uiFontProperties": + { + if (item.Value is JObject jUiFontData) + { + var uiFontData = jUiFontData.ToObject(); + BsonSerializer.Serialize(writer, uiFontData); + return true; + } + } + break; + default: + if (item.Value is Dictionary nestedDictionary) + { + var bsonDoc = new BsonDocument(nestedDictionary); + BsonSerializer.Serialize(writer, bsonDoc); + return true; + } + + if (item.Value is JArray jArray) + { + var listObjects = jArray.ToObject>(); + BsonSerializer.Serialize(writer, listObjects); + return true; + } + + // Para otros tipos primitivos, utilizamos BsonValue.Create + try + { + var bsonValue = BsonValue.Create(item.Value); + switch (bsonValue.BsonType) + { + case BsonType.String: + writer.WriteString(bsonValue.AsString); + break; + + case BsonType.Int32: + writer.WriteInt32(bsonValue.AsInt32); + break; + + case BsonType.Int64: + writer.WriteInt64(bsonValue.AsInt64); + break; + + case BsonType.Double: + writer.WriteDouble(bsonValue.AsDouble); + break; + + case BsonType.Boolean: + writer.WriteBoolean(bsonValue.AsBoolean); + break; + + case BsonType.Array: + writer.WriteStartArray(); + foreach (var arrayValue in bsonValue.AsBsonArray) + switch (arrayValue.BsonType) + { + case BsonType.String: + writer.WriteString(arrayValue.AsString); + break; + + case BsonType.Int32: + writer.WriteInt32(arrayValue.AsInt32); + break; + + case BsonType.Int64: + writer.WriteInt64(arrayValue.AsInt64); + break; + + case BsonType.Double: + writer.WriteDouble(arrayValue.AsDouble); + break; + + case BsonType.Boolean: + writer.WriteBoolean(arrayValue.AsBoolean); + break; + + default: + throw new BsonSerializationException( + $"No se puede serializar el tipo Bson en el array: {arrayValue.BsonType}"); + } + + writer.WriteEndArray(); + break; + + default: + throw new NotSupportedException($"BsonType {bsonValue.BsonType} no es compatible."); + } + + return true; + } + catch (ArgumentException) + { + return false; + } + } + + return false; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/BsonConverters/DictionaryConverter.cs b/adas-core.Domain/Models/BsonConverters/DictionaryConverter.cs new file mode 100644 index 00000000..208bf217 --- /dev/null +++ b/adas-core.Domain/Models/BsonConverters/DictionaryConverter.cs @@ -0,0 +1,61 @@ +using adas_core.Domain.Models.MongoModels; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace adas_core.Domain.Models.BsonConverters; + +public class DictionaryConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + return objectType == typeof(Dictionary); + } + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, + JsonSerializer serializer) + { + var jo = JObject.Load(reader); + Dictionary stepsDictionary = new(); + foreach (var item in jo) + { + var key = item.Key; + + switch (item.Key) + { + case "Steps": + var valueStep = item.Value?.ToObject>(serializer); + stepsDictionary.Add(key, valueStep ?? []); + break; + case "beacons": + var valueBeacon = item.Value?.ToObject>(serializer); + stepsDictionary.Add(key, valueBeacon ?? []); + break; + case "relay": + var valueRelay = item.Value?.ToObject(serializer); + stepsDictionary.Add(key, valueRelay ?? new Relay()); + break; + case "cameras": + var valueCameras = item.Value?.ToObject>(serializer); + stepsDictionary.Add(key, valueCameras ?? []); + break; + default: + var valueDefault = item.Value?.ToObject(serializer); + stepsDictionary.Add(key, valueDefault ?? new object()); + break; + } + } + + return stepsDictionary; + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value == null) + return; + + var stepsDictionary = (Dictionary)value; + var jo = new JObject(); + foreach (var item in stepsDictionary) jo.Add(item.Key, JToken.FromObject(item.Value, serializer)); + jo.WriteTo(writer); + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/ChartValue.cs b/adas-core.Domain/Models/ChartValue.cs new file mode 100644 index 00000000..aab031cb --- /dev/null +++ b/adas-core.Domain/Models/ChartValue.cs @@ -0,0 +1,8 @@ +namespace adas_core.Domain.Models; + +public class ChartValue +{ + public List Labels { get; set; } = []; + + public Dictionary> Series { get; set; } = new(); +} \ No newline at end of file diff --git a/adas-core.Domain/Models/ChatMessage.cs b/adas-core.Domain/Models/ChatMessage.cs new file mode 100644 index 00000000..d7fbef26 --- /dev/null +++ b/adas-core.Domain/Models/ChatMessage.cs @@ -0,0 +1,12 @@ +namespace adas_core.Domain.Models; + +public class ChatMessage +{ + public PatientLocation? LocationFrom { get; set; } + + public PatientLocation? LocationTo { get; set; } + + public DateTime? MessageTime { get; set; } + + public string? Message { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Code.cs b/adas-core.Domain/Models/Code.cs new file mode 100644 index 00000000..b024ba19 --- /dev/null +++ b/adas-core.Domain/Models/Code.cs @@ -0,0 +1,10 @@ +namespace adas_core.Domain.Models; + +public class Code +{ + public string Identifier { get; set; } = string.Empty; + + public string Text { get; set; } = string.Empty; + + public string CodingSystem { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/CodeAction.cs b/adas-core.Domain/Models/CodeAction.cs new file mode 100644 index 00000000..0a2455ae --- /dev/null +++ b/adas-core.Domain/Models/CodeAction.cs @@ -0,0 +1,38 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Utils; + +namespace adas_core.Domain.Models; + +public class CodeAction +{ + public ActionsEnum.ResourceAction Action { get; set; } + public required Code Code { get; set; } + + public static List? Apply(List actions) + { + return Apply(null, actions); + } + + public static List? Apply(List? source, List? actions) + { + source ??= []; + (actions ?? []).ForEach(s => + { + switch (s.Action) + { + case ActionsEnum.ResourceAction.Add: + if (!source.Contains(s.Code)) source.Add(s.Code); + break; + case ActionsEnum.ResourceAction.Delete: + if (source.Contains(s.Code)) source.Remove(s.Code); + break; + + case ActionsEnum.ResourceAction.Update: + if (source.Contains(s.Code)) source[source.IndexOf(s.Code)] = s.Code; + else source.Add(s.Code); + break; + } + }); + return CollectionsUtils.NullIfEmpty(source); + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/CodeStatus.cs b/adas-core.Domain/Models/CodeStatus.cs new file mode 100644 index 00000000..7881d130 --- /dev/null +++ b/adas-core.Domain/Models/CodeStatus.cs @@ -0,0 +1,12 @@ +namespace adas_core.Domain.Models; + +public class CodeStatus +{ + public Code? Code { get; set; } + + public string? Status { get; set; } + + public DateTime? AdministrationTime { get; set; } + + public DateTime? EndAdministrationTime { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/AppointmentDto.cs b/adas-core.Domain/Models/DTO/AppointmentDto.cs new file mode 100644 index 00000000..cb760ad5 --- /dev/null +++ b/adas-core.Domain/Models/DTO/AppointmentDto.cs @@ -0,0 +1,15 @@ +namespace adas_core.Domain.Models.DTO; + +public class AppointmentDto +{ + public string PatientNumber { get; set; } = string.Empty; + + public PatientLocation? Location { get; set; } + + public List? Appointments { get; set; } + + + public DateTime MessageTime { get; set; } + + public bool WaitResult { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/AssetDto.cs b/adas-core.Domain/Models/DTO/AssetDto.cs new file mode 100644 index 00000000..524148cd --- /dev/null +++ b/adas-core.Domain/Models/DTO/AssetDto.cs @@ -0,0 +1,8 @@ +namespace adas_core.Domain.Models.DTO; + +public class AssetDto +{ + public string? Name { get; set; } + public string? Extension { get; set; } + public string? Path { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/ConfigObservationDto.cs b/adas-core.Domain/Models/DTO/ConfigObservationDto.cs new file mode 100644 index 00000000..3479823e --- /dev/null +++ b/adas-core.Domain/Models/DTO/ConfigObservationDto.cs @@ -0,0 +1,7 @@ +namespace adas_core.Domain.Models.DTO; + +public class ConfigObservationDto +{ + public string Id { get; set; } = string.Empty; + public long ItemCount { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/CreateUserWithAuthDto.cs b/adas-core.Domain/Models/DTO/CreateUserWithAuthDto.cs new file mode 100644 index 00000000..278590d7 --- /dev/null +++ b/adas-core.Domain/Models/DTO/CreateUserWithAuthDto.cs @@ -0,0 +1,9 @@ +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Domain.Models.DTO; + +public class CreateUserWithAuthDto +{ + public User User { get; set; } = new(); + public List Authorizations { get; set; } = []; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/DeviceDto.cs b/adas-core.Domain/Models/DTO/DeviceDto.cs new file mode 100644 index 00000000..612c4f90 --- /dev/null +++ b/adas-core.Domain/Models/DTO/DeviceDto.cs @@ -0,0 +1,28 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.DTO; + +public class DeviceDto +{ + public DeviceEvent? Event { get; set; } + public DeviceType DeviceType { get; set; } + public string? MacAddr { get; set; } + public string? SerialNumber { get; set; } + public string? Name { get; set; } + public int? Battery { get; set; } + public string? Color { get; set; } + public bool Connected { get; set; } + public bool Ready { get; set; } + public string? Uuid { get; set; } + public string? Key { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public List? PointOfCareIds { get; set; } = new(); + public DeviceSettings? Settings { get; set; } +} +public class DeviceEvent +{ + public ClickType? ClickType { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/DiagnosisDto.cs b/adas-core.Domain/Models/DTO/DiagnosisDto.cs new file mode 100644 index 00000000..1105dfb2 --- /dev/null +++ b/adas-core.Domain/Models/DTO/DiagnosisDto.cs @@ -0,0 +1,18 @@ +namespace adas_core.Domain.Models.DTO; + +public class DiagnosisDto +{ + public string PatientNumber { get; set; } = string.Empty; + + public PatientLocation? Location { get; set; } + + public ObservationData? ObservationData { get; set; } + + public List? Observations { get; set; } + + public List? Diagnosis { get; set; } + + public DateTime MessageTime { get; set; } + + public bool WaitResult { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/Display/ColorConfigDto.cs b/adas-core.Domain/Models/DTO/Display/ColorConfigDto.cs new file mode 100644 index 00000000..3fb85142 --- /dev/null +++ b/adas-core.Domain/Models/DTO/Display/ColorConfigDto.cs @@ -0,0 +1,104 @@ +namespace adas_core.Domain.Models.DTO.Display; + +public class ColorConfigDto +{ + public LevelColors? Level { get; set; } + public TextColors? Text { get; set; } + public ArrowColors? Arrow { get; set; } + public IndicatorColors? Indicator { get; set; } + public GraphColors? Graph { get; set; } + public StatusBoxNumberColors? BoxNumber { get; set; } + public TherapyColors? Therapy { get; set; } + public TestColors? Test { get; set; } + public ProcedureColors? Procedure { get; set; } + + public class AppearanceSettings + { + public string? TextColor { get; set; } + + public string? BackgroundColor { get; set; } + + public string? Icon { get; set; } + + public string? IconDefault { get; set; } + public double? IconInvertColor { get; set; } + } + + public class StatusBoxNumberColors + { + public AppearanceSettings? Reserved { get; set; } + public AppearanceSettings? InUse { get; set; } + public AppearanceSettings? Available { get; set; } + public AppearanceSettings? Locked { get; set; } + public AppearanceSettings? Transferable { get; set; } + public AppearanceSettings? Exitus { get; set; } + public AppearanceSettings? Altable { get; set; } + } + + public class TherapyColors + { + public AppearanceSettings? Default { get; set; } + public AppearanceSettings? Initialized { get; set; } + public AppearanceSettings? Finished { get; set; } + public AppearanceSettings? InProgress { get; set; } + } + + public class TestColors + { + public AppearanceSettings? Default { get; set; } + public AppearanceSettings? Initialized { get; set; } + public AppearanceSettings? Finished { get; set; } + public AppearanceSettings? Expired { get; set; } + } + + public class ProcedureColors + { + public AppearanceSettings? Default { get; set; } + public AppearanceSettings? Initialized { get; set; } + public AppearanceSettings? Finished { get; set; } + public AppearanceSettings? Expired { get; set; } + } + + public class LevelColors + { + public string? Level1 { get; set; } + public string? Level2 { get; set; } + public string? Level3 { get; set; } + public string? Level4 { get; set; } + public string? Level5 { get; set; } + } + + public class TextColors + { + public string? Normal { get; set; } + public string? Warning { get; set; } + public string? Alert { get; set; } + public string? Improve { get; set; } + public string? Expired { get; set; } + } + + public class ArrowColors + { + public string? Normal { get; set; } + public string? Warning { get; set; } + public string? Alert { get; set; } + public string? Improve { get; set; } + } + + public class IndicatorColors + { + public string? Empty { get; set; } + public string? Warning { get; set; } + public string? Normal { get; set; } + public string? Alert { get; set; } + public string? Background { get; set; } + public string? EmptyBackground { get; set; } + } + + public class GraphColors + { + public string? Normal { get; set; } + public string? Warning { get; set; } + public string? Alert { get; set; } + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/Display/CreateDisplayConfigDto.cs b/adas-core.Domain/Models/DTO/Display/CreateDisplayConfigDto.cs new file mode 100644 index 00000000..57313643 --- /dev/null +++ b/adas-core.Domain/Models/DTO/Display/CreateDisplayConfigDto.cs @@ -0,0 +1,23 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.DTO.Display; + +public class CreateDisplayConfigDto +{ + public DisplayConfigEnums.DisplayType Type { get; set; } = + DisplayConfigEnums.DisplayType + .Unknown; // Lista de unidades a las que se puede subscriber el cliente par recibir las actualizaciones de la seccion + + public string? Hospital { get; set; } + public string? DisplayConfigIdTemplate { get; set; } +} + +public class CreateDisplayConfigCardDto +{ + public CardConfig? CardConfig { get; set; } + public CardDetailsConfig? DetailConfig { get; set; } + public ChartConfig? ChartConfig { get; set; } + public ObjectId? DisplayConfigId { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/Display/DisplayConfigDto.cs b/adas-core.Domain/Models/DTO/Display/DisplayConfigDto.cs new file mode 100644 index 00000000..0abc9baf --- /dev/null +++ b/adas-core.Domain/Models/DTO/Display/DisplayConfigDto.cs @@ -0,0 +1,29 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.DTO.Display; + +public class DisplayConfigDto +{ + public ObjectId Id { get; set; } + public DisplayConfigEnums.DisplayType Type { get; set; } = DisplayConfigEnums.DisplayType.Unknown; + public CardConfig? CardConfig { get; set; } + public ObjectId? CardConfigId { get; set; } + public ObjectId? DetailConfigId { get; set; } + public HomeConfig? HomeConfig { get; set; } + public HeaderConfig? HeaderConfig { get; set; } + public CardDetailsConfig? DetailConfig { get; set; } + public List? DisplaySectionIdList { get; set; } + public string? Hospital { get; set; } + public List? FieldList { get; set; } + public List? GroupedFieldList { get; set; } + + + #region NotMapped + + public List? DisplaySectionList { get; set; } + + #endregion +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/Display/DisplayConfigLocationDto.cs b/adas-core.Domain/Models/DTO/Display/DisplayConfigLocationDto.cs new file mode 100644 index 00000000..06826bd3 --- /dev/null +++ b/adas-core.Domain/Models/DTO/Display/DisplayConfigLocationDto.cs @@ -0,0 +1,7 @@ +namespace adas_core.Domain.Models.DTO.Display; + +public class DisplayConfigLocationDto +{ + public string? DisplayName { get; set; } + public string? UnitName { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/Display/DisplayConfigSummary.cs b/adas-core.Domain/Models/DTO/Display/DisplayConfigSummary.cs new file mode 100644 index 00000000..dd51cd06 --- /dev/null +++ b/adas-core.Domain/Models/DTO/Display/DisplayConfigSummary.cs @@ -0,0 +1,11 @@ +using adas_core.Domain.Enums; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.DTO.Display; + +public class DisplayConfigSummary +{ + public ObjectId Id { get; set; } + public string? Name { get; set; } + public DisplayConfigEnums.DisplayType Type { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/Display/DisplayMinimalDto.cs b/adas-core.Domain/Models/DTO/Display/DisplayMinimalDto.cs new file mode 100644 index 00000000..6762c9c3 --- /dev/null +++ b/adas-core.Domain/Models/DTO/Display/DisplayMinimalDto.cs @@ -0,0 +1,12 @@ +using adas_core.Domain.Enums; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.DTO.Display; + +public class DisplayMinimalDto(MongoModels.Display display) +{ + public ObjectId Id { get; set; } = display.Id; + public string Name { get; set; } = display.Name; + + public DisplayConfigEnums.DisplayType Type { get; set; } = display.Type; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/Display/DisplayNurseDto.cs b/adas-core.Domain/Models/DTO/Display/DisplayNurseDto.cs new file mode 100644 index 00000000..5abf6d80 --- /dev/null +++ b/adas-core.Domain/Models/DTO/Display/DisplayNurseDto.cs @@ -0,0 +1,10 @@ +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Domain.Models.DTO.Display; + +public class DisplayNurseDto : DisplayConfigDto +{ + public List? HomeBanner { get; set; } + public ColorConfig? ColorConfig { get; set; } + public FormConfig? FormConfig { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/Display/DisplayWithPermissionsDto.cs b/adas-core.Domain/Models/DTO/Display/DisplayWithPermissionsDto.cs new file mode 100644 index 00000000..4bcad359 --- /dev/null +++ b/adas-core.Domain/Models/DTO/Display/DisplayWithPermissionsDto.cs @@ -0,0 +1,9 @@ +using adas_core.Domain.Models.AppSettings; + +namespace adas_core.Domain.Models.DTO.Display; + +public class DisplayWithPermissionsDto +{ + public DisplayPermissionTypes? Permissions { get; set; } + public MongoModels.Display? Display { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/Display/MinimalDisplayListDto.cs b/adas-core.Domain/Models/DTO/Display/MinimalDisplayListDto.cs new file mode 100644 index 00000000..38ab768b --- /dev/null +++ b/adas-core.Domain/Models/DTO/Display/MinimalDisplayListDto.cs @@ -0,0 +1,9 @@ +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Domain.Models.DTO.Display; + +public class MinimalDisplayListDto +{ + public List DisplayNurse { get; set; } = []; + public List SmartDisplay { get; set; } = []; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/Display/SmartDisplayDto.cs b/adas-core.Domain/Models/DTO/Display/SmartDisplayDto.cs new file mode 100644 index 00000000..87ba7d64 --- /dev/null +++ b/adas-core.Domain/Models/DTO/Display/SmartDisplayDto.cs @@ -0,0 +1,22 @@ +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Domain.Models.DTO.Display; + +public class SmartDisplayDto : DisplayConfigDto +{ + public bool? HasCameras { get; set; } + public bool? HasSound { get; set; } + public bool? IsRotationEnabled { get; set; } + public bool? CanChangeCameraMode { get; set; } + public bool? CamerasAreActive { get; set; } + public string? CameraStreamType { get; set; } + public List? SensorList { get; set; } + public string? ObservationForIndicator { get; set; } + public List? AlarmFieldList { get; set; } + public List? RequestGroupedFieldList { get; set; } + public bool? Pumps { get; set; } + public List? ChartConfig { get; set; } + public GraphLayout? GraphLayout { get; set; } + public List? CardRotatingLayout { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/Display/UpdateDisplayConfigNameDto.cs b/adas-core.Domain/Models/DTO/Display/UpdateDisplayConfigNameDto.cs new file mode 100644 index 00000000..fd1db56a --- /dev/null +++ b/adas-core.Domain/Models/DTO/Display/UpdateDisplayConfigNameDto.cs @@ -0,0 +1,6 @@ +namespace adas_core.Domain.Models.DTO.Display; + +public class UpdateDisplayConfigNameDto +{ + public string DisplayName { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/MasterListDto.cs b/adas-core.Domain/Models/DTO/MasterListDto.cs new file mode 100644 index 00000000..f0d0ace6 --- /dev/null +++ b/adas-core.Domain/Models/DTO/MasterListDto.cs @@ -0,0 +1,13 @@ +using adas_core.Domain.Enums; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.DTO; + +public class MasterListDto +{ + public ObjectId Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public MasterListType ListType { get; set; } + public int Options { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/MasterListWithPaginatedOptionsDto.cs b/adas-core.Domain/Models/DTO/MasterListWithPaginatedOptionsDto.cs new file mode 100644 index 00000000..06181187 --- /dev/null +++ b/adas-core.Domain/Models/DTO/MasterListWithPaginatedOptionsDto.cs @@ -0,0 +1,21 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.Masters; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.DTO; + +public class MasterListWithPaginatedOptionsDto(MasterList masterList, int pageSize, List units) +{ + public ObjectId Id { get; set; } = masterList.Id; + public string Name { get; set; } = masterList.Name; + public string Description { get; set; } = masterList.Description; + + public MasterListType ListType { get; set; } = masterList.ListType; + public bool CanAddElement { get; set; } = masterList.CanAddElement; + public OptionListDetails? OptionListDetails { get; set; } = masterList.OptionListDetails; + + public int TotalOptionsCount { get; set; } = masterList.Options.Count(); + public List Options { get; set; } = masterList.Options.Take(pageSize).ToList(); + public List Units { get; set; } = units; + public LocaleEnum? DefaultLocale { get; set; } = masterList.DefaultLocale; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/ObservationAlarmDto.cs b/adas-core.Domain/Models/DTO/ObservationAlarmDto.cs new file mode 100644 index 00000000..66d64dd4 --- /dev/null +++ b/adas-core.Domain/Models/DTO/ObservationAlarmDto.cs @@ -0,0 +1,21 @@ +namespace adas_core.Domain.Models.DTO; + +public class ObservationAlarmDto +{ + public string? PatientNumber { get; set; } + public PatientLocation? Location { get; set; } + + public List? Alarms { get; set; } + + public DateTime MessageTime { get; set; } + + public bool WaitResult { get; set; } + + public ObservationData? ObservationData { get; set; } + + public ObservationData? ObsertationData + { + get => ObservationData; + set => ObservationData = value; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/ObservationDto.cs b/adas-core.Domain/Models/DTO/ObservationDto.cs new file mode 100644 index 00000000..56a313f2 --- /dev/null +++ b/adas-core.Domain/Models/DTO/ObservationDto.cs @@ -0,0 +1,18 @@ +namespace adas_core.Domain.Models.DTO; + +public class ObservationDto +{ + public string PatientNumber { get; set; } = string.Empty; + + public PatientLocation? Location { get; set; } + + public ObservationData? ObservationData { get; set; } + + public List? Observations { get; set; } + + public List? Diagnosis { get; set; } + + public DateTime MessageTime { get; set; } + + public bool WaitResult { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/PaginationPairDto.cs b/adas-core.Domain/Models/DTO/PaginationPairDto.cs new file mode 100644 index 00000000..2bb84fbd --- /dev/null +++ b/adas-core.Domain/Models/DTO/PaginationPairDto.cs @@ -0,0 +1,9 @@ +using adas_core.Domain.Models.Filter; + +namespace adas_core.Domain.Models.DTO; + +public class PaginationPairDto +{ + public PaginationFilter ListFilter { get; set; } = new(); + public PaginationFilter OptionFilter { get; set; } = new(); +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/PatientDto.cs b/adas-core.Domain/Models/DTO/PatientDto.cs new file mode 100644 index 00000000..a229134e --- /dev/null +++ b/adas-core.Domain/Models/DTO/PatientDto.cs @@ -0,0 +1,15 @@ +namespace adas_core.Domain.Models.DTO; + +public class PatientDto +{ + public string PatientNumber { get; set; } = string.Empty; + + public PatientLocation? Location { get; set; } + + public Person? Patient { get; set; } + + + public DateTime MessageTime { get; set; } + + public bool WaitResult { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/PocAndUnitDto.cs b/adas-core.Domain/Models/DTO/PocAndUnitDto.cs new file mode 100644 index 00000000..539bcfbb --- /dev/null +++ b/adas-core.Domain/Models/DTO/PocAndUnitDto.cs @@ -0,0 +1,16 @@ +using MongoDB.Bson; + +namespace adas_core.Domain.Models.DTO; + +public class PocAndUnitDto +{ + public List PocList { get; set; } = []; +} + +public class MinimalPocAndUnitDto +{ + public string? PocName { get; set; } + public ObjectId PocId { get; set; } + public string? UnitName { get; set; } + public ObjectId UnitId { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/PointOfCareDto.cs b/adas-core.Domain/Models/DTO/PointOfCareDto.cs new file mode 100644 index 00000000..4e3c10ca --- /dev/null +++ b/adas-core.Domain/Models/DTO/PointOfCareDto.cs @@ -0,0 +1,45 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; +using LightBeacon = adas_core.Domain.Models.MongoModels.LightBeacon; + +namespace adas_core.Domain.Models.DTO; + +public class PointOfCareDto +{ + + public ObjectId Id { get; set; } + public string Room { get; set; } = string.Empty; + public string Bed { get; set; } = string.Empty; + public string? Hall { get; set; } = string.Empty; + public ObjectId UnitId { get; set; } + public PointOfCareConfigurationDto? Configuration { get; set; } + public StatusEnum.PointOfCare Status { get; set; } + public ObjectId? AdmissionId { get; set; } + + #region Unmap + + public string UnitName { get; set; } = string.Empty; + public Unit? Unit { get; set; } + public PatientLocation Location => new(UnitName, Bed, Room); + public List Observations { get; set; } = []; + public bool? HasPatient { get; set; } + public ObjectId? Patientid { get; set; } + public Patient? Patient { get; set; } + public bool? IsActive { get; set; } + public bool? IsVisible { get; set; } + public Admission? Admission { get; set; } + + #endregion +} +public class PointOfCareConfigurationDto +{ + public List BeaconList { get; set; } = []; + public List CameraList { get; set; } = []; + public List? RelayList { get; set; } + public string? Type { get; set; } + + // Esta parte hay que tenerla en cuenta al momento de lanzar las grabaciones automaticas y manuales para la API de OR + // OR espera recibir roomId + public int? Id { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/PumpDto.cs b/adas-core.Domain/Models/DTO/PumpDto.cs new file mode 100644 index 00000000..14818d03 --- /dev/null +++ b/adas-core.Domain/Models/DTO/PumpDto.cs @@ -0,0 +1,17 @@ +using adas_core.Domain.Models.Pumps; + +namespace adas_core.Domain.Models.DTO; + +public class PumpDto +{ + public string PatientNumber { get; set; } = string.Empty; + + public PatientLocation? Location { get; set; } + + public List? PumpObservations { get; set; } + + + public DateTime MessageTime { get; set; } + + public bool WaitResult { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/RecordingAlertDto.cs b/adas-core.Domain/Models/DTO/RecordingAlertDto.cs new file mode 100644 index 00000000..cc0d9acf --- /dev/null +++ b/adas-core.Domain/Models/DTO/RecordingAlertDto.cs @@ -0,0 +1,17 @@ +using adas_core.Domain.Models.Recording; + +namespace adas_core.Domain.Models.DTO; + +public class RecordingAlertDto +{ + public string? PatientNumber { get; set; } + public PatientLocation? Location { get; set; } + + public List? RecordingAlerts { get; set; } + + public RecordingData? RecordingData { get; set; } + + public DateTime MessageTime { get; set; } + + public bool WaitResult { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/TreatmentDto.cs b/adas-core.Domain/Models/DTO/TreatmentDto.cs new file mode 100644 index 00000000..af63ce59 --- /dev/null +++ b/adas-core.Domain/Models/DTO/TreatmentDto.cs @@ -0,0 +1,16 @@ +namespace adas_core.Domain.Models.DTO; + +public class TreatmentDto +{ + public string PatientNumber { get; set; } = string.Empty; + + public PatientLocation? Location { get; set; } + + public List? Treatments { get; set; } + + public PatientTreatment? Treatment { get; set; } + + public DateTime MessageTime { get; set; } + + public bool WaitResult { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/UnitInfoDto.cs b/adas-core.Domain/Models/DTO/UnitInfoDto.cs new file mode 100644 index 00000000..0e187206 --- /dev/null +++ b/adas-core.Domain/Models/DTO/UnitInfoDto.cs @@ -0,0 +1,31 @@ +using System.Text.Json.Serialization; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.DTO; + +public class UnitInfoDto +{ + [JsonConstructor] + public UnitInfoDto() + { + } + + public UnitInfoDto(Unit? unit) + { + Id = unit?.Id; + Name = unit?.Name ?? string.Empty; + Title = unit?.Name ?? string.Empty; + } + + public ObjectId? Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Title { get; set; } = string.Empty; + public string? ConfigObservationId { get; set; } + public long? Admissions { get; set; } + public long? Discharges { get; set; } + public long? Displays { get; set; } + public long? Patients { get; set; } + public long? PointOfCares { get; set; } + public long? VirtualPointOfCares { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/UpdateListNameAndDescriptionDto.cs b/adas-core.Domain/Models/DTO/UpdateListNameAndDescriptionDto.cs new file mode 100644 index 00000000..e076b7a6 --- /dev/null +++ b/adas-core.Domain/Models/DTO/UpdateListNameAndDescriptionDto.cs @@ -0,0 +1,7 @@ +namespace adas_core.Domain.Models.DTO; + +public class UpdateListNameAndDescriptionDto +{ + public string? Name { get; set; } + public string? Description { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/UpdateMasterListDetailsDto.cs b/adas-core.Domain/Models/DTO/UpdateMasterListDetailsDto.cs new file mode 100644 index 00000000..0e82e144 --- /dev/null +++ b/adas-core.Domain/Models/DTO/UpdateMasterListDetailsDto.cs @@ -0,0 +1,9 @@ +using adas_core.Domain.Models.Masters; + +namespace adas_core.Domain.Models.DTO; + +public class UpdateMasterListDetailsDto +{ + public bool? CanAddElement { get; set; } + public OptionListDetails? OptionListDetails { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/UpdateObservationDto.cs b/adas-core.Domain/Models/DTO/UpdateObservationDto.cs new file mode 100644 index 00000000..f60dce9b --- /dev/null +++ b/adas-core.Domain/Models/DTO/UpdateObservationDto.cs @@ -0,0 +1,9 @@ +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Domain.Models.DTO; + +public class UpdateObservationDto +{ + public ConfigObservation? OldConfigObservationItem { get; set; } + public ConfigObservation? NewConfigObservationItem { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/UpdateOptionMasterListDto.cs b/adas-core.Domain/Models/DTO/UpdateOptionMasterListDto.cs new file mode 100644 index 00000000..92ff4c2d --- /dev/null +++ b/adas-core.Domain/Models/DTO/UpdateOptionMasterListDto.cs @@ -0,0 +1,10 @@ +using adas_core.Domain.Models.Masters; + +namespace adas_core.Domain.Models.DTO; + +public class UpdateOptionMasterListDto +{ + public OptionList? UpdatedOption { get; set; } + public OptionList? OldOption { get; set; } + public string? OptionId { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/UpdatePasswordDto.cs b/adas-core.Domain/Models/DTO/UpdatePasswordDto.cs new file mode 100644 index 00000000..901bc7a4 --- /dev/null +++ b/adas-core.Domain/Models/DTO/UpdatePasswordDto.cs @@ -0,0 +1,7 @@ +namespace adas_core.Domain.Models.DTO; + +public class UpdatePasswordDto +{ + public string OldPassword { get; set; } = string.Empty; + public string NewPassword { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/UpdateUnitIdListDto.cs b/adas-core.Domain/Models/DTO/UpdateUnitIdListDto.cs new file mode 100644 index 00000000..e69d2150 --- /dev/null +++ b/adas-core.Domain/Models/DTO/UpdateUnitIdListDto.cs @@ -0,0 +1,38 @@ +using adas_core.Domain.Enums; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.DTO; + +public class UpdateUnitIdListDto +{ + public ObjectId UnitId { get; set; } + + public List MasterListData { get; set; } = []; + + public override string ToString() + { + List items = []; + + items.AddRange(MasterListData.Select(masterListData => $", MasterListData: {masterListData}")); + + + return "UpdateUnitIdListDto[" + string.Join(", ", items) + "]"; + } +} + +public class MasterListData +{ + public ObjectId? MasterListId { get; set; } + public MasterListType? MasterListType { get; set; } + + public override string ToString() + { + List items = + [ + MasterListId != null ? $"identifier: {MasterListId.ToString()}" : "identifier: Null", + MasterListType != null ? $"type: {MasterListType.ToString()}" : "type: null" + ]; + + return "[" + string.Join(", ", items) + "]"; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/DTO/UpdateUserWithAuthDto.cs b/adas-core.Domain/Models/DTO/UpdateUserWithAuthDto.cs new file mode 100644 index 00000000..8850210f --- /dev/null +++ b/adas-core.Domain/Models/DTO/UpdateUserWithAuthDto.cs @@ -0,0 +1,11 @@ +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Domain.Models.DTO; + +public class UpdateUserWithAuthDto +{ + public User User { get; set; } = new(); + public List Authorizations { get; set; } = []; + public List NewAuthorizations { get; set; } = []; + public List AuthsToDelete { get; set; } = []; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Entity.cs b/adas-core.Domain/Models/Entity.cs new file mode 100644 index 00000000..54566b26 --- /dev/null +++ b/adas-core.Domain/Models/Entity.cs @@ -0,0 +1,12 @@ +namespace adas_core.Domain.Models; + +public class Entity +{ + public string? EntityIdentifier { get; set; } + + public string? NamespaceId { get; set; } + + public string? UniversalId { get; set; } + + public string? UniversalIdType { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Field.cs b/adas-core.Domain/Models/Field.cs new file mode 100644 index 00000000..7511136e --- /dev/null +++ b/adas-core.Domain/Models/Field.cs @@ -0,0 +1,18 @@ +//using Newtonsoft.Json; +//using System; +//using System.Collections.Generic; +//using System.Linq; +//using System.Web; + +//namespace smacs_uci_api.Models +//{ +// public class Field +// { +// [JsonProperty("name")] +// public string Name { get; set; } + +// [JsonProperty("last")] +// public int Last { get; set; } +// } +//} + diff --git a/adas-core.Domain/Models/Filter/FilterOptionListElement.cs b/adas-core.Domain/Models/Filter/FilterOptionListElement.cs new file mode 100644 index 00000000..b561ea51 --- /dev/null +++ b/adas-core.Domain/Models/Filter/FilterOptionListElement.cs @@ -0,0 +1,19 @@ +using adas_core.Domain.Models.Masters; + +namespace adas_core.Domain.Models.Filter; + +public class FilterOptionListElement +{ + public string? OptionType { get; set; } + public string? Text { get; set; } + public string? Name { get; set; } + public string? Description { get; set; } + public string? IconDefault { get; set; } + public string? IconCategory { get; set; } + public string? IconColor { get; set; } + public string? Color { get; set; } + public string? BgColor { get; set; } + public LocaleEnum? Locale { get; set; } + public bool? IsFromAdmPanel { get; set; } = false; + public Locale? LocaleItems { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Filter/FilteredRequest.cs b/adas-core.Domain/Models/Filter/FilteredRequest.cs new file mode 100644 index 00000000..2d068abc --- /dev/null +++ b/adas-core.Domain/Models/Filter/FilteredRequest.cs @@ -0,0 +1,91 @@ +using adas_core.Domain.Enums; + +namespace adas_core.Domain.Models.Filter; + +public class FilteredRequest +{ + #region Common region + + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } + public string? PatientId { get; set; } + public string? PatientNumber { get; set; } + public string? Text { get; set; } + public string? DeviceId { get; set; } + public bool? InUse { get; set; } + + #endregion + + #region PUMP + + public PumpEnum.AlarmMode? AlarmMode { get; set; } + public string? AlarmType { get; set; } + public PumpEnum.Mode? PumpMode { get; set; } + public PumpEnum.InfusingStatus? InfusingStatus { get; set; } + public PumpEnum.Status? Status { get; set; } + + #endregion + + #region Treatment + + public List? Observations { get; set; } + + public bool ActiveTreatments { get; set; } + + #endregion + + #region Medicines + + public string? MedicineName { get; set; } + + public string? MedicineCode { get; set; } + + public string? MedicineType { get; set; } + + public string? MedicineGroup { get; set; } + + #endregion + + #region User + + public string? Name { get; set; } + public string? Email { get; set; } + public string? UserName { get; set; } + public StatusEnum.User UserStatus { get; set; } + public string? UserType { get; set; } + + #endregion + + #region Patient + + public DateTime? StartAdmTime { get; set; } + public DateTime? EndAdmTime { get; set; } + + public DateTime? StartDischargeTime { get; set; } + public DateTime? EndDischargeTime { get; set; } + + public DateTime? StartLastObsTime { get; set; } + public DateTime? EndLastObsTime { get; set; } + + public DateTime? StartCreationDateTime { get; set; } + public DateTime? EndCreationDateTime { get; set; } + + public DateTime? StartUpDateTime { get; set; } + public DateTime? EndUpDateTime { get; set; } + + public DateTime? StartBirthDate { get; set; } + public DateTime? EndBirthDate { get; set; } + + #endregion + + #region Displays y PoC + + public string? UnitId { get; set; } + public string? PocId { get; set; } + public string? UnitName { get; set; } + + public DisplayConfigEnums.DisplayType? DisplayType { get; set; } + public StatusEnum.PointOfCare? PointOfCareStatus { get; set; } + + #endregion +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Filter/PaginationFilter.cs b/adas-core.Domain/Models/Filter/PaginationFilter.cs new file mode 100644 index 00000000..e19b9c8d --- /dev/null +++ b/adas-core.Domain/Models/Filter/PaginationFilter.cs @@ -0,0 +1,41 @@ +namespace adas_core.Domain.Models.Filter; + +public record PaginationFilter +{ + public PaginationFilter() + { + } + + public PaginationFilter(int pageNumber, int pageSize, FilteredRequest? filtered) + { + PageNumber = pageNumber < 1 ? 1 : pageNumber; + PageSize = pageSize <= 0 ? 100 : pageSize; + FilteredRequest = filtered; + } + + public int PageNumber { get; set; } = 1; + public int PageSize { get; set; } = 100; + public FilteredRequest? FilteredRequest { get; set; } + + public bool IsEmpty() + { + return PageNumber <= 0 && PageSize <= 0; + } +} + +public static class PaginationFilterExtensions +{ + public static IQueryable ApplyPagination(this IQueryable queryable, PaginationFilter? filter) + { + var modifQueryable = queryable; + if (filter is not { PageNumber: >= 0, PageSize: > 0 }) return modifQueryable; + var pageNumber = filter.PageNumber; + var pageSize = filter.PageSize; + modifQueryable = modifQueryable + .Skip((pageNumber - 1) * pageSize) + .Take(pageSize) + ; + + return modifQueryable; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/GroupedObservation.cs b/adas-core.Domain/Models/GroupedObservation.cs new file mode 100644 index 00000000..6d946a16 --- /dev/null +++ b/adas-core.Domain/Models/GroupedObservation.cs @@ -0,0 +1,96 @@ +using adas_core.Domain.Enums; +using MongoDB.Bson; + +namespace adas_core.Domain.Models; + +public class GroupedObservation +{ + public ObjectId PatientId { get; set; } + public string? Name { get; set; } = string.Empty; + + public string? Group { get; set; } = string.Empty; + + public List Observations { get; set; } = []; + + + public override string ToString() + { + List items = + [ + $"name: {Name}", + $"observations: {Observations} " + ]; + return "PatientObservation[" + string.Join(", ", items) + "]"; + } + + public class GroupedObservationObs + { + public string Name { get; set; } = string.Empty; + + public GroupedObservationObsValue? First { get; set; } + + public GroupedObservationObsValue? Last { get; set; } + + public GroupedObservationObsValue? Min { get; set; } + + public GroupedObservationObsValue? Max { get; set; } + + public object? MinAlert { get; set; } + + public object? MaxAlert { get; set; } + + public GroupedObservationObsValue? Average { get; set; } + + public GroupedObservationObsValue? Sum { get; set; } + + public GroupedObservationObsValue? Count { get; set; } + + public GroupedObservationObsValue? HalfHour { get; set; } + + public GroupedObservationObsValue? LastFilled { get; set; } + + public DateTime Time { get; set; } + + public int? Shift { get; set; } + + public DateTime ShiftDate { get; set; } + + public bool IsFilled { get; set; } + + public override string ToString() + { + List items = [$"name: {Name}"]; + if (First != null) items.Add($"first; {First}"); + if (Last != null) items.Add($"last; {Last}"); + if (Min != null) items.Add($"min; {Min}"); + if (Max != null) items.Add($"max; {Max}"); + if (Average != null) items.Add($"average; {Average}"); + if (Sum != null) items.Add($"sum; {Sum}"); + if (Count != null) items.Add($"count; {Count}"); + if (HalfHour != null) items.Add($"halfhour; {HalfHour}"); + if (MinAlert != null) items.Add($"minAlert; {MinAlert}"); + if (MaxAlert != null) items.Add($"maxAlert; {MaxAlert}"); + items.Add($"time; {Time}"); + items.Add($"shiftDate; {ShiftDate}"); + if (Shift != null) items.Add($"shift; {Shift}"); + + + return "GroupedObservationObs[" + string.Join(", ", items) + "]"; + } + } + + public class GroupedObservationObsValue(object value, DateTime? time, StatusEnum.Type type = StatusEnum.Type.Ok) + { + public StatusEnum.Type Type { get; set; } = type; + + public object Value { get; set; } = value; + + public DateTime? Time { get; set; } = time; + + + public override string ToString() + { + return $"value: {Value}, time: {Time}, status: {Type}"; + } + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/GroupedObservations/Field.cs b/adas-core.Domain/Models/GroupedObservations/Field.cs new file mode 100644 index 00000000..1540fe58 --- /dev/null +++ b/adas-core.Domain/Models/GroupedObservations/Field.cs @@ -0,0 +1,36 @@ +namespace adas_core.Domain.Models.GroupedObservations; + +public class Field +{ + public string? Name { get; set; } + + public int Last { get; set; } = 1; + + public bool OnlyExpired { get; set; } + + //public override bool Equals(object? obj) + //{ + // // Verifica si el objeto pasado es nulo o no es del mismo tipo + // if (obj == null || GetType() != obj.GetType()) return false; + + // // Convierte el objeto pasado al tipo Field + // var other = (Field)obj; + + // // Compara las propiedades una por una, maneja el caso de valores nulos usando el operador de coalescencia nula (??) + // return Name == other.Name && + // Last == other.Last && + // OnlyExpired == other.OnlyExpired; + //} + + //public override int GetHashCode() + //{ + // unchecked + // { + // var hash = 17; + // hash = hash * 23 + (Name?.GetHashCode() ?? 0); + // hash = hash * 23 + Last.GetHashCode(); + // hash = hash * 23 + OnlyExpired.GetHashCode(); + // return hash; + // } + //} +} \ No newline at end of file diff --git a/adas-core.Domain/Models/GroupedObservations/GroupedField.cs b/adas-core.Domain/Models/GroupedObservations/GroupedField.cs new file mode 100644 index 00000000..9f14e10f --- /dev/null +++ b/adas-core.Domain/Models/GroupedObservations/GroupedField.cs @@ -0,0 +1,65 @@ +using adas_core.Domain.Enums; + +namespace adas_core.Domain.Models.GroupedObservations; + +public class GroupedField +{ + public GroupedField() + { + } + + public GroupedField( + string? name, + List? names, + string? group, + List? startTimeShift, + int max, + GroupedObservationEnum.Regularity? regularity, + GroupedObservationEnum.Since since, + List? result, + List? labelList) + { + Name = name; + Names = names ?? []; + Group = group; + StartTimeShift = startTimeShift ?? []; + Max = max; + Regularity = regularity; + Since = since; + Result = result ?? [GroupedObservationEnum.Result.First]; + LabelList = labelList; + } + + public string? Name { get; init; } + + public List Names { get; init; } = []; + + public string? Group { get; init; } + + public List StartTimeShift { get; init; } = []; + + public int Max { get; init; } + + public GroupedObservationEnum.Regularity? Regularity { get; init; } + + public GroupedObservationEnum.Since Since { get; init; } + + public List Result { get; init; } = []; + + public List? LabelList { get; init; } + + public List GetNames() + { + return Names; + } + + public override bool Equals(object? obj) + { + return obj is GroupedField field && Group == field.Group; + } + + public override int GetHashCode() + { + return HashCode.Combine(Name, Group, Max, Regularity, Result); + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/HistoricalLocations.cs b/adas-core.Domain/Models/HistoricalLocations.cs new file mode 100644 index 00000000..ac4da260 --- /dev/null +++ b/adas-core.Domain/Models/HistoricalLocations.cs @@ -0,0 +1,31 @@ +namespace adas_core.Domain.Models; + +public class HistoricalLocation : IEquatable +{ + // Propiedades con get y set para que MongoDB pueda escribir en ellas + public DateTime? AdmTime { get; set; } + public PatientLocation? PatientLocation { get; set; } + + // Constructor vacío necesario para la deserialización de MongoDB + public HistoricalLocation() { } + + // Tu constructor actual + public HistoricalLocation(DateTime admTime, PatientLocation patientLocation) + { + AdmTime = admTime; + PatientLocation = patientLocation ?? throw new ArgumentNullException(nameof(patientLocation)); + } + + public bool Equals(HistoricalLocation? other) + { + if (other == null) return false; + return AdmTime == other.AdmTime && + (PatientLocation?.Equals(other.PatientLocation) ?? other.PatientLocation == null); + } + + public bool IsEmpty() => !AdmTime.HasValue && (PatientLocation == null || PatientLocation.IsEmpty()); + + public bool IsFull() => AdmTime.HasValue && PatientLocation != null && !PatientLocation.IsFullEmpty(); + + public override string ToString() => $"HistoricalLocation[AdmTime: {AdmTime}, PatientLocation: {PatientLocation}]"; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/ManualRecording.cs b/adas-core.Domain/Models/ManualRecording.cs new file mode 100644 index 00000000..31331db0 --- /dev/null +++ b/adas-core.Domain/Models/ManualRecording.cs @@ -0,0 +1,19 @@ +namespace adas_core.Domain.Models; + +public class ManualRecording +{ + //[JsonProperty("patientId")] + //public object PatientId { get; set; } + + //[JsonProperty("roomId")] + //public int RoomId { get; set; } + + public MRecording? Recording { get; set; } +} + +public class MRecording +{ + public DateTime? StartRecordingTime { get; set; } + + public DateTime? StopRecordingTime { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/AccessControlList.cs b/adas-core.Domain/Models/Masters/AccessControlList.cs new file mode 100644 index 00000000..6da988fd --- /dev/null +++ b/adas-core.Domain/Models/Masters/AccessControlList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class AccessControlList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/AllergyList.cs b/adas-core.Domain/Models/Masters/AllergyList.cs new file mode 100644 index 00000000..0fdbac66 --- /dev/null +++ b/adas-core.Domain/Models/Masters/AllergyList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class AllergyList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/AltableOptionList.cs b/adas-core.Domain/Models/Masters/AltableOptionList.cs new file mode 100644 index 00000000..baa6ccc0 --- /dev/null +++ b/adas-core.Domain/Models/Masters/AltableOptionList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class AltableOptionList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/DestinationList.cs b/adas-core.Domain/Models/Masters/DestinationList.cs new file mode 100644 index 00000000..9aa73119 --- /dev/null +++ b/adas-core.Domain/Models/Masters/DestinationList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class DestinationList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/DiagnosisList.cs b/adas-core.Domain/Models/Masters/DiagnosisList.cs new file mode 100644 index 00000000..66690901 --- /dev/null +++ b/adas-core.Domain/Models/Masters/DiagnosisList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class DiagnosisList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/DischargeStatusList.cs b/adas-core.Domain/Models/Masters/DischargeStatusList.cs new file mode 100644 index 00000000..09defaae --- /dev/null +++ b/adas-core.Domain/Models/Masters/DischargeStatusList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class DischargeStatusList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/DoctorList.cs b/adas-core.Domain/Models/Masters/DoctorList.cs new file mode 100644 index 00000000..10d68207 --- /dev/null +++ b/adas-core.Domain/Models/Masters/DoctorList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class DoctorList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/DoctorTypeList.cs b/adas-core.Domain/Models/Masters/DoctorTypeList.cs new file mode 100644 index 00000000..df09f208 --- /dev/null +++ b/adas-core.Domain/Models/Masters/DoctorTypeList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class DoctorTypeList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/GenericList.cs b/adas-core.Domain/Models/Masters/GenericList.cs new file mode 100644 index 00000000..7497984f --- /dev/null +++ b/adas-core.Domain/Models/Masters/GenericList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class GenericList : MasterList; diff --git a/adas-core.Domain/Models/Masters/InsulationList.cs b/adas-core.Domain/Models/Masters/InsulationList.cs new file mode 100644 index 00000000..dcb8bba0 --- /dev/null +++ b/adas-core.Domain/Models/Masters/InsulationList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class InsulationList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/InternalDestinationList.cs b/adas-core.Domain/Models/Masters/InternalDestinationList.cs new file mode 100644 index 00000000..bee4cf7a --- /dev/null +++ b/adas-core.Domain/Models/Masters/InternalDestinationList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class InternalDestinationList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/LanguageBarrierList.cs b/adas-core.Domain/Models/Masters/LanguageBarrierList.cs new file mode 100644 index 00000000..b062446b --- /dev/null +++ b/adas-core.Domain/Models/Masters/LanguageBarrierList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class LanguageBarrierList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/MasterList.cs b/adas-core.Domain/Models/Masters/MasterList.cs new file mode 100644 index 00000000..cff49b7d --- /dev/null +++ b/adas-core.Domain/Models/Masters/MasterList.cs @@ -0,0 +1,102 @@ +using adas_core.Domain.Enums; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.Masters; + +public class MasterList +{ + public ObjectId Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + public string ManualObservationName { get; set; } = "UnknownMasterList"; + public List AutoObservationName { get; set; } = []; + public MasterListType ListType { get; set; } + public bool CanAddElement { get; set; } + public OptionListDetails? OptionListDetails { get; set; } + public LocaleEnum? DefaultLocale { get; set; } + public List Options { get; set; } = []; + + public MasterList ReturnMasterListOptionsInLocaleIfExist(LocaleEnum? localeToReturn) + { + if (localeToReturn == null) return this; + var optionsConverted = new List(); + + foreach (var opt in Options) + { + // 1. Obtener traducción del locale + string? translatedName = null; + + if (opt.LocaleItems != null) + { + var localeProp = opt.LocaleItems + .GetType() + .GetProperty(localeToReturn.Value.ToString()); + + var localeItem = localeProp?.GetValue(opt.LocaleItems) as LocaleItem; + translatedName = localeItem?.Name; + } + + // 2. Usar traducción si existe, sino fallback + var finalName = !string.IsNullOrWhiteSpace(translatedName) + ? translatedName + : opt.Name; + + // 3. Crear nueva opción convertida + var newOpt = new OptionList + { + Id = opt.Id, + Name = finalName, + OptionType = opt.OptionType, + Description = opt.Description, + Color = opt.Color, + BgColor = opt.BgColor, + IconDefault = opt.IconDefault, + IconCategory = opt.IconCategory, + IconColor = opt.IconColor, + InitDate = opt.InitDate, + EndDate = opt.EndDate, + IsDefault = opt.IsDefault, + + // Mantener todos los LocaleItems originales + LocaleItems = opt.LocaleItems + }; + + optionsConverted.Add(newOpt); + } + + // 4. Clonar el master pero con las opciones nuevas + return new MasterList + { + Id = Id, + Name = Name, + Description = Description, + ManualObservationName = ManualObservationName, + AutoObservationName = new List(AutoObservationName), + ListType = ListType, + CanAddElement = CanAddElement, + OptionListDetails = OptionListDetails, + DefaultLocale = DefaultLocale, + Options = optionsConverted + }; + } +} + +public class OptionListDetails +{ + public Element? OptionType { get; set; } + public Element? Name { get; set; } + public Element? IconDefault { get; set; } + public Element? IconCategory { get; set; } + public Element? IconColor { get; set; } + public Element? Color { get; set; } + public Element? BgColor { get; set; } + public Element? Description { get; set; } +} + +public class Element +{ + public string Title { get; set; } = string.Empty; + public bool IsRequired { get; set; } + public bool IsList { get; set; } = false; + public string ListName { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/MobilityOptionList.cs b/adas-core.Domain/Models/Masters/MobilityOptionList.cs new file mode 100644 index 00000000..884a5087 --- /dev/null +++ b/adas-core.Domain/Models/Masters/MobilityOptionList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class MobilityOptionList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/OptionList.cs b/adas-core.Domain/Models/Masters/OptionList.cs new file mode 100644 index 00000000..6e221d70 --- /dev/null +++ b/adas-core.Domain/Models/Masters/OptionList.cs @@ -0,0 +1,73 @@ +using MongoDB.Bson; + +namespace adas_core.Domain.Models.Masters; + +public class OptionList +{ + public ObjectId? Id { get; set; } + public Locale? LocaleItems { get; set; } + public string? OptionType { get; set; } + public string Name { get; set; } = string.Empty; + public string? IconDefault { get; set; } + public string? IconCategory { get; set; } + public string? IconColor { get; set; } + public string? Color { get; set; } + public string? BgColor { get; set; } + public string? Description { get; set; } + public DateTime? InitDate { get; set; } + public DateTime? EndDate { get; set; } + public bool? IsDefault { get; set; } + + public bool AreEqual(OptionList a, OptionList b) + { + return a.OptionType == b.OptionType && + a.Name == b.Name && + a.IconDefault == b.IconDefault && + a.IconCategory == b.IconCategory && + a.IconColor == b.IconColor && + a.Color == b.Color && + a.BgColor == b.BgColor && + a.IsDefault == b.IsDefault && + a.Description == b.Description && + a.InitDate == b.InitDate && + a.EndDate == b.EndDate; + } + + public bool AreEqualExcludeDescription(OptionList a, OptionList b) + { + return a.OptionType == b.OptionType && + a.Name == b.Name && + a.IconDefault == b.IconDefault && + a.IconCategory == b.IconCategory && + a.IconColor == b.IconColor && + a.Color == b.Color && + a.BgColor == b.BgColor && + a.IsDefault == b.IsDefault && + a.InitDate == b.InitDate && + a.EndDate == b.EndDate; + } +} + +public class Locale +{ + public LocaleItem? Es { get; set; } + public LocaleItem? Pt { get; set; } + public LocaleItem? Ca { get; set; } + public LocaleItem? Eng { get; set; } + public LocaleItem? Zh { get; set; } +} + +public class LocaleItem +{ + public string? Name { get; set; } +} + +public enum LocaleEnum +{ + Default, + Es, + Pt, + Ca, + Eng, + Zh +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/OriginList.cs b/adas-core.Domain/Models/Masters/OriginList.cs new file mode 100644 index 00000000..84e9be2c --- /dev/null +++ b/adas-core.Domain/Models/Masters/OriginList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class OriginList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/PassiveSittingList.cs b/adas-core.Domain/Models/Masters/PassiveSittingList.cs new file mode 100644 index 00000000..1b55d244 --- /dev/null +++ b/adas-core.Domain/Models/Masters/PassiveSittingList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class PassiveSittingList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/PatientStatusList.cs b/adas-core.Domain/Models/Masters/PatientStatusList.cs new file mode 100644 index 00000000..fee5d407 --- /dev/null +++ b/adas-core.Domain/Models/Masters/PatientStatusList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class PatientStatusList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/ProcedureList.cs b/adas-core.Domain/Models/Masters/ProcedureList.cs new file mode 100644 index 00000000..b1f1b324 --- /dev/null +++ b/adas-core.Domain/Models/Masters/ProcedureList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class ProcedureList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/ServiceList.cs b/adas-core.Domain/Models/Masters/ServiceList.cs new file mode 100644 index 00000000..0bdf81d0 --- /dev/null +++ b/adas-core.Domain/Models/Masters/ServiceList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class ServiceList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/TestList.cs b/adas-core.Domain/Models/Masters/TestList.cs new file mode 100644 index 00000000..e0868d3d --- /dev/null +++ b/adas-core.Domain/Models/Masters/TestList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class TestList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/TherapeuticCeilingList.cs b/adas-core.Domain/Models/Masters/TherapeuticCeilingList.cs new file mode 100644 index 00000000..46d64a19 --- /dev/null +++ b/adas-core.Domain/Models/Masters/TherapeuticCeilingList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class TherapeuticCeilingList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/TreatmentList.cs b/adas-core.Domain/Models/Masters/TreatmentList.cs new file mode 100644 index 00000000..16a07826 --- /dev/null +++ b/adas-core.Domain/Models/Masters/TreatmentList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class TreatmentList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Masters/VisitOptionList.cs b/adas-core.Domain/Models/Masters/VisitOptionList.cs new file mode 100644 index 00000000..98f39cfc --- /dev/null +++ b/adas-core.Domain/Models/Masters/VisitOptionList.cs @@ -0,0 +1,3 @@ +namespace adas_core.Domain.Models.Masters; + +public class VisitOptionList : MasterList; \ No newline at end of file diff --git a/adas-core.Domain/Models/Medicine.cs b/adas-core.Domain/Models/Medicine.cs new file mode 100644 index 00000000..348e2108 --- /dev/null +++ b/adas-core.Domain/Models/Medicine.cs @@ -0,0 +1,18 @@ +using MongoDB.Bson; + +namespace adas_core.Domain.Models; + +public class Medicine +{ + public ObjectId Id { get; set; } + + public string? Name { get; set; } + + public List Codes { get; set; } = []; + + public List Notes { get; set; } = []; + + public List Type { get; set; } = []; + + public List Group { get; set; } = []; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/Admission.cs b/adas-core.Domain/Models/MongoModels/Admission.cs new file mode 100644 index 00000000..b809c821 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/Admission.cs @@ -0,0 +1,51 @@ +using adas_core.Domain.Models.Masters; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.MongoModels; + +public class Admission +{ + public ObjectId Id { get; set; } + public DateTime AdmissionDate { get; set; } + public string Nhc { get; set; } = string.Empty; + public ObjectId UnitId { get; set; } + public ObjectId? PointOfCareId { get; set; } + public Person? Person { get; set; } + + public OptionList? Origin { get; set; } + public string? OriginAux { get; set; } + + public OptionList? Diagnosis { get; set; } + public string? DiagnosisAux { get; set; } + + public List? Allergies { get; set; } + public OptionList? Insulation { get; set; } + public List? LanguageBarrier { get; set; } + public OptionList? PassiveSitting { get; set; } + + #region UnMap + + public PatientLocation? PatientLocation { get; set; } + + #endregion + + public override string ToString() + { + List items = []; + + if (!string.IsNullOrEmpty(Nhc)) items.Add($"NHC: {Nhc}"); + if (Person != null) items.Add($", Person: {Person}"); + items.Add($"Admission Date: {AdmissionDate}"); + if (PointOfCareId != null) items.Add($"Location: {PointOfCareId}"); + if (PatientLocation != null) items.Add($"Location: {PatientLocation}"); + if (Origin != null) items.Add($", Origin: {Origin.Name}"); + if (Diagnosis != null) items.Add($", Diagnosis; {Diagnosis.Name}"); + if (Allergies != null) + { + items.Add("Allergies:"); + Allergies.ForEach(a => items.Add($", Allergy; {a.Name}")); + } + + return "Admission [" + string.Join(", ", items) + "]"; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/AlarmConfig.cs b/adas-core.Domain/Models/MongoModels/AlarmConfig.cs new file mode 100644 index 00000000..74ad10ad --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/AlarmConfig.cs @@ -0,0 +1,23 @@ +using adas_core.Domain.Enums; + +namespace adas_core.Domain.Models.MongoModels; + +public class AlarmConfig +{ + public string? Name { get; set; } + public bool Enabled { get; set; } + public int EndAfter { get; set; } //seconds + public AlarmItem? Recording { get; set; } + public AlarmItem? Beacon { get; set; } + public AlarmItem? OpenDoor { get; set; } + public int? Priority { get; set; } + public string? Color { get; set; } + public AudioConfig? AudioConfig { get; set; } +} + +public class AudioConfig +{ + public AlarmEnum.AudioAlarmType Type { get; set; } = AlarmEnum.AudioAlarmType.Off; + public string? Path { get; set; } + public int? EndAfter { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/AlarmItem.cs b/adas-core.Domain/Models/MongoModels/AlarmItem.cs new file mode 100644 index 00000000..801478ca --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/AlarmItem.cs @@ -0,0 +1,39 @@ +using adas_core.Domain.Enums; + +namespace adas_core.Domain.Models.MongoModels; + +public class AlarmItem +{ + public bool Enabled { get; set; } + public int StartBefore { get; set; } // seconds + public int EndAfter { get; set; } // seconds overrides + + public string? Color + { + get => BeaconColor.ToString(); + set + { + if (!string.IsNullOrEmpty(value) && Enum.TryParse(value, out AlarmEnum.BeaconColor color)) + BeaconColor = color; + } + } + + public AlarmEnum.Severity Severity { get; set; } = AlarmEnum.Severity.None; + public AlarmEnum.BeaconColor BeaconColor { get; set; } = AlarmEnum.BeaconColor.None; + + public override string ToString() + { + List items = + [ + $"Enabled: {Enabled}", + $"StartBefore: {StartBefore}", + $"EndAfter: {EndAfter}", + $"Severity: {Severity}", + $"BeaconColorEnum: {BeaconColor}" + ]; + if (!string.IsNullOrEmpty(Color)) items.Add($"Color: {Color}"); + + + return "AlarmItem[" + string.Join(", ", items) + "]"; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/Authorizations.cs b/adas-core.Domain/Models/MongoModels/Authorizations.cs new file mode 100644 index 00000000..539bb8dc --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/Authorizations.cs @@ -0,0 +1,25 @@ +using MongoDB.Bson; + +namespace adas_core.Domain.Models.MongoModels; + +public class Authorization +{ + public ObjectId Id { get; set; } + + public ObjectId UserId { get; set; } + + public string? Rol { get; set; } = string.Empty; + + public string? DisplayId { get; set; } = string.Empty; + public string? UnitId { get; set; } = null; + public bool PanelAuthorization { get; set; } + public bool CanUpdate { get; set; } = true; + + #region NotMapped + + public Unit? Unit { get; set; } + public Display? Display { get; set; } + public User? User { get; set; } + + #endregion +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/Box.cs b/adas-core.Domain/Models/MongoModels/Box.cs new file mode 100644 index 00000000..1b77ef7a --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/Box.cs @@ -0,0 +1,33 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.Observations; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.MongoModels; + +public class Box +{ + public Person? AttendingDoctor; + public string? PointOfCare { get; set; } + + public string? Bed { get; set; } + + public bool IsActive { get; set; } = true; + + public bool IsVisible { get; set; } = true; + + public Dictionary? Configuration { get; set; } + + public PatientLocation Location => new(PointOfCare, Bed); + + public bool HasPatient { get; set; } + + public ObjectId? Patientid { get; set; } + + public Patient? Patient { get; set; } + + public StatusEnum.Type Type { get; set; } + + public List? Observations { get; set; } + + public List? Medication { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/Camera.cs b/adas-core.Domain/Models/MongoModels/Camera.cs new file mode 100644 index 00000000..96caf275 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/Camera.cs @@ -0,0 +1,31 @@ +using MongoDB.Bson; + +namespace adas_core.Domain.Models.MongoModels; + +public class Camera +{ + public ObjectId Id { get; set; } + public string? Name { get; set; } = string.Empty; + + public Stream? Streams { get; set; } + + public bool Ptz { get; set; } + + public string? Driver { get; set; } + + public string? Ip { get; set; } + + public string? Username { get; set; } + + public string? Password { get; set; } + public bool InUse { get; private set; } +} + +public class Stream +{ + public string? Rtsp { get; set; } + public string? Jpeg { get; set; } + public string? WebRtc { get; set; } + public string? Hls { get; set; } + public string? Mp4 { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/ConfigObservation.cs b/adas-core.Domain/Models/MongoModels/ConfigObservation.cs new file mode 100644 index 00000000..bc480c74 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/ConfigObservation.cs @@ -0,0 +1,122 @@ +using System.Xml.Serialization; +using adas_core.Domain.Enums; +using MongoDB.Bson; +using Newtonsoft.Json; + +namespace adas_core.Domain.Models.MongoModels; + +public class ConfigObservation +{ + public Dictionary? Grouped; + public ObjectId Id { get; set; } + + public string? Code { get; set; } + + public string? CodingSystem { get; set; } + + public string? OriginalName { get; set; } + + public string? ParentCode { get; set; } + + public string? ParentCodingSystem { get; set; } + + public string? ParentName { get; set; } + + public string? Name { get; set; } + + public string? Units { get; set; } + + public ObservationEnum.ArrowType ArrowType { get; set; } = ObservationEnum.ArrowType.Default; + + public bool ShowArrow { get; set; } = true; + + public bool ShowValue { get; set; } = true; + + + public bool ForceUnits { get; set; } + + public double? MinAlert { get; set; } + + public double? MaxAlert { get; set; } + + public double? MaxWarn { get; set; } + public double? MinWarn { get; set; } + public string? WarnColor { get; set; } + public string? AlertColor { get; set; } + + + public List? AlertValues { get; set; } + public List? WarningValues { get; set; } + + + public bool ForceWarn { get; set; } + + + public bool ForceAlert { get; set; } + public char? Alert { get; set; } + public int? Expires { get; set; } + + public bool ShowOnExpired { get; set; } + public bool? Persist { get; set; } + public string? ColorOnExpired { get; set; } + public RetentionPolicy? RetentionPolicy { get; set; } + public int? RetentionPolicyValue { get; set; } + public string? LevelCondition { get; set; } + public Dictionary? UiConfiguration { get; set; } + public AlarmConfig? Alarm { get; set; } + public string? Description { get; set; } + public object? RequiredValue { get; set; } + public List? Preconditions { get; set; } + public bool CheckObservations { get; set; } + public List? CreateObservation { get; set; } + public ObservationEnum.InsertMode InsertMode { get; set; } = ObservationEnum.InsertMode.Auto; + public bool TimeFromMessageTime { get; set; } = false; + public List? ColorRanges { get; set; } + public DemoConfig? DemoConfig { get; set; } + + public override string ToString() + { + return JsonConvert.SerializeObject(this, Formatting.Indented); + } + + public class ColorRange + { + public ObservationEnum.ValueType? ValueType; + + //Para números + public double? Min { get; set; } + public double? Max { get; set; } + + //Para Strings + public string? MatchText { get; set; } + + //Para booleanos + public bool? MatchBoolean { get; set; } + + // Para fechas (en formato ISO) + public string? MinDate { get; set; } + public string? MaxDate { get; set; } + + //Estética + public string? Color { get; set; } + public string? Label { get; set; } + } +} + +public class DemoConfig +{ + public int? MaxValue { get; set; } + public int? MinValue { get; set; } + public object? ValueOption { get; set; } + public bool? SetRandomValue { get; set; } + public bool? SetInValue { get; set; } + public int? RandomCount { get; set; } + public bool? ValueIsArray { get; set; } + public bool? RequireInitDate { get; set; } + public bool? RequireEndDate { get; set; } + public int? PriorityAlarm { get; set; } + //Campos para el simulador del Admin + public int? Periodicity { get; set; } + public ObservationEnum.XmlType? XmlType { get; set; } + +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/ConfigPumps.cs b/adas-core.Domain/Models/MongoModels/ConfigPumps.cs new file mode 100644 index 00000000..e9ea0cab --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/ConfigPumps.cs @@ -0,0 +1,24 @@ +using adas_core.Domain.Enums; + +namespace adas_core.Domain.Models.MongoModels; + +public class ConfigPumps +{ + public string Id { get; set; } = string.Empty; + + public List? Items { get; set; } + + +} + +public class ConfigPumpItem +{ + public PumpEnum.AlarmType? AlarmType { get; set; } + + public Dictionary? UiConfiguration { get; set; } + + public PumpEnum.PumpMessageType? Type { get; set; } + + public RetentionPolicy? RetentionPolicy { get; set; } + public int? RetentionPolicyValue { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/ConfigUnits.cs b/adas-core.Domain/Models/MongoModels/ConfigUnits.cs new file mode 100644 index 00000000..fb4abea9 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/ConfigUnits.cs @@ -0,0 +1,15 @@ +namespace adas_core.Domain.Models.MongoModels; + +public class ConfigUnits +{ + public string Id { get; set; } = string.Empty; + + public List? Items { get; set; } +} + +public class ConfigUnitItem +{ + public string? Code { get; set; } + + public string? Value { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/Device.cs b/adas-core.Domain/Models/MongoModels/Device.cs new file mode 100644 index 00000000..16a69bb5 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/Device.cs @@ -0,0 +1,37 @@ +using adas_core.Domain.Enums; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.MongoModels; + +public class Device +{ + public ObjectId Id { get; set; } + public List PointOfCareIds { get; set; } = new(); + public DeviceType DeviceType { get; set; } + public string? Name { get; set; } + public DateTime CreatedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public int? Battery { get; set; } + public string? MacAddr { get; set; } + public string? SerialNumber { get; set; } + public string? Color { get; set; } + public bool Connected { get; set; } + public bool Ready { get; set; } + public string? Uuid { get; set; } + public string? Key { get; set; } + public DeviceSettings? Settings { get; set; } +} + +public class DeviceSettings +{ + public DeviceAction? Action { get; set; } +} +public class DeviceAction +{ + public DeviceActionType Type { get; set; } + public ObjectId ConfigObservationId { get; set; } + public string? AlarmName { get; set; } + public object? ValueOnSingleClick { get; set; } + public object? ValueOnDoubleClick { get; set; } + public object? ValueOnHoldClick { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/Discharge.cs b/adas-core.Domain/Models/MongoModels/Discharge.cs new file mode 100644 index 00000000..eabcdb0a --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/Discharge.cs @@ -0,0 +1,28 @@ +using adas_core.Domain.Models.Masters; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.MongoModels; + +public class Discharge +{ + public ObjectId Id { get; set; } + public ObjectId? PointOfCareId { get; set; } + public ObjectId? UnitId { get; set; } + public DateTime DischargeDate { get; set; } + + public ObjectId PatientId { get; set; } + public string? Destination { get; set; } + public OptionList? DestinationOption { get; set; } + public OptionList? ServiceOption { get; set; } + public string? Service { get; set; } + public DateTime? MedicalDischarge { get; set; } + public DateTime? AdminDischarge { get; set; } + public DateTime? NurseDischarge { get; set; } + + #region UnMap + + public Patient? Patient { get; set; } + public PatientLocation? PatientLocation { get; set; } + + #endregion +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/Display.cs b/adas-core.Domain/Models/MongoModels/Display.cs new file mode 100644 index 00000000..a46ce206 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/Display.cs @@ -0,0 +1,23 @@ +using adas_core.Domain.Enums; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.MongoModels; + +public class Display +{ + public ObjectId Id { get; set; } + public ObjectId UnitId { get; set; } + public ObjectId? DisplayConfigId { get; set; } + public required string Name { get; set; } + public List PointOfCareIdList { get; set; } = []; + + public DisplayConfigEnums.DisplayType Type { get; set; } = DisplayConfigEnums.DisplayType.Unknown; + + #region Unmap + + public Unit? Unit { get; set; } + public List PointOfCares { get; set; } = []; + public DisplayConfig? DisplayConfig { get; set; } + + #endregion +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/DisplayCardConfig.cs b/adas-core.Domain/Models/MongoModels/DisplayCardConfig.cs new file mode 100644 index 00000000..a2f90fb7 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/DisplayCardConfig.cs @@ -0,0 +1,118 @@ +using adas_core.Domain.Enums; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.MongoModels; + +#region HomeConfig + +public class CardConfig +{ + public ObjectId Id { get; set; } + public List? Rows { get; set; } +} + +public class RowCardConfig +{ + // Dynamic cells de nurse y Dynamic Obs de monitoring + public List? Cells { get; set; } + public double? GrowPriority { get; set; } + public DisplayConfigEnums.CellType Type { get; set; } + public string? Border { get; set; } + public string? BorderRadius { get; set; } + public string? Title { get; set; } + public string? Size { get; set; } + public string? MarginTop { get; set; } + public string? MarginBottom { get; set; } + public string? MarginLeft { get; set; } + public string? MarginRight { get; set; } + public string? BgColor { get; set; } + public string? PaddingTop { get; set; } + public string? PaddingBottom { get; set; } + public string? PaddingLeft { get; set; } + public string? PaddingRight { get; set; } +} + +public class Cell +{ + public ChartSettings? ChartSettings { get; set; } + public List? TextRules { get; set; } + public DisplayConfigEnums.CellType Type { get; set; } + + public string? SubType { get; set; } + public double? Size { get; set; } + public List? SubObs { get; set; } + public bool? HideName { get; set; } + public bool? IsStatic { get; set; } + public double? GrowPriority { get; set; } + public string? Border { get; set; } + public string? BorderRadius { get; set; } + public string? Name { get; set; } + public bool IsVisible { get; set; } = true; + public bool? IsColumn { get; set; } + public string? BgColor { get; set; } + public string? Title { get; set; } + + public string? ValuePathNested { get; set; } + public List? ValuePathKey { get; set; } + public List? IconValueList { get; set; } + public string? PaddingTop { get; set; } + public string? PaddingBottom { get; set; } + public string? PaddingLeft { get; set; } + public string? PaddingRight { get; set; } + public string? Color { get; set; } + public string? BackgroundColor { get; set; } + public bool? ShowIcon { get; set; } + public string? FlexBasis { get; set; } + public string? Grow { get; set; } + public string? Shrink { get; set; } + public bool? ShowTitle { get; set; } + public DialogConfig? DialogConfig { get; set; } + public bool? IndicatorHorizontal { get; set; } + public bool? ShowIndicator { get; set; } + public bool? OnlyNumber { get; set; } + public bool? ShowArrow { get; set; } + public string? GraphConf { get; set; } + public List? Names { get; set; } + public List? ObservationName { get; set; } + public List?>? ObservationTitle { get; set; } + public DisplayConfigEnums.DirectionEnum? Direction { get; set; } +} + +public class ChartSettings +{ + public LegendLayoutConfig? LegendLayoutConfig { get; set; } + public decimal? LegendGrowPriority { get; set; } + public decimal? ChartGrowPriority { get; set; } +} + +public class DialogConfig +{ + public bool? IsDraggable { get; set; } + public MedicalConfig? MedicalConfig { get; set; } +} + +public class MedicalConfig +{ + public bool HasFinalizeTime { get; set; } + public bool HasStartTime { get; set; } +} + +public class IconValueList +{ + public double? MinValue { get; set; } + public double? MaxValue { get; set; } + public float? Width { get; set; } + public float? Height { get; set; } + public List? IconList { get; set; } +} + +public class HomeConfig +{ + public string? MinColumnSize { get; set; } + public int ColumnsPerBreakpoint { get; set; } + public string? BreakpointSize { get; set; } + public string? CardAspectRatioHeight { get; set; } + public string? CardAspectRatioWidth { get; set; } +} + +#endregion \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/DisplayCardDetailConfig.cs b/adas-core.Domain/Models/MongoModels/DisplayCardDetailConfig.cs new file mode 100644 index 00000000..98bb1e30 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/DisplayCardDetailConfig.cs @@ -0,0 +1,58 @@ +using adas_core.Domain.Enums; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.MongoModels; + +#region DetailsConfig + +public class CardDetailsConfig +{ + public ObjectId Id { get; set; } + public List? Header { get; set; } + public List? NurseRows { get; set; } = []; + public List? SmartSections { get; set; } +} + +public class RowDetailsConfig +{ + // Dynamic cells de nurse y Dynamic Obs de monitoring + public List? Cells { get; set; } + public List? Rows { get; set; } + public double GrowPriority { get; set; } + public string? Title { get; set; } + public DisplayConfigEnums.RowType Type { get; set; } + public DialogConfig? DialogConfig { get; set; } +} + +public class CellDetails +{ + public List? TextRules { get; set; } + public DisplayConfigEnums.CellType Type { get; set; } + public ChartSettings? ChartSettings { get; set; } + public string? SubType { get; set; } + public double GrowPriority { get; set; } + public List? Cells { get; set; } + public string? Name { get; set; } + public string? Title { get; set; } + public bool IsVisible { get; set; } = true; + public string? ValuePathNested { get; set; } + public List? ValuePathKey { get; set; } + public List? IconValueList { get; set; } + public List? ObservationName { get; set; } + public string? PaddingTop { get; set; } + public string? PaddingBottom { get; set; } + public string? PaddingLeft { get; set; } + public string? PaddingRight { get; set; } + public string? Color { get; set; } + public string? BackgroundColor { get; set; } + public bool? ShowIcon { get; set; } + public string? FlexBasis { get; set; } + public string? FlexDirection { get; set; } + public string? Grow { get; set; } + public string? Shrink { get; set; } + public bool? ShowTitle { get; set; } + public bool? IsStatic { get; set; } + public DialogConfig? DialogConfig { get; set; } +} + +#endregion \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/DisplayConfig.cs b/adas-core.Domain/Models/MongoModels/DisplayConfig.cs new file mode 100644 index 00000000..4fd35b00 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/DisplayConfig.cs @@ -0,0 +1,1084 @@ +using System.Collections; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.GroupedObservations; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.MongoModels; + +public class DisplayConfig +{ + public ObjectId Id { get; set; } + public DisplayConfigEnums.DisplayType Type { get; set; } + public ObjectId? CardConfigId { get; set; } + public CardConfig? CardConfig { get; private set; } + public HomeConfig? HomeConfig { get; set; } + public HeaderConfig? HeaderConfig { get; set; } + public ObjectId? DetailConfigId { get; set; } + public CardDetailsConfig? DetailConfig { get; private set; } + + public List DisplaySectionIdList { get; set; } = []; + + public string? Hospital { get; set; } + public ColorConfig? ColorConfig { get; set; } + public List FieldList { get; set; } = []; + public string? MediaFolder { get; set; } + public List? GroupedFieldList { get; set; } + public List? HomeBanner { get; set; } + public FormConfig? FormConfig { get; set; } + public List? ChartConfigIdList { get; set; } + public List? ChartConfig { get; set; } + public bool? HasCameras { get; set; } + public bool? HasSound { get; set; } + public bool? IsRotationEnabled { get; set; } + public bool? CanChangeCameraMode { get; set; } + public bool? CamerasAreActive { get; set; } + public string? CameraStreamType { get; set; } + public List? SensorList { get; set; } + public string? ObservationForIndicator { get; set; } + public List? AlarmFieldList { get; set; } + public List? RequestGroupedFieldList { get; set; } + public bool? Pumps { get; set; } + public GraphLayout? GraphLayout { get; set; } + public List? CardRotatingLayout { get; set; } + + #region NotMapped + + public List DisplaySectionList { get; set; } = []; + + #endregion + + public virtual DisplayConfig MergeConfig(DisplayConfig defaultConfig) + { + // CardConfig ??= defaultConfig.CardConfig; + CardConfigId ??= defaultConfig.CardConfigId; + HomeConfig ??= defaultConfig.HomeConfig; + HeaderConfig ??= defaultConfig.HeaderConfig; + DetailConfigId ??= defaultConfig.DetailConfigId; + Hospital ??= defaultConfig.Hospital; + ColorConfig ??= defaultConfig.ColorConfig; + MediaFolder ??= defaultConfig.MediaFolder; + GroupedFieldList ??= defaultConfig.GroupedFieldList; + if (SensorList?.Count == 0) SensorList = defaultConfig.SensorList; + if (AlarmFieldList?.Count == 0) AlarmFieldList = defaultConfig.AlarmFieldList; + if (FieldList.Count == 0) FieldList = defaultConfig.FieldList; + if (GroupedFieldList?.Count == 0) GroupedFieldList = defaultConfig.GroupedFieldList; + if (RequestGroupedFieldList?.Count == 0) RequestGroupedFieldList = defaultConfig.RequestGroupedFieldList; + if (ChartConfig?.Count == 0) ChartConfig = defaultConfig.ChartConfig; + return this; + } +} +public class StandarDisplay : DisplayConfig +{ + public SectionConfig? SectionConfig { get; set; } + + public override DisplayConfig MergeConfig(DisplayConfig defaultConfig) + { + if (defaultConfig is not StandarDisplay defaultStandarConfig) + return this; // Si no es de tipo DisplayNurse, no combinar. + + HomeConfig ??= defaultStandarConfig.HomeConfig; + ColorConfig = defaultStandarConfig.ColorConfig; + IsRotationEnabled = defaultStandarConfig.IsRotationEnabled; + return base.MergeConfig(defaultConfig); + } +} +public class DisplayNurse : DisplayConfig +{ + public override DisplayConfig MergeConfig(DisplayConfig defaultConfig) + { + if (defaultConfig is not DisplayNurse defaultNurseConfig) return this; + HomeBanner ??= defaultNurseConfig.HomeBanner; + ColorConfig ??= defaultNurseConfig.ColorConfig; + FormConfig ??= defaultNurseConfig.FormConfig; + return base.MergeConfig(defaultConfig); + } +} + +public class PumpDisplay : DisplayConfig +{ + public override DisplayConfig MergeConfig(DisplayConfig config) + { + if (config is not PumpDisplay defaultConfig) return this; + CardConfigId ??= defaultConfig.CardConfigId; + HomeConfig ??= defaultConfig.HomeConfig; + ColorConfig = defaultConfig.ColorConfig; + + GraphLayout ??= defaultConfig.GraphLayout; + Pumps ??= defaultConfig.Pumps; + if (FieldList.Count == 0) FieldList = defaultConfig.FieldList; + if (GroupedFieldList?.Count == 0) GroupedFieldList = defaultConfig.GroupedFieldList; + if (RequestGroupedFieldList?.Count == 0) RequestGroupedFieldList = defaultConfig.RequestGroupedFieldList; + if (ChartConfig?.Count == 0) ChartConfig = defaultConfig.ChartConfig; + + return base.MergeConfig(config); + } +} + +public class SmartDisplay : DisplayConfig +{ + public override DisplayConfig MergeConfig(DisplayConfig defaultConfigToCast) + { + if (defaultConfigToCast is not SmartDisplay defaultConfig) return this; + ColorConfig = defaultConfig.ColorConfig; + HasCameras ??= defaultConfig.HasCameras; + HasSound ??= defaultConfig.HasSound; + IsRotationEnabled ??= defaultConfig.IsRotationEnabled; + CanChangeCameraMode ??= defaultConfig.CanChangeCameraMode; + CamerasAreActive ??= defaultConfig.CamerasAreActive; + CameraStreamType ??= defaultConfig.CameraStreamType; + GraphLayout ??= defaultConfig.GraphLayout; + Pumps ??= defaultConfig.Pumps; + ObservationForIndicator ??= defaultConfig.ObservationForIndicator; + CardRotatingLayout ??= defaultConfig.CardRotatingLayout; + return base.MergeConfig(defaultConfig); + } + + // TO TEST + public List GetDifferentProperties(SmartDisplay? other) + { + // Obtiene las propiedades públicas de la clase SmartDisplay + var properties = typeof(SmartDisplay).GetProperties(); + + var differentProperties = ( + from property in properties + let thisValue = property.GetValue(this) + let otherValue = property.GetValue(other) + where !AreEqual(thisValue, otherValue) + select property.Name + ).ToList(); + + if (ColorConfig != null && !ColorConfig.Equals(other?.ColorConfig)) + differentProperties.Add("ColorConfig"); + + return differentProperties; + } + + // Método para comparar valores de diferentes tipos de propiedades + private static bool AreEqual(object? value1, object? value2) + { + if (value1 == null && value2 == null) return true; + + if (value1 == null || value2 == null) return false; + if (value1 is IEnumerable enumerable1 && value2 is IEnumerable enumerable2) + { + var enumerator1 = enumerable1.GetEnumerator(); + var enumerator2 = enumerable2.GetEnumerator(); + + try + { + while (enumerator1.MoveNext() && enumerator2.MoveNext()) + if (!AreEqual(enumerator1.Current, enumerator2.Current)) + return false; + + return !enumerator1.MoveNext() && !enumerator2.MoveNext(); + } + finally + { + if (enumerator1 is IDisposable disposable1) disposable1.Dispose(); + if (enumerator2 is IDisposable disposable2) disposable2.Dispose(); + } + } + + // Si ambos valores son del mismo tipo, usa Equals para comparar + if (value1.GetType() == value2.GetType()) return value1.Equals(value2); + + // Si los tipos son diferentes, convierte a string y compara + return value1.ToString() == value2.ToString(); + } +} +public class HeaderConfig +{ + public HeaderItem? PartnerLogo { get; set; } + + public HeaderItem? CompanyLogo { get; set; } + + public HeaderItem? MeddisLogo { get; set; } + + public HeaderItem? CenterLogo { get; set; } + + public HeaderItem? UnitName { get; set; } + + public HeaderItem? Cameras { get; set; } + + public HeaderItem? Sensors { get; set; } + + public HeaderItem? Fullscreen { get; set; } + + public HeaderItem? Sounds { get; set; } + + public HeaderItem? Sidebar { get; set; } + + public HeaderItem? CurrentDateTime { get; set; } + + public HeaderItem? SectionTitle { get; set; } + + public class HeaderItem + { + public string? LogoUrl { get; set; } + public bool IsVisible { get; set; } = true; + } +} + +public class MinimalDisplaySection +{ + public string? Name { get; set; } + public ObjectId Id { get; set; } + public bool IsSelected { get; set; } +} +public class SectionConfig +{ + public int? Columns { get; set; } + public int? Rows { get; set; } + public int? RefreshValues { get; set; } + public int? RefreshConfig { get; set; } + public UiFontData? DesignProperties { get; set; } + public List? Steps { get; set; } +} +public class BannerItem +{ + public DisplayConfigEnums.BannerType? Type { get; set; } + public double GrowPriority { get; set; } + public BannerItemConfig? Config { get; set; } +} + +public class BannerItemConfig +{ + public BannerItemTableConfig? BannerItemTableConfig { get; set; } + public BannerItemTableConfig? BannerItemDialogTableConfig { get; set; } + public MedicalStaffConfig? MedicalStaffConfig { get; set; } +} + +public class BannerItemTableConfig +{ + public List? Config { get; set; } + public string? BgColor { get; set; } + public string? TextColor { get; set; } +} + +public class MedicalStaffConfig +{ + public bool? HasTeams { get; set; } + public int? StaffAmount { get; set; } +} + +public class FormItemOverview +{ + public bool Nhc { get; set; } + public bool? Bed { get; set; } + public bool? Name { get; set; } + public bool? LastName { get; set; } + public bool? SecondName { get; set; } + public bool? Genre { get; set; } + public bool? Birthday { get; set; } + public bool? Origin { get; set; } + public bool? OriginAux { get; set; } + public bool? Diagnostic { get; set; } + public bool? DiagnosticAux { get; set; } + public bool? Allergy { get; set; } + public bool? Language { get; set; } + public bool? Insulation { get; set; } + + public bool? Service { get; set; } + public bool? Destination { get; set; } + public bool? DestinationAux { get; set; } + public bool? AdmDischarge { get; set; } + public bool? NurseDischarge { get; set; } + public bool? MedicalDischarge { get; set; } + public bool? IncomingDate { get; set; } + public bool? UciDays { get; set; } +} + +public class HeaderBannerItemTableConfig +{ + public DisplayConfigEnums.CellType? Type { get; set; } + public string? SubType { get; set; } + public List? Field { get; set; } + public double? GrowPriority { get; set; } + public bool? Icon { get; set; } + public string? BgColor { get; set; } + public string? TextColor { get; set; } + public string? Title { get; set; } +} + +public class Sensor +{ + public string? Name { get; set; } + public string? Title { get; set; } + public bool OnlyNumbers { get; set; } + public bool IsGeneral { get; set; } +} + +public class ColorConfig +{ + public LevelColors? Level { get; set; } + public TextColors? Text { get; set; } + public ArrowColors? Arrow { get; set; } + public IndicatorColors? Indicator { get; set; } + public GraphColors? Graph { get; set; } + public StatusBoxNumberColors? BoxNumber { get; set; } + public StatusBoxNumberColors? BoxStatusColor { get; set; } + public TherapyColors? Therapy { get; set; } + public TestColors? Test { get; set; } + public ProcedureColors? Procedure { get; set; } + + //public override bool Equals(object? obj) + //{ + // if (obj == null || GetType() != obj.GetType()) return false; + + // var otherColorConfig = (ColorConfig)obj; + // return Level != null && Level.Equals(otherColorConfig.Level) && + // Text != null && Text.Equals(otherColorConfig.Text) && + // Arrow != null && Arrow.Equals(otherColorConfig.Arrow) && + // Indicator != null && Indicator.Equals(otherColorConfig.Indicator) && + // Graph != null && Graph.Equals(otherColorConfig.Graph) && + // Therapy != null && Therapy.Equals(otherColorConfig.Therapy) && + // Test != null && Test.Equals(otherColorConfig.Test) && + // Procedure != null && Procedure.Equals(otherColorConfig.Procedure) && + // BoxNumber != null && BoxNumber.Equals(otherColorConfig.BoxNumber) && + // BoxStatusColor != null && BoxStatusColor.Equals(otherColorConfig.BoxStatusColor); + //} + + //public override int GetHashCode() + //{ + // unchecked + // { + // var hash = 17; + // hash = hash * 23 + Level?.GetHashCode() ?? 0; + // hash = hash * 23 + Text?.GetHashCode() ?? 0; + // hash = hash * 23 + Arrow?.GetHashCode() ?? 0; + // hash = hash * 23 + Indicator?.GetHashCode() ?? 0; + // hash = hash * 23 + Graph?.GetHashCode() ?? 0; + // hash = hash * 23 + BoxNumber?.GetHashCode() ?? 0; + // hash = hash * 23 + Test?.GetHashCode() ?? 0; + // hash = hash * 23 + Procedure?.GetHashCode() ?? 0; + // hash = hash * 23 + Therapy?.GetHashCode() ?? 0; + // hash = hash * 23 + BoxStatusColor?.GetHashCode() ?? 0; + // return hash; + // } + //} + + public class AppearanceSettings + { + public string? TextColor { get; set; } + public string? BackgroundColor { get; set; } + public string? Icon { get; set; } + public string? IconDefault { get; set; } + public string? IconColor { get; set; } + public string? IconCategory { get; set; } + public double IconInvertColor { get; set; } + + //public override bool Equals(object? obj) + //{ + // if (obj == null || GetType() != obj.GetType()) return false; + + // var otherLevelColors = (AppearanceSettings)obj; + // return TextColor == otherLevelColors.TextColor && + // BackgroundColor == otherLevelColors.BackgroundColor && + // Icon == otherLevelColors.Icon && + // Math.Abs(IconInvertColor - otherLevelColors.IconInvertColor) == 0 && + // IconDefault == otherLevelColors.IconDefault + // ; + //} + + //public override int GetHashCode() + //{ + // unchecked + // { + // var hash = 17; + // hash = hash * 23 + TextColor?.GetHashCode() ?? 0; + // hash = hash * 23 + BackgroundColor?.GetHashCode() ?? 0; + // hash = hash * 23 + IconInvertColor.GetHashCode(); + // hash = hash * 23 + Icon?.GetHashCode() ?? 0; + // hash = hash * 23 + IconDefault?.GetHashCode() ?? 0; + // return hash; + // } + //} + } + + public class StatusBoxNumberColors + { + public AppearanceSettings? Reserved { get; set; } + public AppearanceSettings? InUse { get; set; } + public AppearanceSettings? Available { get; set; } + public AppearanceSettings? Locked { get; set; } + public AppearanceSettings? Transferable { get; set; } + public AppearanceSettings? Exitus { get; set; } + public AppearanceSettings? Altable { get; set; } + + //public override bool Equals(object? obj) + //{ + // if (obj == null || GetType() != obj.GetType()) return false; + + // var otherLevelColors = (StatusBoxNumberColors)obj; + // return Reserved != null && Reserved.Equals(otherLevelColors.Reserved) && + // InUse != null && InUse.Equals(otherLevelColors.InUse) && + // Transferable != null && Transferable.Equals(otherLevelColors.Transferable) && + // Exitus != null && Exitus.Equals(otherLevelColors.Exitus) && + // Altable != null && Altable.Equals(otherLevelColors.Altable) && + // Available != null && Available.Equals(otherLevelColors.Available) && + // Locked != null && Locked.Equals(otherLevelColors.Locked); + //} + + //public override int GetHashCode() + //{ + // unchecked + // { + // var hash = 17; + // hash = hash * 23 + Reserved?.GetHashCode() ?? 0; + // hash = hash * 23 + InUse?.GetHashCode() ?? 0; + // hash = hash * 23 + Available?.GetHashCode() ?? 0; + // hash = hash * 23 + Locked?.GetHashCode() ?? 0; + // return hash; + // } + //} + } + + public class TherapyColors + { + public AppearanceSettings? Default { get; set; } + public AppearanceSettings? Initialized { get; set; } + public AppearanceSettings? Finished { get; set; } + public AppearanceSettings? InProgress { get; set; } + + //public override bool Equals(object? obj) + //{ + // if (obj == null || GetType() != obj.GetType()) return false; + + // var otherLevelColors = (TherapyColors)obj; + // return InProgress != null && InProgress.Equals(otherLevelColors.InProgress) && + // Initialized != null && Initialized.Equals(otherLevelColors.Initialized) && + // Finished != null && Finished.Equals(otherLevelColors.Finished) && + // Default != null && Default.Equals(otherLevelColors.Default); + //} + + //public override int GetHashCode() + //{ + // unchecked + // { + // var hash = 17; + // hash = hash * 23 + InProgress?.GetHashCode() ?? 0; + // hash = hash * 23 + Initialized?.GetHashCode() ?? 0; + // hash = hash * 23 + Default?.GetHashCode() ?? 0; + // return hash; + // } + //} + } + + public class TestColors + { + public AppearanceSettings? Default { get; set; } + public AppearanceSettings? Initialized { get; set; } + public AppearanceSettings? Finished { get; set; } + public AppearanceSettings? Expired { get; set; } + + //public override bool Equals(object? obj) + //{ + // if (obj == null || GetType() != obj.GetType()) return false; + + // var otherLevelColors = (TestColors)obj; + // return Expired != null && Expired.Equals(otherLevelColors.Expired) && + // Initialized != null && Initialized.Equals(otherLevelColors.Initialized) && + // Finished != null && Finished.Equals(otherLevelColors.Finished) && + // Default != null && Default.Equals(otherLevelColors.Default); + //} + + //public override int GetHashCode() + //{ + // unchecked + // { + // var hash = 17; + // hash = hash * 23 + Expired?.GetHashCode() ?? 0; + // hash = hash * 23 + Initialized?.GetHashCode() ?? 0; + // hash = hash * 23 + Finished?.GetHashCode() ?? 0; + // hash = hash * 23 + Default?.GetHashCode() ?? 0; + // return hash; + // } + //} + } + + public class ProcedureColors + { + public AppearanceSettings? Default { get; set; } + public AppearanceSettings? Initialized { get; set; } + public AppearanceSettings? Finished { get; set; } + public AppearanceSettings? Expired { get; set; } + + //public override bool Equals(object? obj) + //{ + // if (obj == null || GetType() != obj.GetType()) return false; + + // var otherLevelColors = (ProcedureColors)obj; + // return Expired != null && Expired.Equals(otherLevelColors.Expired) && + // Initialized != null && Initialized.Equals(otherLevelColors.Initialized) && + // Finished != null && Finished.Equals(otherLevelColors.Finished) && + // Default != null && Default.Equals(otherLevelColors.Default); + //} + + //public override int GetHashCode() + //{ + // unchecked + // { + // var hash = 17; + // hash = hash * 23 + Expired?.GetHashCode() ?? 0; + // hash = hash * 23 + Initialized?.GetHashCode() ?? 0; + // hash = hash * 23 + Finished?.GetHashCode() ?? 0; + // hash = hash * 23 + Default?.GetHashCode() ?? 0; + // return hash; + // } + //} + } + + public class LevelColors + { + public string? Level1 { get; set; } + public string? Level2 { get; set; } + public string? Level3 { get; set; } + public string? Level4 { get; set; } + public string? Level5 { get; set; } + + //public override bool Equals(object? obj) + //{ + // if (obj == null || GetType() != obj.GetType()) return false; + + // var otherLevelColors = (LevelColors)obj; + // return Level1 == otherLevelColors.Level1 && + // Level2 == otherLevelColors.Level2 && + // Level3 == otherLevelColors.Level3 && + // Level4 == otherLevelColors.Level4 && + // Level5 == otherLevelColors.Level5; + //} + + //public override int GetHashCode() + //{ + // unchecked + // { + // var hash = 17; + // hash = hash * 23 + Level1?.GetHashCode() ?? 0; + // hash = hash * 23 + Level2?.GetHashCode() ?? 0; + // hash = hash * 23 + Level3?.GetHashCode() ?? 0; + // hash = hash * 23 + Level4?.GetHashCode() ?? 0; + // hash = hash * 23 + Level5?.GetHashCode() ?? 0; + // return hash; + // } + //} + } + + public class TextColors + { + public string? Normal { get; set; } + public string? Warning { get; set; } + public string? Alert { get; set; } + public string? Improve { get; set; } + public string? Expired { get; set; } + + //public override bool Equals(object? obj) + //{ + // if (obj == null || GetType() != obj.GetType()) return false; + + // var otherTextColors = (TextColors)obj; + // return Normal == otherTextColors.Normal && + // Warning == otherTextColors.Warning && + // Alert == otherTextColors.Alert && + // Improve == otherTextColors.Improve && + // Expired == otherTextColors.Expired; + //} + + //public override int GetHashCode() + //{ + // unchecked + // { + // var hash = 17; + // hash = hash * 23 + Normal?.GetHashCode() ?? 0; + // hash = hash * 23 + Warning?.GetHashCode() ?? 0; + // hash = hash * 23 + Alert?.GetHashCode() ?? 0; + // hash = hash * 23 + Improve?.GetHashCode() ?? 0; + // hash = hash * 23 + Expired?.GetHashCode() ?? 0; + // return hash; + // } + //} + } + + public class ArrowColors + { + public string? Normal { get; set; } + public string? Warning { get; set; } + public string? Alert { get; set; } + public string? Improve { get; set; } + + //public override bool Equals(object? obj) + //{ + // if (obj == null || GetType() != obj.GetType()) return false; + + // var otherArrowColors = (ArrowColors)obj; + // return Normal == otherArrowColors.Normal && + // Warning == otherArrowColors.Warning && + // Alert == otherArrowColors.Alert && + // Improve == otherArrowColors.Improve; + //} + + //public override int GetHashCode() + //{ + // unchecked + // { + // var hash = 17; + // hash = hash * 23 + Normal?.GetHashCode() ?? 0; + // hash = hash * 23 + Warning?.GetHashCode() ?? 0; + // hash = hash * 23 + Alert?.GetHashCode() ?? 0; + // hash = hash * 23 + Improve?.GetHashCode() ?? 0; + // return hash; + // } + //} + } + + public class IndicatorColors + { + public string? Empty { get; set; } + public string? Warning { get; set; } + public string? Normal { get; set; } + public string? Alert { get; set; } + public string? Background { get; set; } + public string? EmptyBackground { get; set; } + + //public override bool Equals(object? obj) + //{ + // if (obj == null || GetType() != obj.GetType()) return false; + + // var otherIndicatorColors = (IndicatorColors)obj; + // return Empty == otherIndicatorColors.Empty && + // Warning == otherIndicatorColors.Warning && + // Normal == otherIndicatorColors.Normal && + // Alert == otherIndicatorColors.Alert && + // Background == otherIndicatorColors.Background && + // EmptyBackground == otherIndicatorColors.EmptyBackground; + //} + + //public override int GetHashCode() + //{ + // unchecked + // { + // var hash = 17; + // hash = hash * 23 + Empty?.GetHashCode() ?? 0; + // hash = hash * 23 + Warning?.GetHashCode() ?? 0; + // hash = hash * 23 + Normal?.GetHashCode() ?? 0; + // hash = hash * 23 + Alert?.GetHashCode() ?? 0; + // hash = hash * 23 + Background?.GetHashCode() ?? 0; + // hash = hash * 23 + EmptyBackground?.GetHashCode() ?? 0; + // return hash; + // } + //} + } + + public class GraphColors + { + public string? Normal { get; set; } + public string? Warning { get; set; } + public string? Alert { get; set; } + + //public override bool Equals(object? obj) + //{ + // if (obj == null || GetType() != obj.GetType()) return false; + + // var otherGraphColors = (GraphColors)obj; + // return Normal == otherGraphColors.Normal && + // Warning == otherGraphColors.Warning && + // Alert == otherGraphColors.Alert; + //} + + //public override int GetHashCode() + //{ + // unchecked + // { + // var hash = 17; + // hash = hash * 23 + Normal?.GetHashCode() ?? 0; + // hash = hash * 23 + Warning?.GetHashCode() ?? 0; + // hash = hash * 23 + Alert?.GetHashCode() ?? 0; + // return hash; + // } + //} + } +} + +public class FormConfig +{ + public FormItemOverview? Admission { get; set; } + public FormItemOverview? Demographic { get; set; } + public FormItemOverview? Discharge { get; set; } + public FormItemOverview? IncomeInfo { get; set; } +} + +// ====================== +// Axis Components +// ====================== +public class AxisLabel +{ + public bool? Show { get; set; } + public string? Color { get; set; } + public decimal? FontSize { get; set; } + public decimal? Margin { get; set; } + public bool? Silent { get; set; } +} + +public class AxisLineStyle +{ + public string? Color { get; set; } +} + +public class AxisLine +{ + public bool? Show { get; set; } + public AxisLineStyle? LineStyle { get; set; } +} + +public class AxisTick +{ + public bool? Show { get; set; } + public short? Length { get; set; } + + public int? Interval { get; set; } +} + +public class ChartConfig +{ + public ObjectId Id { get; set; } + public ChartBaseConfig? BaseConfig { get; set; } + public List? AxesConfig { get; set; } + public List? SeriesConfig { get; set; } + + // TODO: COMBINE ALL PROPERTIES IN SERIES CONFIG AND DELETE THE EXTENDS +} + +// ====================== +// Series Implementations +// ====================== +public class SeriesConfigBase +{ + public string? Key { get; set; } + public string? Color { get; set; } + public DisplayConfigEnums.SeriesType Type { get; set; } + public DisplayConfigEnums.SourceType SourceType { get; set; } + public List? AxesNames { get; set; } + public bool ShowSymbol { get; set; } + public bool ShowColorOnLegend { get; set; } + public bool ShowOnLegend { get; set; } + public GroupedObservationEnum.Result Values { get; set; } + public DisplayConfigEnums.MarkerIcon MarkerIcon { get; set; } + public VisualMap? VisualMap { get; set; } + public string? LineStyle { get; set; } + public int LineWidth { get; set; } + public string? LineType { get; set; } + public DisplayConfigEnums.MarkerIcon Marker { get; set; } + public string? AboveBaselineColor { get; set; } + public string? BelowBaselineColor { get; set; } + public List? CandleKeyList { get; set; } + + //public override bool Equals(object? obj) + //{ + // return obj is SeriesConfigBase other && + // Key == other.Key && + // Color == other.Color && + // Type == other.Type && + // SourceType == other.SourceType && + // AxesNames == other.AxesNames && + // ShowSymbol == other.ShowSymbol && + // ShowColorOnLegend == other.ShowColorOnLegend && + // ShowOnLegend == other.ShowOnLegend && + // Values == other.Values; + //} + + //public override int GetHashCode() + //{ + // return HashCode.Combine(Key, Color, Type, SourceType, AxesNames, ShowSymbol, ShowColorOnLegend, Values); + //} +} + +public class VisualMap +{ + public bool Show { get; set; } //Mostrar en la leyenda + public int Dimension { get; set; } //Eje al que afecta (index) + public string? SerieKey { get; set; } //Serie a la que afecta + public List? Pieces { get; set; } //Rangos de colores y configuraciones de tipos +} + +public class Piece +{ + public double? Opacity { get; set; } //Valor de la opacidad + public DisplayConfigEnums.LineType LineType { get; set; } //Enum tipo de línea + public string? Color { get; set; } //Color del rango + public string? Symbol { get; set; } //Icono del rango + public int? SymbolSize { get; set; } //Tamaño del icono del rango + + //Comparaciones para evaluar rangos + public object? Eq { get; set; } + public object? Neq { get; set; } + public double? Lt { get; set; } + public double? Lte { get; set; } + public double? Gt { get; set; } + public double? Gte { get; set; } +} + +// public class LineSeriesConfig : SeriesConfigBase +// { +// public string? LineStyle { get; set; } +// public int LineWidth { get; set; } +// public string? LineType { get; set; } +// +// public override bool Equals(object? obj) +// { +// return base.Equals(obj) && +// obj is LineSeriesConfig other && +// LineStyle == other.LineStyle && +// LineWidth == other.LineWidth && +// ShowSymbol == other.ShowSymbol; +// } +// +// public override int GetHashCode() +// { +// return HashCode.Combine(base.GetHashCode(), LineStyle, LineWidth, ShowSymbol); +// } +// } + +// public class VerticalMarkerSeriesConfig : SeriesConfigBase +// { +// public DisplayConfigEnums.MarkerIcon Marker { get; set; } +// +// public override bool Equals(object? obj) +// { +// return base.Equals(obj) && +// obj is VerticalMarkerSeriesConfig other && +// Marker == other.Marker; +// } +// +// public override int GetHashCode() +// { +// return HashCode.Combine(base.GetHashCode(), Marker); +// } +// } + +// public class AreaSeriesConfig : SeriesConfigBase +// { +// public string? AboveBaselineColor { get; set; } +// public string? BelowBaselineColor { get; set; } +// public override bool Equals(object? obj) +// { +// return base.Equals(obj) && +// obj is AreaSeriesConfig other && +// AboveBaselineColor == other.AboveBaselineColor && +// BelowBaselineColor == other.BelowBaselineColor; +// } +// +// public override int GetHashCode() +// { +// return HashCode.Combine(AboveBaselineColor, BelowBaselineColor); +// } +// } +// public class CandlestickSeriesConfig : SeriesConfigBase +// { +// public List? CandleKeyList { get; set; } +// public override bool Equals(object? obj) +// { +// return base.Equals(obj) && +// obj is Candle other; +// } +// +// public override int GetHashCode() +// { +// return HashCode.Combine(CandleKeyList); +// } +// } + +public class Candle +{ + public string? Key { get; set; } + public CandleValueType? CandleValueType { get; set; } +} + +public enum CandleValueType +{ + Open, + Close, + UnSet +} + +// ====================== +// Chart Configurations +// ====================== +public class ChartBaseConfig +{ + public string? Title { get; set; } + public string? Top { get; set; } + public string? Right { get; set; } + public string? Bottom { get; set; } + public string? Left { get; set; } + public string? Group { get; set; } + public string? Name { get; set; } + public int BorderWidth { get; set; } + public string? BorderColor { get; set; } + public bool ShowLegend { get; set; } + public bool ShowGrid { get; set; } + public float NumValues { get; set; } + public float BaselineOffset { get; set; } +} + +public class AxisConfig +{ + public DisplayConfigEnums.AxisType Type { get; set; } + public string? KeyName { get; set; } + public DisplayConfigEnums.AxisPosition Position { get; set; } + public object? Min { get; set; } + public object? Max { get; set; } + public AxisLine? AxisLine { get; set; } + public AxisTick? AxisTick { get; set; } + public AxisLabel? AxisLabel { get; set; } + public bool Silent { get; set; } + public DisplayConfigEnums.LabelFormat LabelFormat { get; set; } + public double Offset { get; set; } + public List? CustomLabels { get; set; } + public bool SortLabels { get; set; } + public bool Show { get; set; } + public GroupedObservationEnum.Regularity Regularity { get; set; } +} + +public class GraphLayout +{ + public string? Layout { get; set; } + public List>? ObservationTitle { get; set; } + public List? Name { get; set; } + public string? GraphConf { get; set; } + public List? ObservationName { get; set; } +} + +public class SectionBoxLayout +{ + public DisplayConfigEnums.RowType Type { get; set; } + public string? Name { get; set; } + public string? Icon { get; set; } + public int? GridColumn { get; set; } + public string? PaddingTop { get; set; } + public string? PaddingBottom { get; set; } + public string? PaddingLeft { get; set; } + public string? PaddingRight { get; set; } + public string? BorderColor { get; set; } + public string? BorderWidth { get; set; } + public string? BorderStyle { get; set; } + public string? BorderRadius { get; set; } + public double? HProportion { get; set; } + public string? BgColor { get; set; } + public string? SectionUrl { get; set; } + public string? MinHeight { get; set; } + public bool? Subtitle { get; set; } + public ConditionsConfig? Conditions { get; set; } + public DisplayConfigEnums.DirectionEnum Direction { get; set; } + public List? Rows { get; set; } +} + +public class ConditionsConfig +{ + public string? Condition { get; set; } + public string? FieldCondition { get; set; } +} + +public class RowBoxLayout +{ + public double? GrowPriority { get; set; } + public DisplayConfigEnums.CellType Type { get; set; } = DisplayConfigEnums.CellType.Default; + public DisplayConfigEnums.WebDisplayCellSubtype SubType { get; set; } + public string? BgColor { get; set; } + public string? PaddingTop { get; set; } + public string? PaddingBottom { get; set; } + public string? PaddingLeft { get; set; } + public string? PaddingRight { get; set; } + public List? Observations { get; set; } + public DisplayConfigEnums.DirectionEnum Direction { get; set; } +} + +public class ObservationRowBoxLayout +{ + public List? TextRules { get; set; } + public ChartSettings? ChartSettings { get; set; } + public bool? ShowTitle { get; set; } + + public double? GrowPriority { get; set; } + public DisplayConfigEnums.CellType Type { get; set; } + public string? SubType { get; set; } + public string? Name { get; set; } + public string? Icon { get; set; } + + public string? Title { get; set; } + public int GridColumn { get; set; } + public bool? IsColumn { get; set; } + public bool? IsStatic { get; set; } + public string? GraphConf { get; set; } + public List? Names { get; set; } + public List? ObservationName { get; set; } + public List?>? ObservationTitle { get; set; } + public List? Observations { get; set; } + public string? BgColor { get; set; } + public string? PaddingTop { get; set; } + public string? PaddingBottom { get; set; } + public string? PaddingLeft { get; set; } + public string? PaddingRight { get; set; } + public string? Border { get; set; } + public string? BorderRadius { get; set; } + public string? ValuePathNested { get; set; } + public List? ValuePathKey { get; set; } + public double? Size { get; set; } = 15.0; + public int? Format { get; set; } + public int? Length { get; set; } + public DisplayConfigEnums.DirectionEnum? Direction { get; set; } +} + +public class CardRotatingLayout +{ + // In MS + public int MillisecondsBeforeRotating { get; set; } + public int? Order { get; set; } + public string? Name { get; set; } + public string? Title { get; set; } + public DisplayConfigEnums.RotatingLayoutType Type { get; set; } + public DisplayConfigEnums.RotatingLayoutMode Mode { get; set; } + public ObjectId DataId { get; set; } + public CardConfig Data { get; set; } = new(); +} + +public class ObservationTextRule +{ + public ObservationEnum.ValueType ValueType { get; set; } + public string? Value { get; set; } + + public string? MatchText { get; set; } + public bool? MatchBoolean { get; set; } + public decimal? MatchNumber { get; set; } + public DateTime? MatchDate { get; set; } + public string? Label { get; set; } + public DateTime? MinDate { get; set; } + public DateTime? MaxDate { get; set; } + public decimal? MinNum { get; set; } + public decimal? MaxNum { get; set; } +} + +public class LegendLayoutConfig +{ + public List? Rows { get; set; } +} + +public class LegendLayoutRow +{ + public decimal? GrowPriority { get; set; } + public List? Columns { get; set; } +} + +public class LegendLayoutColumn +{ + public string? Key { get; set; } + public decimal? GrowPriority { get; set; } + public LegendLabel? Label { get; set; } +} + +public class LegendLabel +{ + public string? Name { get; set; } + public string? ColorLabel { get; set; } + public string? ColorIcon { get; set; } + public bool? ShowSymbol { get; set; } + public DisplayConfigEnums.ELegendIconType? IconType { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/HistoricalConfigChanges.cs b/adas-core.Domain/Models/MongoModels/HistoricalConfigChanges.cs new file mode 100644 index 00000000..19a84576 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/HistoricalConfigChanges.cs @@ -0,0 +1,70 @@ +using System.Reflection; +using adas_core.Domain.Enums; +using MongoDB.Bson; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; + +namespace adas_core.Domain.Models.MongoModels; + +public class HistoricalConfigChanges : JsonConverter +{ + public DateTime Time; + + public string Username = string.Empty; + + public ObjectId Id { get; set; } + + public DisplayConfigEnums.ConfigTypes? ConfigType { get; set; } + + public string? OldConfig { get; set; } + + public string NewConfig { get; set; } = string.Empty; + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value != null) + { + var jo = JObject.FromObject(value); + + AddStringEnumConverterAttribute(value, nameof(ConfigType)); + jo.WriteTo(writer); + } + } + + private static void AddStringEnumConverterAttribute(object value, string propertyName) + { + var prop = value.GetType().GetProperty(propertyName); + var attr = new JsonConverterAttribute(typeof(StringEnumConverter)); + + if (prop != null) + { + var attrs = prop.GetCustomAttributes(false); + + // Check if the attribute is not already applied + if (Array.Find(attrs, a => a is JsonConverterAttribute) == null) + { + // Create a new array that includes the existing attributes and the new one + var newAttrs = new object[attrs.Length + 1]; + Array.Copy(attrs, newAttrs, attrs.Length); + newAttrs[attrs.Length] = attr; + + // Use reflection to set the new attributes array + var field = typeof(PropertyInfo).GetField("m_customAttributes", + BindingFlags.Instance | BindingFlags.NonPublic); + field?.SetValue(prop, newAttrs); + } + } + } + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, + JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + public override bool CanConvert(Type objectType) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/HistoricalIds.cs b/adas-core.Domain/Models/MongoModels/HistoricalIds.cs new file mode 100644 index 00000000..5e85d971 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/HistoricalIds.cs @@ -0,0 +1,29 @@ +namespace adas_core.Domain.Models.MongoModels; + +public class HistoricalId(DateTime time, Dictionary patientIds) +{ + // Propiedades + public DateTime? Time { get; set; } = time; + + public Dictionary PatientIds { get; set; } = + patientIds ?? throw new ArgumentNullException(nameof(patientIds)); + + + public bool IsEmpty() + { + return Time == default(DateTime) && PatientIds.Count == 0; + } + + + public bool IsFull() + { + return Time != default(DateTime) && PatientIds.Count > 0; + } + + + public override string ToString() + { + var idsString = string.Join(", ", PatientIds); + return $"HistoricalId[Time: {Time}, PatientIds: {idsString}]"; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/LightBeacon.cs b/adas-core.Domain/Models/MongoModels/LightBeacon.cs new file mode 100644 index 00000000..1bb71f07 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/LightBeacon.cs @@ -0,0 +1,23 @@ +using MongoDB.Bson; + +namespace adas_core.Domain.Models.MongoModels; + +public class LightBeacon +{ + public ObjectId Id { get; set; } + public string Type { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public bool InUse { get; private set; } + public Options Options { get; set; } = new(); +} + +public class Options +{ + public string? Url { get; set; } + + public int? Port { get; set; } + + public string? Password { get; set; } + + public bool Emulate { get; set; } = false; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/Notice.cs b/adas-core.Domain/Models/MongoModels/Notice.cs new file mode 100644 index 00000000..cab393f6 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/Notice.cs @@ -0,0 +1,32 @@ +using MongoDB.Bson; + +namespace adas_core.Domain.Models.MongoModels; + +public class Notice +{ + public ObjectId Id { get; set; } + public DateTime NoticeDate { get; set; } + public string Description { get; set; } = string.Empty; + public string NoticeType { get; set; } = string.Empty; + public ObjectId DisplayId { get; set; } + public List? StaffInfoList { get; set; } + public bool IsFooterEnabled { get; set; } = true; + + + public override string ToString() + { + List items = []; + if (NoticeDate != DateTime.MinValue) items.Add($"Date: {NoticeDate}"); + items.Add($"DisplayId : {DisplayId}"); + items.Add($"Type: {NoticeType}"); + items.Add($"Description: {Description}"); + + return "Notice [" + string.Join(",", items) + "]"; + } +} + +public class StaffInfo +{ + public required string Role { get; set; } + public required string Leader { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/Patient.cs b/adas-core.Domain/Models/MongoModels/Patient.cs new file mode 100644 index 00000000..93edd9fc --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/Patient.cs @@ -0,0 +1,170 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.Masters; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.MongoModels; + +public class Patient +{ + public ObjectId Id { get; set; } + + public string? PatientNumber { get; set; } + + public string? PatientId { get; set; } + + public DateTime? AdmTime { get; set; } + + //Estado del Alta: Altable, no Altable, Exitus, Transferible + public OptionList? DischargeStatus { get; set; } + + public OptionList? Altable { get; set; } + + //Procedencia + public OptionList? Origin { get; set; } + + public string? OriginAux { get; set; } + + //Diagnósticos + public OptionList? Diagnosis { get; set; } + + public string? DiagnosisAux { get; set; } + + //Visitas + public OptionList? Visits { get; set; } + + public OptionList? AccessControl { get; set; } + + //Aislamiento: por defecto No Aislado + public OptionList? Insulation { get; set; } + + //NEMS + public OptionList? PatientStatus { get; set; } + + //Movilidad: por defecto sin asignar + public OptionList? Mobility { get; set; } + + //Techo terapeutico + public OptionList? TherapeuticCeiling { get; set; } + + // Intervenciones + public List? Procedures { get; set; } + + //Pruebas (tests) + public List? Tests { get; set; } + + //Terapias + public List? Treatment { get; set; } + + //Facultativos + public List? Doctors { get; set; } + + //Alergias + public List? Allergies { get; set; } + + public List? LanguageBarrier { get; set; } + public OptionList? PassiveSitting { get; set; } + + public DateTime? DisTime { get; set; } + + public Person? Person { get; set; } + + public Person? AttendingDoctor { get; set; } + + public DateTime? LastObservationDate { get; set; } + + public DateTime? CreationDate { get; set; } + + public DateTime? UpdateDate { get; set; } + + public DateTime? ArchiveDate { get; set; } + + public ObjectId? PointOfCareId { get; set; } + + public ObjectId? UnitId { get; set; } + + public List? HistoricalLocations { get; set; } + + public override string ToString() + { + List items = [$"id: {Id}"]; + if (!string.IsNullOrEmpty(UnitString)) items.Add($"pointOfCare: {UnitString}"); + if (!string.IsNullOrEmpty(Bed)) items.Add($"bed: {Bed}"); + if (!string.IsNullOrEmpty(PatientNumber)) items.Add($"patientNumber: {PatientNumber}"); + if (!string.IsNullOrEmpty(PatientId)) items.Add($"patientId: {PatientId}"); + if (AdmTime.HasValue) items.Add($"admTime: {AdmTime}"); + if (DisTime.HasValue) items.Add($"disTime: {DisTime}"); + if (Person != null) items.Add($"patient: {Person}"); + if (AttendingDoctor != null) items.Add($"attendingDoctor: {AttendingDoctor}"); + if (LastObservationDate.HasValue) items.Add($"lastObservationDate: {LastObservationDate}"); + if (CreationDate.HasValue) items.Add($"creationDate: {CreationDate}"); + if (UpdateDate.HasValue) items.Add($"updateDate: {UpdateDate}"); + if (ArchiveDate.HasValue) items.Add($"archiveDate: {UpdateDate}"); + return "person[" + string.Join(", ", items) + "]"; + } + + public bool IsInActivePoC() + { + return UnitString != null && !Enum.IsDefined(typeof(VirtualPointOfCare), UnitString); + } + + public Patient DeepCopy() + { + return (Patient)MemberwiseClone(); + } + + #region UnMap + + public string? Bed { get; set; } + public string? Room { get; set; } + public string? UnitString { get; set; } + public PointOfCare? PointOfCare { get; set; } + + public PatientLocation Location + { + get => new(UnitString, Bed, Room ?? Bed); + set + { + if (value == null || (UnitString == value.UnitName && Bed == value.Bed)) + return; + + UnitString = value.UnitName; + Bed = value.Bed; + HistoricalLocations ??= []; + + var currentUtc = DateTime.UtcNow; + + bool shouldAdd = true; + if (HistoricalLocations.Count > 0) + { + var last = HistoricalLocations[^1]; + if (last.PatientLocation?.Bed == value.Bed && last.PatientLocation?.UnitName == value.UnitName) + { + shouldAdd = false; + } + else if (currentUtc.ToString("o").Equals(last.AdmTime.ToString())) + { + currentUtc = currentUtc.AddSeconds(1); + } + } + + if (shouldAdd) + { + HistoricalLocations.Add(new HistoricalLocation( + currentUtc, + new PatientLocation(value.UnitName, value.Bed, value.Room) + )); + } + } + } + + #endregion +} + +public class PatientIncomeData +{ + public DateTime? AdmTime { get; set; } + public OptionList? Origin { get; set; } + public string? OriginAux { get; set; } + public OptionList? Diagnosis { get; set; } + public string? DiagnosisAux { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/PatientCarePlan.cs b/adas-core.Domain/Models/MongoModels/PatientCarePlan.cs new file mode 100644 index 00000000..3f85656c --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/PatientCarePlan.cs @@ -0,0 +1,18 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.Masters; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.MongoModels; + +public class PatientCarePlan +{ + public ObjectId Id { get; set; } + public ObjectId PatientId { get; set; } + public ObjectId? PointOfCareId { get; set; } + public string? PatientNumber { get; set; } + public ObjectId? UserId { get; set; } + public OptionList? CarePlan { get; set; } + public string? Description { get; set; } + public DateTime? Time { get; set; } + public ActionsEnum.CrudAction? Action { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/PoCMapping.cs b/adas-core.Domain/Models/MongoModels/PoCMapping.cs new file mode 100644 index 00000000..eddab9aa --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/PoCMapping.cs @@ -0,0 +1,20 @@ +namespace adas_core.Domain.Models.MongoModels; + +public class PoCMapping +{ + public string Id { get; set; } = string.Empty; + + + public List PointOfCares { get; set; } = []; +} + +public class PoCMappingItem +{ + public string OriginalPoC { get; set; } = string.Empty; + + + public string NewPoC { get; set; } = string.Empty; + + + public List> Beds { get; set; } = []; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/PoCSettings.cs b/adas-core.Domain/Models/MongoModels/PoCSettings.cs new file mode 100644 index 00000000..afb5dcf7 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/PoCSettings.cs @@ -0,0 +1,24 @@ +using adas_core.Domain.Enums; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.MongoModels; + +public class PoCSettings +{ + public ObjectId Id { get; set; } + + public PatientLocation? PatientLocation { get; set; } + + public RelayEnum.Status? ManualRelayStatus { get; set; } + + public override string ToString() + { + var result = "Id: " + Id; + + if (PatientLocation != null) result += PatientLocation.ToString(); + if (ManualRelayStatus != null) result += "Relay Manual Status: " + ManualRelayStatus; + + + return result; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/PointOfCare.cs b/adas-core.Domain/Models/MongoModels/PointOfCare.cs new file mode 100644 index 00000000..39d225e0 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/PointOfCare.cs @@ -0,0 +1,46 @@ +using adas_core.Domain.Enums; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.MongoModels; + +public class PointOfCare +{ + public ObjectId Id { get; set; } + public string Room { get; set; } = string.Empty; + public string Bed { get; set; } = string.Empty; + public string? Hall { get; set; } = string.Empty; + public ObjectId UnitId { get; set; } + public PointOfCareConfiguration? Configuration { get; set; } + public StatusEnum.PointOfCare Status { get; set; } + public ObjectId? AdmissionId { get; set; } + + #region Unmap + + public string UnitName { get; set; } = string.Empty; + public Unit? Unit { get; set; } + public PatientLocation Location => new(UnitName, Bed, Room); + public List Observations { get; set; } = []; + public bool? HasPatient { get; set; } + public ObjectId? Patientid { get; set; } + public Patient? Patient { get; set; } + public bool? IsActive { get; set; } + public bool? IsVisible { get; set; } + public Admission? Admission { get; set; } + + #endregion +} + +public class PointOfCareConfiguration +{ + public List? BeaconIdList { get; set; } = []; + public List? CameraIdList { get; set; } = []; + public List? RelayIdList { get; set; } + public List? BeaconList { get; private set; } + public List? CameraList { get; private set; } + public List? RelayList { get; private set; } + public string? Type { get; set; } + + // Esta parte hay que tenerla en cuenta al momento de lanzar las grabaciones automaticas y manuales para la API de OR + // OR espera recibir roomId + public int? Id { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/PumpElement.cs b/adas-core.Domain/Models/MongoModels/PumpElement.cs new file mode 100644 index 00000000..6a4cd783 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/PumpElement.cs @@ -0,0 +1,14 @@ +namespace adas_core.Domain.Models.MongoModels; + +public class PumpElement +{ + public string? Id { get; set; } + public string? Text { get; set; } + public string? CodingSystem { get; set; } + public object? Value { get; set; } + public string? Units { get; set; } + public DateTime? Time { get; set; } + public char? Result { get; set; } + public string? ValueId { get; set; } + public string? ValueCodingSystem { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/Relay.cs b/adas-core.Domain/Models/MongoModels/Relay.cs new file mode 100644 index 00000000..fac267b4 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/Relay.cs @@ -0,0 +1,55 @@ +using adas_core.Domain.Enums; +using MongoDB.Bson; +using Newtonsoft.Json; + +namespace adas_core.Domain.Models.MongoModels; + +public class Relay +{ + public ObjectId Id { get; set; } + + public RelayEnum.Type Type { get; set; } = RelayEnum.Type.Door; + + public string? Driver { get; init; } + + public string Ip { get; init; } = string.Empty; + + public int Port { get; init; } + + public int RelayNumber { get; init; } = 1; + + public string? RelayName { get; init; } + + public int Total { get; init; } = 1; + + public int RefreshTime { get; set; } = 60; + + public bool Open { get; set; } + + public RelayEnum.Status Status { get; set; } = RelayEnum.Status.NotInitialized; + + public string? Username { get; init; } + + public string? Password { get; init; } + + public RelayEnum.Mode Mode { get; set; } = RelayEnum.Mode.OpenedOnClosedOff; + + public int? RebootDelay { get; set; } + + public bool Cache { get; set; } = true; + public bool InUse { get; private set; } + + public RelayEnum.Status? ManualRelayStatus { get; set; } + + public bool Equals(Relay other) + { + return Driver == other.Driver && Ip == other.Ip && Port == other.Port && RelayNumber == other.RelayNumber && + RelayName == other.RelayName && Total == other.Total && Username == other.Username && + Password == other.Password; + } + + public override string ToString() + { + return JsonConvert.SerializeObject(this); + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/Section.cs b/adas-core.Domain/Models/MongoModels/Section.cs new file mode 100644 index 00000000..b05dc9ab --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/Section.cs @@ -0,0 +1,49 @@ +using adas_core.Domain.Enums; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.MongoModels; + +public class Section +{ + // ReSharper disable once InconsistentNaming + public ObjectId _id { get; set; } + + public string Id { get; set; } = string.Empty; + + + public string? SectionTitle { get; set; } + + + public string? PointOfCare { get; set; } + + + public DateTime? LastUpdate { get; set; } + + public Dictionary Configuration { get; set; } = []; + + public SectionConfig? SectionConfig { get; set; } + public UiFontData? DesignProperties { get; set; } + + public Dictionary> PointOfCareList { get; set; } = new(); + + public List Items { get; set; } = []; + + public StatusEnum.Type? Status { get; set; } = null; + + public bool Equals(Section? other) + { + return other != null && + Id == other.Id; + } + + public override string ToString() + { + return "[Section id: " + Id + "]"; + } + + public class SectionItem + { + public string? Group { get; set; } + public List Boxes { get; set; } = []; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/ServiceConfig.cs b/adas-core.Domain/Models/MongoModels/ServiceConfig.cs new file mode 100644 index 00000000..60050c52 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/ServiceConfig.cs @@ -0,0 +1,22 @@ +using MongoDB.Bson; + +namespace adas_core.Domain.Models.MongoModels; + +public class ServiceConfig +{ + public ObjectId Id { get; set; } + + public string StrId { get; set; } = string.Empty; + + + public List Service { get; set; } = []; + + + public ServiceConfigTheme? Theme { get; set; } + + + public string[]? BoxObservations { get; set; } + + + public string Score { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/ServiceConfigService.cs b/adas-core.Domain/Models/MongoModels/ServiceConfigService.cs new file mode 100644 index 00000000..ef553711 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/ServiceConfigService.cs @@ -0,0 +1,17 @@ +namespace adas_core.Domain.Models.MongoModels; + +public class ServiceConfigService +{ + public string Screen { get; set; } = string.Empty; + + + public List Sections { get; set; } = []; +} + +public class ServiceConfigServiceSection +{ + public string BoxId { get; set; } = string.Empty; + + + public string Box { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/ServiceConfigTheme.cs b/adas-core.Domain/Models/MongoModels/ServiceConfigTheme.cs new file mode 100644 index 00000000..d24f08d8 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/ServiceConfigTheme.cs @@ -0,0 +1,24 @@ +namespace adas_core.Domain.Models.MongoModels; + +public class ServiceConfigTheme +{ + public string DefaultTheme { get; set; } = string.Empty; + + + public List Timetables { get; set; } = []; +} + +public class ServiceConfigThemeTimetable +{ + public string Theme { get; set; } = string.Empty; + + + public string StartDay { get; set; } = string.Empty; + + + public string EndDay { get; set; } = string.Empty; + + public string StartHour { get; set; } = string.Empty; + + public string EndHour { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/Step.cs b/adas-core.Domain/Models/MongoModels/Step.cs new file mode 100644 index 00000000..8256213f --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/Step.cs @@ -0,0 +1,62 @@ +using adas_core.Domain.Enums; + +namespace adas_core.Domain.Models.MongoModels; + +public class Step +{ + public int? RotationTime { get; set; } + + + public string? Text { get; set; } + + + public string? Class { get; set; } + + + public UiFontData? DesignProperties { get; set; } + + public DisplayConfigEnums.StepType? Type { get; set; } = DisplayConfigEnums.StepType.Standar; +} + +public class UiFontData +{ + public double? WidthStepBed { get; set; } + + public double? HeightStepBed { get; set; } + + public double? FontSizeValue { get; set; } + + public double? FontSizeMediumValue { get; set; } + + public double? FontSizeHeaderValue { get; set; } + + public double? FontSizeLittleValue { get; set; } + + public double? FontSizeMicroValue { get; set; } + + public double? FontSizeNanoValue { get; set; } + + public double? FontSizeTextValue { get; set; } + + public double? FontSizeMediumTextValue { get; set; } + + public double? FontSizeLittleTextValue { get; set; } + + public double? FontSizeName { get; set; } + + public double? FontSizeLittleName { get; set; } + + public double? FontSizeUnit { get; set; } + + public string? BorderColor { get; set; } + + public string? BackGroundColor { get; set; } + + public double? BoxMarginTop { get; set; } + + public double? BoxMarginBot { get; set; } + + public double? BoxMarginLeft { get; set; } + + public double? BoxMarginRight { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/Unit.cs b/adas-core.Domain/Models/MongoModels/Unit.cs new file mode 100644 index 00000000..22063a43 --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/Unit.cs @@ -0,0 +1,88 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.Masters; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.MongoModels; + +public class Unit +{ + public ObjectId Id { get; set; } + public string? Title { get; set; } + + public string? Name { get; set; } + + // Ya que la configuración de la unidad (anterior section) se va a poder actualizar + // tiene sentido mantener esta propiedad aunque no se está usando en el modelo anterior + public DateTime? LastUpdate { get; set; } + + // Anteriormente SectionItem + public List? PointOfCareIds { get; set; } + public int PocCount => PointOfCares?.Count ?? 0; + + public StatusEnum.Type? Status { get; set; } + public UnitConfiguration Configuration { get; set; } = new(); + + public ObjectId? AltableOptionListId { get; set; } + public ObjectId? AllergyListId { get; set; } + public ObjectId? DestinationListId { get; set; } + public ObjectId? InternalDestinationListId { get; set; } + public ObjectId? DiagnosisListId { get; set; } + public ObjectId? DoctorListId { get; set; } + public ObjectId? DoctorTypeListId { get; set; } + public ObjectId? InsulationListId { get; set; } + public ObjectId? MobilityOptionListId { get; set; } + public ObjectId? OriginListId { get; set; } + public ObjectId? PatientStatusListId { get; set; } + public ObjectId? ProcedureListId { get; set; } + public ObjectId? TestListId { get; set; } + public ObjectId? ServiceListId { get; set; } + public ObjectId? TherapeuticCeilingListId { get; set; } + public ObjectId? TreatmentListId { get; set; } + public ObjectId? VisitOptionListId { get; set; } + public ObjectId? AccessControlListId { get; set; } + public ObjectId? LanguageBarrierListId { get; set; } + public ObjectId? DischargeStatusListId { get; set; } + public ObjectId? PassiveSittingListId { get; set; } + public ObjectId? GenericListId { get; set; } + + #region Unmap + + public List? PointOfCares { get; set; } + public AltableOptionList? AltableOptionList { get; set; } + public AllergyList? AllergyList { get; set; } + public DestinationList? DestinationList { get; set; } + public InternalDestinationList? InternalDestinationList { get; set; } + public DiagnosisList? DiagnosisList { get; set; } + public DoctorList? DoctorList { get; set; } + public DoctorTypeList? DoctorTypeList { get; set; } + public InsulationList? InsulationList { get; set; } + public MobilityOptionList? MobilityOptionList { get; set; } + public OriginList? OriginList { get; set; } + public PatientStatusList? PatientStatusList { get; set; } + public ProcedureList? ProcedureList { get; set; } + public TestList? TestList { get; set; } + public ServiceList? ServiceList { get; set; } + public TherapeuticCeilingList? TherapeuticCeilingList { get; set; } + public TreatmentList? TreatmentList { get; set; } + public VisitOptionList? VisitOptionList { get; set; } + public AccessControlList? AccessControlList { get; set; } + + public DischargeStatusList? DischargeStatusList { get; set; } + public LanguageBarrierList? LanguageBarrierList { get; set; } + public PassiveSittingList? PassiveSittingList { get; set; } + public GenericList? GenericList { get; set; } + + #endregion +} + +public class UnitConfiguration +{ + public bool AutoAdt { get; set; } + public bool ManualDischarge { get; set; } = true; + public bool ManualAdmit { get; set; } = true; + public bool ManualMove { get; set; } = true; + public bool ManualEdit { get; set; } = true; + public ObjectId? PlanDisplayConfiguration { get; set; } + public ObjectId? SmartDisplayConfiguration { get; set; } + public ObjectId? StandarDisplayConfiguration { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/MongoModels/User.cs b/adas-core.Domain/Models/MongoModels/User.cs new file mode 100644 index 00000000..f4822d2e --- /dev/null +++ b/adas-core.Domain/Models/MongoModels/User.cs @@ -0,0 +1,24 @@ +using adas_core.Domain.Enums; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.MongoModels; + +public class User +{ + public ObjectId Id { get; set; } + public string UserName { get; set; } = string.Empty; + public string Password { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public DateTime? LockExpirationDate { get; set; } + public bool IsEnabled { get; set; } = true; + public DateTime? LastLogin { get; set; } + public UserEnum.Type Type { get; set; } = UserEnum.Type.Local; + + #region NotMapped + + public List? Authorization { get; set; } + public string IpAddress { get; set; } = string.Empty; + + #endregion +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Note.cs b/adas-core.Domain/Models/Note.cs new file mode 100644 index 00000000..f667e87c --- /dev/null +++ b/adas-core.Domain/Models/Note.cs @@ -0,0 +1,22 @@ +namespace adas_core.Domain.Models; + +public class Note +{ + public string? CommentType { get; set; } = string.Empty; + + public string Comment { get; set; } = string.Empty; + + public DateTime? EnteredTime { get; set; } + + public override string ToString() + { + List items = []; + + if (!string.IsNullOrEmpty(CommentType)) items.Add($", commentType: {CommentType}"); + if (!string.IsNullOrEmpty(Comment)) items.Add($"comment: {Comment}"); + if (EnteredTime != null) items.Add($", enteredTime: {EnteredTime}"); + + + return "note[" + string.Join(", ", items) + "]"; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/ObservaitonRetentionResult.cs b/adas-core.Domain/Models/ObservaitonRetentionResult.cs new file mode 100644 index 00000000..77674530 --- /dev/null +++ b/adas-core.Domain/Models/ObservaitonRetentionResult.cs @@ -0,0 +1,19 @@ +using adas_core.Domain.Enums; + +namespace adas_core.Domain.Models; + +public class ObservatitonRetentionResult +{ + public ObservatitonRetentionResult() + { + } + + public ObservatitonRetentionResult(RetentionPolicy retentionPolicy, int? retentionPolicyValue) + { + RetentionPolicy = retentionPolicy; + RetentionPolicyValue = retentionPolicyValue; + } + + public RetentionPolicy RetentionPolicy { get; set; } + public int? RetentionPolicyValue { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/ObservationData.cs b/adas-core.Domain/Models/ObservationData.cs new file mode 100644 index 00000000..5afc9900 --- /dev/null +++ b/adas-core.Domain/Models/ObservationData.cs @@ -0,0 +1,14 @@ +namespace adas_core.Domain.Models; + +public class ObservationData +{ + public string? CodingSystem { get; set; } + + public string? Code { get; set; } + + public string? Text { get; set; } + + public DateTime? Time { get; set; } + + public object? Value { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Observations/BoolObservation.cs b/adas-core.Domain/Models/Observations/BoolObservation.cs new file mode 100644 index 00000000..b4ddb1f0 --- /dev/null +++ b/adas-core.Domain/Models/Observations/BoolObservation.cs @@ -0,0 +1,16 @@ +namespace adas_core.Domain.Models.Observations; + +public class BoolObservation : Observation +{ + public bool Value { get; set; } + + internal static BoolObservation? From(List obs) + { + if (obs.Count > 0) + return new BoolObservation + { + Value = (bool)obs[0].Value + }; + return null; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Observations/ChartObservation.cs b/adas-core.Domain/Models/Observations/ChartObservation.cs new file mode 100644 index 00000000..a6551fcb --- /dev/null +++ b/adas-core.Domain/Models/Observations/ChartObservation.cs @@ -0,0 +1,17 @@ +namespace adas_core.Domain.Models.Observations; + +public class ChartObservation : Observation +{ + public List Values { get; set; } = []; + public List SecondaryValues { get; set; } = []; + public List Labels { get; set; } = []; + public double Min { get; set; } + public double Max { get; set; } + public double? MinWarn { get; set; } + public double? MaxWarn { get; set; } + + internal static ChartObservation From(List obs) + { + return obs == null ? throw new ArgumentNullException(nameof(obs)) : new ChartObservation(); + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Observations/DoubleObservation.cs b/adas-core.Domain/Models/Observations/DoubleObservation.cs new file mode 100644 index 00000000..7c1094eb --- /dev/null +++ b/adas-core.Domain/Models/Observations/DoubleObservation.cs @@ -0,0 +1,24 @@ +using adas_core.Domain.Utils; + +namespace adas_core.Domain.Models.Observations; + +public class DoubleObservation : Observation +{ + public double? Value { get; set; } + public double? LastValue { get; set; } + + public static DoubleObservation? From(List obs) + { + if (obs.Count > 0) + { + var val = new DoubleObservation + { + Value = obs[0].Value.ToDouble() + }; + if (obs.Count > 1) val.LastValue = obs[1].Value.ToDouble(); + return val; + } + + return null; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Observations/FullObservation.cs b/adas-core.Domain/Models/Observations/FullObservation.cs new file mode 100644 index 00000000..954d333b --- /dev/null +++ b/adas-core.Domain/Models/Observations/FullObservation.cs @@ -0,0 +1,35 @@ +namespace adas_core.Domain.Models.Observations; + +public class FullObservation : Observation +{ + public DateTime LastObservationDate; + + public DateTime ObservationDate; + public string? Text { get; set; } + public required object Value { get; set; } + public object? LastValue { get; set; } + public double? Min { get; set; } + public double? Max { get; set; } + + public double? MinAlert { get; set; } + public double? MaxAlert { get; set; } + + public double? MinWarn { get; set; } + public double? MaxWarn { get; set; } + + public static FullObservation? From(List obs) + { + if (obs.Count > 0) + return new FullObservation + { + Text = obs[0].Name, + Value = obs[0].Value, + Min = obs[0].Min, + Max = obs[0].Max, + ObservationDate = obs[0].Time, + LastValue = obs.Count > 1 ? obs[1].Value : null, + LastObservationDate = obs.Count > 1 ? obs[1].Time : DateTime.MinValue + }; + return null; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Observations/Medication.cs b/adas-core.Domain/Models/Observations/Medication.cs new file mode 100644 index 00000000..a23c61e4 --- /dev/null +++ b/adas-core.Domain/Models/Observations/Medication.cs @@ -0,0 +1,131 @@ +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Utils; + +namespace adas_core.Domain.Models.Observations; + +public class Medication : Observation +{ + public Medication() + { + } + + public Medication(string id, Dictionary obj) + { + if (obj.TryGetValue("MDC_184504", out var mode)) PumpMode = new PumpMode(mode); + + if (obj.TryGetValue("MDC_184519", out var status)) PumpStatus = new PumpStatus(status); + + if (obj.TryGetValue("MDC_157784", out var flow)) FlowFluid = new FlowFluid(flow); + + if (obj.TryGetValue("MDC_157993", out var fluid)) VolFluidTotal = new VolumeFluidDeliveryTotal(fluid); + + if (obj.TryGetValue("MDCX_PUMP_DELIVERY_PRESSURE", out var pressure)) + PumpDeliveryPressure = new PumpDeliveryPressure(pressure); + + if (obj.TryGetValue("MDC_DEV_PUMP_ACTIVE_SOURCES", out var source)) + PumpActiveSources = new PumpActiveSources(source); + + if (obj.TryGetValue("MDC_184514", out var drug)) Drug = new Drug(drug); + + if (obj.TryGetValue("MDC_157872", out var volume)) VolumeFluidTbiRemain = new VolumeFluidTbiRemain(volume); + + if (obj.TryGetValue("MDC_157916", out var remain)) VolumeFluidTimeRemain = new VolumeFluidTimePdRemain(remain); + + Id = id; + } + + public string? Id { get; set; } + public PumpMode? PumpMode { get; set; } + public PumpStatus? PumpStatus { get; set; } + public FlowFluid? FlowFluid { get; set; } + public VolumeFluidDeliveryTotal? VolFluidTotal { get; set; } + public VolumeFluidTbiRemain? VolumeFluidTbiRemain { get; set; } + public VolumeFluidTimePdRemain? VolumeFluidTimeRemain { get; set; } + public PumpDeliveryPressure? PumpDeliveryPressure { get; set; } + public PumpActiveSources? PumpActiveSources { get; set; } + public Drug? Drug { get; set; } +} + +public class DrugValue +{ + public DrugValue() + { + } + + public DrugValue(PumpElement pumpElement) + { + Id = pumpElement.Id; + Text = pumpElement.Text; + Time = pumpElement.Time; + Units = pumpElement.Units; + CodingSystem = pumpElement.CodingSystem; + Result = pumpElement.Result; + } + + public string? Id { get; set; } + public string? Text { get; set; } + public string? Units { get; set; } + public DateTime? Time { get; set; } + public string? CodingSystem { get; set; } + public char? Result { get; set; } = 'R'; + + public void Copy(DrugValue common) + { + Id = common.Id; + Text = common.Text; + Units = common.Units; + Time = common.Time; + CodingSystem = common.CodingSystem; + Result = common.Result; + } +} + +public abstract class DrugDoubleValue : DrugValue +{ + protected DrugDoubleValue(PumpElement pumpElement) : base(pumpElement) + { + if (double.TryParse(pumpElement.Value?.ToString(), out var valueParsed)) + Value = valueParsed; + } + + public double? Value { get; set; } +} + +public abstract class DrugStringValue(PumpElement pumpElement) : DrugValue(pumpElement) +{ + public string? Value { get; set; } = pumpElement.Value?.ToString(); +} + +public class FlowFluid(PumpElement pumpElement) : DrugDoubleValue(pumpElement); + +public class VolumeFluidDeliveryTotal(PumpElement pumpElement) : DrugDoubleValue(pumpElement); + +public class PumpDeliveryPressure(PumpElement pumpElement) : DrugDoubleValue(pumpElement); + +public class VolumeFluidTbiRemain(PumpElement pumpElement) : DrugDoubleValue(pumpElement); + +public class VolumeFluidTimePdRemain(PumpElement pumpElement) : DrugDoubleValue(pumpElement); + +public class PumpActiveSources(PumpElement pumpElement) : DrugStringValue(pumpElement) +{ + public string? ValueId { get; set; } = pumpElement.ValueId; + public string? ValueCodingSystem { get; set; } = pumpElement.ValueCodingSystem; +} + +public class Drug(PumpElement pumpElement) : DrugStringValue(pumpElement); + +public class PumpMode : DrugStringValue +{ + public PumpMode(PumpElement pumpElement) : base(pumpElement) + { + Value = pumpElement.Value?.ToString()?.SubstringAfter("pump-mode-").Replace("-", "_"); + } +} + +public class PumpStatus : DrugStringValue +{ + public PumpStatus(PumpElement pumpElement) : base(pumpElement) + { + Value = pumpElement.Value?.ToString()?.SubstringAfter("pump-status-").Replace("-", "_"); + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Observations/Observation.cs b/adas-core.Domain/Models/Observations/Observation.cs new file mode 100644 index 00000000..c91512e1 --- /dev/null +++ b/adas-core.Domain/Models/Observations/Observation.cs @@ -0,0 +1,5 @@ +namespace adas_core.Domain.Models.Observations; + +public class Observation +{ +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Observations/ObservationPaginatedResult.cs b/adas-core.Domain/Models/Observations/ObservationPaginatedResult.cs new file mode 100644 index 00000000..479f597c --- /dev/null +++ b/adas-core.Domain/Models/Observations/ObservationPaginatedResult.cs @@ -0,0 +1,7 @@ +namespace adas_core.Domain.Models.Observations; + +public class ObservationPaginatedResult +{ + public int Count { get; set; } = 0; + public List Data { get; set; } = []; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Observations/ObservationsRequest.cs b/adas-core.Domain/Models/Observations/ObservationsRequest.cs new file mode 100644 index 00000000..55887cd3 --- /dev/null +++ b/adas-core.Domain/Models/Observations/ObservationsRequest.cs @@ -0,0 +1,14 @@ +namespace adas_core.Domain.Models.Observations; + +public class ObservationsRequest +{ + public string? PatientNumber { get; set; } + + public List? ObservationNames { get; set; } + + public DateTime? StartTime { get; set; } + public DateTime? EndTime { get; set; } + + public int PageNumber { get; set; } = 1; + public int PageSize { get; set; } = 10; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Observations/PredictMedicationObservation.cs b/adas-core.Domain/Models/Observations/PredictMedicationObservation.cs new file mode 100644 index 00000000..06c76bc1 --- /dev/null +++ b/adas-core.Domain/Models/Observations/PredictMedicationObservation.cs @@ -0,0 +1,7 @@ +namespace adas_core.Domain.Models.Observations; + +public class PredictMedicationObservation +{ + public string MedicineText { get; set; } = string.Empty; + public double DegreeSimilarity { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Observations/StringObservation.cs b/adas-core.Domain/Models/Observations/StringObservation.cs new file mode 100644 index 00000000..9d04cd5c --- /dev/null +++ b/adas-core.Domain/Models/Observations/StringObservation.cs @@ -0,0 +1,16 @@ +namespace adas_core.Domain.Models.Observations; + +public class StringObservation : Observation +{ + public string Value { get; set; } = string.Empty; + + public static StringObservation? From(List obs) + { + if (obs.Count > 0) + return new StringObservation + { + Value = (string)obs[0].Value + }; + return null; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/PatientAllergiesValue.cs b/adas-core.Domain/Models/PatientAllergiesValue.cs new file mode 100644 index 00000000..f6671ba5 --- /dev/null +++ b/adas-core.Domain/Models/PatientAllergiesValue.cs @@ -0,0 +1,16 @@ +namespace adas_core.Domain.Models; + +public class PatientAllergiesValue +{ + public string? Type { get; set; } + + public string? Value { get; set; } + + public string? Notes { get; set; } + + + public override string ToString() + { + return $"Tipo: {Type}, Valor: {Value}, Notas: {Notes}"; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/PatientAppointment.cs b/adas-core.Domain/Models/PatientAppointment.cs new file mode 100644 index 00000000..8dc797cc --- /dev/null +++ b/adas-core.Domain/Models/PatientAppointment.cs @@ -0,0 +1,116 @@ +using adas_core.Domain.Enums; +using MongoDB.Bson; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace adas_core.Domain.Models; + +public class PatientAppointment : JsonConverter +{ + public ObjectId Id { get; set; } + + public ObjectId PatientId { get; set; } + + public Person? Patient { get; set; } + + public List Timings { get; set; } = []; + + public DateTime? CreateTime { get; set; } + + public DateTime UpdateTime { get; set; } + + public Entity? PlacerOrder { get; set; } + + public Entity? FillerOrder { get; set; } + + public string? EventReason { get; set; } + + public string? AppointmentReason { get; set; } + + public string? AppointmentType { get; set; } + + public OperationType? AppointmentOperationType { get; set; } + + public OperationType? AppointmentStatus { get; set; } + + public double Duration { get; set; } + + public string? VisitNumber { get; set; } + + public string? PatientClass { get; set; } + + public bool EpisodeActivation { get; set; } = false; + + public Person? PlacerContact { get; set; } + + public Person? FillerContact { get; set; } + + public List ResourceGroups { get; set; } = []; + + public List Allergies { get; set; } = []; + + + public List ApplyResourceGroups(PatientAppointment? apdb = null) + { + var i = 0; + ResourceGroups = ResourceGroups.Select(rg => + { + var rgdb = apdb != null && apdb.ResourceGroups.Count > i ? apdb.ResourceGroups[i] : null; + + rg.Services = CodeAction.Apply(rgdb?.Services, rg.ServicesActions); + rg.Resources = CodeAction.Apply(rgdb?.Resources, rg.ResourcesActions); + rg.Locations = PatientLocationAction.Apply(rgdb?.Locations, rg.LocationsActions); + rg.Personnel = PersonResourceAction.Apply(rgdb?.Personnel, rg.PersonnelActions); + return rg; + }).ToList(); + return ResourceGroups; + } + + public override bool CanConvert(Type objectType) + { + return objectType == typeof(PatientAppointment); + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value != null) + { + var jo = JObject.FromObject(value); + jo.Property("Id")?.Remove(); + jo.WriteTo(writer); + } + } + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, + JsonSerializer serializer) + { + throw new NotImplementedException(); + } +} + +public class PatientAppointmentResourceGroup +{ + public List? Services { get; set; } + + public List? Resources { get; set; } + + public List? Locations { get; set; } + + public List? Personnel { get; set; } + + /** + * ApiRequest resource actions + */ + public List? ServicesActions { get; set; } + + public List? ResourcesActions { get; set; } + public List? LocationsActions { get; set; } + public List? PersonnelActions { get; set; } +} + +public class Allergies +{ + public Code? AllergenType { get; set; } + + public Code? Allergen { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/PatientDiagnosis.cs b/adas-core.Domain/Models/PatientDiagnosis.cs new file mode 100644 index 00000000..71e86724 --- /dev/null +++ b/adas-core.Domain/Models/PatientDiagnosis.cs @@ -0,0 +1,68 @@ +using MongoDB.Bson; + +namespace adas_core.Domain.Models; + +public class PatientDiagnosis +{ + public ObjectId Id { get; set; } + + public ObjectId PatientId { get; set; } + + public DateTime Time { get; set; } + + public DateTime? UpdateDate { get; set; } + + //MSH Time of HL7 + public DateTime MessageTime { get; set; } + + public string? CodingSystem { get; set; } + + public string? Description { get; set; } + + public string? Label { get; set; } + + public string? Code { get; set; } + + public string? State { get; set; } + + public string? Category { get; set; } + + public DateTime? StartTime { get; set; } + + public DateTime? EndTime { get; set; } + + //Deprecated + public string? DiagnosisCode + { + get => Code; + set => Code = value; + } + + //Deprecated + public string? DiagnosisSystem + { + get => CodingSystem; + set => CodingSystem = value; + } + + public override string ToString() + { + List items = + [ + $"id: {Id}", + $"time: {Time}", + $"patientid: {PatientId}" + ]; + if (!string.IsNullOrEmpty(CodingSystem)) items.Add($"label: {CodingSystem}"); + if (!string.IsNullOrEmpty(Label)) items.Add($"label: {Label}"); + if (!string.IsNullOrEmpty(Code)) items.Add($"diagnosisCode: {Code}"); + if (!string.IsNullOrEmpty(State)) items.Add($"state: {State}"); + if (!string.IsNullOrEmpty(Category)) items.Add($"category: {Category}"); + items.Add($"messageTime: {MessageTime}"); + items.Add($"startTime: {StartTime}"); + items.Add($"startTime: {StartTime}"); + items.Add($"endTime: {EndTime}"); + + return "PatientDiagnosis[" + string.Join(", ", items) + "]"; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/PatientDrainagesValue.cs b/adas-core.Domain/Models/PatientDrainagesValue.cs new file mode 100644 index 00000000..b13a2558 --- /dev/null +++ b/adas-core.Domain/Models/PatientDrainagesValue.cs @@ -0,0 +1,12 @@ +namespace adas_core.Domain.Models; + +public class PatientDrainagesValue +{ + public string? Type { get; set; } + + public string? Location { get; set; } + + public int? Volume { get; set; } + + public int? Height { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/PatientICCA.cs b/adas-core.Domain/Models/PatientICCA.cs new file mode 100644 index 00000000..50c4d076 --- /dev/null +++ b/adas-core.Domain/Models/PatientICCA.cs @@ -0,0 +1,22 @@ +namespace adas_core.Domain.Models; + +public class PatientIcca +{ + public PatientLocation Location { get; set; } = new(string.Empty, string.Empty); + + public string? PatientNumber { get; set; } + + public string? PatientId { get; set; } + public DateTime AdmitTime { get; set; } + + + public PatientIcca DeepCopy() + { + var other = (PatientIcca)MemberwiseClone(); + other.Location = new PatientLocation(Location.UnitName, Location.Bed); + other.PatientNumber = new string(PatientNumber); + other.PatientId = new string(PatientId); + other.AdmitTime = AdmitTime; + return other; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/PatientIntravenousLinesValue.cs b/adas-core.Domain/Models/PatientIntravenousLinesValue.cs new file mode 100644 index 00000000..b94aeb5e --- /dev/null +++ b/adas-core.Domain/Models/PatientIntravenousLinesValue.cs @@ -0,0 +1,17 @@ +namespace adas_core.Domain.Models; + +public class PatientIntravenousLinesValue +{ + public string? Type { get; set; } + + public string? Duration { get; set; } + + public string? Action { get; set; } + + public string? Location { get; set; } + + public DateTime? InsertTime { get; set; } + + + public DateTime? RemoveTime { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/PatientLocation.cs b/adas-core.Domain/Models/PatientLocation.cs new file mode 100644 index 00000000..5d1d729d --- /dev/null +++ b/adas-core.Domain/Models/PatientLocation.cs @@ -0,0 +1,63 @@ +namespace adas_core.Domain.Models; + +public class PatientLocation : IEquatable +{ + public PatientLocation(string? unitName, string? bed, string? room) + { + UnitName = unitName; + Bed = bed; + Room = room; + } + + public PatientLocation(string? unitName, string? bed) + { + UnitName = unitName; + Bed = bed; + Room = bed; + } + + public PatientLocation() + { + // Constructor vacío + } + + public string? UnitName { get; init; } + + public string? Bed { get; set; } + public string? Room { get; init; } + + public bool Equals(PatientLocation? other) + { + return UnitName == other?.UnitName && Bed == other?.Bed && Room == other?.Room; + } + + public bool IsEmpty() + { + return string.IsNullOrEmpty(UnitName) && string.IsNullOrEmpty(Bed); + } + + public bool IsFullEmpty() + { + return string.IsNullOrEmpty(UnitName) && string.IsNullOrEmpty(Bed) && string.IsNullOrEmpty(Room); + } + + public override string ToString() + { + return "PatientLocation[Unit: " + UnitName + ",room: " + Room + " ,bed: " + Bed + "]"; + } + + //public override bool Equals(object? obj) + //{ + // return obj is PatientLocation objPl && Equals(objPl); + //} + + //public override int GetHashCode() + //{ + // var hashCode = 511378865; + // if (UnitName != null) + // hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(UnitName); + // // ReSharper disable once NonReadonlyMemberInGetHashCode + // if (Bed != null) hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Bed); + // return hashCode; + //} +} \ No newline at end of file diff --git a/adas-core.Domain/Models/PatientLocationAction.cs b/adas-core.Domain/Models/PatientLocationAction.cs new file mode 100644 index 00000000..58b93fe8 --- /dev/null +++ b/adas-core.Domain/Models/PatientLocationAction.cs @@ -0,0 +1,42 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Utils; + +namespace adas_core.Domain.Models; + +public class PatientLocationAction +{ + public ActionsEnum.ResourceAction? Action { get; set; } + + public PatientLocation? Location { get; set; } + + + public static List? Apply(List? actions) + { + return Apply(null, actions); + } + + public static List? Apply(List? source, List? actions) + { + source ??= []; + (actions ?? []).ForEach(s => + { + if (s.Location != null) + switch (s.Action) + { + case ActionsEnum.ResourceAction.Add: + if (!source.Contains(s.Location)) source.Add(s.Location); + break; + case ActionsEnum.ResourceAction.Delete: + if (source.Contains(s.Location)) source.Remove(s.Location); + break; + + case ActionsEnum.ResourceAction.Update: + if (source.Contains(s.Location)) source[source.IndexOf(s.Location)] = s.Location; + else source.Add(s.Location); + break; + } + }); + + return CollectionsUtils.NullIfEmpty(source); + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/PatientObservation.cs b/adas-core.Domain/Models/PatientObservation.cs new file mode 100644 index 00000000..c7a3f17e --- /dev/null +++ b/adas-core.Domain/Models/PatientObservation.cs @@ -0,0 +1,90 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; + +namespace adas_core.Domain.Models; + +public class PatientObservation : BasePatientObservationValue +{ + private string? _result; + + public PatientObservation() + { + } + + public PatientObservation(ObjectId patientId, object value, string? name) + { + PatientId = patientId; + Value = value; + Name = name; + } + + public double? Max { get; set; } + + public double? Min { get; set; } + + public double? MaxWarn { get; set; } + + public double? MinWarn { get; set; } + + public string? WarnColor { get; set; } + + public string? AlertColor { get; set; } + + public bool ShowOnExpired { get; set; } = true; + public ObservationEnum.InsertMode InsertMode { get; set; } = ObservationEnum.InsertMode.Auto; + + public bool? Persist { get; set; } + + public string? ColorOnExpired { get; set; } + + //MSH Time of HL7 + public DateTime MessageTime { get; set; } + + public string? Result + { + get => _result; + set => _result = value == "F" ? null : value; + } + + public override string ToString() + { + var items = ToListString(); + items.Add($"value: {Value}"); + if (Max != null) items.Add($"max: {Max}"); + if (Min != null) items.Add($"min: {Min}"); + if (MaxWarn != null) items.Add($"maxWarn: {MaxWarn}"); + if (MinWarn != null) items.Add($"minWarn: {MinWarn}"); + if (!string.IsNullOrEmpty(Result)) items.Add($"result: {Result}"); + items.Add($"status: {Status}"); + if (Expires != null) items.Add($"expires: {Expires}"); + if (!ShowOnExpired) items.Add($"showOnExpired: {ShowOnExpired}"); + if (ColorOnExpired != null) items.Add($"colorOnExpired: {ColorOnExpired}"); + if (Persist != null) items.Add($"persist: {Persist}"); + return "PatientObservation[" + string.Join(", ", items) + "]"; + } + + public PatientObservation DeepCopy() + { + return (PatientObservation)MemberwiseClone(); + } + + + #region calculated properties + + public StatusEnum.Type Status { get; set; } = StatusEnum.Type.Ok; + + public int Level { get; set; } + + public int? Expires { get; set; } //seconds + + + [BsonDefaultValue(false)] public bool Expired { get; set; } + + public Dictionary? UiConfiguration { get; set; } + + public AlarmConfig? Alarm { get; set; } + + #endregion +} \ No newline at end of file diff --git a/adas-core.Domain/Models/PatientObservationAlarm.cs b/adas-core.Domain/Models/PatientObservationAlarm.cs new file mode 100644 index 00000000..e9a568e7 --- /dev/null +++ b/adas-core.Domain/Models/PatientObservationAlarm.cs @@ -0,0 +1,48 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Domain.Models; + +public class PatientObservationAlarm : BasePatientObservationValue +{ + public InactivationState? InactivationState { get; set; } + public AlarmEnum.EventPhase EventPhase { get; set; } = AlarmEnum.EventPhase.Continue; + public string? Event { get; set; } + public string? EventId { get; set; } + public AlarmEnum.ObservationAlarmState? State { get; set; } + public AlarmEnum.ObservationAlarmPriority? Priority { get; set; } + public int? PriorityLevel { get; set; } + public AlarmConfig? AlarmConfig { get; set; } + public AlarmEnum.ObservationAlarmType? Type { get; set; } + public string? AlertColor { get; set; } + + //MSH Time of HL7 + public DateTime MessageTime { get; set; } + public bool? Persist { get; set; } + + public bool Expired { get; set; } + + public int? Expires { get; set; } + + public List? Sources { get; set; } +} + +public class InactivationState +{ + public AlarmEnum.AudioVideoState? Audio { get; set; } + public AlarmEnum.AudioVideoState? Visual { get; set; } + public bool? Acknowledge { get; set; } +} + +public class Source +{ + public string? Code { get; set; } + + public string? OriginalName { get; set; } + + public string? CodeSystem { get; set; } + public string? Units { get; set; } + + public object? Value { get; set; } + public string? Result { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/PatientRecordingAlert.cs b/adas-core.Domain/Models/PatientRecordingAlert.cs new file mode 100644 index 00000000..060ec76e --- /dev/null +++ b/adas-core.Domain/Models/PatientRecordingAlert.cs @@ -0,0 +1,17 @@ +namespace adas_core.Domain.Models; + +public class PatientRecordingAlert : BasePatientObservation +{ + public bool IsRecording { get; set; } + + public override string ToString() + { + var items = ToListString(); + items.Add($"patientId: {PatientId}"); + if (!string.IsNullOrEmpty(Name)) items.Add($"source: {Name}"); + items.Add($"isRecording: {IsRecording}"); + items.Add($"time: {Time}"); + + return "PatientRecordingAlert[" + string.Join(", ", items) + "]"; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/PatientTreatment.cs b/adas-core.Domain/Models/PatientTreatment.cs new file mode 100644 index 00000000..d2606f95 --- /dev/null +++ b/adas-core.Domain/Models/PatientTreatment.cs @@ -0,0 +1,181 @@ +using System.Reflection; +using adas_core.Domain.Enums; +using MongoDB.Bson; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; + +namespace adas_core.Domain.Models; + +public class PatientTreatment : JsonConverter +{ + //If bolopom plus to bolus opiates. + public bool BoloPom = false; + + //If single Dose is true, finish at end of actual shift. + public bool SingleDose = false; + public ObjectId Id { get; set; } + + + public ObjectId PatientId { get; set; } + + + // ORC Common Order + + public OrderControlType OrderControl { get; set; } + + public Entity? PlacerOrder { get; set; } + + public Entity? FillerOrder { get; set; } + + + public string OrderStatus { get; set; } = string.Empty; + + + public DateTime? OrderTime { get; set; } + + + public DateTime? EndTime { get; set; } + + + public DateTime? StartTime { get; set; } + + // RXO Pharmacy/Treatment Route + + public List RequestedGiveCodes { get; set; } = []; + + public string RequestedGiveTreatment { get; set; } = string.Empty; + + // RXA - Pharmacy/Treatment Administration + public List RequestedGiveCodesStatus { get; set; } = []; + + + public double? RequestedGiveAmountMinimum { get; set; } + + + public double? RequestedGiveAmountMaximum { get; set; } + + + public Code? RequestedGiveUnits { get; set; } + + + public Code? RequestedDosageForm { get; set; } + + // NTE Notes and comments + + public List Notes { get; set; } = []; + + // RXR Pharmacy/Treatment Route + public List Routes { get; set; } = []; + + //MSH Time of HL7 + + public DateTime MessageTime { get; set; } + + + public string? SystemId { get; set; } + + + public override string ToString() + { + try + { + List items = + [ + $"id: {Id}", + $"patientid: {PatientId}", + $"orderControl: {OrderControl}" + ]; + if (PlacerOrder != null) items.Add($"placerOrder: {PlacerOrder}"); + if (FillerOrder != null) items.Add($"fillerOrder: {FillerOrder}"); + if (!string.IsNullOrEmpty(OrderStatus)) items.Add($"orderStatus: {OrderStatus}"); + + if (OrderTime != null) items.Add($"orderTime: {OrderTime}"); + if (StartTime != null) items.Add($"startTime: {StartTime}"); + if (EndTime != null) items.Add($"endTime: {EndTime}"); + + if (RequestedGiveCodes.Any()) RequestedGiveCodes.ForEach(c => items.Add($"RequestedGiveCodes: {c}")); + + + if (!string.IsNullOrEmpty(RequestedGiveTreatment)) + items.Add($"requestedGiveTreatment: {RequestedGiveTreatment}"); + + if (RequestedGiveCodesStatus.Any()) RequestedGiveCodesStatus.ForEach(c => items.Add($"{c}")); + + if (RequestedGiveAmountMinimum != null) + items.Add($"requestedGiveAmountMinimum: {RequestedGiveAmountMinimum}"); + if (RequestedGiveAmountMaximum != null) + items.Add($"requestedGiveAmountMaximum: {RequestedGiveAmountMaximum}"); + + if (RequestedGiveUnits != null) items.Add($"requestedGiveUnits: {RequestedGiveUnits}"); + if (RequestedDosageForm != null) items.Add($"requestedDosageForm: {RequestedDosageForm}"); + + Notes.ForEach(n => items.Add($"{n}")); + + Routes.ForEach(r => items.Add($"{r}")); + + items.Add($"singleDose: {SingleDose}"); + items.Add($"boloPom: {BoloPom}"); + items.Add($"messageTime: {MessageTime}"); + + if (!string.IsNullOrEmpty(SystemId)) items.Add($"systemId: {SystemId}"); + + + return "PatientTreatment[" + string.Join(", ", items) + "]"; + } + catch (Exception ex) + { + //Log.Error("Error parsing PatientTreatment to string. Exception: {ex}", ex); + return "Error deserielizing:" + ex.Message; + } + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value != null) + { + var jo = JObject.FromObject(value); + + // Add [JsonConverter(typeof(StringEnumConverter))] attribute to specified properties + AddStringEnumConverterAttribute(value, nameof(OrderControl)); + + jo.WriteTo(writer); + } + } + + private static void AddStringEnumConverterAttribute(object value, string propertyName) + { + var prop = value.GetType().GetProperty(propertyName); + var attr = new JsonConverterAttribute(typeof(StringEnumConverter)); + + if (prop != null) + { + var attrs = prop.GetCustomAttributes(false); + + // Check if the attribute is not already applied + if (Array.Find(attrs, a => a is JsonConverterAttribute) == null) + { + // Create a new array that includes the existing attributes and the new one + var newAttrs = new object[attrs.Length + 1]; + Array.Copy(attrs, newAttrs, attrs.Length); + newAttrs[attrs.Length] = attr; + + // Use reflection to set the new attributes array + var field = typeof(PropertyInfo).GetField("m_customAttributes", + BindingFlags.Instance | BindingFlags.NonPublic); + field?.SetValue(prop, newAttrs); + } + } + } + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, + JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + public override bool CanConvert(Type objectType) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Person.cs b/adas-core.Domain/Models/Person.cs new file mode 100644 index 00000000..a07ef89b --- /dev/null +++ b/adas-core.Domain/Models/Person.cs @@ -0,0 +1,117 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Domain.Models; + +public class Person : IEquatable +{ + public string? LastName { get; set; } + + public string? FirstName { get; set; } + + public string? SecondName { get; set; } + + public DateTime? BirthDate { get; set; } + + public string? Language { get; set; } + + public PatientEnum.Gender Gender { get; set; } = PatientEnum.Gender.Unknown; + + public Dictionary? Ids { get; set; } + + + public List? HistoricalIds { get; set; } + + public bool Equals(Person? other) + { + return other != null && + LastName == other.LastName && + FirstName == other.FirstName && + SecondName == other.SecondName && + BirthDate == other.BirthDate && + Language == other.Language && + Gender == other.Gender && CheckIdsAreEqual(other); + } + + public void SetIds(Dictionary patientIds, DateTime? time = null) + { + Ids = patientIds; + HistoricalIds ??= new List(); + + DateTime targetTime = time ?? DateTime.UtcNow; + + var existingOption = HistoricalIds.FirstOrDefault(c => c.Time == targetTime); + + if (existingOption != null) + { + foreach (var kvp in patientIds) + { + existingOption.PatientIds[kvp.Key] = kvp.Value; + } + } + else + { + HistoricalIds.Add(new HistoricalId(targetTime, new Dictionary(patientIds))); + } + } + + public bool IsEmpty() + { + return string.IsNullOrEmpty(LastName) && string.IsNullOrEmpty(FirstName) && string.IsNullOrEmpty(SecondName) && + BirthDate == null && Gender == PatientEnum.Gender.Unknown && (Ids == null || Ids.Count == 0); + } + + public bool IsEmptyDontCheckIds() + { + return string.IsNullOrEmpty(LastName) && string.IsNullOrEmpty(FirstName) && string.IsNullOrEmpty(SecondName) && + BirthDate == null && Gender == PatientEnum.Gender.Unknown; + } + + //public override bool Equals(object? obj) + //{ + // return Equals(obj as Person); + //} + + private bool CheckIdsAreEqual(Person other) + { + if (Ids == null && other.Ids == null) return true; + if (Ids == null || other.Ids == null) return false; + if (Ids.Count != other.Ids.Count) return false; + + foreach (var kvp in Ids) + if (!other.Ids.TryGetValue(kvp.Key, out var otherValue) || otherValue != kvp.Value) + return false; + return true; + } + + //public override int GetHashCode() + //{ + // var hashCode = -373112803; + // if (LastName != null) + // hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(LastName); + // if (FirstName != null) + // hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(FirstName); + // if (SecondName != null) + // hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(SecondName); + // if (BirthDate != null) hashCode = hashCode * -1521134295 + BirthDate.GetHashCode(); + // if (Language != null) + // hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(Language); + // hashCode = hashCode * -1521134295 + Gender.GetHashCode(); + // if (Ids != null) + // hashCode = hashCode * -1521134295 + EqualityComparer>.Default.GetHashCode(Ids); + // return hashCode; + //} + + public override string ToString() + { + List items = []; + if (!string.IsNullOrEmpty(FirstName)) items.Add($"firstName: {FirstName}"); + if (!string.IsNullOrEmpty(LastName)) items.Add($", lastName: {LastName}"); + if (!string.IsNullOrEmpty(SecondName)) items.Add($"secondName: {SecondName}"); + if (BirthDate.HasValue) items.Add($"birthDate: {BirthDate}"); + if (!string.IsNullOrEmpty(Language)) items.Add($"language: {Language}"); + items.Add($"gender: {Gender}"); + if (Ids?.Count > 0) items.Add($"ids: {string.Join(',', Ids.Select(i => $"[{i.Key}:{i.Value}]"))}"); + return "Person[" + string.Join(", ", items) + "]"; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/PersonResource.cs b/adas-core.Domain/Models/PersonResource.cs new file mode 100644 index 00000000..ed9350ad --- /dev/null +++ b/adas-core.Domain/Models/PersonResource.cs @@ -0,0 +1,21 @@ +namespace adas_core.Domain.Models; + +public class PersonResource +{ + public Person? Person { get; set; } + + + //private string ResourceType { get; } = string.Empty; + + //public override bool Equals(object? obj) + //{ + // return obj is PersonResource resource && + // EqualityComparer.Default.Equals(Person, resource.Person) && + // ResourceType == resource.ResourceType; + //} + + //public override int GetHashCode() + //{ + // return HashCode.Combine(Person, ResourceType); + //} +} \ No newline at end of file diff --git a/adas-core.Domain/Models/PersonResourceAction.cs b/adas-core.Domain/Models/PersonResourceAction.cs new file mode 100644 index 00000000..b8bcc100 --- /dev/null +++ b/adas-core.Domain/Models/PersonResourceAction.cs @@ -0,0 +1,40 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Utils; + +namespace adas_core.Domain.Models; + +public class PersonResourceAction +{ + public ActionsEnum.ResourceAction? Action { get; set; } + + + public PersonResource? Resource { get; set; } + + public static List? Apply(List actions) + { + return Apply(null, actions); + } + + public static List? Apply(List? source, List? actions) + { + source ??= []; + (actions ?? []).ForEach(s => + { + if (s.Resource != null) + switch (s.Action) + { + case ActionsEnum.ResourceAction.Add: + if (!source.Contains(s.Resource)) source.Add(s.Resource); + break; + case ActionsEnum.ResourceAction.Delete: + if (source.Contains(s.Resource)) source.Remove(s.Resource); + break; + case ActionsEnum.ResourceAction.Update: + if (source.Contains(s.Resource)) source[source.IndexOf(s.Resource)] = s.Resource; + else source.Add(s.Resource); + break; + } + }); + return CollectionsUtils.NullIfEmpty(source); + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Providers/ProvidersSettings.cs b/adas-core.Domain/Models/Providers/ProvidersSettings.cs new file mode 100644 index 00000000..3fe26f80 --- /dev/null +++ b/adas-core.Domain/Models/Providers/ProvidersSettings.cs @@ -0,0 +1,8 @@ +namespace adas_core.Domain.Models.Providers; + +public class ProvidersSettings +{ + public string Name { get; set; } = string.Empty; + + public string Url { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Providers/ResultModelPredictMedicationADAS.cs b/adas-core.Domain/Models/Providers/ResultModelPredictMedicationADAS.cs new file mode 100644 index 00000000..626fae22 --- /dev/null +++ b/adas-core.Domain/Models/Providers/ResultModelPredictMedicationADAS.cs @@ -0,0 +1,25 @@ +namespace adas_core.Domain.Models.Providers; + +public abstract class ResultModelPredictMedicationAdas +{ + public string MedicineIdentifier { get; set; } = string.Empty; + + public string MedicineText { get; set; } = string.Empty; + + public string TreatmentPatientId { get; set; } = string.Empty; + + public string Gender { get; set; } = string.Empty; + + public string BirthDate { get; set; } = string.Empty; + + public string CodingSystem { get; set; } = string.Empty; + + public string PatientNumber { get; set; } = string.Empty; + + public double DegreeSimilarity { get; set; } + + public override string ToString() + { + return $"{MedicineText} | {DegreeSimilarity}"; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Providers/ResultModelPredictOfStayADAS.cs b/adas-core.Domain/Models/Providers/ResultModelPredictOfStayADAS.cs new file mode 100644 index 00000000..a3f73b6a --- /dev/null +++ b/adas-core.Domain/Models/Providers/ResultModelPredictOfStayADAS.cs @@ -0,0 +1,17 @@ +namespace adas_core.Domain.Models.Providers; + +public class ResultModelPredictOfStayAdas +{ + public string Result { get; set; } = string.Empty; + + public List Payload { get; set; } = []; +} + +public abstract class Payload +{ + public double PercentageMin { get; set; } + + public double PercentageMax { get; set; } + + public double Duration { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Pumps/CommonPumpTypes.cs b/adas-core.Domain/Models/Pumps/CommonPumpTypes.cs new file mode 100644 index 00000000..0b8d7fa8 --- /dev/null +++ b/adas-core.Domain/Models/Pumps/CommonPumpTypes.cs @@ -0,0 +1,20 @@ +namespace adas_core.Domain.Models.Pumps; + +public abstract class CommonPumpTypes +{ + // Tipos auxiliares para valores con unidades (idénticos a PatientPumpObservation pero anidados aquí) + + public class PumpValue + { + public double? Value { get; set; } + public string? Units { get; set; } + public override string ToString() => $"{{Value: {Value}, Units: {Units}}}"; + } + + public class SyringeDetails + { + public string? Type { get; set; } + public string? Manufacturer { get; set; } + public PumpValue? Volume { get; set; } + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Pumps/PumpAlarmEvent.cs b/adas-core.Domain/Models/Pumps/PumpAlarmEvent.cs new file mode 100644 index 00000000..77ef050f --- /dev/null +++ b/adas-core.Domain/Models/Pumps/PumpAlarmEvent.cs @@ -0,0 +1,37 @@ +using adas_core.Domain.Enums; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.Pumps; + +public class PumpAlarmEvent +{ + public ObjectId Id { get; set; } + + // Dispositivo / contexto + public string? DeviceId { get; set; } + public string? RackId { get; set; } + public string? DeviceTypeMdc { get; set; } + + public string? InfusionId { get; set; } + public DateTime Time { get; set; } + + public ObjectId? PatientId { get; set; } + + // Alarma + public PumpEnum.AlarmType? AlarmType { get; set; } + public string? AlarmDescription { get; set; } + public string? AlarmState { get; set; } // active/inactive/latched + public string? AlarmInactivationState { get; set; } // enabled / alert-acknowledged~audio-paused + public string? AlarmPriority { get; set; } // PL/PM/PH + public string? AlarmTypeMdc { get; set; } // e.g., "ST" + public PumpEnum.EventPhase? EventPhase { get; set; } // start/continue/end + + public string? AlertSourceMdc { get; set; } // p.ej. MDC_DEV_PUMP_INFUS_SYRINGE_VMD + + // Ubicación física / red (opcionales) + public string? DeviceIp { get; set; } + public string? PillarAssembly { get; set; } + public string? PillarRackSlot { get; set; } + + +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Pumps/PumpAlarmState.cs b/adas-core.Domain/Models/Pumps/PumpAlarmState.cs new file mode 100644 index 00000000..50167566 --- /dev/null +++ b/adas-core.Domain/Models/Pumps/PumpAlarmState.cs @@ -0,0 +1,39 @@ +using adas_core.Domain.Enums; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.Pumps +{ + /// + /// Estado actual de alarmas activas por bomba. + /// Una fila por alarma activa (DeviceId + AlarmType o AlarmCodeMdc). + /// Se actualiza vía PumpService: start/continue → upsert, end → delete. + /// + public class PumpAlarmState + { + public ObjectId Id { get; set; } + + // Bomba + public string? DeviceId { get; set; } + + // Paciente (si la bomba lo envía) + public ObjectId? PatientId { get; set; } + + // Tipo / Identificador de alarma + public PumpEnum.AlarmType? AlarmType { get; set; } + public string? AlarmCodeMdc { get; set; } + + // Estado actual + public string? AlarmDescription { get; set; } + public string? AlarmPriority { get; set; } // PL/PM/PH + public string? AlarmState { get; set; } // active / inactive / latched + public PumpEnum.EventPhase? LastPhase { get; set; } // start / continue / end + + // Timestamps + public DateTime FirstSeen { get; set; } = DateTime.UtcNow; + public DateTime LastUpdated { get; set; } = DateTime.UtcNow; + + // Contexto (opcional) + public string? AlertSourceMdc { get; set; } + public string? InfusionId { get; set; } + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Pumps/PumpObservation.cs b/adas-core.Domain/Models/Pumps/PumpObservation.cs new file mode 100644 index 00000000..87ee5193 --- /dev/null +++ b/adas-core.Domain/Models/Pumps/PumpObservation.cs @@ -0,0 +1,108 @@ +using adas_core.Domain.Enums; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.Pumps +{ + /// + /// Datos unificados de entrada (del HL7): transporta la información + /// de observaciones (PCD-01/10), alarmas (PCD-04) y eventos. El servicio + /// decidirá cómo dividir y persistir en las colecciones destino. + /// + public class PumpObservation + { + public ObjectId Id { get; set; } + public ObjectId? PatientId { get; set; } + public DateTime Time { get; set; } + + // Identificadores / dispositivo + public string? DeviceId { get; set; } + public string? RackId { get; set; } + public string? DeviceTypeMdc { get; set; } + + // Infusión / tiempo + public string? InfusionId { get; set; } + + /// Tipo semántico de este DTO: Observation (PCD-01/10) o Alarm (PCD-04). + public PumpEnum.PumpMessageType MessageType { get; set; } + + #region PROPIEDADES DE LA CLASE ORIGINAL + //necesarios para que funcione Alaris + //TODO verificar + public int? TotalAux { get; set; } + public CommonPumpTypes.PumpValue? Pressure { get; set; } + public CommonPumpTypes.PumpValue? DiluentVolume { get; set; } + public CommonPumpTypes.PumpValue? PatientHeight { get; set; } + public int GatewayNumber { get; set; } + public int? Expires { get; set; } + public Dictionary? UiConfiguration { get; set; } + public CommonPumpTypes.PumpValue? BasalRate { get; set; } + public string? Code { get; set; } + public string? Name { get; set; } + + public int? Number { get; set; } + public int? Total { get; set; } + public bool? IsAux { get; set; } + public PumpEnum.AlarmMode? AlarmMode { get; set; } + + #endregion + + + // ============================ + // OBSERVACIONES (PCD-01 / 10) + // ============================ + public bool? IsInfusing { get; set; } + public PumpEnum.InfusingStatus? InfusingStatus { get; set; } + public PumpEnum.Status? Status { get; set; } + public PumpEnum.Mode? PumpMode { get; set; } + + public string? ActiveSourceInfo { get; set; } // Primary/Clinician/Loading/Priming + public string? InfusionModeDetail { get; set; } // Texto del modo + public string? NotDeliveringReason { get; set; } + public string? Source { get; set; } // primary-source, clinician-source... + + public CommonPumpTypes.PumpValue? FlowFluid { get; set; } // actual + public CommonPumpTypes.PumpValue? Rate { get; set; } // programada + public CommonPumpTypes.PumpValue? VolumeInfused { get; set; } // segmento + public CommonPumpTypes.PumpValue? FluidDelivTotal { get; set; } // total + public CommonPumpTypes.PumpValue? FluidDelivTotalSet { get; set; } // acumulado + public CommonPumpTypes.PumpValue? VolumeRemaining { get; set; } + public CommonPumpTypes.PumpValue? Vtbi { get; set; } + public CommonPumpTypes.PumpValue? TimeRemaining { get; set; } // seg + public CommonPumpTypes.PumpValue? TimeProgrammed { get; set; } // seg + + // Medicación / dosis + public string? DrugName { get; set; } + public string? DrugId { get; set; } + public CommonPumpTypes.PumpValue? Concentration { get; set; } + public CommonPumpTypes.PumpValue? DoseRate { get; set; } + public CommonPumpTypes.PumpValue? DrugAmount { get; set; } // TBI + public CommonPumpTypes.PumpValue? DrugDoseDelivered { get; set; } // total + + public CommonPumpTypes.PumpValue? PatientWeight { get; set; } + public CommonPumpTypes.SyringeDetails? Syringe { get; set; } + + // Ubicación física / red + public string? DeviceIp { get; set; } + public string? PillarAssembly { get; set; } // "1,1,4,pillar-orientation-vertical,slot-rackdirection-down" + public string? PillarRackSlot { get; set; } // "1,2,3,NA" + + // ============================ + // ALARMAS (PCD-04) + // ============================ + public PumpEnum.AlarmType? AlarmType { get; set; } + public string? AlarmDescription { get; set; } + public string? AlarmState { get; set; } // active/inactive/latched + public string? AlarmInactivationState { get; set; } // enabled / alert-acknowledged~audio-paused + public string? AlarmPriority { get; set; } // PL/PM/PH + public string? AlarmTypeMdc { get; set; } // e.g., "ST" + public PumpEnum.EventPhase? EventPhase { get; set; } // start/continue/end + public string? AlertSourceMdc { get; set; } + + // ============================ + // EVENTOS (PCD-10) + // ============================ + public PumpEnum.Event? Event { get; set; } + } + + +} diff --git a/adas-core.Domain/Models/Pumps/PumpState.cs b/adas-core.Domain/Models/Pumps/PumpState.cs new file mode 100644 index 00000000..ce0b058e --- /dev/null +++ b/adas-core.Domain/Models/Pumps/PumpState.cs @@ -0,0 +1,82 @@ +using adas_core.Domain.Enums; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.Pumps +{ + /// + /// Snapshot vivo de la bomba, indexado por DeviceId. + /// Se sobrescribe con cada mensaje del HL7. + /// Contiene siempre el último valor conocido de la bomba. + /// + public class PumpState + { + public ObjectId Id { get; set; } + + // Identificación bomba + public string? DeviceId { get; set; } + public string? RackId { get; set; } + public string? DeviceTypeMdc { get; set; } + + // Ubicación física + public string? DeviceIp { get; set; } + public string? PillarAssembly { get; set; } + public string? PillarRackSlot { get; set; } + + // Paciente asociado (si existe) + public ObjectId? PatientId { get; set; } + + // Infusión activa + public string? InfusionId { get; set; } + public DateTime LastUpdated { get; set; } + + // Estado general + public bool IsInfusing { get; set; } + public PumpEnum.InfusingStatus? InfusingStatus { get; set; } + public PumpEnum.Status? Status { get; set; } + public PumpEnum.Mode? PumpMode { get; set; } + + public string? InfusionModeDetail { get; set; } + public string? ActiveSourceInfo { get; set; } + public string? NotDeliveringReason { get; set; } + public string? Source { get; set; } + + // Métricas + public CommonPumpTypes.PumpValue? FlowFluid { get; set; } + public CommonPumpTypes.PumpValue? Rate { get; set; } + public CommonPumpTypes.PumpValue? VolumeInfused { get; set; } + public CommonPumpTypes.PumpValue? FluidDelivTotal { get; set; } + public CommonPumpTypes.PumpValue? FluidDelivTotalSet { get; set; } + public CommonPumpTypes.PumpValue? VolumeRemaining { get; set; } + public CommonPumpTypes.PumpValue? Vtbi { get; set; } + public CommonPumpTypes.PumpValue? TimeRemaining { get; set; } + public CommonPumpTypes.PumpValue? TimeProgrammed { get; set; } + + // Medicación + public string? DrugName { get; set; } + public string? DrugId { get; set; } + public CommonPumpTypes.PumpValue? Concentration { get; set; } + public CommonPumpTypes.PumpValue? DoseRate { get; set; } + public CommonPumpTypes.PumpValue? DrugAmount { get; set; } + public CommonPumpTypes.PumpValue? DrugDoseDelivered { get; set; } + + public CommonPumpTypes.PumpValue? PatientWeight { get; set; } + public CommonPumpTypes.SyringeDetails? Syringe { get; set; } + + // Última alarma recibida + public PumpEnum.AlarmType? AlarmType { get; set; } + public string? AlarmDescription { get; set; } + public string? AlarmState { get; set; } + public string? AlarmInactivationState { get; set; } + public string? AlarmPriority { get; set; } + public string? AlarmCodeMdc { get; set; } + + // Evento último + public PumpEnum.Event? Event { get; set; } + public PumpEnum.EventPhase? EventPhase { get; set; } + + // Comunicación / Relay + public string? CommStatus { get; set; } + public string? RelayState { get; set; } + public string? RelayGuid { get; set; } + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Recording/AccessGrant.cs b/adas-core.Domain/Models/Recording/AccessGrant.cs new file mode 100644 index 00000000..741ed894 --- /dev/null +++ b/adas-core.Domain/Models/Recording/AccessGrant.cs @@ -0,0 +1,24 @@ +namespace adas_core.Domain.Models.Recording; + +public class AccessGrant +{ + private int _expiresIn; + + public string? AccessToken { get; set; } + + public int ExpiresIn + { + get => _expiresIn; + set + { + _expiresIn = value; + Expires = DateTime.Now.AddSeconds(value); + } + } + + public string? Scope { get; set; } + public string? TokenType { get; set; } + public DateTime Expires { get; private set; } + + public bool IsExpired => DateTime.Now.CompareTo(Expires) >= 0; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Recording/Patient.cs b/adas-core.Domain/Models/Recording/Patient.cs new file mode 100644 index 00000000..84779b6a --- /dev/null +++ b/adas-core.Domain/Models/Recording/Patient.cs @@ -0,0 +1,12 @@ +namespace adas_core.Domain.Models.Recording; + +public class Patient +{ + /// + /// PatientNumber + /// + public string Id { get; set; } = string.Empty; + + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Recording/RecordingData.cs b/adas-core.Domain/Models/Recording/RecordingData.cs new file mode 100644 index 00000000..8c73c07f --- /dev/null +++ b/adas-core.Domain/Models/Recording/RecordingData.cs @@ -0,0 +1,37 @@ +using adas_core.Domain.Enums; +using Newtonsoft.Json; + +namespace adas_core.Domain.Models.Recording; + +public class RecordingData +{ + public Patient? Patient { get; set; } + public int RoomId { get; set; } + public DateTime? StartRecordingTime { get; set; } + public DateTime? StopRecordingTime { get; set; } + public DateTime? EventTime { get; set; } + public AlarmEnum.Type? AlarmType { get; set; } + public AlarmEnum.Severity Severity { get; set; } = AlarmEnum.Severity.None; + public string? AlarmDescription { get; set; } + public string? AlarmName { get; set; } + public string Store { get; set; } = string.Empty; + public int Retry { get; set; } + public string Status { get; set; } = string.Empty; + + public string GenerateStore() + { + return Store = $"{RoomId}_{DateTime.Now:yyyyMMdd-HHmmss}"; + } + + public override string ToString() + { + try + { + return JsonConvert.SerializeObject(this, Formatting.None); + } + catch + { + return base.ToString()!; + } + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Recording/VideoDto.cs b/adas-core.Domain/Models/Recording/VideoDto.cs new file mode 100644 index 00000000..933d13e2 --- /dev/null +++ b/adas-core.Domain/Models/Recording/VideoDto.cs @@ -0,0 +1,39 @@ +using adas_core.Domain.Enums; + +namespace adas_core.Domain.Models.Recording; + +public record VideoDto +{ + public string Id { get; set; } = null!; // + public string? PatientId { get; set; } + public int RoomId { get; set; } + public int? CameraId { get; set; } + public string? DiagnosisId { get; set; } + public string? ProcedureId { get; set; } + public string? ServiceId { get; set; } + public int? DiseaseId { get; set; } + public int? ThumbnailId { get; set; } + public bool BlockChange { get; set; } + public string Sid { get; set; } = null!; // + public DateTime Date { get; set; } // + public DateTime? EndDate { get; set; } // + public string Store { get; set; } = null!; // + public string? DiagnosisDescription { get; set; } // + public string? ProcedureDescription { get; set; } // + public string? Observations { get; set; } // + public int Duration { get; set; } // + public StatusEnum.Video VideoStatus { get; set; } = StatusEnum.Video.Initialized; + public DateTime? UpdateDate { get; set; } + public string? DeletedBy { get; set; } + public DateTime? StartDate { get; set; } + public AlarmEnum.Type? Alarm { get; set; } + + public string? Alert { get; set; } + + public string? PublishedFilename { get; set; } + public string? PublishedExportDir { get; set; } + public string? PublishedVideoPath { get; set; } + + public string? SecondaryProcId { get; set; } + public string? SecondaryDiagnosisId { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Responses/BeaconResponse.cs b/adas-core.Domain/Models/Responses/BeaconResponse.cs new file mode 100644 index 00000000..1ecc25b4 --- /dev/null +++ b/adas-core.Domain/Models/Responses/BeaconResponse.cs @@ -0,0 +1,21 @@ +using MongoDB.Bson; + +namespace adas_core.Domain.Models.Responses; + +public class BeaconResponse(string color, ObjectId poc, ObjectId unitId) : IEquatable +{ + private string Color { get; } = color; + private ObjectId PointOfCareId { get; } = poc; + + private ObjectId UnitId { get; } = unitId; + + public bool Equals(BeaconResponse? other) + { + return PointOfCareId == other?.PointOfCareId && UnitId == other.UnitId; + } + + public override string ToString() + { + return "BeaconResponse[pointOfCare: " + PointOfCareId + ", bed: " + UnitId + ", color: " + Color + "]"; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Responses/BoxObservationsResponse.cs b/adas-core.Domain/Models/Responses/BoxObservationsResponse.cs new file mode 100644 index 00000000..b8c8922c --- /dev/null +++ b/adas-core.Domain/Models/Responses/BoxObservationsResponse.cs @@ -0,0 +1,17 @@ +using adas_core.Domain.Models.Observations; + +namespace adas_core.Domain.Models.Responses; + +public class BoxObservationsResponse +{ + public List Medication = []; + + public Dictionary Observations = new(); + public string? Id { get; set; } + + public string? Stream { get; set; } + + public bool? IsVisible { get; set; } + + public bool? HasPatient { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Responses/DisplayConfigMinimalResponse.cs b/adas-core.Domain/Models/Responses/DisplayConfigMinimalResponse.cs new file mode 100644 index 00000000..8e2218ee --- /dev/null +++ b/adas-core.Domain/Models/Responses/DisplayConfigMinimalResponse.cs @@ -0,0 +1,26 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.DTO.Display; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.Responses; + +public class DisplayConfigMinimalResponse +{ + public DisplayConfigMinimalResponse() + { + } + + public DisplayConfigMinimalResponse(DisplayConfigSummary displayConfig, bool? isInUse = false) + { + Id = displayConfig.Id; + Type = displayConfig.Type; + Hospital = displayConfig.Name; + IsInUse = isInUse; + } + + public ObjectId Id { get; set; } + public DisplayConfigEnums.DisplayType Type { get; set; } = DisplayConfigEnums.DisplayType.Unknown; + public string? Hospital { get; set; } + + public bool? IsInUse { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Responses/LoginResponse.cs b/adas-core.Domain/Models/Responses/LoginResponse.cs new file mode 100644 index 00000000..934d5638 --- /dev/null +++ b/adas-core.Domain/Models/Responses/LoginResponse.cs @@ -0,0 +1,19 @@ +namespace adas_core.Domain.Models.Responses; + +public class LoginResponse +{ + public string Token { get; set; } = ""; + public string RefreshToken { get; set; } = ""; + public DateTime Expiration { get; set; } = DateTime.MinValue; + + public bool IsExpired() + { + if (Expiration != DateTime.MinValue) + { + if (Expiration < DateTime.UtcNow) return true; + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Responses/PaginationResponse.cs b/adas-core.Domain/Models/Responses/PaginationResponse.cs new file mode 100644 index 00000000..e4554b81 --- /dev/null +++ b/adas-core.Domain/Models/Responses/PaginationResponse.cs @@ -0,0 +1,22 @@ +namespace adas_core.Domain.Models.Responses; + +public class PaginationResponse +{ + public PaginationResponse(List data, int pageNumber, int pageSize, long totalRecords) + { + PageNumber = pageNumber; + PageSize = pageSize; + Data = data; + TotalRecords = totalRecords; + var totalPages = TotalRecords / (double)PageSize; + var roundedTotalPages = Convert.ToInt64(Math.Ceiling(totalPages)); + TotalPages = roundedTotalPages; + } + + public int PageNumber { get; set; } + public int PageSize { get; set; } + + public long TotalPages { get; set; } + public long TotalRecords { get; set; } + public List Data { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Responses/PatientSearch.cs b/adas-core.Domain/Models/Responses/PatientSearch.cs new file mode 100644 index 00000000..4f5c121e --- /dev/null +++ b/adas-core.Domain/Models/Responses/PatientSearch.cs @@ -0,0 +1,17 @@ +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Domain.Models.Responses; + +public class PatientSearch( + Patient? patient, + Patient? archivePatient, + Admission? admission, + bool isArchived, + bool hasResult) +{ + public Patient? Patient { get; set; } = patient; + public Patient? ArchivedPatient { get; set; } = archivePatient; + public Admission? Admission { get; set; } = admission; + public bool IsArchived { get; set; } = isArchived; + public bool HasResult { get; set; } = hasResult; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Responses/Response.cs b/adas-core.Domain/Models/Responses/Response.cs new file mode 100644 index 00000000..26588d01 --- /dev/null +++ b/adas-core.Domain/Models/Responses/Response.cs @@ -0,0 +1,24 @@ +namespace adas_core.Domain.Models.Responses; + +public class Response +{ + public Response() + { + } + + public Response(T data) + { + Succeeded = true; + Message = string.Empty; + Errors = null; + Data = data; + } + + public T? Data { get; set; } + public bool? Succeeded { get; set; } + + public string[]? Errors { get; set; } + public string? Message { get; set; } +} + +public record UpdateResponse(long Changes, T? Data); \ No newline at end of file diff --git a/adas-core.Domain/Models/SignalR/Message.cs b/adas-core.Domain/Models/SignalR/Message.cs new file mode 100644 index 00000000..5685b1f7 --- /dev/null +++ b/adas-core.Domain/Models/SignalR/Message.cs @@ -0,0 +1,38 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.GroupedObservations; +using Newtonsoft.Json; + +namespace adas_core.Domain.Models.SignalR; + +public class Message +{ + public long IdMessage { get; set; } + + public MessageData? Data { get; set; } + + public string? Version { get; set; } + + public List? GroupedFields { get; set; } + + public string OperationStr + { + get => Operation.ToString("g"); + set => Operation = (SubscriptionEnum.Type)Enum.Parse(typeof(SubscriptionEnum.Type), value); + } + + public SubscriptionEnum.Type Operation { get; set; } + + + public override string? ToString() + { + try + { + return JsonConvert.SerializeObject(this, Formatting.None, + new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore }); + } + catch + { + return base.ToString(); + } + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/SignalR/MessageData.cs b/adas-core.Domain/Models/SignalR/MessageData.cs new file mode 100644 index 00000000..ddea79cb --- /dev/null +++ b/adas-core.Domain/Models/SignalR/MessageData.cs @@ -0,0 +1,70 @@ +using System.IdentityModel.Tokens.Jwt; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Domain.Models.SignalR; + +public class MessageData +{ + public bool IsDemo { get; set; } = false; + public LocaleEnum Locale { get; set; } + public string? PatientId { get; set; } + public string? Section { get; set; } + public SectionConfig? SectionConfig { get; set; } + public List Locations { get; set; } = []; + public List LocationIds { get; set; } = []; + + public bool SectionInfo { get; set; } = true; + + public int Last { get; set; } = 2; + + public bool Pumps { get; set; } = true; + + public bool RecordingAlerts { get; set; } = true; + + public bool Recordings { get; set; } = true; + + public bool Treatments { get; set; } = true; + + public bool Appointments { get; set; } = true; + + public bool Diagnosis { get; set; } = true; + + public bool Relay { get; set; } = true; + + public bool Beacons { get; set; } = true; + + public List CurrentRecordings { get; set; } = []; + + public bool Chat { get; set; } = true; + + public string? ChatMessage { get; set; } + + public PatientLocation? ChatMessageToLocation { get; set; } + + public DateTime? ChatMessageDate { get; set; } + + // PointOfCare Subscription + public PatientLocation? Location { get; set; } + + public List? Fields { get; set; } + + public List? Alarmfields { get; set; } + + public List? GroupedFields { get; set; } + + public string TimeZoneId { get; set; } = "Romance Standard Time"; + + public JwtSecurityToken? Token { get; set; } + public string? Action { get; set; } + public string? Rol { get; set; } + public string? UserName { get; set; } + + #region Display + + public string? DisplayId { get; set; } + + #endregion +} \ No newline at end of file diff --git a/adas-core.Domain/Models/SignalR/Recording.cs b/adas-core.Domain/Models/SignalR/Recording.cs new file mode 100644 index 00000000..ebccee8e --- /dev/null +++ b/adas-core.Domain/Models/SignalR/Recording.cs @@ -0,0 +1,31 @@ +using adas_core.Domain.Enums; + +namespace adas_core.Domain.Models.SignalR; + +public class Recording +{ + public string Id { get; set; } = null!; + public string? PatientId { get; set; } + public int RoomId { get; set; } + public int? CameraId { get; set; } + public string? DiagnosisId { get; set; } + public string? ProcedureId { get; set; } + public string? ServiceId { get; set; } + public int? DiseaseId { get; set; } + public int? ThumbnailId { get; set; } + public string Sid { get; set; } = null!; // + public DateTime Date { get; set; } // + public DateTime? EndDate { get; set; } // + public string Store { get; set; } = null!; // + public string? DiagnosisDescription { get; set; } // + public string? ProcedureDescription { get; set; } // + public string? Observations { get; set; } // + public int Duration { get; set; } // + public string Status { get; set; } = StatusEnum.Video.Initialized.ToString(); // + public DateTime? UpdateDate { get; set; } + public string? DeletedBy { get; set; } + public DateTime? StartDate { get; set; } + public string? Alarm { get; set; } + public int? Retry { get; set; } + public string? Alert { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/SystemAlerts/ApiClients.cs b/adas-core.Domain/Models/SystemAlerts/ApiClients.cs new file mode 100644 index 00000000..9ca91da3 --- /dev/null +++ b/adas-core.Domain/Models/SystemAlerts/ApiClients.cs @@ -0,0 +1,8 @@ +namespace adas_core.Domain.Models.SystemAlerts; + +public class ApiClients +{ + public string? Name { get; set; } + public int ConnectedClients { get; set; } + public List? IpClients { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/SystemAlerts/Performance.cs b/adas-core.Domain/Models/SystemAlerts/Performance.cs new file mode 100644 index 00000000..30ebe913 --- /dev/null +++ b/adas-core.Domain/Models/SystemAlerts/Performance.cs @@ -0,0 +1,13 @@ +namespace adas_core.Domain.Models.SystemAlerts; + +public class Performance +{ + public string? Name { get; set; } + public double ValueConsumed { get; set; } + public double ValueTotal { get; set; } + + + public double PercentageConsumed { get; set; } + + public string? Unit { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/SystemAlerts/Queue.cs b/adas-core.Domain/Models/SystemAlerts/Queue.cs new file mode 100644 index 00000000..6635e80c --- /dev/null +++ b/adas-core.Domain/Models/SystemAlerts/Queue.cs @@ -0,0 +1,8 @@ +namespace adas_core.Domain.Models.SystemAlerts; + +public class Queue +{ + public string? Name { get; set; } + public ulong Messages { get; set; } + public ulong Consumers { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/Timing.cs b/adas-core.Domain/Models/Timing.cs new file mode 100644 index 00000000..8159e65a --- /dev/null +++ b/adas-core.Domain/Models/Timing.cs @@ -0,0 +1,9 @@ +namespace adas_core.Domain.Models; + +public class Timing +{ + public DateTime? StartTime { get; set; } + + + public DateTime? EndTime { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/TreatmentRoute.cs b/adas-core.Domain/Models/TreatmentRoute.cs new file mode 100644 index 00000000..e1f071e4 --- /dev/null +++ b/adas-core.Domain/Models/TreatmentRoute.cs @@ -0,0 +1,16 @@ +namespace adas_core.Domain.Models; + +public class TreatmentRoute +{ + public Code? Route { get; set; } + + public override string ToString() + { + List items = []; + + if (Route != null) items.Add($", code: {Route}"); + + + return "treatmentRoute[" + string.Join(", ", items) + "]"; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/UiConfig.cs b/adas-core.Domain/Models/UiConfig.cs new file mode 100644 index 00000000..29579f22 --- /dev/null +++ b/adas-core.Domain/Models/UiConfig.cs @@ -0,0 +1,6 @@ +namespace adas_core.Domain.Models; + +public class UiConfig +{ + public Dictionary? UiConfiguration; +} \ No newline at end of file diff --git a/adas-core.Domain/Models/ValueConfig.cs b/adas-core.Domain/Models/ValueConfig.cs new file mode 100644 index 00000000..57e89a2c --- /dev/null +++ b/adas-core.Domain/Models/ValueConfig.cs @@ -0,0 +1,21 @@ +namespace adas_core.Domain.Models; + +public class ValueCondif +{ + public string Obsid { get; set; } = string.Empty; + + + public string Name { get; set; } = string.Empty; + + + public double Min { get; set; } + + + public double Max { get; set; } + + + public double? MaxWarn { get; set; } + + + public double? MinWarn { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Models/WebMessageNotification.cs b/adas-core.Domain/Models/WebMessageNotification.cs new file mode 100644 index 00000000..8e4ed9bb --- /dev/null +++ b/adas-core.Domain/Models/WebMessageNotification.cs @@ -0,0 +1,18 @@ +namespace adas_core.Domain.Models; + +public class WebMessageNotification +{ + //{"notification": + //{"title":"Notifications are cool", + //"body":"Know how to send notifications through Angular with this article!", + //"icon":"https://www.shareicon.net/data/256x256/2015/10/02/110808_blog_512x512.png", + //"vibrate":[100,50,100], + //"data":{"url":"https://medium.com/@arjenbrandenburgh/angulars-pwa-swpush-and-swupdate-15a7e5c154ac"}} + public string? Title { get; set; } + + public string? Body { get; set; } + + public int[]? Vibrate { get; set; } + + public string? Icon { get; set; } +} \ No newline at end of file diff --git a/adas-core.Domain/Permissions.cs b/adas-core.Domain/Permissions.cs new file mode 100644 index 00000000..fa5671fc --- /dev/null +++ b/adas-core.Domain/Permissions.cs @@ -0,0 +1,21 @@ +using adas_core.Domain.Enums; + +namespace adas_core.Domain; + +public class Permissions +{ + public SourcePermissions Write { get; set; } = new() + { + Actions = [ActionsEnum.Actions.View, ActionsEnum.Actions.Write] + }; + + public SourcePermissions Read { get; set; } = new() + { + Actions = [ActionsEnum.Actions.View] + }; +} + +public class SourcePermissions +{ + public List Actions { get; set; } = []; +} \ No newline at end of file diff --git a/adas-core.Domain/QueryCustomizer.cs b/adas-core.Domain/QueryCustomizer.cs new file mode 100644 index 00000000..3e320a73 --- /dev/null +++ b/adas-core.Domain/QueryCustomizer.cs @@ -0,0 +1,78 @@ +using System.Linq.Expressions; +using Microsoft.EntityFrameworkCore; + +namespace adas_core.Domain; + +/// +/// Allow to pass to a +/// multiple requests like: +/// * Include (to fetch eagerly relations inside T entity) +/// Use a Builder pattern. +/// +/// +/// var queryCustomizer = QueryCustomizer<Video>.New() +/// .Include(v => v.Room) +/// .Include(v => v.Clips) +/// .Include(v => v.Diagnosis) +/// .Include(v => v.Movies) +/// .Include(v => v.Patient) +/// .Include(v => v.Service) +/// .Include(v => v.Thumbnail) +/// .Include(v => v.VideoDoctors) +/// .Include(v => v.Procedure) +/// .Include(v => v.Recordings) +/// .Include(v => v.Chapters) +/// .Include(v => v.LogDownloadVideoUsers) +/// .Include("VideoDoctors.Doctor") +/// ; +/// var v = _videoRepository.GetById(id, queryCustomizer); +/// +/// +/// +public class QueryCustomizer where T : class +{ + private readonly List>> _includeLinqExpressions = []; + private readonly List _includeLinqStringExpressions = []; + + public static QueryCustomizer New() + { + return new QueryCustomizer(); + } + + public QueryCustomizer Include(Expression> includeExpression) + { + _includeLinqExpressions.Add(includeExpression); + return this; + } + + public QueryCustomizer Include(params Expression>[] includeExpressions) + { + _includeLinqExpressions.AddRange(includeExpressions); + return this; + } + + public QueryCustomizer Include(string includeExpression) + { + _includeLinqStringExpressions.Add(includeExpression); + return this; + } + + public QueryCustomizer Include(params string[] includeExpressions) + { + _includeLinqStringExpressions.AddRange(includeExpressions); + return this; + } + + + public IQueryable AddToQueryable(IQueryable queryable) + { + var resultQueryable = queryable; + resultQueryable = _includeLinqExpressions + .Aggregate(resultQueryable, (current, expression) => current.Include(expression)) + ; + resultQueryable = _includeLinqStringExpressions + .Aggregate(resultQueryable, (current, expression) => current.Include(expression)) + ; + return resultQueryable; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/AuthUtils.cs b/adas-core.Domain/Utils/AuthUtils.cs new file mode 100644 index 00000000..75babf83 --- /dev/null +++ b/adas-core.Domain/Utils/AuthUtils.cs @@ -0,0 +1,47 @@ +using adas_core.Domain.Models.Responses; + +namespace adas_core.Domain.Utils; + +public sealed class AuthUtils +{ + private LoginResponse _loginResponse = new(); + + static AuthUtils() + { + InternalInstance ??= new AuthUtils(); + } + + public AuthUtils() + { + InternalInstance = this; + } + + public LoginResponse LoginResponse + { + get + { + lock (_loginResponse) + { + return _loginResponse; + } + } + set + { + lock (_loginResponse) + { + _loginResponse = value; + } + } + } + + private static AuthUtils InternalInstance { get; set; } + public static AuthUtils Instance => InternalInstance; + + public LoginResponse GetLoginResponse() + { + lock (_loginResponse) + { + return _loginResponse; + } + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/BsonUtils.cs b/adas-core.Domain/Utils/BsonUtils.cs new file mode 100644 index 00000000..66046ab5 --- /dev/null +++ b/adas-core.Domain/Utils/BsonUtils.cs @@ -0,0 +1,26 @@ +using MongoDB.Bson; + +namespace adas_core.Domain.Utils; + +public static class BsonUtils +{ + public static object? ToObject(this BsonValue val) + { + if (val.IsInt32) return val.AsInt32; + + if (val.IsInt64) return val.AsInt64; + + if (val.IsDouble) return val.AsDouble; + + if (val.IsValidDateTime) return val.ToUniversalTime(); + + if (val.IsString) return val.AsString; + + return null; + } + + public static BsonValue? Get(this BsonDocument doc, string key) + { + return doc.TryGetValue(key, out var value) ? value : null; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/CacheUtils.cs b/adas-core.Domain/Utils/CacheUtils.cs new file mode 100644 index 00000000..2738b0ce --- /dev/null +++ b/adas-core.Domain/Utils/CacheUtils.cs @@ -0,0 +1,365 @@ +using System.Text.RegularExpressions; +using MongoDB.Bson; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; + +namespace adas_core.Domain.Utils +{ + + // TTL RESOLVER POR ENTIDAD / CLAVE + /// + /// Resuelve TTLs a partir de CacheSettings teniendo en cuenta: + /// - El backend seleccionado por entidad (None | Cache | Redis) + /// - El TTL de la entidad para ese backend + /// - Caída a GlobalSeconds del backend si no hay TTL específico + /// - Si el TTL resultante es null o menor o igual a 0 sin expiración + /// + public static class CacheKeyTtl + { + private static TimeSpan? SecondsOrNull(int? seconds) + => seconds is > 0 + ? TimeSpan.FromSeconds(seconds.Value) + : null; + + private static int? FromInMemory(Func selector, CacheSettings s) + => selector(s.InMemory.Ttl); + + private static int? FromRedis(Func? selector, CacheSettings? s) + => selector?.Invoke(s?.Redis.Ttl ?? new TtlSettings()); + + private static TimeSpan? GlobalInMemory(CacheSettings? s) + => SecondsOrNull(s?.InMemory.Ttl.GlobalSeconds); + + private static TimeSpan? GlobalRedis(CacheSettings? s) + => SecondsOrNull(s?.Redis.Ttl.GlobalSeconds); + + /// + /// Resuelve TTL para una entidad concreta, respetando el backend configurado para dicha entidad. + /// + public static TimeSpan? ResolveForEntity(CacheSettings? settings, CacheEnum.EntityType entity) + { + // Selección del backend según la entidad + if (settings == null) return null; + + var mode = entity switch + { + CacheEnum.EntityType.Patients => settings.Patients, + CacheEnum.EntityType.Displays => settings.Displays, + CacheEnum.EntityType.PumpObservations => settings.PumpObservations, + CacheEnum.EntityType.Appointments => settings.Appointments, + CacheEnum.EntityType.PontOfCare => settings.PointOfCares, + CacheEnum.EntityType.GroupedObservations => settings.GroupedObservations, + CacheEnum.EntityType.PatientObservations => settings.PatientObservations, + _ => CacheEnum.Mode.Cache + }; + + if (!settings.IsEnabled || mode == CacheEnum.Mode.None) + return null; + + // Selector de TTL específico por entidad + Func selector = entity switch + { + CacheEnum.EntityType.Patients => x => x.PatientsSeconds, + CacheEnum.EntityType.Displays => x => x.DisplaysSeconds, + CacheEnum.EntityType.PumpObservations => x => x.PumpObservationsSeconds, + CacheEnum.EntityType.Appointments => x => x.AppointmentsSeconds, + CacheEnum.EntityType.PontOfCare => x => x.PointOfCaresSeconds, + CacheEnum.EntityType.GroupedObservations => x => x.GroupedObservationsSeconds, + CacheEnum.EntityType.PatientObservations => x => x.PatientObservationsSeconds, + CacheEnum.EntityType.ConfigObservations => x => x.ConfigObservationsSeconds, + _ => x => x.GlobalSeconds + }; + + // Resolver en función del backend elegido para la entidad + return mode switch + { + CacheEnum.Mode.Cache => SecondsOrNull(FromInMemory(selector, settings)) + ?? GlobalInMemory(settings), + + CacheEnum.Mode.Redis => SecondsOrNull(FromRedis(selector, settings)) + ?? GlobalRedis(settings), + + _ => null + }; + + } + + /// + /// Dada una clave, clasifica la entidad y resuelve el TTL para esa clave. + /// + public static TimeSpan? ResolveForKey(CacheSettings settings, string key) + { + var entity = CacheKeyClassifier.Classify(key); + return ResolveForEntity(settings, entity); + } + } + + + // CACHE KEYS (GENERATION) + /// + /// Generador centralizado de claves de caché. + /// - Prefijos normalizados para que el CacheDispatcher clasifique el backend. + /// - Overloads "KeyWithTtl" para devolver (key, ttl) en una llamada. + /// + public static class CacheKeys + { + #region ConfigObservations + + public static string ConfigObservationsAll() + => "configObservations:all"; + + public static (string Key, TimeSpan? Ttl) ConfigObservationsAllKeyWithTtl( + CacheSettings? settings) + { + // lo cual usará CacheEnum.EntityType.ConfigObservations (debes añadirlo) + var key = ConfigObservationsAll(); + var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.ConfigObservations); + return (key, ttl); + } + #endregion + + #region DisplayConfig + public static string DisplayBase(ObjectId displayId) + => $"configDisplays:display:{displayId}:base"; + + public static (string Key, TimeSpan? Ttl) DisplayBaseKeyWithTtl( + CacheSettings? settings, + ObjectId displayId) + { + var key = DisplayBase(displayId); + var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Displays); + return (key, ttl); + } + + public static string DisplayWithConfig(ObjectId displayId) + => $"configDisplays:display:{displayId}:config"; + + public static (string Key, TimeSpan? Ttl) DisplayWithConfigKeyWithTtl( + CacheSettings? settings, + ObjectId displayId) + { + var key = DisplayWithConfig(displayId); + var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Displays); + return (key, ttl); + } + + #endregion + + #region PointOfCare + public static string PointOfCareBase(ObjectId pocId) + => $"pointOfCare:{pocId}:base"; + + public static (string Key, TimeSpan? Ttl) PointOfCareBaseKeyWithTtl( + CacheSettings? settings, ObjectId pocId) + { + var key = PointOfCareBase(pocId); + var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PontOfCare); + return (key, ttl); + } + + public static string PointOfCareWithInfoKeyWithTtl(ObjectId pocId) + => $"pointOfCare:{pocId}:withInfo"; + + public static (string Key, TimeSpan? Ttl) PointOfCareWithInfoKeyWithTtl( + CacheSettings? settings, ObjectId pocId) + { + var key = PointOfCareWithInfoKeyWithTtl(pocId); + var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PontOfCare); + return (key, ttl); + } + + #endregion + + #region Observations + public static string LatestObservations(ObjectId patientId, IEnumerable fieldNames, int? last = null) + => $"patients:latestObs:{patientId}:{Normalize(fieldNames)}:{last ?? 0}"; + + public static (string Key, TimeSpan? Ttl) LatestObservationsKeyWithTtl( + CacheSettings? settings, ObjectId patientId, IEnumerable fieldNames, int? last = null) + { + var key = LatestObservations(patientId, fieldNames, last); + var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PatientObservations); + return (key, ttl); + } + + #endregion + + #region PumpObservations + public static string LatestPumps(ObjectId patientId, int? last = null) + => $"pumpObs:latest:{patientId}:{last ?? 0}"; + + public static (string Key, TimeSpan? Ttl) LatestPumpsKeyWithTtl( + CacheSettings settings, ObjectId patientId, int? last = null) + { + var key = LatestPumps(patientId, last); + var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PumpObservations); + return (key, ttl); + } + + #endregion + + #region Appointments + public static string PatientAppointmentsToday(ObjectId patientId) + { + var dateKey = DateTime.UtcNow.ToString("yyyyMMdd"); + return $"appointments:patient:{patientId}:{dateKey}"; + } + + public static (string Key, TimeSpan? Ttl) PatientAppointmentsTodayKeyWithTtl( + CacheSettings? settings, + ObjectId patientId) + { + var key = PatientAppointmentsToday(patientId); + var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Appointments); + return (key, ttl); + } + + public static string PocAppointmentsToday(ObjectId pocId) + { + var dateKey = DateTime.UtcNow.ToString("yyyyMMdd"); + return $"appointments:PoC:{pocId}:{dateKey}"; + } + + public static (string Key, TimeSpan? Ttl) PocAppointmentsTodayKeyWithTtl( + CacheSettings? settings, + ObjectId pocId) + { + var key = PocAppointmentsToday(pocId); + var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PontOfCare); + return (key, ttl); + } + + #endregion + + #region Grouped Observations (claves: "groupedObs:{field}:patient:{id}") + public static string GroupedObs(ObjectId patientId, string groupedFieldName) + => $"groupedObs:{groupedFieldName}:patient:{patientId}"; + + public static (string Key, TimeSpan? Ttl) GroupedObsKeyWithTtl( + CacheSettings? settings, ObjectId patientId, string groupedFieldName) + { + var key = GroupedObs(patientId, groupedFieldName); + var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.GroupedObservations); + return (key, ttl); + } + + #endregion + + //Helpers + public static string Normalize(IEnumerable items) + => string.Join("|", items + .Where(s => !string.IsNullOrWhiteSpace(s)) + .Select(s => s.Trim()) + .OrderBy(s => s, StringComparer.Ordinal)); + } + + + // CLASSIFIER + /// + /// Clasificador de claves basado en prefijos. + /// Permite al CacheDispatcher seleccionar el backend adecuado (Redis, InMemory, None). + /// + public static class CacheKeyClassifier + { + public static CacheEnum.EntityType Classify(string key) + { + if (key.StartsWith("patients:", StringComparison.OrdinalIgnoreCase)) + return CacheEnum.EntityType.Patients; + + if (key.StartsWith("displays:", StringComparison.OrdinalIgnoreCase) || + key.StartsWith("configDisplays:", StringComparison.OrdinalIgnoreCase)) + return CacheEnum.EntityType.Displays; + + if (key.StartsWith("pumpObs:", StringComparison.OrdinalIgnoreCase)) + return CacheEnum.EntityType.PumpObservations; + + if (key.StartsWith("appointments:", StringComparison.OrdinalIgnoreCase)) + return CacheEnum.EntityType.Appointments; + + if (key.StartsWith("groupedObs:", StringComparison.OrdinalIgnoreCase)) + return CacheEnum.EntityType.GroupedObservations; + + if (key.StartsWith("configObservations:", StringComparison.OrdinalIgnoreCase)) + return CacheEnum.EntityType.ConfigObservations; + + return CacheEnum.EntityType.Unknown; + } + } + + // PATTERNS + /// + /// Patrones para DeleteByPattern (Redis) u operaciones masivas por prefijo. + /// + public static class CacheKeyPatterns + { + public static string ForEntity(CacheEnum.EntityType type) + => type switch + { + CacheEnum.EntityType.Patients => "patients:*", + CacheEnum.EntityType.Displays => "displays:*", + CacheEnum.EntityType.PumpObservations => "pumpObs:*", + CacheEnum.EntityType.Appointments => "appointments:*", + CacheEnum.EntityType.GroupedObservations => "groupedObs:*", + CacheEnum.EntityType.ConfigObservations => "configObservation:*", + _ => "*" + }; + + public static string ByPrefix(string prefix) + => $"{prefix}:*"; + + public static string ByPatient(string prefix, string patientId) + => $"{prefix}:*:{patientId}*"; + + public static string ByDate(string prefix, string date) + => $"{prefix}:*:{date}"; + } + + // INSPECTOR (diagnóstico) + /// + /// Utilidad para extraer información estructurada útil para trazas. + /// No usada por la lógica de caché, pero sí por logs y diagnósticos. + /// + public static class CacheKeyInspector + { + private static readonly Regex _patientRegex = + new(@"patients:(?[^:]+)", RegexOptions.Compiled); + + private static readonly Regex _groupedRegex = + new(@"groupedObs:(?[^:]+):(?[^:]+)", RegexOptions.Compiled); + + private static readonly Regex _appointmentDayRegex = + new(@"appointments:(?.+):(?\d{8})", RegexOptions.Compiled); + + public static string? ExtractPatientId(string key) + { + var match = _patientRegex.Match(key); + return match.Success ? match.Groups["id"].Value : null; + } + + public static (string? Field, string? PatientId) ExtractGroupedObservationInfo(string key) + { + var match = _groupedRegex.Match(key); + return !match.Success + ? (null, null) + : (match.Groups["field"].Value, match.Groups["id"].Value); + } + + public static (string? Location, DateTime? Date) ExtractAppointmentInfo(string key) + { + var match = _appointmentDayRegex.Match(key); + if (!match.Success) + return (null, null); + + var location = match.Groups["location"].Value; + var dateStr = match.Groups["date"].Value; + + if (DateTime.TryParseExact( + dateStr, "yyyyMMdd", null, + System.Globalization.DateTimeStyles.None, + out var date)) + return (location, date); + + return (location, null); + } + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/CardConfigExtensions.cs b/adas-core.Domain/Utils/CardConfigExtensions.cs new file mode 100644 index 00000000..63906fc5 --- /dev/null +++ b/adas-core.Domain/Utils/CardConfigExtensions.cs @@ -0,0 +1,40 @@ +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Domain.Utils; + +public static class CardConfigExtensions +{ + // Método principal para extraer todos los nombres + public static List GetAllObservationNames(this CardConfig config) + { + if (config.Rows == null) return []; + + // 1. Usar SelectMany para aplanar la lista de Rows a una lista de Cells. + var allCells = config.Rows.Where(r => r.Cells != null) + .SelectMany(r => r.Cells!); + + // 2. Usar SelectMany y el método recursivo para obtener todos los nombres de todas las Cells. + var observationNames = allCells.SelectMany(ExtractObservationNames) + .Distinct() // Opcional: para asegurar que los nombres sean únicos + .ToList(); + + return observationNames; + } + + // Método auxiliar RECURSIVO para extraer nombres de una Cell y sus SubObs + private static IEnumerable ExtractObservationNames(Cell cell) + { + // 1. Si la Cell tiene ObservationName, devolver esos nombres. + if (cell.ObservationName != null) + // Retornar los elementos de la lista ObservationName + foreach (var name in cell.ObservationName) + yield return name; + + // 2. Si la Cell tiene SubObs, llamar recursivamente al método para cada SubObs. + if (cell.SubObs == null) yield break; + { + foreach (var name in cell.SubObs.SelectMany(ExtractObservationNames)) + yield return name; + } + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/CollectionsUtils.cs b/adas-core.Domain/Utils/CollectionsUtils.cs new file mode 100644 index 00000000..45642395 --- /dev/null +++ b/adas-core.Domain/Utils/CollectionsUtils.cs @@ -0,0 +1,48 @@ +using System.Collections; + +namespace adas_core.Domain.Utils; + +public static class CollectionsUtils +{ + public static bool IsNullOrEmpty(this List? value) + { + return value == null || !value.Any(); + } + + public static bool IsEmptyOrNull(List? value) + { + return value == null || !value.Any(); + } + + public static bool HasElements(List? value) + { + return value != null && value.Any(); + } + + public static bool IsNotEmpty(List value) + { + return !IsEmptyOrNull(value); + } + + public static List EmptyIfNull(List? value) + { + return value ?? []; + } + + public static List? NullIfEmpty(List value) + { + return IsEmptyOrNull(value) ? null : value; + } + + public static Array ToArray(ICollection collection, Type type) + { + var result = Array.CreateInstance(type, collection.Count); + collection.CopyTo(result, 0); + return result; + } + + public static List IfEmptyOrNull(List list, List defaultList) + { + return IsEmptyOrNull(list) ? defaultList : list; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/ComparableDictionary.cs b/adas-core.Domain/Utils/ComparableDictionary.cs new file mode 100644 index 00000000..f02f236d --- /dev/null +++ b/adas-core.Domain/Utils/ComparableDictionary.cs @@ -0,0 +1,20 @@ +using System.Text; + +namespace adas_core.Domain.Utils; + +public class ComparableDictionary : Dictionary where TKey : notnull where TValue : notnull +{ + public override int GetHashCode() + { + StringBuilder str = new(); + foreach (var item in this) + { + str.Append(item.Key); + str.Append('_'); + str.Append(item.Value); + str.Append("%%"); + } + + return str.ToString().GetHashCode(); + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/ComplexObjectValueTypeSerializer.cs b/adas-core.Domain/Utils/ComplexObjectValueTypeSerializer.cs new file mode 100644 index 00000000..4a3d1632 --- /dev/null +++ b/adas-core.Domain/Utils/ComplexObjectValueTypeSerializer.cs @@ -0,0 +1,314 @@ +using System.Collections; +using System.Text.RegularExpressions; +using adas_core.Domain.Models; +using adas_core.Domain.Models.Masters; +using MongoDB.Bson; +using MongoDB.Bson.IO; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using Newtonsoft.Json; +using JsonConvert = Newtonsoft.Json.JsonConvert; + +namespace adas_core.Domain.Utils; + +public partial class ComplexObjectValueTypeSerializer : SerializerBase +{ + [GeneratedRegex("ObjectId\\((.[a-f0-9]{24}.)\\)")] + private static partial Regex ObjectIdRegex(); + + public override object Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + try + { + switch (context.Reader.GetCurrentBsonType()) + { + case BsonType.Null: + context.Reader.ReadNull(); + return false; + case BsonType.Boolean: return context.Reader.ReadBoolean(); + case BsonType.Double: return context.Reader.ReadDouble(); + case BsonType.String: return context.Reader.ReadString(); + case BsonType.Int32: return context.Reader.ReadInt32(); + case BsonType.Int64: return context.Reader.ReadInt64(); + case BsonType.DateTime: return context.Reader.ReadDateTime(); + case BsonType.Decimal128: return context.Reader.ReadDecimal128(); + case BsonType.ObjectId: return context.Reader.ReadObjectId(); + case BsonType.EndOfDocument: + context.Reader.ReadEndDocument(); + return false; + case BsonType.Binary: return context.Reader.ReadBytes(); + case BsonType.Undefined: + context.Reader.ReadUndefined(); + return false; + case BsonType.RegularExpression: return context.Reader.ReadRegularExpression(); + case BsonType.JavaScript: return context.Reader.ReadJavaScript(); + case BsonType.Symbol: return context.Reader.ReadSymbol(); + case BsonType.JavaScriptWithScope: return context.Reader.ReadJavaScriptWithScope(); + case BsonType.Timestamp: return context.Reader.ReadTimestamp(); + case BsonType.MinKey: + context.Reader.ReadMinKey(); + return false; + case BsonType.MaxKey: + context.Reader.ReadMaxKey(); + return false; + case BsonType.Document: + //default: + { + //if (context.Reader.GetCurrentBsonType() != BsonType.Document) + // throw new Exception("Unexpected BsonType: " + context.Reader.GetCurrentBsonType()); + var bsonDocument = + BsonSerializer.Deserialize(context.Reader, typeof(BsonDocument)) as BsonDocument ?? + throw new Exception("BsonDocument is null"); + var t = bsonDocument["_t"].AsString ?? + throw new Exception("Can't deserialize object. No type found."); + var isArray = t.EndsWith("]"); + + string dirtyJson; + if (isArray) + { + if (!bsonDocument.TryGetValue("_v", out var arrayValue)) + throw new Exception("Can't deserialize object. No array value found."); + dirtyJson = arrayValue.ToJson(); + bsonDocument.Remove("_v"); + + if (t.Contains("String[]")) + t = "System.String[], mscorlib"; + } + else + { + if (!t.Contains('.')) + t = $"adas-core.Domain.Models.{t}"; + if (t.Contains( + "adas-core.Models")) //esta comprobación se hace porque al cambiar a clean code en la BBDD se estaba guardando el espacio de nombres antiguo + t = t.Replace("adas-core.Models", "adas-core.Domain.Models"); + bsonDocument.Remove("_t"); + + dirtyJson = bsonDocument.ToJson(); + } + var settings = new JsonSerializerSettings + { + Converters = { new ObjectIdConverter() } + }; + var cleanJson = ObjectIdRegex().Replace(dirtyJson, m => m.Groups[1].Value); + + var type = Type.GetType(t) ?? throw new Exception($"Type not found: {t}"); + + return JsonConvert.DeserializeObject(cleanJson, type, settings) ?? + throw new Exception("Can't deserialize object"); + } + case BsonType.Array: + { + var bsonArray = BsonSerializer.Deserialize(context.Reader); + var dirtyJsonArray = bsonArray.ToJson(new JsonWriterSettings + { + OutputMode = JsonOutputMode.CanonicalExtendedJson + }); + + // Convertir el BsonArray completo a una cadena JSON (dirtyJsonArray contiene {"$oid": "..."}) + //var dirtyJsonArray = bsonArray.ToJson(); + + // Obtener el tipo de los valores del array (necesitas esto incluso para el caso vacío) + var elementType = GetElementTypeFromBsonArray(bsonArray); + + if (bsonArray.Count == 0) + { + // Caso Array Vacío + // Si el tipo se pudo determinar (p. ej., si la información está en metadatos), usa ese tipo. + // Si no, devuelve una List vacía. + if (elementType == null) return new List(); + + var elementTypeListEmpty = typeof(List<>).MakeGenericType(elementType); + return Activator.CreateInstance(elementTypeListEmpty)!; + } + + // El array no está vacío. Ahora revisamos el tipo. + if (elementType == null) + throw new Exception("No se pudo determinar el tipo de los elementos del array."); + + // APLICAR LA LIMPIEZA DE ObjectId AL JSON COMPLETO DEL ARRAY + // Esto cambia: {"_id": {"$oid": "..."}} a: {"_id": "..."} + var cleanJsonArray = ObjectIdRegex().Replace(dirtyJsonArray, m => m.Groups[1].Value); + cleanJsonArray = Regex.Replace( + cleanJsonArray, + "\"_id\"\\s*:\\s*\\{\\s*\"\\$oid\"\\s*:\\s*\"([^\"]+)\"\\s*\\}", + "\"id\":\"$1\"" + ); + var elementTypeList = typeof(List<>).MakeGenericType(elementType); + var settings = new JsonSerializerSettings + { + Converters = { new ObjectIdConverter() } + }; + // Usar el JSON limpio para la deserialización + return JsonConvert.DeserializeObject(cleanJsonArray, elementTypeList, settings) + ?? throw new Exception("Can't deserialize BSON array."); + } + default: + throw new Exception("Unexpected BsonType: " + context.Reader.GetCurrentBsonType()); + } + } + catch (Exception ex) + { + throw new Exception("Unexpected BsonType: " + ex.GetBaseException()); + } + } + + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value) + { + switch (value) + { + case null: + context.Writer.WriteNull(); + return; + case bool b: + BooleanSerializer.Instance.Serialize(context, args, b); + return; + case double b: + DoubleSerializer.Instance.Serialize(context, args, b); + return; + case string b: + StringSerializer.Instance.Serialize(context, args, b); + return; + case int b: + Int32Serializer.Instance.Serialize(context, args, b); + return; + case long b: + Int64Serializer.Instance.Serialize(context, args, b); + return; + case Decimal128 b: + new Decimal128Serializer().Serialize(context, args, b); + return; + case DateTime b: + new DateTimeSerializer().Serialize(context, args, b); + return; + case OptionList optionList: + BsonSerializer.Serialize(context.Writer, optionList.GetType(), optionList); + return; + case IList optionListList: + BsonSerializer.Serialize(context.Writer, optionListList.GetType(), optionListList); + return; + case PatientAllergiesValue pav: + BsonSerializer.Serialize(context.Writer, pav.GetType(), pav); + return; + case IList pavList: + BsonSerializer.Serialize(context.Writer, pavList.GetType(), pavList); + return; + + case ICollection i: + var t = i.GetType().GetElementType() ?? typeof(object); + var serializerType = typeof(ArraySerializer<>).MakeGenericType(t); + var ctor = serializerType.GetConstructor([]); + var serializer = ctor?.Invoke([]); + var serializeMethod = serializerType.GetMethod("Serialize"); + var array = CollectionsUtils.ToArray(i, t); + serializeMethod?.Invoke(serializer!, [context, args, array]); + return; + default: + var json = JsonConvert.SerializeObject(value); + var document = BsonDocument.Parse(json); + document["_t"] = value.GetType().FullName; + BsonSerializer.Serialize(context.Writer, typeof(BsonDocument), document); + return; + } + } + + private Type? GetElementTypeFromBsonArray(BsonArray bsonArray) + { + if (bsonArray.Count == 1 && bsonArray.Values.Any(c => c.IsBsonNull)) + return typeof(OptionList); + foreach (var element in bsonArray) + if (element is BsonDocument bsonDocument) + { + if (bsonDocument.Contains("_t")) + { + var t = bsonDocument["_t"].AsString; + if (t.Contains('.')) + { + t = $"adas-core_Domain.Models.{t}"; + t = t.Replace("adas-core_Models", "adas-core_Domain.Models"); + } + + var type = Type.GetType(t); + if (type != null) return type; + } + else + { + var instance = BsonSerializer.Deserialize(bsonDocument, typeof(OptionList)); + if (instance != null && bsonDocument.Contains("name")) return typeof(OptionList); + instance = BsonSerializer.Deserialize(bsonDocument, typeof(List)); + if (instance != null) + return typeof(List); + + instance = BsonSerializer.Deserialize(bsonDocument, typeof(PatientAllergiesValue)); + if (instance != null) + return typeof(PatientAllergiesValue); + + instance = BsonSerializer.Deserialize(bsonDocument, typeof(PatientDrainagesValue)); + if (instance != null) + return typeof(PatientDrainagesValue); + + instance = BsonSerializer.Deserialize(bsonDocument, typeof(PatientIntravenousLinesValue)); + if (instance != null) + return typeof(PatientIntravenousLinesValue); + } + } + + return null; + } + + public class NullIgnoringListSerializer : SerializerBase> + { + public override List Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + var bsonType = context.Reader.GetCurrentBsonType(); + if (bsonType == BsonType.Null) + { + context.Reader.ReadNull(); + return []; + } + + var list = new List(); + context.Reader.ReadStartArray(); + while (context.Reader.ReadBsonType() != BsonType.EndOfDocument) + { + var element = BsonSerializer.Deserialize(context.Reader); + if (element != null) list.Add(element); + } + + context.Reader.ReadEndArray(); + return list; + } + + public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, List value) + { + context.Writer.WriteStartArray(); + foreach (var item in value) BsonSerializer.Serialize(context.Writer, item?.GetType(), item); + context.Writer.WriteEndArray(); + } + } + // public class IgnoreEmptyStringSerializer : SerializerBase + // { + // public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, string value) + // { + // if (string.IsNullOrEmpty(value)) + // { + // context.Writer.WriteNull(); + // } + // else + // { + // context.Writer.WriteString(value); + // } + // } + // + // public override string Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + // { + // if (string.IsNullOrEmpty(context.Reader.ReadString())) + // { + // return ""; + // } + // else + // { + // return context.Reader.ReadString(); + // } + // } + // } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/CryptoAdas.cs b/adas-core.Domain/Utils/CryptoAdas.cs new file mode 100644 index 00000000..80741982 --- /dev/null +++ b/adas-core.Domain/Utils/CryptoAdas.cs @@ -0,0 +1,52 @@ +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using adas_core.Domain.Models.GroupedObservations; +using MongoDB.Bson; + +namespace adas_core.Domain.Utils; + +public static class CryptoAdas +{ + private static readonly Regex PassRegex = new("^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,}$"); + + public static string CreateMd5(string input) + { + using var md5 = MD5.Create(); + var inputBytes = Encoding.ASCII.GetBytes(input); + var hashBytes = md5.ComputeHash(inputBytes); + + var sb = new StringBuilder(); + for (var i = hashBytes.Length - 1; i >= 0; i--) sb.Append(hashBytes[i].ToString("X2")); + + var res = sb.ToString(); + return res; + } + + public static string CreateMd5GroupedObs(GroupedField gF, ObjectId patientId) + { + var names = CollectionsUtils.IfEmptyOrNull(gF.Names, [gF.Name ?? string.Empty]); + + return CreateMd5(patientId + + string.Join("+", names.Select(c => c.ToString())) + + gF.Max + + gF.Since + + gF.Regularity + + string.Join("+", gF.Result.Select(c => c.ToString()))); + } + + public static string CreateBCrypt(string input) + { + return BCrypt.Net.BCrypt.HashPassword(input); + } + + public static bool VerifyPassword(string loginPass, string passwordHash) + { + return BCrypt.Net.BCrypt.Verify(loginPass, passwordHash); + } + + public static bool IsStrongPassword(string password) + { + return PassRegex.IsMatch(password); + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/DetailConfigExtension.cs b/adas-core.Domain/Utils/DetailConfigExtension.cs new file mode 100644 index 00000000..7bdceb56 --- /dev/null +++ b/adas-core.Domain/Utils/DetailConfigExtension.cs @@ -0,0 +1,53 @@ +using adas_core.Domain.Models.MongoModels; + +namespace adas_core.Domain.Utils; + +public static class DetailConfigExtension +{ + // Método principal para iniciar la extracción + public static List GetAllObservationNames(this CardDetailsConfig config) + { + if (config.NurseRows == null) return []; + + // Se usa el método auxiliar para recorrer todas las RowDetailsConfig + var observationNames = ExtractNamesFromRows(config.NurseRows) + .Distinct() // Opcional: para nombres únicos + .ToList(); + + return observationNames; + } + + // --- Auxiliar 1: Recorre la anidación de Filas (RowDetailsConfig) --- + private static IEnumerable ExtractNamesFromRows(List rows) + { + foreach (var row in rows) + { + // 1. EXTRAER de las CELDAS (Cells) + if (row.Cells != null) + // Usar SelectMany para aplanar los resultados del método recursivo de Celdas + foreach (var name in row.Cells.SelectMany(ExtractNamesFromCells)) + yield return name; + + // 2. EXTRAER de las FILAS ANIDADAS (Rows) + if (row.Rows != null) + // Llamada recursiva: Volver a este mismo método para procesar las filas anidadas + foreach (var name in ExtractNamesFromRows(row.Rows)) + yield return name; + } + } + + // --- Auxiliar 2: Recorre la anidación de Celdas (CellDetails) --- + private static IEnumerable ExtractNamesFromCells(CellDetails cell) + { + // 1. EXTRAER nombres del nivel actual + if (cell.ObservationName != null) + foreach (var name in cell.ObservationName) + yield return name; + + // 2. EXTRAER de las CELDAS ANIDADAS (Cells) + if (cell.Cells != null) + // Llamada recursiva: Volver a este mismo método para procesar las celdas anidadas + foreach (var name in cell.Cells.SelectMany(ExtractNamesFromCells)) + yield return name; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/DictionaryEx.cs b/adas-core.Domain/Utils/DictionaryEx.cs new file mode 100644 index 00000000..dfba26e9 --- /dev/null +++ b/adas-core.Domain/Utils/DictionaryEx.cs @@ -0,0 +1,24 @@ +namespace adas_core.Domain.Utils; + +public static class DictionaryEx +{ + public static TV? GetValue(this IDictionary dict, TK key, TV? defaultValue = default) + { + return dict.TryGetValue(key, out var value) ? value : defaultValue; + } + + public static string ToListString(this IDictionary dict, string itemSeparator = ", ", + string keySeparator = ": ") + { + var s = dict.Keys.Select(key => key + keySeparator + dict[key]).ToList(); + + return string.Join(itemSeparator, s); + } + + public static TValue? TryGetAndReturn(this IDictionary dictionary, TKey key) + where TKey : notnull where TValue : class + { + if (!dictionary.TryGetValue(key, out var retValue)) retValue = null; + return retValue; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/EnumUtils.cs b/adas-core.Domain/Utils/EnumUtils.cs new file mode 100644 index 00000000..a0cd175c --- /dev/null +++ b/adas-core.Domain/Utils/EnumUtils.cs @@ -0,0 +1,20 @@ +using System.ComponentModel; +using System.Reflection; + +namespace adas_core.Domain.Utils; + +public static class EnumUtils +{ + public static string GetDescription(Enum value) + { + var enumMember = value.GetType().GetMember(value.ToString()).FirstOrDefault(); + var descriptionAttribute = + enumMember == null + ? null + : enumMember.GetCustomAttribute(typeof(DescriptionAttribute)) as DescriptionAttribute; + return + descriptionAttribute == null + ? value.ToString() + : descriptionAttribute.Description; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/EquatableDictionary.cs b/adas-core.Domain/Utils/EquatableDictionary.cs new file mode 100644 index 00000000..7d18bca0 --- /dev/null +++ b/adas-core.Domain/Utils/EquatableDictionary.cs @@ -0,0 +1,43 @@ +namespace adas_core.Domain.Utils; + +public sealed class EquatableDictionary + : Dictionary, IEquatable> + where TKey : notnull where TValue : notnull +{ + public bool Equals(ComparableDictionary? other) + { + if (other is null) return false; + if (Count != other.Count) return false; + foreach (var pair in this) + { + if (!other.TryGetValue(pair.Key, out var otherValue)) return false; + if (!EqualityComparer.Default.Equals(pair.Value, otherValue)) return false; + } + + return true; + } + //private readonly Dictionary dictionary = new(); + + public override bool Equals(object? other) + { + return Equals(other as ComparableDictionary); + } + + public override int GetHashCode() + { + var hash = 0; + foreach (var pair in this) + { + var miniHash = 17; + miniHash = miniHash * 31 + + EqualityComparer.Default.GetHashCode(pair.Key); + miniHash = miniHash * 31 + + EqualityComparer.Default.GetHashCode(pair.Value); + hash ^= miniHash; + } + + return hash; + } + + // Implementation of IDictionary<,> which just delegates to the dictionary +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/GlobalData.cs b/adas-core.Domain/Utils/GlobalData.cs new file mode 100644 index 00000000..169107cf --- /dev/null +++ b/adas-core.Domain/Utils/GlobalData.cs @@ -0,0 +1,15 @@ +namespace adas_core.Domain.Utils; + +public static class GlobalData +{ + public static Dictionary Data = new(); + + public static void AddData(string key, object value) + { + var exists = Data.ContainsKey(key); + if (exists) + Data[key] = value; + else + Data.Add(key, value); + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/Hl7Utils.cs b/adas-core.Domain/Utils/Hl7Utils.cs new file mode 100644 index 00000000..930953d4 --- /dev/null +++ b/adas-core.Domain/Utils/Hl7Utils.cs @@ -0,0 +1,61 @@ +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Logging; + +namespace adas_core.Domain.Utils; + +public static class Hl7Utils +{ + /// + /// Evalúa si se debe permitir el auto ADT basándose en la configuración de la unidad actual y la antigua. + /// + /// Configuración de la unidad (Location) + /// Configuración de la antigua unidad (OldLocation) + /// Logger para registrar advertencias + /// Tipo de mensaje HL7 que se va a procesar su entrada + /// true si se permite; false en caso contrario. + public static bool ManageAutoAdt(Unit? unitConfig, Unit? oldUnitConfig, ILogger logger, string type) + { + // Variables de estado: + var locationKnown = unitConfig != null; + var oldLocationKnown = oldUnitConfig != null; + var locationAutoAdt = unitConfig?.Configuration.AutoAdt ?? false; + var oldLocationAutoAdt = oldUnitConfig?.Configuration.AutoAdt ?? false; + + // Creamos una clave que combine ambas informaciones: + // Formato: "LK-LA:OLK-OLA" donde LK = locationKnown (1=si, 0=no) y LA = locationAutoAdt (1=si, 0=no) + var key = $"{(locationKnown ? "1" : "0")}-" + + $"{(locationAutoAdt ? "1" : "0")}/" + + $"{(oldLocationKnown ? "1" : "0")}-" + + $"{(oldLocationAutoAdt ? "1" : "0")}"; + + // Evaluamos la clave según los escenarios: + // Caso 1: "1-1/1-1" → Ambas conocidas y AutoAdt true → Permitido. + // Caso 2: "1-0/1-1" → Ambas conocidas, pero location.AutoAdt false → No permitido. + // Caso 3: "0-0/1-1" → Location desconocida, oldLocation conocida con AutoAdt true → Permitido. + // Caso 4: "1-1/0-0" → Location conocida con AutoAdt true, oldLocation desconocida → Permitido. + // Caso 5: "1-1/1-0" → Ambas conocidas, pero oldLocation.AutoAdt false → No permitido. + // Caso 6: "1-0/1-0" → Ambas conocidas, ambas con AutoAdt false → No permitido. + // Caso 7: "0-0/1-0" → Location desconocida, pero oldLocation.AutoAdt false → No permitido. + // Caso 8: "1-0/0-0" → Location conocida con AutoAdt false, y oldLocation desconocida → No permitido. + // Caso 9: "0-0/0-0" → Ambas desconocidas → No permitido. + switch (key) + { + case "1-1/1-1": // Caso 1 + case "0-0/1-1": // Caso 3 + case "1-1/0-0": // Caso 4 + return true; + case "1-0/1-1": // Caso 2 + case "1-1/1-0": // Caso 5 + case "1-0/1-0": // Caso 6 + case "0-0/1-0": // Caso 7 + case "1-0/0-0": // Caso 8 + logger.LogWarning( + "Deny permission for auto {HL7} on Location: {Location}, OldLocation: {OldLocation} check: {Key}", + type, unitConfig?.Name, oldUnitConfig?.Name, key); + return false; + default: // Caso 9 u otro escenario no contemplado + logger.LogWarning("Both units are unknown on Auto {HL7} check: {Key}", type, key); + return false; + } + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/Interfaces/IMappingUtils.cs b/adas-core.Domain/Utils/Interfaces/IMappingUtils.cs new file mode 100644 index 00000000..c028c64d --- /dev/null +++ b/adas-core.Domain/Utils/Interfaces/IMappingUtils.cs @@ -0,0 +1,9 @@ +namespace adas_core.Domain.Utils.Interfaces; + +public interface IMappingUtils +{ + (string type, string name, string group)? SearchByCode(object code, string category); + int GetComplexityValue(string name, double? peso = null); + + // void ConvertStringToDoubleInMapping(); +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/JsonExtensions.cs b/adas-core.Domain/Utils/JsonExtensions.cs new file mode 100644 index 00000000..a4f06e9e --- /dev/null +++ b/adas-core.Domain/Utils/JsonExtensions.cs @@ -0,0 +1,16 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Bson; + +namespace adas_core.Domain.Utils; + +public static class JsonExtensions +{ + public static void CopyToBson(string inputPath, string outputPath, FileMode fileMode = FileMode.CreateNew) + { + using var textReader = File.OpenText(inputPath); + using var jsonReader = new JsonTextReader(textReader); + using var oFileStream = new FileStream(outputPath, fileMode); + using var dataWriter = new BsonDataWriter(oFileStream); + dataWriter.WriteToken(jsonReader); + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/JwtHelper.cs b/adas-core.Domain/Utils/JwtHelper.cs new file mode 100644 index 00000000..371b7649 --- /dev/null +++ b/adas-core.Domain/Utils/JwtHelper.cs @@ -0,0 +1,70 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Text; +using Microsoft.IdentityModel.Tokens; + +namespace adas_core.Domain.Utils; + +public abstract class JwtHelper +{ + public static JwtSecurityToken GetJwtToken( + string username, + string secret, + string issuer, + string audience, + int expiration, + Claim[]? additionalClaims = null + ) + { + var claims = new[] + { + new Claim(JwtRegisteredClaimNames.Sub, username), + new Claim(ClaimTypes.Name, username), + new Claim(ClaimTypes.NameIdentifier, username), + // this guarantees the token is unique + new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) + }; + + if (additionalClaims != null) + { + var claimList = new List(claims); + claimList.AddRange(additionalClaims); + claims = claimList.ToArray(); + } + + var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)); + + var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); + + var expireDate = DateTime.UtcNow.AddMinutes(expiration); + + return new JwtSecurityToken( + issuer, + audience, + expires: expireDate, + claims: claims, + signingCredentials: creds + ); + } + + + public static string GenerateRefreshToken() + { + var randomNumber = new byte[64]; + using var rng = RandomNumberGenerator.Create(); + rng.GetBytes(randomNumber); + return Convert.ToBase64String(randomNumber); + } + + /// + /// return username from claim + /// + /// + /// + public static string? GetUsernameFromPrincipal(ClaimsPrincipal principal) + { + var nameClaim = principal.Claims.FirstOrDefault(c => c.Type.Equals(ClaimTypes.NameIdentifier)); + return nameClaim?.Value; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/Mapper.cs b/adas-core.Domain/Utils/Mapper.cs new file mode 100644 index 00000000..bb55c8ad --- /dev/null +++ b/adas-core.Domain/Utils/Mapper.cs @@ -0,0 +1,57 @@ +using System.Dynamic; +using System.Reflection; + +namespace adas_core.Domain.Utils; + +public static class Mapper + // We can only use reference types + where T : class +{ + private static readonly Dictionary PropertyMap; + + static Mapper() + { + // At this point we can convert each + // property name to lower case so we avoid + // creating a new string more than once. + PropertyMap = + typeof(T) + .GetProperties() + .ToDictionary( + p => p.Name.ToLower(), + p => p + ); + } + + public static void Map(ExpandoObject source, T destination) + { + // Might as well take care of null references early. + if (source == null) throw new ArgumentNullException(nameof(source)); + + if (destination == null) throw new ArgumentNullException(nameof(destination)); + + // By iterating the KeyValuePair of + // source we can avoid manually searching the keys of + // source as we see in your original code. + foreach (var kv in source) + if (PropertyMap.TryGetValue(kv.Key.ToLower(), out var p)) + { + var propType = p.PropertyType; + if (kv.Value == null) + { + if (propType is { IsByRef: false } && propType.Name != "Nullable`1") + // Throw if type is a value type + // but not Nullable<> + throw new ArgumentException("not nullable"); + } + else if (kv.Value.GetType() != propType) + { + // You could make this a bit less strict + // but I don't recommend it. + p.SetValue(destination, Convert.ToDouble(kv.Value), null); + } + + p.SetValue(destination, kv.Value, null); + } + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/MappingUtils.cs b/adas-core.Domain/Utils/MappingUtils.cs new file mode 100644 index 00000000..192f159b --- /dev/null +++ b/adas-core.Domain/Utils/MappingUtils.cs @@ -0,0 +1,185 @@ +using System.Globalization; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Utils.Interfaces; +using Microsoft.Extensions.Options; + +namespace adas_core.Domain.Utils; + +public sealed class MappingUtils : IMappingUtils +{ + private readonly List? _cccData; + private bool _isTransformedValue; + + public MappingUtils(IOptions apiSettings) + { + var cccMappingData = apiSettings.Value.MappingInterventions; + + _cccData = cccMappingData; + _isTransformedValue = false; + } + +/* + public (string Tipo, string Nombre)? SearchByCode(double code) + { + ConvertStringToDoubleInMapping(); + var data = _cccData; + + foreach (var dato in data) + { + foreach (var rango in dato.Codes) + { + if (rango.FinalValue == null || rango.FinalValue.Equals("")) // Es un valor unico + { + if ((double)rango.InitialValue == code) + return (dato.Type, dato.Name); + } + else // Es un rango + { + if (code >= (double)rango.InitialValue && code <= (double)rango.FinalValue) + return (dato.Type, dato.Name); + } + } + } + + return null; // No se encontro el code + }*/ + public (string type, string name, string group)? SearchByCode(object code, string category) + { + // esta funcion conviete a double los string que permitan conversion si no se puede los deja como string + ConvertStringToDoubleInMapping(); + + var data = _cccData; + + if (data == null) + return null; + + // Si el parámetro es un double, buscar directamente en los valores numéricos + if (code is double searchValue) + { + foreach (var dato in data) + foreach (var rango in dato.Codes) + if (rango.FinalValue is null or "") // Es un valor unico + { + if (rango.InitialValue is double initialDouble && Math.Abs(initialDouble - searchValue) == 0 && + category == dato.Category.ToString()) + return (dato.Type, dato.Name, dato.Group)!; + } + else // Es un rango + { + if (rango is { InitialValue: double initialDouble, FinalValue: double finalDouble } && + searchValue >= initialDouble && searchValue <= finalDouble + && category == dato.Category.ToString()) + return (dato.Type, dato.Name, dato.Group)!; + } + } + // Si el parámetro es un string + else if (code is string searchString) + { + // Intentar convertir el string a double para buscar entre valores numericos + if (double.TryParse(searchString, NumberStyles.Float, CultureInfo.InvariantCulture, + out var searchValueAsDouble)) + { + foreach (var dato in data) + foreach (var rango in dato.Codes) + if (rango.FinalValue is null or "") // Es un valor unico + { + if (rango.InitialValue is double initialDouble && + Math.Abs(initialDouble - searchValueAsDouble) == 0 && category == dato.Category.ToString()) + return (dato.Type, dato.Name, dato.Group)!; + } + else // Es un rango + { + if (rango is { InitialValue: double initialDouble, FinalValue: double finalDouble } && + searchValueAsDouble >= initialDouble && searchValueAsDouble <= finalDouble + && category == dato.Category.ToString()) + return (dato.Type, dato.Name, dato.Group)!; + } + } + else + { + // Si no es convertible a double, buscar directamente entre los valores de tipo string + foreach (var dato in data) + foreach (var rango in dato.Codes) + { + if (rango.InitialValue is string initialString && initialString == searchString && + category == dato.Category.ToString()) + return (dato.Type, dato.Name, dato.Group)!; + + if (rango.FinalValue is string finalString && finalString == searchString && + category == dato.Category.ToString()) + return (dato.Type, dato.Name, dato.Group)!; + } + } + } + + // No se encontro el codigo + return null; + } + + + public int GetComplexityValue(string name, double? peso = null) + { + ConvertStringToDoubleInMapping(); + var data = _cccData; + + if (data == null) return 0; // No encontro ningun nombre que conincida con el dado + + foreach (var dato in data) + if (name.Equals(dato.Name)) + { + if (dato.Value is + { + Count: 1 + }) //value es una lista, si contiene un solo elemento retornaremos el initial value de ese elemento + return Convert.ToInt32(dato.Value[0].InitialValue); + + if (dato.Value is { Count: > 1 } && peso != null) + foreach (var rango in dato.Value) + if (peso >= (double?)rango.InitialValue && peso <= (double?)rango.FinalValue) + return rango.ValueContributed; + } + + return 0; // No encontro ningun nombre que conincida con el dado + } + + private void ConvertStringToDoubleInMapping() + { + // esto es para garantizar que el punto (".") sea reconocido como separador decimal + var culture = CultureInfo.InvariantCulture; + if (_isTransformedValue) + { + } + else if (_cccData != null) + { + foreach (var mapping in _cccData) + { + // Recorrer la lista Codes + foreach (var code in mapping.Codes) + { + if (code.InitialValue is string initialValueString && + double.TryParse(initialValueString, culture, out var initialValueDouble)) + code.InitialValue = initialValueDouble; // Convertir y guardar como double + + if (code.FinalValue is string finalValueString && + double.TryParse(finalValueString, culture, out var finalValueDouble)) + code.FinalValue = finalValueDouble; // Convertir y guardar como double + } + + // Recorrer la lista Value (si no es nula) + if (mapping.Value != null) + foreach (var value in mapping.Value) + { + if (value.InitialValue is string initialValueString && + double.TryParse(initialValueString, culture, out var initialValueDouble)) + value.InitialValue = initialValueDouble; // Convertir y guardar como double + + if (value.FinalValue is string finalValueString && + double.TryParse(finalValueString, culture, out var finalValueDouble)) + value.FinalValue = finalValueDouble; // Convertir y guardar como double + } + } //Fin ford + + _isTransformedValue = true; + } + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/MediaUtils.cs b/adas-core.Domain/Utils/MediaUtils.cs new file mode 100644 index 00000000..5157da50 --- /dev/null +++ b/adas-core.Domain/Utils/MediaUtils.cs @@ -0,0 +1,120 @@ +using File = TagLib.File; + +namespace adas_core.Domain.Utils; + +public static class MediaUtils +{ + public static readonly Dictionary Mapping = new(StringComparer.InvariantCultureIgnoreCase) + { + { ".aac", "audio/aac" }, + { ".abw", "application/x-abiword" }, + { ".apng", "image/apng" }, + { ".arc", "application/x-freearc" }, + { ".avif", "image/avif" }, + { ".avi", "video/x-msvideo" }, + { ".azw", "application/vnd.amazon.ebook" }, + { ".bin", "application/octet-stream" }, + { ".bmp", "image/bmp" }, + { ".bz", "application/x-bzip" }, + { ".bz2", "application/x-bzip2" }, + { ".cda", "application/x-cdf" }, + { ".csh", "application/x-csh" }, + { ".css", "text/css" }, + { ".csv", "text/csv" }, + { ".doc", "application/msword" }, + { ".docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document" }, + { ".eot", "application/vnd.ms-fontobject" }, + { ".epub", "application/epub+zip" }, + { ".gz", "application/gzip" }, + { ".gif", "image/gif" }, + { ".htm", "text/html" }, + { ".html", "text/html" }, + { ".ico", "image/vnd.microsoft.icon" }, + { ".ics", "text/calendar" }, + { ".jar", "application/java-archive" }, + { ".jpeg", "image/jpeg" }, + { ".jpg", "image/jpeg" }, + { ".js", "text/javascript" }, + { ".json", "application/json" }, + { ".jsonld", "application/ld+json" }, + { ".mid", "audio/midi" }, + { ".midi", "audio/midi" }, + { ".mjs", "text/javascript" }, + { ".mp3", "audio/mpeg" }, + { ".mp4", "video/mp4" }, + { ".mpeg", "video/mpeg" }, + { ".mpkg", "application/vnd.apple.installer+xml" }, + { ".odp", "application/vnd.oasis.opendocument.presentation" }, + { ".ods", "application/vnd.oasis.opendocument.spreadsheet" }, + { ".odt", "application/vnd.oasis.opendocument.text" }, + { ".oga", "audio/ogg" }, + { ".ogv", "video/ogg" }, + { ".ogx", "application/ogg" }, + { ".opus", "audio/ogg" }, + { ".otf", "font/otf" }, + { ".png", "image/png" }, + { ".pdf", "application/pdf" }, + { ".php", "application/x-httpd-php" }, + { ".ppt", "application/vnd.ms-powerpoint" }, + { ".pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation" }, + { ".rar", "application/vnd.rar" }, + { ".rtf", "application/rtf" }, + { ".sh", "application/x-sh" }, + { ".svg", "image/svg+xml" }, + { ".tar", "application/x-tar" }, + { ".tif", "image/tiff" }, + { ".tiff", "image/tiff" }, + { ".ts", "video/mp2t" }, + { ".ttf", "font/ttf" }, + { ".txt", "text/plain" }, + { ".vsd", "application/vnd.visio" }, + { ".wav", "audio/wav" }, + { ".weba", "audio/webm" }, + { ".webm", "video/webm" }, + { ".webp", "image/webp" }, + { ".woff", "font/woff" }, + { ".woff2", "font/woff2" }, + { ".xhtml", "application/xhtml+xml" }, + { ".xls", "application/vnd.ms-excel" }, + { ".xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }, + { ".xml", "application/xml" }, + { ".xul", "application/vnd.mozilla.xul+xml" }, + { ".zip", "application/zip" }, + { ".3gp", "video/3gpp" }, + { ".3g2", "video/3gpp2" }, + { ".7z", "application/x-7z-compressed" }, + { ".flv", "video/x-flv" }, + { ".m3u8", "application/x-mpegURL" }, + { ".mov", "video/quicktime" }, + { ".wmv", "video/x-ms-wmv" } + }; + + public static string GetMimeType(string filePath) + { + var extension = Path.GetExtension(filePath).ToLowerInvariant(); + return Mapping.TryGetValue(extension, out var mimeType) ? mimeType : "application/octet-stream"; + } + + public static bool IsVideoFile(string filePath) + { + // Extensiones de video soportadas + var videoExtensions = new[] + { ".mp4", ".avi", ".mkv", ".mpeg", ".ogv", ".webm", ".3gp", ".3g2", ".mov", ".wmv", ".m3u8" }; + return videoExtensions.Contains(Path.GetExtension(filePath), StringComparer.InvariantCultureIgnoreCase); + } + + public static long? GetVideoDuration(string videoPath) + { + try + { + var file = File.Create(videoPath); + var duration = file.Properties.Duration; + return (long)duration.TotalMilliseconds; + } + catch (Exception ex) + { + Console.WriteLine($"Error: {ex.Message}"); + return null; + } + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/NumberUtils.cs b/adas-core.Domain/Utils/NumberUtils.cs new file mode 100644 index 00000000..4f1c424e --- /dev/null +++ b/adas-core.Domain/Utils/NumberUtils.cs @@ -0,0 +1,24 @@ +namespace adas_core.Domain.Utils; + +public static class NumberUtils +{ + public static bool IsNumber(this object value) + { + return value is sbyte + || value is byte + || value is short + || value is ushort + || value is int + || value is uint + || value is long + || value is ulong + || value is float + || value is double + || value is decimal; + } + + public static double ToDouble(this object value) + { + return Convert.ToDouble(value); + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/ObjectIdConverter.cs b/adas-core.Domain/Utils/ObjectIdConverter.cs new file mode 100644 index 00000000..646ff213 --- /dev/null +++ b/adas-core.Domain/Utils/ObjectIdConverter.cs @@ -0,0 +1,41 @@ +using MongoDB.Bson; +using Newtonsoft.Json; + +namespace adas_core.Domain.Utils; + +public class ObjectIdConverter : JsonConverter +{ + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value is ObjectId objectId) + // Serializa el ObjectId como un string + writer.WriteValue(objectId.ToString()); + else + writer.WriteNull(); + } + + public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, + JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.String) + { + var stringValue = reader.Value as string; + // Verifica si el valor es el valor predeterminado de ObjectId + if (!string.IsNullOrEmpty(stringValue) && + stringValue != "000000000000000000000000" && + ObjectId.TryParse(stringValue, out var oid)) + return oid; + } + + // Nullables reciben null, no-null vuelven Empty + if (objectType == typeof(ObjectId)) + return ObjectId.Empty; + // Retorna null o ObjectId.Empty para manejar el valor predeterminado de ObjectId + return null; + } + + public override bool CanConvert(Type objectType) + { + return typeof(ObjectId).IsAssignableFrom(objectType); + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/ObjectUtils.cs b/adas-core.Domain/Utils/ObjectUtils.cs new file mode 100644 index 00000000..abe92011 --- /dev/null +++ b/adas-core.Domain/Utils/ObjectUtils.cs @@ -0,0 +1,25 @@ +using System.Text; + +namespace adas_core.Domain.Utils; + +public static class ObjectUtils +{ + public static string ToStr(this object? obj) + { + if (obj == null) return "[OBJ NULL]"; + var type = obj.GetType(); + //if (type == null) return "[OBJ TYPE NULL]"; + StringBuilder sb = new(); + sb.Append(type.Name + "["); + + foreach (var property in obj.GetType().GetProperties()) + { + var value = obj.GetType().GetProperty(property.Name)?.GetValue(obj); + if (value == null) continue; + sb.Append($"{property.Name}: {value},"); + } + + sb.Append(']'); + return sb.ToString(); + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/RelayHelper.cs b/adas-core.Domain/Utils/RelayHelper.cs new file mode 100644 index 00000000..75947840 --- /dev/null +++ b/adas-core.Domain/Utils/RelayHelper.cs @@ -0,0 +1,118 @@ +using System.Net; +using System.Web; +using adas_core.Domain.Models.MongoModels; +using Serilog; + +namespace adas_core.Domain.Utils; + +public class RelayHelper +{ + public static bool GetRelayStatusFromApiRest(Relay relay) + { + UriBuilder builder = new() + { + Scheme = "https", + Host = "localhost", + Port = 7186, + Path = $"{relay.RelayNumber}/status" + }; + + var query = HttpUtility.ParseQueryString(builder.Query); + query["driver"] = relay.Driver; + query["host"] = relay.Ip; + query["port"] = relay.Port.ToString(); + query["relays"] = "8"; //todo:tendría que recoger el total + + builder.Query = query.ToString(); + HttpRequestMessage request = new() + { + RequestUri = builder.Uri, + Method = HttpMethod.Get + }; + + + try + { + using var client = new HttpClient(); + var httpResponse = client.SendAsync(request).Result; + + if (httpResponse.StatusCode.Equals(HttpStatusCode.OK)) + { + var responseContent = httpResponse.Content; + var response = responseContent.ReadAsStringAsync().Result; + + + return Convert.ToBoolean(response); + } + + return false; + } + catch (Exception e) + { + Log.Debug("Exception Getting Relay Status From Api Rest: {relay}", e); + return false; + } + } + + + //TODO son provisionales mientras se añade como nuget Smacs.Divers + public static void PowerOnRelay(Relay relay) + { + UriBuilder builder = new() + { + Scheme = "https", + Host = "localhost", + Port = 7186, + Path = $"{relay.RelayNumber}/powerOn" + }; + + PowerRelay(relay, builder); + } + + public static void PowerOffRelay(Relay relay) + { + UriBuilder builder = new() + { + Scheme = "https", + Host = "localhost", + Port = 7186, + Path = $"{relay.RelayNumber}/powerOff" + }; + + PowerRelay(relay, builder); + } + + private static void PowerRelay(Relay relay, UriBuilder builder) + { + var query = HttpUtility.ParseQueryString(builder.Query); + query["driver"] = relay.Driver; + query["host"] = relay.Ip; + query["port"] = relay.Port.ToString(); + query["relays"] = "8"; //todo:tendría que recoger el total + + builder.Query = query.ToString(); + HttpRequestMessage request = new() + { + RequestUri = builder.Uri, + Method = HttpMethod.Post + }; + + try + { + using var client = new HttpClient(); + var httpResponse = client.SendAsync(request).Result; + + if (!httpResponse.StatusCode.Equals(HttpStatusCode.OK)) Log.Information("relay: {relay} powered", relay); + } + catch (Exception e) + { + Log.Debug("Exception {e} powering Relay: {relay}", e.Message, relay); + } + } + + + public static bool GetRelayStatus(Relay relay) + { + return false; + } +} \ No newline at end of file diff --git a/adas-core.Domain/Utils/StringEx.cs b/adas-core.Domain/Utils/StringEx.cs new file mode 100644 index 00000000..942fc791 --- /dev/null +++ b/adas-core.Domain/Utils/StringEx.cs @@ -0,0 +1,34 @@ +namespace adas_core.Domain.Utils; + +public static class StringEx +{ + public static bool IsEmpty(this string source) + { + return string.IsNullOrEmpty(source); + } + + public static bool IsEmptyOrWhiteSpace(this string source) + { + return string.IsNullOrWhiteSpace(source); + } + + public static string SubstringAfter(this string source, string value) + { + if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(value)) return source; + + var index = source.IndexOf(value, StringComparison.Ordinal); + return index >= 0 + ? source.Substring(index + value.Length) + : source; + } + + public static string SubstringBefore(this string source, string value) + { + if (string.IsNullOrEmpty(source) || string.IsNullOrEmpty(value)) return source; + + var index = source.IndexOf(value, StringComparison.Ordinal); + return index >= 0 + ? source.Substring(0, index) + : source; + } +} \ No newline at end of file diff --git a/adas-core.Domain/adas-core.Domain.csproj b/adas-core.Domain/adas-core.Domain.csproj new file mode 100644 index 00000000..f36d59dc --- /dev/null +++ b/adas-core.Domain/adas-core.Domain.csproj @@ -0,0 +1,27 @@ + + + + net8.0 + enable + enable + adas_core.Domain + + + + + + + + + + + + + + + + + + + + diff --git a/adas-core.Infrastructure/Migrations/MongoMigrations/U_0_1_0_UpdateDataPatien.cs b/adas-core.Infrastructure/Migrations/MongoMigrations/U_0_1_0_UpdateDataPatien.cs new file mode 100644 index 00000000..ae2e3cc0 --- /dev/null +++ b/adas-core.Infrastructure/Migrations/MongoMigrations/U_0_1_0_UpdateDataPatien.cs @@ -0,0 +1,496 @@ +using MongoDB.Bson; +using MongoDB.Driver; +using MongoMigrations.Core; + +namespace adas_core.Infrastructure.Migrations.MongoMigrations; + +// ReSharper disable once InconsistentNaming +public class U_0_1_0_UpdateDataPatien : Migration +{ + public U_0_1_0_UpdateDataPatien() : base(10) + { + Description = + "Transforma person.historicalIds y historicalLocations en arrays con fechas y realiza merge sobre patients."; + } + + public override void Update() + { + var collectionPatient = Database.GetCollection("patients"); + + var pipelinePatient = new[] + { + new BsonDocument("$addFields", new BsonDocument + { + { + "person.historicalIds", new BsonDocument("$cond", new BsonDocument + { + // si ya es array → no tocar + { "if", new BsonDocument("$isArray", "$person.historicalIds") }, + { "then", "$person.historicalIds" }, + // si es document → migrar + { + "else", new BsonDocument("$map", new BsonDocument + { + { "input", new BsonDocument("$objectToArray", "$person.historicalIds") }, + { "as", "id" }, + { + "in", new BsonDocument + { + { + "time", + new BsonDocument("$dateFromString", + new BsonDocument("dateString", "$$id.k")) + }, + { "patientIds", "$$id.v" } + } + } + }) + } + }) + }, + { + "historicalLocations", new BsonDocument("$cond", new BsonDocument + { + { "if", new BsonDocument("$isArray", "$historicalLocations") }, + { "then", "$historicalLocations" }, + { + "else", new BsonDocument("$map", new BsonDocument + { + { "input", new BsonDocument("$objectToArray", "$historicalLocations") }, + { "as", "location" }, + { + "in", new BsonDocument + { + { + "admTime", + new BsonDocument("$dateFromString", + new BsonDocument("dateString", "$$location.k")) + }, + { "patientLocation", "$$location.v" } + } + } + }) + } + }) + } + }), + new BsonDocument("$merge", new BsonDocument + { + { "into", "patients" }, + { "whenMatched", "replace" }, // aquí SÍ es seguro (no hay _t) + { "whenNotMatched", "discard" } + }) + }; + + collectionPatient.Aggregate(pipelinePatient).ToList(); + + var collectionAdmission = Database.GetCollection("admissions"); + + var pipelineAdmission = new[] + { + new BsonDocument("$addFields", new BsonDocument + { + { + "person.historicalIds", new BsonDocument("$cond", new BsonDocument + { + // si ya es array → no tocar + { "if", new BsonDocument("$isArray", "$person.historicalIds") }, + { "then", "$person.historicalIds" }, + // si es document → migrar + { + "else", new BsonDocument("$map", new BsonDocument + { + { "input", new BsonDocument("$objectToArray", "$person.historicalIds") }, + { "as", "id" }, + { + "in", new BsonDocument + { + { + "time", + new BsonDocument("$dateFromString", + new BsonDocument("dateString", "$$id.k")) + }, + { "patientIds", "$$id.v" } + } + } + }) + } + }) + }, + { + "historicalLocations", new BsonDocument("$cond", new BsonDocument + { + { "if", new BsonDocument("$isArray", "$historicalLocations") }, + { "then", "$historicalLocations" }, + { + "else", new BsonDocument("$map", new BsonDocument + { + { "input", new BsonDocument("$objectToArray", "$historicalLocations") }, + { "as", "location" }, + { + "in", new BsonDocument + { + { + "admTime", + new BsonDocument("$dateFromString", + new BsonDocument("dateString", "$$location.k")) + }, + { "patientLocation", "$$location.v" } + } + } + }) + } + }) + } + }), + new BsonDocument("$merge", new BsonDocument + { + { "into", "admissions" }, + { "whenMatched", "replace" }, // aquí SÍ es seguro (no hay _t) + { "whenNotMatched", "discard" } + }) + }; + + collectionAdmission.Aggregate(pipelineAdmission).ToList(); + + var collectionArchivePatient = Database.GetCollection("archive_patient"); + + var pipelineArchivePatient = new[] + { + new BsonDocument("$addFields", new BsonDocument + { + { + "person.historicalIds", new BsonDocument("$cond", new BsonDocument + { + // si ya es array → no tocar + { "if", new BsonDocument("$isArray", "$person.historicalIds") }, + { "then", "$person.historicalIds" }, + // si es document → migrar + { + "else", new BsonDocument("$map", new BsonDocument + { + { "input", new BsonDocument("$objectToArray", "$person.historicalIds") }, + { "as", "id" }, + { + "in", new BsonDocument + { + { + "time", + new BsonDocument("$dateFromString", + new BsonDocument("dateString", "$$id.k")) + }, + { "patientIds", "$$id.v" } + } + } + }) + } + }) + }, + { + "historicalLocations", new BsonDocument("$cond", new BsonDocument + { + { "if", new BsonDocument("$isArray", "$historicalLocations") }, + { "then", "$historicalLocations" }, + { + "else", new BsonDocument("$map", new BsonDocument + { + { "input", new BsonDocument("$objectToArray", "$historicalLocations") }, + { "as", "location" }, + { + "in", new BsonDocument + { + { + "admTime", + new BsonDocument("$dateFromString", + new BsonDocument("dateString", "$$location.k")) + }, + { "patientLocation", "$$location.v" } + } + } + }) + } + }) + } + }), + new BsonDocument("$merge", new BsonDocument + { + { "into", "archive_patient" }, + { "whenMatched", "replace" }, // aquí SÍ es seguro (no hay _t) + { "whenNotMatched", "discard" } + }) + }; + + collectionArchivePatient.Aggregate(pipelineArchivePatient).ToList(); + } + + // NO usado por MongoMigrations.Core + // Solo para ejecución manual si hiciera falta + public void Down() + { + var collectionPatient = Database.GetCollection("patients"); + + var pipelinePatient = new[] + { + new BsonDocument("$addFields", new BsonDocument + { + { + "person.historicalIds", new BsonDocument("$cond", new BsonDocument + { + // si ya es document → no tocar + { "if", new BsonDocument("$not", new BsonArray { new BsonDocument("$isArray", "$person.historicalIds") }) }, + { "then", "$person.historicalIds" }, + // si es array → revertir + { + "else", new BsonDocument("$arrayToObject", new BsonArray + { + new BsonDocument + { + { + "$map", new BsonDocument + { + { "input", "$person.historicalIds" }, + { "as", "id" }, + { + "in", new BsonDocument + { + { + "k", new BsonDocument("$dateToString", new BsonDocument + { + { "format", "%Y-%m-%dT%H:%M:%S.%LZ" }, + { "date", "$$id.time" } + }) + }, + { "v", "$$id.patientIds" } + } + } + } + } + } + }) + } + }) + }, + { + "historicalLocations", new BsonDocument("$cond", new BsonDocument + { + { "if", new BsonDocument("$not", new BsonArray { new BsonDocument("$isArray", "$historicalLocations") }) }, + { "then", "$historicalLocations" }, + { + "else", new BsonDocument("$arrayToObject", new BsonArray + { + new BsonDocument + { + { + "$map", new BsonDocument + { + { "input", "$historicalLocations" }, + { "as", "location" }, + { + "in", new BsonDocument + { + { + "k", new BsonDocument("$dateToString", new BsonDocument + { + { "format", "%Y-%m-%dT%H:%M:%S.%LZ" }, + { "date", "$$location.admTime" } + }) + }, + { "v", "$$location.patientLocation" } + } + } + } + } + } + }) + } + }) + } + }), + new BsonDocument("$merge", new BsonDocument + { + { "into", "patients" }, + { "whenMatched", "replace" }, + { "whenNotMatched", "discard" } + }) + }; + + collectionPatient.Aggregate(pipelinePatient).ToList(); + + var collectionAdm = Database.GetCollection("admissions"); + + var pipelineAdm = new[] + { + new BsonDocument("$addFields", new BsonDocument + { + { + "person.historicalIds", new BsonDocument("$cond", new BsonDocument + { + // si ya es document → no tocar + { "if", new BsonDocument("$not", new BsonArray { new BsonDocument("$isArray", "$person.historicalIds") }) }, + { "then", "$person.historicalIds" }, + // si es array → revertir + { + "else", new BsonDocument("$arrayToObject", new BsonArray + { + new BsonDocument + { + { + "$map", new BsonDocument + { + { "input", "$person.historicalIds" }, + { "as", "id" }, + { + "in", new BsonDocument + { + { + "k", new BsonDocument("$dateToString", new BsonDocument + { + { "format", "%Y-%m-%dT%H:%M:%S.%LZ" }, + { "date", "$$id.time" } + }) + }, + { "v", "$$id.patientIds" } + } + } + } + } + } + }) + } + }) + }, + { + "historicalLocations", new BsonDocument("$cond", new BsonDocument + { + { "if", new BsonDocument("$not", new BsonArray { new BsonDocument("$isArray", "$historicalLocations") }) }, + { "then", "$historicalLocations" }, + { + "else", new BsonDocument("$arrayToObject", new BsonArray + { + new BsonDocument + { + { + "$map", new BsonDocument + { + { "input", "$historicalLocations" }, + { "as", "location" }, + { + "in", new BsonDocument + { + { + "k", new BsonDocument("$dateToString", new BsonDocument + { + { "format", "%Y-%m-%dT%H:%M:%S.%LZ" }, + { "date", "$$location.admTime" } + }) + }, + { "v", "$$location.patientLocation" } + } + } + } + } + } + }) + } + }) + } + }), + new BsonDocument("$merge", new BsonDocument + { + { "into", "admissions" }, + { "whenMatched", "replace" }, + { "whenNotMatched", "discard" } + }) + }; + + collectionAdm.Aggregate(pipelineAdm).ToList(); + var collectionArcP = Database.GetCollection("archive_patient"); + + var pipelineArcP = new[] + { + new BsonDocument("$addFields", new BsonDocument + { + { + "person.historicalIds", new BsonDocument("$cond", new BsonDocument + { + // si ya es document → no tocar + { "if", new BsonDocument("$not", new BsonArray { new BsonDocument("$isArray", "$person.historicalIds") }) }, + { "then", "$person.historicalIds" }, + // si es array → revertir + { + "else", new BsonDocument("$arrayToObject", new BsonArray + { + new BsonDocument + { + { + "$map", new BsonDocument + { + { "input", "$person.historicalIds" }, + { "as", "id" }, + { + "in", new BsonDocument + { + { + "k", new BsonDocument("$dateToString", new BsonDocument + { + { "format", "%Y-%m-%dT%H:%M:%S.%LZ" }, + { "date", "$$id.time" } + }) + }, + { "v", "$$id.patientIds" } + } + } + } + } + } + }) + } + }) + }, + { + "historicalLocations", new BsonDocument("$cond", new BsonDocument + { + { "if", new BsonDocument("$not", new BsonArray { new BsonDocument("$isArray", "$historicalLocations") }) }, + { "then", "$historicalLocations" }, + { + "else", new BsonDocument("$arrayToObject", new BsonArray + { + new BsonDocument + { + { + "$map", new BsonDocument + { + { "input", "$historicalLocations" }, + { "as", "location" }, + { + "in", new BsonDocument + { + { + "k", new BsonDocument("$dateToString", new BsonDocument + { + { "format", "%Y-%m-%dT%H:%M:%S.%LZ" }, + { "date", "$$location.admTime" } + }) + }, + { "v", "$$location.patientLocation" } + } + } + } + } + } + }) + } + }) + } + }), + new BsonDocument("$merge", new BsonDocument + { + { "into", "archive_patient" }, + { "whenMatched", "replace" }, + { "whenNotMatched", "discard" } + }) + }; + + collectionArcP.Aggregate(pipelineArcP).ToList(); + } +} diff --git a/adas-core.Infrastructure/Migrations/MongoMigrations/U_0_1_1_UpdateDisplayConfigDriver.cs b/adas-core.Infrastructure/Migrations/MongoMigrations/U_0_1_1_UpdateDisplayConfigDriver.cs new file mode 100644 index 00000000..8cf9b377 --- /dev/null +++ b/adas-core.Infrastructure/Migrations/MongoMigrations/U_0_1_1_UpdateDisplayConfigDriver.cs @@ -0,0 +1,66 @@ +using MongoDB.Bson; +using MongoDB.Driver; +using MongoMigrations.Core; + +namespace adas_core.Infrastructure.Migrations.MongoMigrations; + +public class U_0_1_1_UpdateDisplayConfigDriver : Migration +{ + public U_0_1_1_UpdateDisplayConfigDriver() : base(11) + { + Description = + "Genera el campo Type en todos los documentos de la coleccion DisplayConfig por que el nuevo driver no expone _t para polimorfia"; + } + + public override void Update() + { + var collection = Database.GetCollection("config_displays"); + + var pipeline = new[] + { + new BsonDocument("$set", new BsonDocument + { + { + "type", new BsonDocument("$switch", new BsonDocument + { + { + "branches", new BsonArray + { + new BsonDocument + { + { "case", new BsonDocument("$eq", new BsonArray { "$_t", "DisplayNurse" }) }, + { "then", "DisplayNurse" } + }, + new BsonDocument + { + { "case", new BsonDocument("$eq", new BsonArray { "$_t", "StandarDisplay" }) }, + { "then", "StandarDisplay" } + }, + new BsonDocument + { + { "case", new BsonDocument("$eq", new BsonArray { "$_t", "SmartDisplay" }) }, + { "then", "SmartDisplay" } + } + } + }, + { "default", "Unknown" } + }) + } + }), + + new BsonDocument("$unset", "_t"), + + new BsonDocument("$merge", new BsonDocument + { + { "into", "config_displays" }, + { "whenMatched", "merge" }, + { "whenNotMatched", "discard" } + }) + }; + + collection.AggregateToCollection(pipeline); + // collection.AggregateToCollection(pipeline, "config_displays"); + + } + +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Migrations/MongoMigrations/U_0_1_2_UpdatePointOfCareConfig.cs b/adas-core.Infrastructure/Migrations/MongoMigrations/U_0_1_2_UpdatePointOfCareConfig.cs new file mode 100644 index 00000000..260af8b8 --- /dev/null +++ b/adas-core.Infrastructure/Migrations/MongoMigrations/U_0_1_2_UpdatePointOfCareConfig.cs @@ -0,0 +1,59 @@ +using MongoDB.Bson; +using MongoDB.Driver; +using MongoMigrations.Core; + +namespace adas_core.Infrastructure.Migrations.MongoMigrations; + +public class U_0_1_2_UpdatePointOfCareConfig : Migration +{ + public U_0_1_2_UpdatePointOfCareConfig() : base(12) + { + Description = + "Borra los elementos de configuracion de poc, relay, camera y beacon y deja el place holder del nuevo modelo"; + } + + public override void Update() + { + var collection = Database.GetCollection("pointOfCares"); + + var filter = Builders.Filter.Exists("configuration"); + var documents = collection.Find(filter).ToList(); + + foreach (var doc in documents) + { + var configuration = doc["configuration"].AsBsonDocument; + + // Listas nuevas de IDs + var beaconIdList = new BsonArray(); + var cameraIdList = new BsonArray(); + var relayIdList = new BsonArray(); + + // 1. Procesar Beacons (si existen) + if (configuration.Contains("beacons") && configuration["beacons"].IsBsonArray) + { + configuration.Remove("beacons"); + } + + configuration["beaconIdList"] = beaconIdList; + + // 2. Procesar Cameras (si existen) + if (configuration.Contains("cameras") && configuration["cameras"].IsBsonArray) + { + configuration.Remove("cameras"); + } + + configuration["cameraIdList"] = cameraIdList; + + // 3. Procesar RelayList (si existen) + if (configuration.Contains("relayList") && configuration["relayList"].IsBsonArray) + { + configuration.Remove("relayList"); + } + + configuration["relayIdList"] = relayIdList; + + // Actualizar el documento en la base de datos + collection.ReplaceOne(Builders.Filter.Eq("_id", doc["_id"]), doc); + } + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Migrations/MongoMigrations/U_0_1_3_UpdateLanguageBarrier.cs b/adas-core.Infrastructure/Migrations/MongoMigrations/U_0_1_3_UpdateLanguageBarrier.cs new file mode 100644 index 00000000..532b1e6e --- /dev/null +++ b/adas-core.Infrastructure/Migrations/MongoMigrations/U_0_1_3_UpdateLanguageBarrier.cs @@ -0,0 +1,50 @@ +using MongoDB.Bson; +using MongoDB.Driver; +using MongoMigrations.Core; + +namespace adas_core.Infrastructure.Migrations.MongoMigrations; + +public class U_0_1_3_UpdateLanguageBarrier : Migration +{ + public U_0_1_3_UpdateLanguageBarrier() : base(13) + { + Description = + "Borra los elementos de configuracion de poc, relay, camera y beacon y deja el place holder del nuevo modelo"; + } + + public override void Update() + { + string[] collectionsToUpdate = { "patients", "admissions" }; + + foreach (var collectionName in collectionsToUpdate) + { + var collection = Database.GetCollection(collectionName); + + // Filtramos documentos donde languageBarrier existe y NO es ya un array + var filter = Builders.Filter.And( + Builders.Filter.Exists("languageBarrier"), + Builders.Filter.Not(Builders.Filter.Type("languageBarrier", BsonType.Array)) + ); + + var documents = collection.Find(filter).ToList(); + + foreach (var doc in documents) + { + var currentValue = doc["languageBarrier"]; + BsonArray newArray = new BsonArray(); + + // Si el valor actual no es nulo, lo añadimos como primer elemento del array + if (!currentValue.IsBsonNull) + { + newArray.Add(currentValue); + } + + // Actualizamos el campo en el documento Bson + doc["languageBarrier"] = newArray; + + // Guardamos los cambios en la base de datos + collection.ReplaceOne(Builders.Filter.Eq("_id", doc["_id"]), doc); + } + } + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/AdmissionRepository.cs b/adas-core.Infrastructure/Repositories/AdmissionRepository.cs new file mode 100644 index 00000000..64675771 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/AdmissionRepository.cs @@ -0,0 +1,478 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Serilog; +using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; + +namespace adas_core.Infrastructure.Repositories; + +public class AdmissionRepository : MongoRepository, IAdmissionRepository +{ + private readonly ApiSettings _apiSettings; + + public AdmissionRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + } + + + public override string GetCollectionName() + { + return _apiSettings.Admissions; + } + + + public override async Task InsertOneAsync(Admission admission) + { + try + { + admission.AdmissionDate = DateTime.UtcNow; + await base.InsertOneAsync(admission); + } + catch (Exception e) + { + Log.Warning("Exception trying to insert admission: {admission}. Exception {e}", admission, e); + } + } + + public async Task Delete(ObjectId id) + { + try + { + var filter = Builders.Filter.Eq(x => x.Id, id); + await Collection.DeleteOneAsync(filter, null); + } + catch (Exception e) + { + Log.Error("Exception trying to delete admission: {id}. Exception {e}", id, e); + throw; + } + } + + public async Task Update(Admission admission) + { + try + { + await UpdateOneAsync(admission.Id, admission); + } + catch (Exception e) + { + Log.Error("Exception trying to update admission: {admission}. Exception {e}", admission, e); + throw; + } + } + + public async Task UpdateLocation(ObjectId id, ObjectId newLocation) + { + var filterBuilder = Builders.Filter; + var filter = filterBuilder.Eq(p => p.Id, id); + + var update = Builders.Update + .Set(p => p.PointOfCareId, newLocation); + + await Collection.UpdateOneAsync(filter, update); + } + + + public async Task UpdatePatient(ObjectId id, Person patient) + { + var filterBuilder = Builders.Filter; + var filter = filterBuilder.Eq(p => p.Id, id); + + var update = Builders.Update + .Set(p => p.Person, patient); + + await Collection.UpdateOneAsync(filter, update); + } + + public async Task> FindAll() + { + try + { + var result = await Collection.FindAsync(_ => true); + return await result.ToListAsync(); + } + catch (Exception ex) + { + Log.Error("Error getting all admissions. Exception: {ex}", ex); + return []; + } + } + + public async Task SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId) + { + if (string.IsNullOrWhiteSpace(patientNumber)) return null; + + // Paciente ubicado en un PoC pero en diferente unidad + var patient = await Collection.Find(Builders.Filter.And( + Builders.Filter.Eq(p => p.Nhc, patientNumber), + Builders.Filter.Ne(p => p.UnitId, unitId) + )).ToListAsync(); + // Si hay mas de una coincidencia devolvemos null por que puede no estar completo el patientNumber + if (patient.Count > 1) return null; + return patient.FirstOrDefault(); + } + + public async Task FindById(ObjectId id) + { + try + { + var filter = Builders.Filter.Eq(p => p.Id, id); + var result = await Collection.FindAsync(filter); + + return await result.FirstOrDefaultAsync(); + } + catch (Exception ex) + { + Log.Error("Error searching admission by id: {id}. Exception: {ex}", id, ex); + return null; + } + } + + public async Task FindByNhc(string nhc) + { + try + { + var filter = Builders.Filter.Eq(p => p.Nhc, nhc); + var result = await Collection.FindAsync(filter); + + return await result.FirstOrDefaultAsync(); + } + catch (Exception ex) + { + Log.Error("Error searching admission by NHC: {nhc}. Exception: {ex}", nhc, ex); + return null; + } + } + + public async Task> FindByLocation(PatientLocation location) + { + try + { + var filter = Builders.Filter.Where(p => + p.PatientLocation != null && + location.UnitName == p.PatientLocation.UnitName && + location.Bed == p.PatientLocation.Bed && + location.Room == p.PatientLocation.Room); + + return await Collection.Find(filter).ToListAsync(); + } + catch (Exception ex) + { + Log.Error("Error searching admission by location: {location}. Exception: {ex}", location, ex); + return []; + } + } + + public async Task?> FindByOrigin(string origin) + { + try + { + var filter = string.IsNullOrEmpty(origin) + ? Builders.Filter.Empty + : Builders.Filter.Where(p => p.Origin != null && origin.Equals(p.Origin.Name)); + + var result = await Collection.FindAsync(filter); + + return result.ToEnumerable(); + } + catch (Exception ex) + { + Log.Error("Error searching admissions by origin: {origin}. Exception: {ex}", origin, ex); + return null; + } + } + + public async Task InsertOneAsyncAndReturn(Admission origin) + { + try + { + await Collection.InsertOneAsync(origin); + return origin; + } + catch (Exception ex) + { + Log.Error(ex.Message); + return null; + } + } + + public async Task> GetAdmissionByUnitIdWithOutPoC(ObjectId unitId) + { + try + { + var filter = Builders.Filter.Where(p => p.UnitId == unitId && p.PointOfCareId == null); + var result = await Collection.FindAsync(filter); + + return result.ToList(); + } + catch (Exception ex) + { + Log.Error(ex.Message); + return []; + } + } + + public async Task CountByUnitId(ObjectId unitId) + { + try + { + var filter = Builders.Filter.Where(p => p.UnitId == unitId); + var result = await Collection.CountDocumentsAsync(filter); + return result; + } + catch (Exception ex) + { + Log.Error(ex.Message); + return 0; + } + } + + public async Task> FindByPointOfCareId(ObjectId pocId) + { + try + { + var filter = Builders.Filter.Where(p => p.PointOfCareId == pocId); + var result = await Collection.FindAsync(filter); + + return result.ToList(); + } + catch (Exception ex) + { + Log.Error(ex.Message); + return []; + } + } + + public async Task> FindByUnitIds(List unitIds) + { + var filterUnit = Builders.Filter.In("unitId", unitIds); + return await Collection.Find(filterUnit).ToListAsync(); + } + + public async Task> UpdateMasterListOption(List unitIds, + UpdateOptionMasterListDto opt, string typeName) + { + var isParsed = Enum.TryParse(typeName, out var parsedTypeName); + if (isParsed) + { + var filterUnit = Builders.Filter.In("unitId", unitIds); + switch (parsedTypeName) + { + case MasterListType.OriginList: + var originFilter = Builders.Filter.Eq( + "origin.name", opt.OldOption?.Name + ); + var filterUpdateorigin = Builders.Filter.And(filterUnit, originFilter); + var updateorigin = Builders.Update + .Set("origin.name", opt.UpdatedOption?.Name); + await Collection.UpdateManyAsync(filterUpdateorigin, updateorigin); + + await Collection.UpdateManyAsync(Builders.Filter.And(filterUnit, + Builders.Filter.Eq( + "originAux", opt.OldOption?.Name + )), Builders.Update + .Set("originAux", opt.UpdatedOption?.Name)); + + // Filtro para devolver los documentos actualizados (OR entre diagnosis.name y diagnosisAux) + var originFilterToReturn = Builders.Filter.Eq("origin.name", opt.UpdatedOption?.Name); + var originAuxFilterToReturn = Builders.Filter.Eq("originAux", opt.UpdatedOption?.Name); + var filterToReturnorigin = Builders.Filter.And( + filterUnit, + Builders.Filter.Or(originFilterToReturn, originAuxFilterToReturn) + ); + + // Devolver los documentos actualizados + var updatedDocumentsorigin = await Collection.Find(filterToReturnorigin).ToListAsync(); + return updatedDocumentsorigin; + case MasterListType.DiagnosisList: + var diagnosisFilter = Builders.Filter.Eq( + "diagnosis.name", opt.OldOption?.Name + ); + var filterUpdateDiagnosis = Builders.Filter.And(filterUnit, diagnosisFilter); + var updateDiagnosis = Builders.Update + .Set("diagnosis.name", opt.UpdatedOption?.Name) + .Set("diagnosis.description", opt.UpdatedOption?.Description); + await Collection.UpdateManyAsync(filterUpdateDiagnosis, updateDiagnosis); + + await Collection.UpdateManyAsync(Builders.Filter.And(filterUnit, + Builders.Filter.Eq( + "diagnosisAux", opt.OldOption?.Name + )), Builders.Update + .Set("diagnosisAux", opt.UpdatedOption?.Name)); + + // Filtro para devolver los documentos actualizados (OR entre diagnosis.name y diagnosisAux) + var diagnosisFilterToReturn = + Builders.Filter.Eq("diagnosis.name", opt.UpdatedOption?.Name); + var diagnosisAuxFilterToReturn = + Builders.Filter.Eq("diagnosisAux", opt.UpdatedOption?.Name); + var filterToReturn = Builders.Filter.And( + filterUnit, + Builders.Filter.Or(diagnosisFilterToReturn, diagnosisAuxFilterToReturn) + ); + + // Devolver los documentos actualizados + var updatedDocuments = await Collection.Find(filterToReturn).ToListAsync(); + return updatedDocuments; + case MasterListType.AllergyList: + // allergies [] + break; + case MasterListType.InsulationList: + // insulation + break; + case MasterListType.LanguageBarrierList: + // languageBarrier + break; + case MasterListType.PassiveSittingList: + // passiveSitting + break; + } + } + + return new List(); + } + + public async Task> DeleteMasterListOption(List unitIds, OptionList opt, + string typeName) + { + var isParsed = Enum.TryParse(typeName, out var parsedTypeName); + if (isParsed) + { + var filterUnit = Builders.Filter.In("unitId", unitIds); + switch (parsedTypeName) + { + case MasterListType.OriginList: + // origin + // originAux + // Filtro para devolver los documentos actualizados (OR entre diagnosis.name y diagnosisAux) + var originFilterToReturn = Builders.Filter.Eq("origin.name", opt.Name); + var originAuxFilterToReturn = Builders.Filter.Eq("originAux", opt.Name); + var filterToReturnOrigin = Builders.Filter.And( + filterUnit, + Builders.Filter.Or(originFilterToReturn, originAuxFilterToReturn) + ); + + // Devolver los documentos actualizados + var updatedDocumentOrigin = await Collection.Find(filterToReturnOrigin).ToListAsync(); + + var originFilter = Builders.Filter.Eq( + "origin.name", opt.Name + ); + var filterUpdateOrigin = Builders.Filter.And(filterUnit, originFilter); + var updateorigin = Builders.Update + .Set(x => x.Origin, null); + await Collection.UpdateManyAsync(filterUpdateOrigin, updateorigin); + + await Collection.UpdateManyAsync(Builders.Filter.And(filterUnit, + Builders.Filter.Eq( + "originAux", opt.Name + )), Builders.Update + .Set(x => x.OriginAux, "")); + + + return updatedDocumentOrigin; + + case MasterListType.DiagnosisList: + + // Filtro para devolver los documentos actualizados (OR entre diagnosis.name y diagnosisAux) + var diagnosisFilterToReturn = Builders.Filter.Eq("diagnosis.name", opt.Name); + var diagnosisAuxFilterToReturn = Builders.Filter.Eq("diagnosisAux", opt.Name); + var filterToReturn = Builders.Filter.And( + filterUnit, + Builders.Filter.Or(diagnosisFilterToReturn, diagnosisAuxFilterToReturn) + ); + + // Devolver los documentos actualizados + var updatedDocuments = await Collection.Find(filterToReturn).ToListAsync(); + + var diagnosisFilter = Builders.Filter.Eq( + "diagnosis.name", opt.Name + ); + var filterUpdateDiagnosis = Builders.Filter.And(filterUnit, diagnosisFilter); + var updateDiagnosis = Builders.Update + .Set(x => x.Diagnosis, null); + await Collection.UpdateManyAsync(filterUpdateDiagnosis, updateDiagnosis); + + await Collection.UpdateManyAsync(Builders.Filter.And(filterUnit, + Builders.Filter.Eq( + "diagnosisAux", opt.Name + )), Builders.Update + .Set(x => x.DiagnosisAux, null)); + + + return updatedDocuments; + case MasterListType.AllergyList: + // allergies [] + break; + case MasterListType.InsulationList: + // insulation + break; + case MasterListType.LanguageBarrierList: + // languageBarrier + break; + case MasterListType.PassiveSittingList: + // passiveSitting + break; + } + } + + return new List(); + } + + public async Task DeleteAdmissionsByUnitId(ObjectId unitId) + { + try + { + var filter = Builders.Filter.Eq(p => p.UnitId, unitId); + await Collection.DeleteManyAsync(filter); + return true; + } + catch (Exception e) + { + Log.Logger.Error(e.Message); + return false; + } + } + + public override async Task CreateIndexes() + { + var optionsUq = new CreateIndexOptions + { + Background = true, + Unique = true, + PartialFilterExpression = Builders.Filter.Exists(p => p.Nhc) + }; + + var indexes = new List> + { + new("{ nhc: 1 }", optionsUq) + }; + + await MongoUtils.EnsureIndexes(Collection, indexes); + } + + public async Task?> FindByDiagnosis(string diagnosis) + { + try + { + var filter = + Builders.Filter.Where(p => p.Diagnosis != null && diagnosis.Equals(p.Diagnosis.Name)); + var result = await Collection.FindAsync(filter); + + return result.ToEnumerable(); + } + catch (Exception ex) + { + Log.Error("Error searching admissions by diagnosis: {origin}. Exception: {ex}", diagnosis, ex); + return null; + } + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/AlarmRepository.cs b/adas-core.Infrastructure/Repositories/AlarmRepository.cs new file mode 100644 index 00000000..b63a11e2 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/AlarmRepository.cs @@ -0,0 +1,185 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; + +namespace adas_core.Infrastructure.Repositories; + +public class AlarmRepository : MongoRepository, IAlarmRepository +{ + private readonly ApiSettings _apiSettings; + private readonly ILogger _logger; + + public AlarmRepository(IOptions apiSettings, IMongoDatabase database, ILogger logger) + : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _logger = logger; + _apiSettings = apiSettings.Value; + } //For testing + + + + public async Task> AggregatedPatientLastObservationsByField(ObjectId patientId, + List? filterObservations = null) + { + try + { + var results = new List(); + IAsyncCursor? cursor; + var builder = Builders.Filter; + + if (filterObservations != null) + { + foreach (var obs in filterObservations) + { + FilterDefinition filter; + + if (obs is { OnlyExpired: true, Name: not null }) + filter = builder.And( + builder.Eq(o => o.PatientId, patientId), + builder.Eq(o => o.Name, obs.Name), + builder.Eq("Expired", obs.OnlyExpired) + //builder.Eq(o => o.Expired, obs.OnlyExpired) + ); + else + filter = builder.And( + builder.Eq(o => o.PatientId, patientId), + builder.Eq(o => o.Name, obs.Name) + ); + + cursor = await Collection.FindAsync( + filter, + new FindOptions + { + Sort = Builders.Sort.Descending("time").Descending("id"), + Limit = obs.Last + }); + + results.AddRange(cursor.ToEnumerable()); + } + } + else + { + var filter = builder.Eq(o => o.PatientId, patientId); + + cursor = await Collection.FindAsync( + filter, + new FindOptions + { Sort = Builders.Sort.Descending("time").Descending("id") }); + + results.AddRange(cursor.ToEnumerable()); + } + + return results; + } + catch (Exception ex) + { + _logger.LogError("Error aggregated patient last observations by field {exMessage}", ex.Message); + throw; + } + } + + public async Task> AggregatedPatientNotExpiredObservationsByField( + ObjectId patientId, + List? filterObservations, + List configAlarm) + { + try + { + var results = new List(); + IAsyncCursor? cursor; + var builder = Builders.Filter; + + if (filterObservations != null) + { + foreach (var obs in filterObservations) + { + FilterDefinition filter; + var conf = configAlarm.FirstOrDefault(c => c.Name == obs.Name); + + if (obs is { OnlyExpired: true, Name: not null }) + filter = builder.And( + builder.Eq(o => o.PatientId, patientId), + builder.Eq(o => o.Name, obs.Name), + builder.Eq("Expired", obs.OnlyExpired) + //builder.Eq(o => o.Expired, obs.OnlyExpired) + ); + else + filter = builder.And( + builder.Eq(o => o.PatientId, patientId), + builder.Eq(o => o.Name, obs.Name) + ); + + if (conf is { Expires: not null }) + { + var dateNow = DateTime.UtcNow.AddSeconds(conf.Expires.Value * -1); + filter = builder.And( + filter, + builder.Gte(o => o.Time, dateNow) + ); + } + + cursor = await Collection.FindAsync( + filter, + new FindOptions + { Sort = Builders.Sort.Descending("time").Descending("id") }); + + results.AddRange(cursor.ToEnumerable()); + } + } + else + { + var filter = builder.Eq(o => o.PatientId, patientId); + + cursor = await Collection.FindAsync( + filter, + new FindOptions + { Sort = Builders.Sort.Descending("time").Descending("id") }); + + results.AddRange(cursor.ToEnumerable()); + } + + return results; + } + catch (Exception ex) + { + _logger.LogError("Error aggregated patient last observations by field {exMessage}", ex.Message); + throw; + } + } + + public override string GetCollectionName() + { + return _apiSettings.PatientsAlarms ?? "patients_alarms"; + } + + public override async Task CreateIndexes() + { + try + { + var options = new CreateIndexOptions { Background = true, Unique = false }; + var indexes = new List> + { + new("{ patientid: 1 }", options), + new("{ patientid: 1, name: 1 }", options), + new("{ name: 1 }", options), + new("{ patientid: 1, name: 1 , time: 1}", options) + }; + await MongoUtils.EnsureIndexes(Collection, indexes); + } + catch (Exception e) + { + _logger.LogError( + "error creating indexes for observation collection {eMessage} TRACE: {eStackTrace}", e.Message, + e.StackTrace); + throw; + } + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/AppointmentArchiveRepository.cs b/adas-core.Infrastructure/Repositories/AppointmentArchiveRepository.cs new file mode 100644 index 00000000..f68120b1 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/AppointmentArchiveRepository.cs @@ -0,0 +1,61 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using Microsoft.Extensions.Options; +using MongoDB.Driver; +using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; + +namespace adas_core.Infrastructure.Repositories; + +public class AppointmentArchiveRepository : MongoRepository, IAppointmentArchiveRepository +{ + private readonly ApiSettings _apiSettings; + + + public AppointmentArchiveRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + } //For testing + + + + public override async Task InsertOneAsync(PatientAppointment appointment) + { + await Collection.InsertOneAsync(appointment); + } + + + public async Task DeleteBeforeDate(DateTime date) + { + var filter = Builders.Filter.Lt(pa => pa.CreateTime, date); + await Collection.DeleteManyAsync(filter); + } + + public async Task InsertBatch(IEnumerable appointment) + { + var writes = new List>(); + writes.AddRange(appointment.Select(d => new InsertOneModel(d))); + + var bulkInsert = await Collection.BulkWriteAsync(writes); + + return bulkInsert.InsertedCount; + } + + public override string GetCollectionName() + { + return _apiSettings.ArchivePatientsAppointments ?? "archive_patients_appointments"; + } + + public override async Task CreateIndexes() + { + var options = new CreateIndexOptions { Background = true, Unique = false }; + + var indexes = new List> + { + new("{ patientid: 1 }", options) + }; + + await MongoUtils.EnsureIndexes(Collection, indexes); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/AppointmentRepository.cs b/adas-core.Infrastructure/Repositories/AppointmentRepository.cs new file mode 100644 index 00000000..385c27b9 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/AppointmentRepository.cs @@ -0,0 +1,144 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; + +namespace adas_core.Infrastructure.Repositories; + +public class AppointmentRepository : MongoRepository, IAppointmentRepository +{ + private readonly ApiSettings _apiSettings; + + public AppointmentRepository( + IOptions apiSettings, + IMongoDatabase database) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + } //For testing + + + + public override string GetCollectionName() + { + return _apiSettings.PatientsAppointments ?? "patients_appointments"; + } + + public async Task> GetByPatient(ObjectId patientId) + { + var filter = Builders.Filter.Eq(ob => ob.PatientId, patientId); + var options = new FindOptions + { + Sort = Builders.Sort.Descending("createTime") + }; + + var result = await Collection.FindAsync(filter, options); + + return result.ToList(); + } + public async Task> FindByPoC(PointOfCare poc) + { + var location = new PatientLocation() + { + Bed = poc.Bed, + Room = poc.Room, + UnitName = poc.UnitName + }; + return await FindByLocation(location); + } + + + public override async Task InsertOneAsync(PatientAppointment appointment) + { + appointment.CreateTime ??= DateTime.UtcNow; + await base.InsertOneAsync(appointment); + } + + public async Task Update(PatientAppointment appointment) + { + await UpdateOneAsync(appointment.Id, appointment); + } + + public new async Task DeleteAsync(ObjectId id) + { + var filter = Builders.Filter.Eq(t => t.Id, id); // Replace 'T' with your actual class name. + await Collection.DeleteOneAsync(filter); + } + + + public Task> FindByPatientIdAsync(ObjectId patientId) + { + var filter = Builders.Filter.Eq(ob => ob.PatientId, patientId); + return Collection.FindAsync(filter); + } + + + public async Task DeleteByPatientId(ObjectId patientId) + { + var filter = Builders.Filter.Eq(po => po.PatientId, patientId); + await Collection.DeleteManyAsync(filter); + } + + + public async Task FindByPatientAndVisitNumber(ObjectId patientId, string visitNumber) + { + var builder = Builders.Filter; + var filter = builder.And( + builder.Eq(ob => ob.PatientId, patientId), + builder.Eq(ob => ob.VisitNumber, visitNumber) + ); + + var result = await Collection.FindAsync(filter); + + return await result.FirstOrDefaultAsync(); + } + + + public async Task FindByPatientAndReason(ObjectId patientId, string? appointmentReason) + { + var builder = Builders.Filter; + var filter = builder.And( + builder.Eq(ob => ob.PatientId, patientId), + builder.Eq(ob => ob.AppointmentReason, appointmentReason) + ); + + var result = await Collection.FindAsync(filter); + + return await result.FirstOrDefaultAsync(); + } + + public async Task> FindByLocation(PatientLocation location) + { + var builder = Builders.Filter; + var existsFilter = builder.Exists(rg => rg.Locations); + var locationFilter = builder.ElemMatch(rg => rg.Locations, + l => l.Bed == location.Bed && l.UnitName == location.UnitName); + + var combinedFilter = builder.And(existsFilter, locationFilter); + + var filter = Builders.Filter.ElemMatch(pa => pa.ResourceGroups, combinedFilter); + + var result = await Collection.FindAsync(filter); + return result?.ToList()??[]; + } + + public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId) + { + await UpdateManyObjectIdAsync(nameId, id, oldId); + } + + public override async Task CreateIndexes() + { + var options = new CreateIndexOptions { Background = true, Unique = false }; + var indexes = new List> + { + new("{ patientid: 1 }", options) + }; + + await MongoUtils.EnsureIndexes(Collection, indexes); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/ArchivePatientCarePlanRepository.cs b/adas-core.Infrastructure/Repositories/ArchivePatientCarePlanRepository.cs new file mode 100644 index 00000000..1d6020b0 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/ArchivePatientCarePlanRepository.cs @@ -0,0 +1,183 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; + +namespace adas_core.Infrastructure.Repositories; + +public class ArchivePatientCarePlanRepository : MongoRepository, IArchivePatientCarePlanRepository +{ + #region Properties + + private readonly ApiSettings _apiSettings; + private readonly ILogger _logger; + + #endregion + + #region Constructor + + + public ArchivePatientCarePlanRepository( + IOptions apiSettings, + IMongoDatabase database, + ILogger logger) : base(database) + { + _logger = logger; + _apiSettings = apiSettings.Value; + } + + public override async Task CreateIndexes() + { + var options = new CreateIndexOptions { Background = true, Unique = false }; + + var indexes = new List> + { + new("{ patientId: 1 }", options) + }; + + await MongoUtils.EnsureIndexes(Collection, indexes); + } + + #endregion + + #region Create + + public override async Task InsertOneAsync(PatientCarePlan patient) + { + try + { + await base.InsertOneAsync(patient); + } + catch (Exception e) + { + _logger.LogError( + "Exception trying to insert patient: {patient}. on archive patient procedure Exception {e}", patient, + e); + throw; + } + } + + public override async Task InsertManyAsync(List patient) + { + try + { + await base.InsertManyAsync(patient); + } + catch (Exception e) + { + _logger.LogError( + "Exception trying to insert many PatientCarePlan. on archive patient procedure Exception {e}", e); + throw; + } + } + + #endregion + + #region Read + + public override string GetCollectionName() + { + return _apiSettings.ArchivePatientProcedure ?? "archive_patients_care_plan"; + } + + public async Task?> FindByPatientId(ObjectId patientId) + { + var result = await Collection.FindAsync(Builders.Filter.Eq(p => p.PatientId, patientId)); + return result.ToList(); + } + + public async Task?> FindByPatientId(string patientId) + { + var isParsed = ObjectId.TryParse(patientId, out var patientIdParsed); + if (!isParsed) return []; + var result = await Collection.FindAsync(Builders.Filter.Eq(p => p.PatientId, patientIdParsed)); + return result.ToList(); + } + + public async Task?> FindByPatientNumber(string patientId) + { + var result = await Collection.FindAsync(Builders.Filter.Eq(p => p.PatientNumber, patientId)); + return result.ToList(); + } + + public async Task> FindAll() + { + var result = await Collection.Find(Builders.Filter.Empty).ToListAsync(); + return result; + } + + public async Task?> FindByIds(ObjectId oldPatientId, string? oldPatientPatientId, + string? oldPatientPatientNumber) + { + var patientFound = await FindByPatientId(oldPatientId); + if (patientFound != null) return patientFound; + if (oldPatientPatientId != null) + { + patientFound = await FindByPatientId(oldPatientPatientId); + if (patientFound is { Count: > 0 }) return patientFound; + } + + if (oldPatientPatientNumber != null) + { + patientFound = await FindByPatientNumber(oldPatientPatientNumber); + if (patientFound is { Count: > 0 }) return patientFound; + } + + return []; + } + + #endregion + + #region Update + + // public async Task Update(Patient patient) + // { + // patient.UpdateDate = DateTime.UtcNow; + // await UpdateOneAsync(patient.Id, patient); + // } + // + // public async void UpdatePatientData(Patient oldPatient, Patient newPatient) + // { + // var filterBuilder = Builders.Filter; + // var updateBuilder = Builders.Update + // .Set(p => p.Person, newPatient.Person) + // .Set(p => p.UpdateDate, DateTime.UtcNow) + // .Set(p => p.Location, newPatient.Location) + // .Set(p => p.PatientNumber, newPatient.PatientNumber) + // .Set(p => p.UnitString, newPatient.UnitString) + // .Set(p => p.Room, newPatient.Room) + // .Set(p => p.PatientId, newPatient.PatientId); + // var filter = filterBuilder.Eq(p => p.Id, oldPatient.Id); + // var update = updateBuilder; + // + // await Collection.UpdateOneAsync(filter, update); + // } + + // public async void UpdateProcedure(Patient patientFound) + // { + // var filterBuilder = Builders.Filter; + // var updateBuilder = Builders.Update + // .Set(p => p.Procedures, patientFound.Procedures); + // var filter = filterBuilder.Eq(p => p.Id, patientFound.Id); + // var update = updateBuilder; + // + // await Collection.UpdateOneAsync(filter, update); + // } + // + // public async void UpdateTreatment(Patient patientFound) + // { + // var filterBuilder = Builders.Filter; + // var updateBuilder = Builders.Update + // .Set(p => p.Treatment, patientFound.Treatment); + // var filter = filterBuilder.Eq(p => p.Id, patientFound.Id); + // var update = updateBuilder; + // + // await Collection.UpdateOneAsync(filter, update); + // } + + #endregion +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/AuthorityRepository.cs b/adas-core.Infrastructure/Repositories/AuthorityRepository.cs new file mode 100644 index 00000000..1ad81a6a --- /dev/null +++ b/adas-core.Infrastructure/Repositories/AuthorityRepository.cs @@ -0,0 +1,132 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Infrastructure.Repositories; + +public class AuthorityRepository( + IOptions apiSettings, + IMongoDatabase database, + ILogger logger) + : MongoRepository(database), IAuthorityRepository +{ + private readonly ApiSettings _apiSettings = apiSettings.Value; + private readonly ILogger _logger = logger; + + public void CreateNewAuthority(string roleName, ObjectId userId) + { + var newAuthorization = new Authorization + { + UserId = userId, + DisplayId = ObjectId.GenerateNewId().ToString(), + Rol = roleName + }; + + Collection.InsertOne(newAuthorization); + _logger.LogDebug("New authority created for user {UserId} with role {RoleName}", userId, roleName); + } + + public async Task> GetUserAuthorities(ObjectId userId) + { + var filter = Builders.Filter.Eq(p => p.UserId, userId); + var result = await Collection.FindAsync(filter); + _logger.LogDebug("Retrieved authorities for user {UserId}", userId); + return result.ToList(); + } + + public async Task> GetAllAuthorities() + { + var result = await Collection.FindAsync(Builders.Filter.Empty); + _logger.LogDebug("Retrieved all authorities"); + return result.ToList(); + } + + public async Task> GetByUnitId(ObjectId unitId) + { + var filter = Builders.Filter.Eq(a => a.UnitId, unitId.ToString()); + + var result = await Collection.FindAsync(filter); + + _logger.LogDebug("Retrieved authorities for unit {UnitId}", unitId); + + return result.ToList(); + } + + public async Task DeleteAllAuthoritiesByUser(ObjectId userId) + { + try + { + var filter = Builders.Filter.Eq(p => p.UserId, userId); + await Collection.DeleteManyAsync(filter); + + _logger.LogDebug("Deleted all authorities for user {UserId}", userId); + + return true; + } + catch (Exception) + { + return false; + } + } + + + public async Task DeleteAllAuthoritiesByUnit(ObjectId unitId) + { + try + { + var filter = Builders.Filter.Eq(p => p.UnitId, unitId.ToString()); + await Collection.DeleteManyAsync(filter); + _logger.LogDebug("Deleted all authorities for unit {UnitId}", unitId); + return true; + } + catch (Exception) + { + return false; + } + } + + public async Task DeleteAllAuthoritiesByDisplay(ObjectId displayId) + { + try + { + var filter = Builders.Filter.Eq(p => p.DisplayId, displayId.ToString()); + await Collection.DeleteManyAsync(filter); + _logger.LogDebug("Deleted all authorities for display {DisplayId}", displayId); + return true; + } + catch (Exception) + { + return false; + } + } + + public override string GetCollectionName() + { + return _apiSettings.Authorizations; + } + + public async Task GetById(ObjectId authId) + { + var filter = Builders.Filter.Eq(p => p.Id, authId); + return await Collection.Find(filter).FirstOrDefaultAsync(); + } + + public sealed override async Task InsertInitialLoad() + { + var user = Db.GetCollection(apiSettings.Value.Users); + var filter = Builders.Filter.Eq(p => p.UserName, "System"); + var search = await user.FindAsync(filter); + var systemUSer = search.FirstOrDefault(); + if (systemUSer != null) + { + var auths = await GetUserAuthorities(systemUSer.Id); + if (!auths.Exists(c => c.PanelAuthorization)) + await InsertOneAsync(new Authorization + { CanUpdate = false, PanelAuthorization = true, Rol = "AuthAdmin", UserId = systemUSer.Id }); + } + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/CameraRepository.cs b/adas-core.Infrastructure/Repositories/CameraRepository.cs new file mode 100644 index 00000000..95ed217a --- /dev/null +++ b/adas-core.Infrastructure/Repositories/CameraRepository.cs @@ -0,0 +1,150 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Newtonsoft.Json; +using Serilog; +using System.Text.RegularExpressions; + +namespace adas_core.Infrastructure.Repositories; + +public class CameraRepository : MongoRepository, ICameraRepository +{ + private readonly ApiSettings _apiSettings; + + + public CameraRepository(IMongoDatabase database, IOptions apiSettings) : base(database) + { + _apiSettings = apiSettings.Value; + } + + public override string GetCollectionName() + { + return _apiSettings.Cameras; + } + + public async Task GetById(ObjectId cameraId) + { + try + { + var filter = Builders.Filter.Eq(x => x.Id, cameraId); + var result = await Collection.FindAsync(filter, null); + return result.FirstOrDefault(); + } + catch (Exception e) + { + Log.Error("Exception trying to get relay by id: {id}. Exception {e}", cameraId, e); + return null; + } + } + + public async Task GetByName(string name) + { + var filterBuilder = Builders.Filter; + + var filter = filterBuilder.Eq(r => r.Name, name); + + return await Collection.Find(filter).FirstOrDefaultAsync(); + } + + public List GetCameraInList(List configurationRelayList) + { + var filterBuilder = Builders.Filter; + + var filter = filterBuilder.And( + filterBuilder.In(r => r.Id, configurationRelayList)); + + return Collection.Find(filter).ToList(); + } + + public IFindFluent GetPaginatedCameras(PaginationFilter filter) + { + var filterBuilder = Builders.Filter; + var sort = Builders.Sort.Ascending("name"); + var filters = new List>(); + + if (filter.FilteredRequest == null) + return CreateFindFluent(filters, sort); + + if (!string.IsNullOrWhiteSpace(filter.FilteredRequest.Text)) + { + var textFilter = filter.FilteredRequest.Text; + + if (textFilter.Length > 100) + throw new BadRequestException("Text filter too long"); + + var safeInput = Regex.Escape(textFilter); + + filters.Add( + filterBuilder.Regex( + p => p.Name, + new BsonRegularExpression(safeInput, "i") + ) + ); + } + + return CreateFindFluent(filters, sort); + } + + public async Task InsertOneCamera(Camera camera) + { + try + { + await Collection.InsertOneAsync(camera); + return await GetById(camera.Id); + } + catch (Exception ex) + { + Log.Error("Error inserting camera: {camera}. Exception: {ex}", + JsonConvert.SerializeObject(camera, Formatting.Indented), ex); + + return null; + } + } + + public async Task UpdateCameraAsync(ObjectId objectId, Camera camera) + { + var filter = Builders.Filter.Eq("_id", objectId); + var update = Builders.Update + .Set(c => c.Streams, camera.Streams) + .Set(c => c.Name, camera.Name) + .Set(c => c.Username, camera.Username) + .Set(c => c.Password, camera.Password) + .Set(c => c.Driver, camera.Driver) + .Set(c => c.Ip, camera.Ip) + .Set(c => c.Streams, camera.Streams) + .Set(c => c.Ptz, camera.Ptz); + return await Collection.FindOneAndUpdateAsync(filter, update, + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); + } + + public async Task> GetSearchByNameCameras(string textToSearch) + { + if (string.IsNullOrWhiteSpace(textToSearch)) + return []; + + if (textToSearch.Length > 100) + throw new BadRequestException("Search text too long"); + + var safeInput = Regex.Escape(textToSearch); + + var filter = Builders.Filter.Regex( + c => c.Name, + new BsonRegularExpression(safeInput, "i") + ); + + return await Collection.Find(filter).ToListAsync(); + } + + private IFindFluent CreateFindFluent(List> filters, SortDefinition sort) + { + var combinedFilter = filters.Any() + ? Builders.Filter.And(filters) + : Builders.Filter.Empty; + return Collection.Find(combinedFilter).Sort(sort); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/ConfigObservationRepository.cs b/adas-core.Infrastructure/Repositories/ConfigObservationRepository.cs new file mode 100644 index 00000000..4c7ab45c --- /dev/null +++ b/adas-core.Infrastructure/Repositories/ConfigObservationRepository.cs @@ -0,0 +1,245 @@ +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.AppSettings; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using System.Text.RegularExpressions; + +namespace adas_core.Infrastructure.Repositories; + +public class ConfigObservationRepository : MongoRepository, IConfigObservationRepository +{ + private readonly ApiSettings _apiSettings; + private readonly IMasterListServiceFactory _masterListServiceFactory; + + public ConfigObservationRepository(IOptions apiSettings, IMongoDatabase database, + IMasterListServiceFactory masterListServiceFactory) : base(database) + { + _masterListServiceFactory = masterListServiceFactory; + _apiSettings = apiSettings.Value; + } + + + public override string GetCollectionName() + { + return _apiSettings.ConfigObservations ?? "config_observations"; + } + + public async Task FindById(ObjectId id) + { + var result = await Collection.FindAsync(Builders.Filter.Eq(x => x.Id, id)); + return await result.FirstOrDefaultAsync(); + } + + public async Task Update(ConfigObservation configObservation) + { + await UpdateOneAsync(configObservation.Id, configObservation); + return configObservation; + } + + public async Task Delete(ObjectId id) + { + return await DeleteAsync(id); + } + + public async Task> FindAllIds() + { + var allCollection = await Collection.FindAsync(_ => true); + + return allCollection.ToList().Select(item => item.Id).ToList(); + } + + public async Task> FindAll() + { + var result = await Collection.FindAsync(_ => true); + return await result.ToListAsync(); + } + + public async Task> GetConfigNames(string id) + { + var allConfigs = await Collection.Find(_ => true).ToListAsync(); + + var distinctNames = allConfigs + .Where(item => !string.IsNullOrWhiteSpace(item.Name)) + .Select(item => item.Name!.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + return distinctNames; + } + + public async Task> GetConfigNames() + { + var allConfigs = await Collection.Find(_ => true).ToListAsync(); + + var distinctNames = allConfigs + .Where(item => !string.IsNullOrWhiteSpace(item.Name)) + .Select(item => item.Name!.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + return distinctNames; + } + + public async Task Count() + { + var filter = Builders.Filter.Empty; + var result = await Collection.CountDocumentsAsync(filter); + return result; + } + + public async Task> GetPaginatedItems(PaginationFilter filter) + { + var builder = Builders.Filter; + var filterDefinition = builder.Empty; + + if (!string.IsNullOrWhiteSpace(filter.FilteredRequest?.Text)) + { + var searchText = filter.FilteredRequest.Text; + + if (searchText.Length > 100) + throw new BadRequestException("Search text too long"); + + var searchTextEscaped = Regex.Escape(searchText); + + var regex = new BsonRegularExpression(searchTextEscaped, "i"); + + var searchFilters = new List> + { + builder.Regex(x => x.Name, regex), + builder.Regex(x => x.CodingSystem, regex), + builder.Regex(x => x.OriginalName, regex), + builder.Regex(x => x.Code, regex), + builder.Regex("uiConfiguration.screenLabel", regex) + }; + + filterDefinition = builder.Or(searchFilters); + } + + var sortDefinition = Builders.Sort.Ascending(x => x.Name); + + var skip = Math.Max(0, (filter.PageNumber - 1) * filter.PageSize); + var limit = Math.Min(filter.PageSize, 100); // límite defensivo + + var items = await Collection + .Find(filterDefinition) + .Sort(sortDefinition) + .Skip(skip) + .Limit(limit) + .ToListAsync(); + + return items; + } + public async Task FindByName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + if (name.Length > 100) + throw new BadRequestException("Name too long"); + + var safeName = Regex.Escape(name); + + var filter = Builders.Filter.Regex( + x => x.Name, + new BsonRegularExpression($"^{safeName}$", "i") + ); + + return await Collection + .Find(filter) + .FirstOrDefaultAsync(); + } + + + public async Task GetByCodeSysAndCode(string? codingSystem, string? code) + { + var builder = Builders.Filter; + var filter = builder.Empty; + + if (!string.IsNullOrEmpty(codingSystem) && !string.IsNullOrEmpty(code)) + { + filter &= builder.Eq(x => x.CodingSystem, codingSystem); + filter &= builder.Eq(x => x.Code, code); + } + + if (filter == builder.Empty) return null; + + var result = await Collection.FindAsync(filter); + + return await result.FirstOrDefaultAsync(); + } + + public async Task InsertOneAsyncAndReturn(ConfigObservation configObservationItem) + { + await Collection.InsertOneAsync(configObservationItem); + return configObservationItem; + } + + public async Task> FindAllByName(string name) + { + var builder = Builders.Filter; + var filter = builder.Empty; + + if (!string.IsNullOrEmpty(name)) filter &= builder.Eq(x => x.Name, name); + + if (filter == builder.Empty) return []; + + var result = await Collection.FindAsync(filter); + + return await result.ToListAsync(); + } + + public async Task GetSingleConfigObservationItem(string? code, string? codingSystem, + string? name, string? originalName) + { + var builder = Builders.Filter; + var filterDefinition = builder.Empty; + if (!string.IsNullOrEmpty(codingSystem)) filterDefinition &= builder.Eq(x => x.CodingSystem, codingSystem); + + if (!string.IsNullOrEmpty(code)) filterDefinition &= builder.Eq(x => x.Code, code); + + if (!string.IsNullOrEmpty(name)) filterDefinition &= builder.Eq(x => x.Name, name); + + if (!string.IsNullOrEmpty(originalName)) filterDefinition &= builder.Eq(x => x.OriginalName, originalName); + return await Collection + .Find(filterDefinition) + .FirstOrDefaultAsync(); + } + + public sealed override async Task InsertInitialLoad() + { + var stringNurseObs = _masterListServiceFactory.StringNurseObs(); + var isInitialized = await FindAll(); + + if (isInitialized.Count > 0) + { + var existingNames = isInitialized + .Where(c => c.Name != null) // Filtrar nulls para evitar errores de referencia + .Select(c => c.Name!) // Seleccionar solo los nombres (usamos '!' si confías en el filtro anterior) + .ToList(); + var missingItems = stringNurseObs + .Where(x => !existingNames.Contains(x)) + .ToList(); + + foreach (var item in missingItems) + await InsertOneAsync(new ConfigObservation + { + Name = item, Code = "NURSE-ADAS", CodingSystem = "ADAS", + InsertMode = ObservationEnum.InsertMode.Manual + }); + return; + } + + var masterListObsConfig = stringNurseObs.Select(c => new ConfigObservation + { + Name = c, Code = "NURSE-ADAS", CodingSystem = "ADAS", InsertMode = ObservationEnum.InsertMode.Manual + }) + .ToList(); + foreach (var item in masterListObsConfig) await InsertOneAsync(item); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/ConfigPumpsRepository.cs b/adas-core.Infrastructure/Repositories/ConfigPumpsRepository.cs new file mode 100644 index 00000000..95f113b8 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/ConfigPumpsRepository.cs @@ -0,0 +1,57 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; +using MongoDB.Driver; + +namespace adas_core.Infrastructure.Repositories; + +public class ConfigPumpsRepository : MongoRepository, IConfigPumpsRepository +{ + private readonly ApiSettings _apiSettings; + + public ConfigPumpsRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + } //For testing + + + + public override string GetCollectionName() + { + return _apiSettings.ConfigPumps ?? "config_pumps"; + } + + public async Task?> GetAllConfigs() + { + var result = await Collection.FindAsync(Builders.Filter.Empty); + + return result.ToList(); + } + + public async Task FindById(string id) + { + var result = await Collection.FindAsync(Builders.Filter.Eq(x => x.Id, id)); + + return await result.FirstOrDefaultAsync(); + } + + public async Task UpdateConfig(ConfigPumps config) + { + var filter = Builders.Filter.Eq("_id", config.Id); + var update = Builders.Update.Set(c => c.Items, config.Items); + + return await Collection.FindOneAndUpdateAsync(filter, update, + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); + } + + public async Task DeleteConfig(ConfigPumps config) + { + var filter = Builders.Filter.Eq("_id", config.Id); + await Collection.DeleteOneAsync(filter); + + var result = await FindById(config.Id); + return result == null; + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/ConfigUnitsRepository.cs b/adas-core.Infrastructure/Repositories/ConfigUnitsRepository.cs new file mode 100644 index 00000000..59237c13 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/ConfigUnitsRepository.cs @@ -0,0 +1,32 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; +using MongoDB.Driver; + +namespace adas_core.Infrastructure.Repositories; + +public class ConfigUnitsRepository : MongoRepository, IConfigUnitsRepository +{ + private readonly ApiSettings _apiSettings; + + public ConfigUnitsRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + } //For testing + + + + + public override string GetCollectionName() + { + return _apiSettings.ConfigUnits ?? "config_units"; + } + + public async Task FindById(string id) + { + var resutl = await Collection.FindAsync(Builders.Filter.Eq(x => x.Id, id)); + return resutl.FirstOrDefault(); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/DeviceRepository.cs b/adas-core.Infrastructure/Repositories/DeviceRepository.cs new file mode 100644 index 00000000..9ffda786 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/DeviceRepository.cs @@ -0,0 +1,81 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; + +namespace adas_core.Infrastructure.Repositories; + +public class DeviceRepository : MongoRepository, IDeviceRepository +{ + private readonly ApiSettings _apiSettings; + + public DeviceRepository(ApiSettings apiSettings, IMongoDatabase database) : base(database) + { + _apiSettings = apiSettings; + } + + public override string GetCollectionName() + { + return _apiSettings.Devices ?? "devices"; + } + + public override async Task CreateIndexes() + { + var options = new CreateIndexOptions { Background = true, Unique = true }; + var indexes = new List> + { + new(Builders.IndexKeys.Ascending(d => d.MacAddr), options), + new(Builders.IndexKeys.Ascending(d => d.SerialNumber), new CreateIndexOptions { Background = true }), + new(Builders.IndexKeys.Ascending(d => d.Uuid), new CreateIndexOptions { Background = true }), + new(Builders.IndexKeys.Ascending(d => d.Key), new CreateIndexOptions { Background = true }) + }; + + await MongoUtils.EnsureIndexes(Collection, indexes); + } + + public async Task FindByMacAddr(string deviceDtoMacAddr) + { + return await Collection + .Find(d => d.MacAddr == deviceDtoMacAddr) + .FirstOrDefaultAsync(); + } + + public async Task FindBySerialNumber(string deviceDtoSerialNumber) + { + return await Collection + .Find(d => d.SerialNumber == deviceDtoSerialNumber) + .FirstOrDefaultAsync(); + } + + public async Task FindByUuid(string deviceDtoUuid) + { + return await Collection + .Find(d => d.Uuid == deviceDtoUuid) + .FirstOrDefaultAsync(); + } + + public async Task FindByKey(string deviceDtoKey) + { + return await Collection + .Find(d => d.Key == deviceDtoKey) + .FirstOrDefaultAsync(); + } + + public async Task UpdateDeviceStats(ObjectId id, DeviceDto deviceExist) + { + var update = Builders.Update + .Set(d => d.Battery, deviceExist.Battery) + .Set(d => d.Connected, deviceExist.Connected) + .Set(d => d.Ready, deviceExist.Ready) + .Set(d => d.Name, deviceExist.Name) + .Set(d => d.UpdatedAt, DateTime.UtcNow); + + await Collection.UpdateOneAsync( + d => d.Id == id, + update + ); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/DiagnosisArchiveRepository.cs b/adas-core.Infrastructure/Repositories/DiagnosisArchiveRepository.cs new file mode 100644 index 00000000..33710f73 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/DiagnosisArchiveRepository.cs @@ -0,0 +1,58 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using Microsoft.Extensions.Options; +using MongoDB.Driver; +using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; + +namespace adas_core.Infrastructure.Repositories; + +public class DiagnosisArchiveRepository : MongoRepository, IDiagnosisArchiveRepository +{ + private readonly ApiSettings _apiSettings; + + public DiagnosisArchiveRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + } //For testing + + + public override async Task InsertOneAsync(PatientDiagnosis patientDiagnosis) + { + await Collection.InsertOneAsync(patientDiagnosis); + } + + public async Task DeleteBeforeDate(DateTime date) + { + var filter = Builders.Filter.Lt(po => po.Time, date); + await Collection.DeleteManyAsync(filter); + } + + public async Task InsertBatch(IEnumerable observations) + { + var writes = new List>(); + writes.AddRange(observations.Select(d => new InsertOneModel(d))); + + var bulkInsert = await Collection.BulkWriteAsync(writes); + + return bulkInsert.InsertedCount; + } + + public override string GetCollectionName() + { + return _apiSettings.ArchivePatientsDiagnosis ?? "archive_patients_diagnosis"; + } + + public override async Task CreateIndexes() + { + var options = new CreateIndexOptions { Background = true, Unique = false }; + + var indexes = new List> + { + new("{ patientid: 1 }", options) + }; + + await MongoUtils.EnsureIndexes(Collection, indexes); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/DiagnosisRepository.cs b/adas-core.Infrastructure/Repositories/DiagnosisRepository.cs new file mode 100644 index 00000000..8c5982c5 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/DiagnosisRepository.cs @@ -0,0 +1,92 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; + +namespace adas_core.Infrastructure.Repositories; + +public class DiagnosisRepository : MongoRepository, IDiagnosisRepository +{ + private readonly ApiSettings _apiSettings; + + + public DiagnosisRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + } //For testing + + + public override string GetCollectionName() + { + return _apiSettings.PatientsDiagnosis ?? "patients_diagnosis"; + } + + public async Task> GetByPatient(ObjectId patientId) + { + var filter = Builders.Filter.Eq(ob => ob.PatientId, patientId); + var result = await Collection.FindAsync(filter, + new FindOptions { Sort = Builders.Sort.Descending("time") }); + + return result.ToList(); + } + + public new async Task DeleteAsync(ObjectId id) + { + var filter = Builders.Filter.Eq(t => t.Id, id); + await Collection.DeleteOneAsync(filter); + } + + + public override async Task InsertOneAsync(PatientDiagnosis diagnosis) + { + await Collection.InsertOneAsync(diagnosis); + } + + public async Task DeleteByPatientId(ObjectId patientId) + { + var filter = Builders.Filter.Eq(po => po.PatientId, patientId); + await Collection.DeleteManyAsync(filter); + } + + public async Task FindByPatientIdAndCode(ObjectId patientId, string? code, string? codingSystem) + { + var builder = Builders.Filter; + var filter = builder.And( + builder.Eq(ob => ob.PatientId, patientId), + builder.Eq(ob => ob.Code, code), + builder.Eq(ob => ob.CodingSystem, codingSystem) + ); + + var result = await Collection.FindAsync(filter); + + return await result.FirstOrDefaultAsync(); + } + + + public async Task> FindByPatientIdAsync(ObjectId patientId) + { + var filter = Builders.Filter.Eq(ob => ob.PatientId, patientId); + + return await Collection.FindAsync(filter); + } + + public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId) + { + await UpdateManyObjectIdAsync(nameId, id, oldId); + } + + public override async Task CreateIndexes() + { + var options = new CreateIndexOptions { Background = true, Unique = false }; + var indexes = new List> + { + new("{ patientid: 1 }", options) + }; + + await MongoUtils.EnsureIndexes(Collection, indexes); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/DischargeRepository.cs b/adas-core.Infrastructure/Repositories/DischargeRepository.cs new file mode 100644 index 00000000..9d75aa06 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/DischargeRepository.cs @@ -0,0 +1,333 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Serilog; +using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; + +namespace adas_core.Infrastructure.Repositories; + +public class DischargeRepository : MongoRepository, IDischargeRepository +{ + private readonly ApiSettings _apiSettings; + + public DischargeRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + _apiSettings = apiSettings.Value; + } + + public override string GetCollectionName() + { + return _apiSettings.Discharges; + } + + public override async Task InsertOneAsync(Discharge discharge) + { + try + { + discharge.DischargeDate = DateTime.UtcNow; + await base.InsertOneAsync(discharge); + } + catch (Exception e) + { + Log.Warning("Exception trying to insert discharge: {discharge}. Exception {e}", discharge, e); + } + } + + public async Task Delete(ObjectId id) + { + try + { + var filter = Builders.Filter.Eq(x => x.Id, id); + await Collection.DeleteOneAsync(filter, null); + } + catch (Exception e) + { + Log.Error("Exception trying to delete discharge: {id}. Exception {e}", id, e); + throw; + } + } + + public async Task Update(Discharge discharge) + { + try + { + await UpdateOneAsync(discharge.Id, discharge); + } + catch (Exception e) + { + Log.Error("Exception trying to update discharge: {discharge}. Exception {e}", discharge, e); + throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + } + } + + + public async Task UpdateUnit(ObjectId id, string unit) + { + var filterBuilder = Builders.Filter; + var filter = filterBuilder.Eq(p => p.Id, id); + + var update = Builders.Update + .Set(p => p.PatientLocation!.UnitName, unit); + + await Collection.UpdateOneAsync(filter, update); + } + + public async Task UpdatePatient(ObjectId id, Patient patient) + { + var filterBuilder = Builders.Filter; + var filter = filterBuilder.Eq(p => p.Id, id); + + var update = Builders.Update + .Set(p => p.Patient, patient); + + await Collection.UpdateOneAsync(filter, update); + } + + + public async Task> FindAll() + { + try + { + var result = await Collection.FindAsync(_ => true); + return await result.ToListAsync(); + } + catch (Exception ex) + { + Log.Error("Error getting all discharges. Exception: {ex}", ex); + return []; + } + } + + public async Task FindById(ObjectId id) + { + try + { + var filter = Builders.Filter.Eq(p => p.Id, id); + var result = await Collection.FindAsync(filter); + + return await result.FirstOrDefaultAsync(); + } + catch (Exception ex) + { + Log.Error("Error searching discharge by id: {id}. Exception: {ex}", id, ex); + return null; + } + } + + + public async Task?> FindByUnit(string unit) + { + try + { + var filter = Builders.Filter.Eq(p => p.PatientLocation!.UnitName, unit); + var result = await Collection.FindAsync(filter); + + return result.ToEnumerable(); + } + catch (Exception ex) + { + Log.Error("Error searching discharge by Unit id: {unit}. Exception: {ex}", unit, ex); + return null; + } + } + + public async Task CountByUnitId(ObjectId unitId) + { + try + { + return await Collection.CountDocumentsAsync( + Builders.Filter.Eq(p => p.UnitId, unitId)); + } + catch (Exception e) + { + Log.Error(e.Message); + return 0; + } + } + + public async Task?> FindByDestination(string destination) + { + try + { + var filter = string.IsNullOrEmpty(destination) + ? Builders.Filter.Empty + : Builders.Filter.Eq(p => p.Destination, destination); + + var result = await Collection.FindAsync(filter); + + return result.ToEnumerable(); + } + catch (Exception ex) + { + Log.Error("Error searching discharges by destination: {destination}. Exception: {ex}", destination, ex); + return null; + } + } + + public async Task?> GetDischargesByUnitIds(IEnumerable? unitIds) + { + var filterUnit = Builders.Filter.In("unitId", unitIds); + return await Collection.Find(filterUnit).ToListAsync(); + } + + + public async Task?> FindByPoCId(ObjectId pocId) + { + try + { + var filter = Builders.Filter.Eq(p => p.PointOfCareId, pocId); + + var result = await Collection.FindAsync(filter); + + return result.ToEnumerable(); + } + catch (Exception ex) + { + Log.Error("Error searching discharges by PointOfCare: {destination}. Exception: {ex}", pocId, ex); + return null; + } + } + + public async Task?> FindByService(string service) + { + try + { + var filter = Builders.Filter.Eq(p => p.Service, service); + var result = await Collection.FindAsync(filter); + + return result.ToEnumerable(); + } + catch (Exception ex) + { + Log.Error("Error searching discharges by service: {origin}. Exception: {ex}", service, ex); + return null; + } + } + + public async Task GetDischargeByLocation(PatientLocation location) + { + try + { + var filter = Builders.Filter.Eq(p => p.PatientLocation, location); + var result = await Collection.FindAsync(filter); + + return await result.FirstOrDefaultAsync(); + } + catch (Exception e) + { + Log.Error($"Unable to get discharge by location on repository Exception: {e}"); + return null; + } + } + + public async Task GetDischargeByPointOfCareId(ObjectId id) + { + try + { + var filter = Builders.Filter.Eq(p => p.PointOfCareId, id); + var result = await Collection.FindAsync(filter); + + return await result.FirstOrDefaultAsync(); + } + catch (Exception e) + { + Log.Error($"Unable to get discharge by location on repository Exception: {e}"); + return null; + } + } + + public Task> UpdateMasterListOption(List unitIds, UpdateOptionMasterListDto opt, + string typeName) + { + var isParsed = Enum.TryParse(typeName, out var parsedTypeName); + if (isParsed) + switch (parsedTypeName) + { + case MasterListType.DestinationList: + // destination + // destinationOption + break; + case MasterListType.ServiceList: + // service + break; + } + + return Task.FromResult>(new List()); + } + + + public async Task DeleteByUnitId(ObjectId unitId) + { + try + { + var filter = Builders.Filter.Where(p => p.UnitId == unitId); + await Collection.DeleteManyAsync(filter); + return true; + } + catch (Exception ex) + { + Log.Error(ex.Message); + return false; + } + } + + public Task> DeleteMasterListOption(List unitIds, OptionList opt, string typeName) + { + var isParsed = Enum.TryParse(typeName, out var parsedTypeName); + if (isParsed) + switch (parsedTypeName) + { + case MasterListType.DestinationList: + // destination + // destinationOption + break; + case MasterListType.ServiceList: + // service + break; + } + + return Task.FromResult>(new List()); + } + + public async Task GetByPatientId(ObjectId patientId) + { + try + { + var filter = Builders.Filter.Eq(p => p.PatientId, patientId); + var result = await Collection.FindAsync(filter); + + return await result.FirstOrDefaultAsync(); + } + catch (Exception e) + { + Log.Error($"Unable to get discharge by patient id on repository Exception: {e}"); + return null; + } + } + + public override async Task CreateIndexes() + { + var optionsUq = new CreateIndexOptions + { + Background = true, + Unique = true, + PartialFilterExpression = Builders.Filter.Exists(p => p.MedicalDischarge) & + Builders.Filter.Exists(p => p.AdminDischarge) + }; + var indexes = new List> + { + new("{ medicalDischarge: 1 }", optionsUq), + new("{ adminDischarge: 1 }", optionsUq) + }; + + await MongoUtils.EnsureIndexes(Collection, indexes); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/DisplayCardConfigRepository.cs b/adas-core.Infrastructure/Repositories/DisplayCardConfigRepository.cs new file mode 100644 index 00000000..76ee5f48 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/DisplayCardConfigRepository.cs @@ -0,0 +1,97 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Infrastructure.Repositories; + +public class DisplayCardConfigRepository : MongoRepository, IDisplayCardConfigRepository +{ + private readonly ApiSettings _apiSettings; + private readonly ILogger _logger; + + + public DisplayCardConfigRepository( + IMongoDatabase database, + ApiSettings apiSettings, + ILogger logger + ) : base(database) + { + _apiSettings = apiSettings; + _logger = logger; + } + + public override string GetCollectionName() + { + return _apiSettings.DisplayCardConfig; + } + + public async Task> GetAll() + { + var result = await Collection.Find(Builders.Filter.Empty).ToListAsync(); + return result; + } + + public async Task GetById(ObjectId configId) + { + try + { + var result = await Collection.FindAsync(Builders.Filter.Eq(p => p.Id, configId)); + return await result.FirstOrDefaultAsync(); + } + catch (Exception ex) + { + _logger.LogError("Error on config card display repository on GetById Exception: {ex}", ex.Message); + return null; + } + } + + public async Task InsertOneAsyncAndReturn(CardConfig config) + { + try + { + await Collection.InsertOneAsync(config); + return config; + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + return null; + } + } + + public async Task> UpdateOne(CardConfig? config) + { + try + { + if (config == null) return new UpdateResponse(0, null); + var filter = Builders.Filter.Eq(c => c.Id, config.Id); + var update = Builders.Update.Set(c => c.Rows, config.Rows); + + var result = await Collection.UpdateOneAsync(filter, update); + + var updatedDoc = await Collection.Find(filter).FirstOrDefaultAsync(); + + return new UpdateResponse(result.ModifiedCount, updatedDoc); + } + catch (Exception e) + { + _logger.LogError(e.Message); + return new UpdateResponse(0, null); + } + } + + public async Task DeleteOne(ObjectId configId) + { + return await DeleteAsync(configId); + } + + public Task UpdateCardConfigId(ObjectId? displayConfigId, ObjectId? resultId) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/DisplayChartConfigRepository.cs b/adas-core.Infrastructure/Repositories/DisplayChartConfigRepository.cs new file mode 100644 index 00000000..7ac7c892 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/DisplayChartConfigRepository.cs @@ -0,0 +1,96 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Infrastructure.Repositories; + +public class DisplayChartConfigRepository : MongoRepository, IDisplayChartConfigRepository +{ + private readonly ApiSettings _apiSettings; + private readonly ILogger _logger; + + + + public DisplayChartConfigRepository(IMongoDatabase database, ApiSettings apiSettings, + ILogger logger) : base(database) + { + _apiSettings = apiSettings; + _logger = logger; + } + + public override string GetCollectionName() + { + return _apiSettings.DisplayChartConfig; + } + + public async Task> GetAll() + { + var result = await Collection.Find(Builders.Filter.Empty).ToListAsync(); + return result; + } + + public async Task GetById(ObjectId configId) + { + try + { + var result = await Collection.FindAsync(Builders.Filter.Eq(p => p.Id, configId)); + return await result.FirstOrDefaultAsync(); + } + catch (Exception ex) + { + _logger.LogError("Error on config card display repository on GetById Exception: {ex}", ex.Message); + return null; + } + } + + public async Task InsertOneAsyncAndReturn(ChartConfig config) + { + try + { + await Collection.InsertOneAsync(config); + return config; + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + return null; + } + } + + public async Task> UpdateOne(ChartConfig? config) + { + try + { + if (config == null) return new UpdateResponse(0, null); + var filter = Builders.Filter.Eq(c => c.Id, config.Id); + var update = Builders.Update + .Set(c => c.BaseConfig, config.BaseConfig) + .Set(c => c.AxesConfig, config.AxesConfig) + .Set(c => c.SeriesConfig, config.SeriesConfig); + + // Realizamos la actualización + var result = await Collection.UpdateOneAsync(filter, update); + + // Buscamos el documento actual (ya actualizado o el existente si no hubo cambios) + var updatedDoc = await Collection.Find(filter).FirstOrDefaultAsync(); + + // result.ModifiedCount será 1 si cambió algo, o 0 si los datos eran idénticos + return new UpdateResponse(result.ModifiedCount, updatedDoc); + } + catch (Exception e) + { + _logger.LogError(e.Message); + return new UpdateResponse(0, null); + } + } + + public async Task DeleteOne(ObjectId configId) + { + return await DeleteAsync(configId); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/DisplayConfigRepository.cs b/adas-core.Infrastructure/Repositories/DisplayConfigRepository.cs new file mode 100644 index 00000000..8b68f068 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/DisplayConfigRepository.cs @@ -0,0 +1,775 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.DTO.Display; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using adas_core.Domain.Utils; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Newtonsoft.Json; +using Serilog; +using System.Text.RegularExpressions; + +namespace adas_core.Infrastructure.Repositories; + +public class DisplayConfigRepository : MongoRepository, IDisplayConfigRepository +{ + private readonly ApiSettings _apiSettings; + + private readonly ILogger _logger; + private readonly IUnitRepository _unitRepository; + + + + + public DisplayConfigRepository( + IMongoDatabase database, + ApiSettings apiSettings, + ILogger logger, + IUnitRepository unitRepository) : base(database) + { + _apiSettings = apiSettings; + _logger = logger; + _unitRepository = unitRepository; + } + + public override string GetCollectionName() + { + return _apiSettings.DisplaysConfig; + } + + public async Task> GetAll() + { + var result = await Collection.Find(Builders.Filter.Empty).ToListAsync(); + return result; + } + + public IFindFluent GetAllPaginated(PaginationFilter filter) + { + var filterBuilder = Builders.Filter; + var sort = Builders.Sort.Ascending("hospital"); + var filters = new List>(); + + if (filter.FilteredRequest == null) + return CreateFindFluentMinimal(filters, sort); + + if (!string.IsNullOrWhiteSpace(filter.FilteredRequest.Text)) + { + var textFilter = filter.FilteredRequest.Text; + + if (textFilter.Length > 100) + throw new BadRequestException("Text filter too long"); + + var textFilterEscaped = Regex.Escape(textFilter); + + var orFilters = new List> + { + filterBuilder.Regex(p => p.Hospital, new BsonRegularExpression(textFilterEscaped, "i")) + }; + + // búsqueda por Id solo si es válido + if (ObjectId.TryParse(textFilter, out var id)) + { + orFilters.Add(filterBuilder.Eq("_id", id)); + } + + filters.Add(filterBuilder.Or(orFilters)); + } + + if (filter.FilteredRequest.DisplayType != null) + { + filters.Add(filterBuilder.Eq(d => d.Type, filter.FilteredRequest.DisplayType)); + } + + return CreateFindFluentMinimal(filters, sort); + } + + public async Task> GetByType(DisplayConfigEnums.DisplayType type) + { + var filter = Builders.Filter.Eq(p => p.Type, type); + var result = await Collection.Find(filter).ToListAsync(); + return result; + } + + public async Task GetById(ObjectId id) + { + try + { + var aggregate = Collection.Aggregate() + .Match(Builders.Filter.Eq(p => p.Id, id)) + + // CardConfig + .Lookup( + _apiSettings.DisplayCardConfig, + "cardConfigId", + "_id", + "cardConfig" + ) + .Unwind("cardConfig", new AggregateUnwindOptions + { + PreserveNullAndEmptyArrays = true + }) + + // DetailConfig + .Lookup( + _apiSettings.DisplayDetailConfig, + "detailConfigId", + "_id", + "detailConfig" + ) + .Unwind("detailConfig", new AggregateUnwindOptions + { + PreserveNullAndEmptyArrays = true + }) + // ChartConfig + .Lookup( + _apiSettings.DisplayChartConfig, + "chartConfigIdList", + "_id", + "chartConfig" + ) + // Lookup para cardRotatingLayout.dataId + .Lookup( + _apiSettings.DisplayCardConfig, + "cardRotatingLayout.dataId", + "_id", + "rotatingLayoutData" + ) + + // Enriquecer cada elemento del array + .AppendStage(new BsonDocument("$addFields", + new BsonDocument("cardRotatingLayout", + new BsonDocument("$map", + new BsonDocument + { + { "input", "$cardRotatingLayout" }, + { "as", "item" }, + { + "in", + new BsonDocument("$mergeObjects", new BsonArray + { + "$$item", + new BsonDocument("data", + new BsonDocument("$arrayElemAt", new BsonArray + { + new BsonDocument("$filter", new BsonDocument + { + { "input", "$rotatingLayoutData" }, + { "as", "d" }, + { + "cond", + new BsonDocument("$eq", new BsonArray + { + "$$d._id", + "$$item.dataId" + }) + } + }), + 0 + }) + ) + }) + } + }) + ) + )) + + // limpiar auxiliar + .AppendStage(new BsonDocument("$unset", "rotatingLayoutData")) + .As(); + + return await aggregate.FirstOrDefaultAsync(); + } + catch (Exception ex) + { + _logger.LogError("Error on config display repository on GetById Exception: {ex}", ex.Message); + return null; + } + } + + public async Task GetDefault(DisplayConfigEnums.DisplayType type) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(p => p.Type, type), + Builders.Filter.Eq(p => p.Hospital, "Default") + ); + + var result = await Collection.FindAsync(filter); + return result.FirstOrDefault(); + } + + public async Task InsertOneAsyncAndReturn(DisplayConfig config) + { + try + { + await Collection.InsertOneAsync(config); + return config; + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + return null; + } + } + + public async Task UpdateSmartDisplay(ObjectId displayConfigId, SmartDisplay? newDisplayConfig) + { + try + { + if (newDisplayConfig == null) return null; + + var filter = Builders.Filter.Eq("Id", displayConfigId); + var update = Builders.Update + .Set(c => ((SmartDisplay)c).Pumps, newDisplayConfig.Pumps) + .Set(c => ((SmartDisplay)c).HasCameras, newDisplayConfig.HasCameras) + .Set(c => ((SmartDisplay)c).HasSound, newDisplayConfig.HasSound) + .Set(c => ((SmartDisplay)c).CamerasAreActive, newDisplayConfig.CamerasAreActive) + .Set(c => ((SmartDisplay)c).IsRotationEnabled, newDisplayConfig.IsRotationEnabled) + .Set(c => ((SmartDisplay)c).CanChangeCameraMode, newDisplayConfig.CanChangeCameraMode) + .Set(c => ((SmartDisplay)c).FieldList, newDisplayConfig.FieldList) + .Set(c => ((SmartDisplay)c).ChartConfig, newDisplayConfig.ChartConfig) + .Set(c => ((SmartDisplay)c).GraphLayout, newDisplayConfig.GraphLayout) + .Set(c => ((SmartDisplay)c).SensorList, newDisplayConfig.SensorList) + .Set(c => ((SmartDisplay)c).AlarmFieldList, newDisplayConfig.AlarmFieldList) + .Set(c => c.HomeConfig, newDisplayConfig.HomeConfig) + .Set(c => c.DetailConfigId, newDisplayConfig.DetailConfigId) + .Set(c => ((SmartDisplay)c).CameraStreamType, newDisplayConfig.CameraStreamType) + .Set(c => ((SmartDisplay)c).ColorConfig, newDisplayConfig.ColorConfig) + .Set(c => ((SmartDisplay)c).RequestGroupedFieldList, newDisplayConfig.RequestGroupedFieldList) + .Set(c => c.Hospital, newDisplayConfig.Hospital) + // .Set(c => c.CardConfig, newDisplayConfig.CardConfig) + .Set(c => c.DisplaySectionIdList, newDisplayConfig.DisplaySectionIdList) + ; + + var c = await Collection.FindOneAndUpdateAsync(filter, update, + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); + return c as SmartDisplay; + } + catch (Exception e) + { + _logger.LogError($"Error on config display repository on UpdateSmartDisplay Exception: {e.Message}"); + return null; + } + } + + public async Task UpdateConfigColor(ObjectId objectIdConfigDisplay, ColorConfig colorConfig) + { + var filter = Builders.Filter.Eq("Id", objectIdConfigDisplay); + var updateDefinition = new List>(); + if (colorConfig.Level != null) + updateDefinition.Add(Builders.Update.Set("colorConfig.level", colorConfig.Level)); + if (colorConfig.Text != null) + updateDefinition.Add(Builders.Update.Set("colorConfig.text", colorConfig.Text)); + if (colorConfig.Arrow != null) + updateDefinition.Add(Builders.Update.Set("colorConfig.arrow", colorConfig.Arrow)); + if (colorConfig.Indicator != null) + updateDefinition.Add(Builders.Update.Set("colorConfig.indicator", colorConfig.Indicator)); + if (colorConfig.Graph != null) + updateDefinition.Add(Builders.Update.Set("colorConfig.graph", colorConfig.Graph)); + if (colorConfig.BoxNumber != null) + updateDefinition.Add(Builders.Update.Set("colorConfig.boxNumber", colorConfig.BoxNumber)); + if (colorConfig.BoxStatusColor != null) + updateDefinition.Add( + Builders.Update.Set("colorConfig.boxStatusColor", colorConfig.BoxStatusColor)); + if (colorConfig.Test != null) + updateDefinition.Add(Builders.Update.Set("colorConfig.test", colorConfig.Test)); + if (colorConfig.Therapy != null) + updateDefinition.Add(Builders.Update.Set("colorConfig.therapy", colorConfig.Therapy)); + if (colorConfig.Procedure != null) + updateDefinition.Add(Builders.Update.Set("colorConfig.procedure", colorConfig.Procedure)); + var update = Builders.Update.Combine(updateDefinition); + try + { + var result = await Collection.UpdateOneAsync(filter, update); + if (result.ModifiedCount > 0) return true; // Retorna el objeto actualizado si la modificación fue exitosa + + return false; // Retorna nulo si no se encontró el documento o no se modificó + } + catch (Exception ex) + { + Log.Error("Error UpdateConfigColor: {name}. Exception: {ex}", objectIdConfigDisplay, ex); + return false; + } + } + + public async Task UpdateHeaderConfig(ObjectId objectIdConfigDisplay, HeaderConfig headerConfig) + { + var filter = Builders.Filter.Eq("Id", objectIdConfigDisplay); + var updateDefinition = new List>(); + if (headerConfig.PartnerLogo != null) + updateDefinition.Add( + Builders.Update.Set("headerConfig.partnerLogo", headerConfig.PartnerLogo)); + if (headerConfig.CompanyLogo != null) + updateDefinition.Add( + Builders.Update.Set("headerConfig.companyLogo", headerConfig.CompanyLogo)); + if (headerConfig.CenterLogo != null) + updateDefinition.Add( + Builders.Update.Set("headerConfig.centerLogo", headerConfig.CenterLogo)); + if (headerConfig.MeddisLogo != null) + updateDefinition.Add( + Builders.Update.Set("headerConfig.meddisLogo", headerConfig.MeddisLogo)); + if (headerConfig.UnitName != null) + updateDefinition.Add(Builders.Update.Set("headerConfig.unitName", headerConfig.UnitName)); + if (headerConfig.Cameras != null) + updateDefinition.Add(Builders.Update.Set("headerConfig.cameras", headerConfig.Cameras)); + if (headerConfig.Sensors != null) + updateDefinition.Add(Builders.Update.Set("headerConfig.sensors", headerConfig.Sensors)); + if (headerConfig.Fullscreen != null) + updateDefinition.Add( + Builders.Update.Set("headerConfig.fullscreen", headerConfig.Fullscreen)); + if (headerConfig.Sounds != null) + updateDefinition.Add(Builders.Update.Set("headerConfig.sounds", headerConfig.Sounds)); + if (headerConfig.Sidebar != null) + updateDefinition.Add(Builders.Update.Set("headerConfig.sidebar", headerConfig.Sidebar)); + if (headerConfig.CurrentDateTime != null) + updateDefinition.Add(Builders.Update.Set("headerConfig.currentDateTime", + headerConfig.CurrentDateTime)); + if (headerConfig.SectionTitle != null) + updateDefinition.Add( + Builders.Update.Set("headerConfig.sectionTitle", headerConfig.SectionTitle)); + var update = Builders.Update.Combine(updateDefinition); + try + { + var result = await Collection.UpdateOneAsync(filter, update); + if (result.ModifiedCount > 0) return true; + + return false; + } + catch (Exception ex) + { + Log.Error("Error UpdateConfigColor: {name}. Exception: {ex}", objectIdConfigDisplay, ex); + return false; + } + } + + public async Task UpdateSetHomeBanner(ObjectId objectIdConfigDisplay, List bannerItems) + { + var filter = Builders.Filter.Eq("Id", objectIdConfigDisplay); + var update = Builders.Update + .Set(c => ((DisplayNurse)c).HomeBanner, bannerItems); + try + { + var result = await Collection.UpdateOneAsync(filter, update); + if (result.ModifiedCount > 0) return true; // Retorna el objeto actualizado si la modificación fue exitosa + + return false; // Retorna nulo si no se encontró el documento o no se modificó + } + catch (Exception ex) + { + Log.Error("Error UpdateConfigColor: {name}. Exception: {ex}", objectIdConfigDisplay, ex); + return false; + } + } + + public async Task UpdateBaseConfig(ObjectId objectIdConfigDisplay, DisplayConfig baseConfig) + { + var filter = Builders.Filter.Eq("Id", objectIdConfigDisplay); + // TODO NO ESPERA EL CARDCONFIG + var (updateDefinition, _) = GetBaseUpdateDefinition(baseConfig); + var update = Builders.Update.Combine(updateDefinition); + + try + { + var result = await Collection.UpdateOneAsync(filter, update); + if (result.ModifiedCount > 0) + // await UpdateFieldList(objectIdConfigDisplay, GenerateFieldListFromStrig(fieldList)); + return true; // Retorna el objeto actualizado si la modificación fue exitosa + + return false; // Retorna nulo si no se encontró el documento o no se modificó + } + catch (Exception ex) + { + Log.Error("Error UpdateBaseConfig: {name}. Exception: {ex}", objectIdConfigDisplay, ex); + return false; + } + } + + public async Task UpdateDisplayNurse(ObjectId displayConfigId, DisplayNurseDto? newDisplayConfig, + List nurseObs) + { + try + { + if (newDisplayConfig == null) return null; + var filter = Builders.Filter.Eq("Id", displayConfigId); + var updateDefinition = new List>(); + if (newDisplayConfig.CardConfigId != null) + updateDefinition.Add( + Builders.Update.Set(c => c.CardConfigId, newDisplayConfig.CardConfigId)); + if (newDisplayConfig.DetailConfigId != null) + updateDefinition.Add( + Builders.Update.Set(c => c.DetailConfigId, newDisplayConfig.DetailConfigId)); + if (newDisplayConfig.HomeConfig != null) + updateDefinition.Add( + Builders.Update.Set(c => c.HomeConfig, newDisplayConfig.HomeConfig)); + if (newDisplayConfig.Hospital != null) + updateDefinition.Add(Builders.Update.Set(c => c.Hospital, newDisplayConfig.Hospital)); + if (newDisplayConfig.HeaderConfig != null) + updateDefinition.Add( + Builders.Update.Set(c => c.HeaderConfig, newDisplayConfig.HeaderConfig)); + if (newDisplayConfig.HomeBanner != null && newDisplayConfig.HomeBanner.Count != 0) + updateDefinition.Add(Builders.Update.Set(c => ((DisplayNurse)c).HomeBanner, + newDisplayConfig.HomeBanner)); + if (newDisplayConfig.ColorConfig != null) + updateDefinition.Add(Builders.Update.Set(c => ((DisplayNurse)c).ColorConfig, + newDisplayConfig.ColorConfig)); + if (newDisplayConfig.FormConfig != null) + updateDefinition.Add(Builders.Update.Set(c => ((DisplayNurse)c).FormConfig, + newDisplayConfig.FormConfig)); + + var originalList = ExtractObservationFields(newDisplayConfig, nurseObs); + if (originalList.Count > 0) + updateDefinition.Add( + Builders.Update.Set(c => ((DisplayNurse)c).FieldList, originalList)); + + var update = Builders.Update.Combine(updateDefinition); + var c = await Collection.FindOneAndUpdateAsync(filter, update, + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); + if (c == null) + { + _logger.LogError("Error on config display repository on UpdateDisplayNurse unable to FindOneAndUpdate"); + return null; + } + + return c as DisplayNurse; + } + catch (Exception e) + { + _logger.LogError("Error on config display repository on UpdateDisplayNurse Exception: {eMessage}", + e.Message); + return null; + } + } + + public async Task UpdateDisplayConfigHospitalName(ObjectId objectIdConfigDisplay, string name) + { + var filter = Builders.Filter.Eq(c => c.Id, objectIdConfigDisplay); + var update = Builders.Update.Set(c => c.Hospital, name); + + var updatedConfig = await Collection.FindOneAndUpdateAsync( + filter, + update, + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After } + ); + + if (updatedConfig == null) + { + _logger.LogError("Error in UpdateDisplayConfigHospitalName: Unable to find and update DisplayConfig."); + return false; + } + + return true; + } + + public async Task UpdateFieldList(ObjectId objectIdConfigDisplay, List fields) + { + var filter = Builders.Filter.Eq(x => x.Id, objectIdConfigDisplay); + var update = Builders.Update.Set(x => x.FieldList, fields); + + var result = await Collection.UpdateOneAsync(filter, update); + return result.ModifiedCount > 0; + } + + public async Task GetDefaultByUnitIdAndType(ObjectId unitId, + DisplayConfigEnums.DisplayType displayType) + { + var unit = await _unitRepository.FindById(unitId); + switch (displayType) + { + case DisplayConfigEnums.DisplayType.DisplayNurse: + if (unit?.Configuration.PlanDisplayConfiguration == null) return null; + return await GetById(unit.Configuration.PlanDisplayConfiguration.Value) as DisplayNurse; + case DisplayConfigEnums.DisplayType.SmartDisplay: + if (unit?.Configuration.SmartDisplayConfiguration == null) return null; + return await GetById(unit.Configuration.SmartDisplayConfiguration.Value) as SmartDisplay; + case DisplayConfigEnums.DisplayType.StandarDisplay: + if (unit?.Configuration.StandarDisplayConfiguration == null) return null; + return await GetById(unit.Configuration.StandarDisplayConfiguration.Value) as StandarDisplay; + default: return null; + } + } + + public async Task DeleteDisplayConfig(ObjectId objectIdConfigDisplay) + { + return await DeleteAsync(objectIdConfigDisplay); + } + + public Task> GetAllCompact() + { + var filter = Builders.Filter.Empty; + return Collection + .Find(filter) + .Project(d => new DisplayConfigMinimalResponse + { + Id = d.Id, + Hospital = d.Hospital ?? "", + Type = d.Type + }).ToListAsync(); + } + + public async Task> GetAllByCardConfigId(ObjectId cardConfigId) + { + var filter = Builders.Filter.Eq(d => d.CardConfigId, cardConfigId); + + var result = await Collection.Find(filter) + .Project(d => d.Id) + .ToListAsync(); + + return result; + } + public async Task> GetAllByCardConfigIdAndRotating(ObjectId cardConfigId) + { + var mainFilter = Builders.Filter.Eq(d => d.CardConfigId, cardConfigId); + + var rotatingFilter = Builders.Filter.ElemMatch( + "cardRotatingLayout", + Builders.Filter.Eq("dataId", cardConfigId) + ); + + var combinedFilter = Builders.Filter.Or(mainFilter, rotatingFilter); + + var result = await Collection.Find(combinedFilter) + .Project(d => d.Id) + .ToListAsync(); + + return result; + } + + public async Task UpdateCardConfigId(ObjectId? displayConfigId, ObjectId? resultId) + { + var filter = Builders.Filter.Eq(x => x.Id, displayConfigId); + var update = Builders.Update.Set(x => x.CardConfigId, resultId); + + var result = await Collection.UpdateOneAsync(filter, update); + return result.ModifiedCount > 0; + } + + public async Task AddChartId(ObjectId? displayConfigId, ObjectId newChartIdToAdd) + { + var filter = Builders.Filter.Eq(x => x.Id, displayConfigId); + var update = Builders.Update + .AddToSet(c => ((SmartDisplay)c).ChartConfigIdList, newChartIdToAdd); + + var result = await Collection.UpdateOneAsync(filter, update); + return result.ModifiedCount > 0; + } + + public async Task UpdateDeletedChartConfig(ObjectId deletedId) + { + try + { + var filter = Builders.Filter.AnyEq("chartConfigIdList", deletedId); + + var update = Builders.Update.Pull("chartConfigIdList", deletedId); + + return await Collection.UpdateManyAsync(filter, update); + } + catch (Exception ex) + { + _logger.LogError("Error actualizando referencias de ChartConfig borrado: {ex}", ex.Message); + throw; + } + } + + public Task GetChartConfig(ObjectId objectIdConfigChart) + { + throw new NotImplementedException(); + } + + public async Task UpdateDetailConfigId(ObjectId? displayConfigId, ObjectId? resultId) + { + var filter = Builders.Filter.Eq(x => x.Id, displayConfigId); + var update = Builders.Update.Set(x => x.DetailConfigId, resultId); + + var result = await Collection.UpdateOneAsync(filter, update); + return result.ModifiedCount > 0; + } + + public async Task> GetAllByCardDetailConfigId(ObjectId baseConfigId) + { + var filter = Builders.Filter.Eq(d => d.DetailConfigId, baseConfigId); + + var result = await Collection.Find(filter) + .Project(d => d.Id) + .ToListAsync(); + + return result; + } + + public sealed override async Task InsertInitialLoad() + { + // 1. Obtener todos los valores y castear al tipo IEnumerable + var allTypes = (DisplayConfigEnums.DisplayType[])Enum.GetValues(typeof(DisplayConfigEnums.DisplayType)); + + // 2. Usar LINQ para filtrar y convertir de nuevo a un array (o lista) + var displayTypesToIterate = allTypes + .Where(dt => dt != DisplayConfigEnums.DisplayType.Unknown) // Filtra el valor 'Unknown' + .ToArray(); + foreach (var type in displayTypesToIterate) + { + var defaultConfigByType = await GetDefault(type); + if (defaultConfigByType == null) + switch (type) + { + case DisplayConfigEnums.DisplayType.DisplayNurse: + var nurse = new DisplayNurse + { + Hospital = "Default", + Type = DisplayConfigEnums.DisplayType.DisplayNurse, + ColorConfig = new ColorConfig(), + FormConfig = new FormConfig + { + Admission = new FormItemOverview { Nhc = true }, + Demographic = new FormItemOverview { Nhc = true }, + Discharge = new FormItemOverview { Nhc = true }, + IncomeInfo = new FormItemOverview { Nhc = true } + }, + HomeBanner = [] + }; + await InsertOneAsyncAndReturn(nurse); + break; + case DisplayConfigEnums.DisplayType.SmartDisplay: + var smart = new SmartDisplay + { + Hospital = "Default", + Type = DisplayConfigEnums.DisplayType.SmartDisplay, + ColorConfig = new ColorConfig() + }; + await InsertOneAsyncAndReturn(smart); + break; + case DisplayConfigEnums.DisplayType.StandarDisplay: + var standar = new StandarDisplay + { + Type = DisplayConfigEnums.DisplayType.StandarDisplay, + Hospital = "Default" + }; + await InsertOneAsyncAndReturn(standar); + break; + } + } + // Si alguno no existe crearlos + } + + private List ExtractObservationFields(DisplayNurseDto newDisplayConfig, List nurseObs) + { + var fieldSet = new HashSet(); + var regex = new Regex(@"""ManualObservationName""\s*:\s*\[\s*((?:""[^""]*""\s*,?\s*)+)\]", + RegexOptions.Compiled); + + // --- 1. CardConfig --- + if (newDisplayConfig.CardConfig?.Rows is { Count: > 0 }) + FillHashSet(JsonConvert.SerializeObject(newDisplayConfig.CardConfig.Rows), regex, fieldSet); + if (newDisplayConfig.CardConfig?.Rows is { Count: > 0 }) + foreach (var row in newDisplayConfig.CardConfig.Rows) + ExtractFromCells(row.Cells, fieldSet); + // --- 2. DetailConfig --- + if (newDisplayConfig.DetailConfig?.NurseRows is { Count: > 0 }) + FillHashSet(JsonConvert.SerializeObject(newDisplayConfig.DetailConfig?.NurseRows), regex, fieldSet); + if (newDisplayConfig.DetailConfig?.NurseRows is { Count: > 0 }) + foreach (var row in newDisplayConfig.DetailConfig.NurseRows) + ExtractFromDetailsCells(row.Cells, fieldSet); + // --- Result: convert to List --- + return fieldSet + .Distinct() + .Select(name => new Field { Name = name, Last = nurseObs.Contains(name) ? 1 : 2 }) + .ToList(); + } + + private void FillHashSet(string newDisplayConfig, Regex regex, HashSet fieldSet) + { + var matches = regex.Matches(newDisplayConfig); + + foreach (Match match in matches) + if (match.Groups.Count > 1) + { + var arrayContent = match.Groups[1].Value; + var items = Regex.Matches(arrayContent, @"""([^""]+)"""); + foreach (Match item in items) fieldSet.Add(item.Groups[1].Value); + } + } + + private void ExtractFromCells(List? cells, HashSet fieldSet) + { + if (cells is null) + return; + + foreach (var cell in cells) + { + // 1. Extraer ObservationName + if (cell.ObservationName is { Count: > 0 }) + foreach (var obsName in cell.ObservationName) + fieldSet.Add(obsName); + + // 2. Recursión: sub-observaciones + if (cell.SubObs is { Count: > 0 }) + ExtractFromCells(cell.SubObs, fieldSet); + } + } + + private void ExtractFromDetailsCells(List? cells, HashSet fieldSet) + { + if (cells is null) + return; + + foreach (var cell in cells) + { + // 1. Extraer ObservationName + if (cell.ObservationName is { Count: > 0 }) + foreach (var obsName in cell.ObservationName) + fieldSet.Add(obsName); + + // 2. Recursión: sub-observaciones + if (cell.Cells is { Count: > 0 }) + ExtractFromDetailsCells(cell.Cells, fieldSet); + } + } + + private IFindFluent CreateFindFluentMinimal( + List> filters, + SortDefinition sort) + { + var combinedFilter = filters.Any() + ? Builders.Filter.And(filters) + : Builders.Filter.Empty; + + return Collection + .Find(combinedFilter) + .Sort(sort) + .Project(d => new DisplayConfigSummary + { + Id = d.Id, + Name = d.Hospital ?? "", + Type = d.Type + }); + } + + private (List> Updates, List Fields) GetBaseUpdateDefinition( + DisplayConfig baseConfig) + { + var fieldList = new List(); + var updateDefinition = new List>(); + // if (baseConfig.CardConfig != null) + // { + // updateDefinition.Add(Builders.Update.Set(c => c.CardConfig, baseConfig.CardConfig)); + // fieldList.AddRange(baseConfig.CardConfig.GetAllObservationNames()); + // } + if (baseConfig.DetailConfig != null) + { + updateDefinition.Add(Builders.Update.Set(c => c.DetailConfig, baseConfig.DetailConfig)); + fieldList.AddRange(baseConfig.DetailConfig.GetAllObservationNames()); + } + + if (baseConfig.HomeConfig != null) + updateDefinition.Add(Builders.Update.Set(c => c.HomeConfig, baseConfig.HomeConfig)); + if (baseConfig.Hospital != null) + updateDefinition.Add(Builders.Update.Set(c => c.Hospital, baseConfig.Hospital)); + if (baseConfig.HeaderConfig != null) + updateDefinition.Add(Builders.Update.Set(c => c.HeaderConfig, baseConfig.HeaderConfig)); + return (updateDefinition, fieldList); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/DisplayDetailConfigRepository.cs b/adas-core.Infrastructure/Repositories/DisplayDetailConfigRepository.cs new file mode 100644 index 00000000..743d240f --- /dev/null +++ b/adas-core.Infrastructure/Repositories/DisplayDetailConfigRepository.cs @@ -0,0 +1,104 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Infrastructure.Repositories; + +public class DisplayDetailConfigRepository : MongoRepository, IDisplayDetailConfigRepository +{ + private readonly ApiSettings _apiSettings; + private readonly ILogger _logger; + + + + public DisplayDetailConfigRepository( + IMongoDatabase database, + ApiSettings apiSettings, + ILogger logger + ) : base(database) + { + _apiSettings = apiSettings; + _logger = logger; + } + + public override string GetCollectionName() + { + return _apiSettings.DisplayDetailConfig; + } + + public async Task> GetAll() + { + var result = await Collection.Find(Builders.Filter.Empty).ToListAsync(); + return result; + } + + public async Task GetById(ObjectId configId) + { + try + { + var result = await Collection.FindAsync(Builders.Filter.Eq(p => p.Id, configId)); + return await result.FirstOrDefaultAsync(); + } + catch (Exception ex) + { + _logger.LogError("Error on config card display repository on GetById Exception: {ex}", ex.Message); + return null; + } + } + + public async Task InsertOneAsyncAndReturn(CardDetailsConfig config) + { + try + { + await Collection.InsertOneAsync(config); + return config; + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + return null; + } + } + + public async Task> UpdateOne(CardDetailsConfig? config) + { + try + { + if (config == null) return new UpdateResponse(0, null); + var filter = Builders.Filter.Eq(c => c.Id, config.Id); + var update = Builders.Update + .Set(c => c.NurseRows, config.NurseRows) + .Set(c => c.SmartSections, config.SmartSections) + .Set(c => c.Header, config.Header); + + // Realizamos la actualización + var result = await Collection.UpdateOneAsync(filter, update); + + // Buscamos el documento actual (ya actualizado o el existente si no hubo cambios) + var updatedDoc = await Collection.Find(filter).FirstOrDefaultAsync(); + + // result.ModifiedCount será 1 si cambió algo, o 0 si los datos eran idénticos + return new UpdateResponse(result.ModifiedCount, updatedDoc); + } + catch (Exception e) + { + _logger.LogError(e.Message); + return new UpdateResponse(0, null); + } + } + + public async Task DeleteOne(ObjectId configId) + { + return await DeleteAsync(configId); + } + + public Task UpdateCardConfigId(ObjectId? displayConfigId, ObjectId? resultId) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/DisplayRepository.cs b/adas-core.Infrastructure/Repositories/DisplayRepository.cs new file mode 100644 index 00000000..5571f6b4 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/DisplayRepository.cs @@ -0,0 +1,355 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Serilog; +using System.Text.RegularExpressions; +using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; + +namespace adas_core.Infrastructure.Repositories; + +public class DisplayRepository : MongoRepository, IDisplayRepository +{ + #region Properties + + private readonly ApiSettings _apiSettings; + + private readonly ILogger _logger; + // private readonly Idisplay _logger; + + #endregion + + #region Constructor + + public DisplayRepository( + IOptions apiSettings, + IMongoDatabase database, + ILogger logger) : base(database) + { + _logger = logger; + _apiSettings = apiSettings.Value; + } + + + + #endregion + + #region Methods + + #region Create + + public override async Task CreateIndexes() + { + var options = new CreateIndexOptions { Background = true, Unique = false }; + + var indexes = new List> + { + new("{ displayConfigId: 1 }", options), + new("{ unitId: 1 }", options) + }; + + await MongoUtils.EnsureIndexes(Collection, indexes); + } + + #endregion + + #region Read + + public override string GetCollectionName() + { + return _apiSettings.Displays; + } + + public async Task> GetAll() + { + var result = await Collection.Find(Builders.Filter.Empty).ToListAsync(); + return result; + } + + public IFindFluent GetPaginatedDisplays(PaginationFilter filter) + { + var filterBuilder = Builders.Filter; + var sort = Builders.Sort.Ascending("name"); + var filters = new List>(); + + if (filter.FilteredRequest == null) + return CreateFindFluent(filters, sort); + + // Text filter seguro + if (!string.IsNullOrWhiteSpace(filter.FilteredRequest.Text)) + { + var textFilter = filter.FilteredRequest.Text; + + if (textFilter.Length > 100) + throw new BadRequestException("Text filter too long"); + + var escapedTextFilter = Regex.Escape(textFilter); + + filters.Add(filterBuilder.Regex( + d => d.Name, + new BsonRegularExpression(escapedTextFilter, "i") + )); + } + + // UnitId + if (!string.IsNullOrWhiteSpace(filter.FilteredRequest.UnitId) && + ObjectId.TryParse(filter.FilteredRequest.UnitId, out var unitId)) + { + filters.Add(filterBuilder.Eq(d => d.UnitId, unitId)); + } + // UnitName fallback + else if (!string.IsNullOrWhiteSpace(filter.FilteredRequest.UnitName)) + { + filters.Add(filterBuilder.Eq(d => d.Unit!.Name, filter.FilteredRequest.UnitName)); + } + + // DisplayType + if (filter.FilteredRequest.DisplayType != null) + { + filters.Add(filterBuilder.Eq(d => d.Type, filter.FilteredRequest.DisplayType)); + } + + return CreateFindFluent(filters, sort); + } + + private IFindFluent CreateFindFluent(List> filters, + SortDefinition sort) + { + var combinedFilter = filters.Any() + ? Builders.Filter.And(filters) + : Builders.Filter.Empty; // Filtra todo si no hay filtros + return Collection.Find(combinedFilter).Sort(sort); + } + + public async Task> GetByPointOfCare(PointOfCare pointOfCare) + { + var filter = Builders.Filter.AnyEq(x => x.PointOfCareIdList, pointOfCare.Id); + var result = await Collection.Find(filter).ToListAsync(); + return result; + } + + public async Task GetByName(string name) + { + var result = await Collection.FindAsync(Builders.Filter.Eq(p => p.Name, name)); + return await result.FirstOrDefaultAsync(); + } + + public async Task GetById(ObjectId id) + { + var result = await Collection.FindAsync(Builders.Filter.Eq(p => p.Id, id)); + + return await result.FirstOrDefaultAsync(); + } + + public async Task GetByIdWithConfigDisplay(ObjectId id) + { + var pipeline = new BsonDocument[] + { + new("$match", new BsonDocument("_id", id)), + new("$lookup", new BsonDocument + { + { "from", "config_displays" }, + { "localField", "displayConfigId" }, // Asumiendo que este es el campo que refiere a config_display + { "foreignField", "_id" }, + { "as", "DisplayNurse" } + }), + new("$unwind", new BsonDocument + { + { "path", "$configDisplay" }, + { "preserveNullAndEmptyArrays", true } + }) + }; + + var result = await Collection.AggregateAsync(pipeline, new AggregateOptions { AllowDiskUse = true }); + return await result.FirstOrDefaultAsync(); + } + + public async Task> GetByUnitId(ObjectId id) + { + var filter = Builders.Filter.Eq(p => p.UnitId, id); + var result = await Collection.Find(filter).ToListAsync(); + return result; + } + + public async Task CountByUnitId(ObjectId unitId) + { + try + { + return await Collection.CountDocumentsAsync(Builders.Filter.Eq(p => p.UnitId, unitId)); + } + catch (Exception e) + { + _logger.LogError(e.Message); + return 0; + } + } + + public async Task> GetByConfigId(ObjectId id) + { + var result = await Collection.FindAsync(Builders.Filter.Eq(p => p.DisplayConfigId, id)); + return result.ToList(); + } + + public async Task> GetByCardConfigId(ObjectId configId) + { + try + { + var aggregate = Collection.Aggregate() + // 1. Unimos la colección Display con DisplayConfig + .Lookup( + _apiSettings.DisplaysConfig, // Nombre de la colección externa + "displayConfigId", // Campo local en la colección 'Display' + "_id", // Campo en la colección 'DisplayConfig' + "displayConfig" // Nombre de la propiedad en la clase C# (debe coincidir) + ) + // 2. Convertimos el array resultante del lookup en un objeto único + .Unwind("displayConfig", new AggregateUnwindOptions + { + PreserveNullAndEmptyArrays = false // Si no tiene config, no nos interesa + }) + // 3. Filtramos por la propiedad interna del objeto ya "unido" + // Nota: Usamos el nombre del campo tal cual está en el BSON (normalmente camelCase) + .Match(Builders.Filter.Eq("displayConfig.cardConfigId", configId)) + + // 4. Casteamos el resultado de vuelta a nuestra clase Display + .As(); + + return await aggregate.ToListAsync(); + } + catch (Exception ex) + { + _logger.LogError("Error en GetByCardConfigId: {ex}", ex.Message); + return []; + } + } + + public async Task IsDisplayConfigInUse(ObjectId displayConfigId) + { + return await Collection.CountDocumentsAsync( + Builders.Filter.Eq(p => p.DisplayConfigId, displayConfigId)); + } + + #endregion + + #region Update + + public async Task UpdatePointOfCareList(ObjectId objectId, List listPocObId) + { + try + { + var filter = Builders.Filter.Eq("Id", objectId); + var update = Builders.Update + .Set(c => c.PointOfCareIdList, listPocObId); + + return await Collection.FindOneAndUpdateAsync(filter, update, + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); + } + catch (Exception e) + { + _logger.LogError($"Unable to update pointOfCareList from Display Exception {e}"); + return null; + } + } + + public async Task UpdateConfig(ObjectId objectId, DisplayConfig config) + { + try + { + var filter = Builders.Filter.Eq("Id", objectId); + var update = Builders.Update + .Set(c => c.DisplayConfig, config); + + return await Collection.FindOneAndUpdateAsync(filter, update, + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); + } + catch (Exception e) + { + _logger.LogError($"Unable to update config from Display Exception {e.Message}"); + return null; + } + } + + public async Task UpdateConfigId(ObjectId objectId, ObjectId displayConfigId) + { + try + { + var filter = Builders.Filter.Eq("Id", objectId); + var update = Builders.Update + .Set(c => c.DisplayConfigId, displayConfigId); + + return await Collection.FindOneAndUpdateAsync(filter, update, + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); + } + catch (Exception e) + { + _logger.LogError($"Unable to update config from Display Exception {e.Message}"); + return null; + } + } + + public async Task UpdateConfigPreset(ObjectId objectIdDisplay, ObjectId objectIdConfigDisplay) + { + try + { + var filter = Builders.Filter.Eq("Id", objectIdDisplay); + var update = Builders.Update + .Set(c => c.DisplayConfigId, objectIdConfigDisplay); + + return await Collection.FindOneAndUpdateAsync(filter, update, + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); + } + catch (Exception e) + { + _logger.LogError($"Unable to update config from Display Exception: {e.Message}"); + return null; + } + } + + public async Task UpdateName(Display display, string name) + { + try + { + var filter = Builders.Filter.Eq("_id", display.Id); + + var update = Builders.Update + .Set(d => d.Name, name); + + return await Collection.FindOneAndUpdateAsync(filter, update, + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); + } + catch (Exception ex) + { + _logger.LogError(ex.Message); + throw; + } + } + + #endregion + + #region Delete + + public async Task DeleteManyByUnitId(ObjectId unitId) + { + try + { + var filter = Builders.Filter.Where(p => p.UnitId == unitId); + await Collection.DeleteManyAsync(filter); + + return true; + } + catch (Exception e) + { + Log.Logger.Error(e.Message); + return false; + } + } + + #endregion + + #endregion +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/HistoricalConfigChangesRepository.cs b/adas-core.Infrastructure/Repositories/HistoricalConfigChangesRepository.cs new file mode 100644 index 00000000..53cdc807 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/HistoricalConfigChangesRepository.cs @@ -0,0 +1,143 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; + +namespace adas_core.Infrastructure.Repositories; + +public class HistoricalConfigChangesRepository : MongoRepository, + IHistoricalConfigChangesRepository +{ + private readonly ApiSettings _apiSettings; + private readonly ILogger _logger; + + public HistoricalConfigChangesRepository(IOptions apiSettings, IMongoDatabase database, + ILogger logger) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + _logger = logger; + } + + + public override string GetCollectionName() + { + return _apiSettings.HistoricalConfigChanges ?? "historicalConfigChanges"; + } + + public override async Task InsertOneAsync(HistoricalConfigChanges historicalConfigChanges) + { + try + { + await Collection.InsertOneAsync(historicalConfigChanges); + return await FindById(historicalConfigChanges.Id); + } + catch (Exception ex) + { + _logger.LogError("Error inserting historicalConfigChanges {exMessage}", ex.Message); + return null; + } + } + + + public async Task Delete(ObjectId id) + { + return await DeleteAsync(id); + } + + public async Task> FindAll() + { + var result = await Collection.FindAsync(_ => true); + return await result.ToListAsync(); + } + + public async Task> FindAllIds() + { + List listCollection = []; + var allCollection = await Collection.FindAsync(_ => true); + + listCollection.AddRange(allCollection.ToList().Select(item => item.Id)); + + return listCollection; + } + + public async Task FindById(ObjectId id) + { + var result = await Collection.FindAsync(Builders.Filter.Eq(x => x.Id, id)); + return await result.FirstOrDefaultAsync(); + } + + + public async Task Update(HistoricalConfigChanges historicalConfigChanges) + { + var filter = Builders.Filter.Eq("_id", historicalConfigChanges.Id); + var update = Builders.Update + .Set(c => c, historicalConfigChanges); + + return await Collection.FindOneAndUpdateAsync( + filter, + update, + new FindOneAndUpdateOptions + { + ReturnDocument = ReturnDocument.After + }); + } + + public async Task> FindLastHistoricalConfigChangesByType( + DisplayConfigEnums.ConfigTypes cfgType, int num = 10) + { + var filterDefinitionBuilder = Builders.Filter; + var filter = filterDefinitionBuilder.Eq(c => c.ConfigType, cfgType); + + var result = await Collection.FindAsync( + filter, + new FindOptions + { Sort = Builders.Sort.Descending("time"), Limit = num } + ); + + return await result.ToListAsync(); + } + + public async Task> FindLastHistoricalConfigChangesByUser(string user, + DisplayConfigEnums.ConfigTypes? cfgType = null, int num = 10) + { + var filterDefinitionBuilder = Builders.Filter; + var filter = filterDefinitionBuilder.Eq(c => c.Username, user); + + if (cfgType != null) filter &= filterDefinitionBuilder.Eq(c => c.ConfigType, cfgType.Value); + + var result = await Collection.FindAsync( + filter, + new FindOptions + { Sort = Builders.Sort.Descending("time"), Limit = num } + ); + + return await result.ToListAsync(); + } + + public override async Task CreateIndexes() + { + try + { + var options = new CreateIndexOptions { Background = true, Unique = false }; + var indexes = new List> + { + new("{ configType: 1, time:-1 }", options), + new("{ username: 1, time: -1 }", options) + }; + await MongoUtils.EnsureIndexes(Collection, indexes); + } + catch (Exception e) + { + _logger.LogError( + "error creating indexes for HistoricalConfigChanges collection {eMessage} TRACE: {eStackTrace}", + e.Message, e.StackTrace); + throw; + } + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/LightBeaconRepository.cs b/adas-core.Infrastructure/Repositories/LightBeaconRepository.cs new file mode 100644 index 00000000..5576114f --- /dev/null +++ b/adas-core.Infrastructure/Repositories/LightBeaconRepository.cs @@ -0,0 +1,118 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Serilog; +using System.Text.RegularExpressions; + +namespace adas_core.Infrastructure.Repositories; + +public class LightBeaconRepository : MongoRepository, ILightBeaconRepository +{ + private readonly ApiSettings _apiSettings; + + + public LightBeaconRepository(IMongoDatabase database, IOptions apiSettings) : base(database) + { + _apiSettings = apiSettings.Value; + } + + public override string GetCollectionName() + { + return _apiSettings.LightBeacons; + } + + public List GetLightBeaconInList(List configurationRelayList) + { + var filterBuilder = Builders.Filter; + + var filter = filterBuilder.And( + filterBuilder.In(r => r.Id, configurationRelayList)); + + return Collection.Find(filter).ToList(); + } + + public async Task GetById(ObjectId relayId) + { + try + { + var filter = Builders.Filter.Eq(x => x.Id, relayId); + var result = await Collection.FindAsync(filter, null); + return result.FirstOrDefault(); + } + catch (Exception e) + { + Log.Error("Exception trying to get relay by id: {id}. Exception {e}", relayId, e); + return null; + } + } + + public async Task GetByName(string? requestRelayName) + { + var filterBuilder = Builders.Filter; + + var filter = filterBuilder.Eq(r => r.Name, requestRelayName); + + return await Collection.Find(filter).FirstOrDefaultAsync(); + } + + public async Task InsertOneAsyncAndReturn(LightBeacon beacon) + { + try + { + await Collection.InsertOneAsync(beacon); + return beacon; + } + catch (Exception ex) + { + Log.Error(ex.Message); + return null; + } + } + + public IFindFluent GetPaginatedRelays(PaginationFilter filter) + { + var filterBuilder = Builders.Filter; + var sort = Builders.Sort.Ascending("name"); + var filters = new List>(); + + if (filter.FilteredRequest == null) + return CreateFindFluent(filters, sort); + + if (!string.IsNullOrWhiteSpace(filter.FilteredRequest.Text)) + { + var textFilter = filter.FilteredRequest.Text; + + if (textFilter.Length > 100) + throw new BadRequestException("Text filter too long"); + + var textFilterEscaped = Regex.Escape(textFilter); + + filters.Add( + filterBuilder.Regex( + p => p.Name, + new BsonRegularExpression(textFilterEscaped, "i") + ) + ); + } + + return CreateFindFluent(filters, sort); + } + + public Task> GetSearchByName(string textToSearch) + { + throw new NotImplementedException(); + } + + private IFindFluent CreateFindFluent(List> filters, SortDefinition sort) + { + var combinedFilter = filters.Any() + ? Builders.Filter.And(filters) + : Builders.Filter.Empty; + return Collection.Find(combinedFilter).Sort(sort); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/MasterListRepository.cs b/adas-core.Infrastructure/Repositories/MasterListRepository.cs new file mode 100644 index 00000000..5526f8aa --- /dev/null +++ b/adas-core.Infrastructure/Repositories/MasterListRepository.cs @@ -0,0 +1,1395 @@ +using System.Text; +using System.Text.RegularExpressions; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.Masters; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using Serilog; + +namespace adas_core.Infrastructure.Repositories; + +public class MasterListRepository : MongoRepository, IMasterListRepository where T : MasterList +{ + private readonly ApiSettings _apiSettings; + + public MasterListRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + } + + + public override string GetCollectionName() + { + return typeof(T) switch + { + { } t when t == typeof(AltableOptionList) => _apiSettings.AltableOptionList, + { } t when t == typeof(AllergyList) => _apiSettings.AllergyList, + { } t when t == typeof(DestinationList) => _apiSettings.DestinationList, + { } t when t == typeof(DiagnosisList) => _apiSettings.DiagnosisList, + { } t when t == typeof(DischargeStatusList) => _apiSettings.DischargeStatusList, + { } t when t == typeof(DoctorList) => _apiSettings.DoctorList, + { } t when t == typeof(DoctorTypeList) => _apiSettings.DoctorTypeList, + { } t when t == typeof(InternalDestinationList) => _apiSettings.InternalDestinationList, + { } t when t == typeof(InsulationList) => _apiSettings.InsulationList, + { } t when t == typeof(LanguageBarrierList) => _apiSettings.LanguageBarrierList, + { } t when t == typeof(MobilityOptionList) => _apiSettings.MobilityOptionList, + { } t when t == typeof(OriginList) => _apiSettings.OriginList, + { } t when t == typeof(PatientStatusList) => _apiSettings.PatientStatusList, + { } t when t == typeof(ProcedureList) => _apiSettings.ProcedureList, + { } t when t == typeof(TestList) => _apiSettings.TestList, + { } t when t == typeof(ServiceList) => _apiSettings.ServiceList, + { } t when t == typeof(TherapeuticCeilingList) => _apiSettings.TherapeuticCeilingList, + { } t when t == typeof(TreatmentList) => _apiSettings.TreatmentList, + { } t when t == typeof(VisitOptionList) => _apiSettings.VisitOptionList, + { } t when t == typeof(AccessControlList) => _apiSettings.AccessControlList, + { } t when t == typeof(PassiveSittingList) => _apiSettings.PassiveSittingList, + { } t when t == typeof(GenericList) => _apiSettings.GenericList, + _ => "list_master" + }; + } + + public override async Task InsertOneAsync(T entity) + { + try + { + await base.InsertOneAsync(entity); + } + catch (Exception e) + { + Log.Error("Exception trying to insert entity: {entity}. Exception: {e}", entity, e); + throw; + } + } + + public async Task Delete(ObjectId id) + { + try + { + var filter = Builders.Filter.Eq(doc => doc.Id, id); + await Collection.DeleteOneAsync(filter); + } + catch (Exception e) + { + Log.Error("Exception deleting entity {entity}: {id}. Exception {e}", typeof(T).Name, id, e); + throw; + } + } + + public async Task Update(T entity) + { + try + { + var filter = Builders.Filter.Eq(doc => doc.Id, entity.Id); + await Collection.ReplaceOneAsync(filter, entity, new ReplaceOptions { IsUpsert = true }); + } + catch (Exception e) + { + Log.Error("Exception trying to update entity: {entity}. Exception: {e}", entity, e); + throw; + } + } + + public async Task UpdateFullMasterListOption(ObjectId id, OptionList newOpt) + { + var filter = Builders.Filter.Where(o => o.Id == id && o.Options.Any(opt => opt.Id == newOpt.Id) + ); + var update = Builders.Update + .Set("options.$.name", newOpt.Name) + .Set("options.$.description", newOpt.Description) + .Set("options.$.bgColor", newOpt.BgColor) + .Set("options.$.color", newOpt.Color) + .Set("options.$.endDate", newOpt.EndDate) + .Set("options.$.optionType", newOpt.OptionType) + .Set("options.$.isDefault", newOpt.IsDefault) + .Set("options.$.initDate", newOpt.InitDate) + .Set("options.$.iconDefault", newOpt.IconDefault) + .Set("options.$.iconCategory", newOpt.IconCategory) + .Set("options.$.iconColor", newOpt.IconColor) + .Set("options.$.localeItems", newOpt.LocaleItems) + ; + var options = new FindOneAndUpdateOptions + { + ReturnDocument = ReturnDocument.After + }; + var updatedList = await Collection.FindOneAndUpdateAsync(filter, update, options); + + return updatedList; + } + + public async Task FindById(ObjectId id) + { + try + { + var filter = Builders.Filter.Eq(p => p.Id, id); + var projection = Builders.Projection.Slice("options", 100); // Limita a 100 elementos en 'options' + + var options = new FindOptions + { + Projection = projection + }; + + var result = await Collection.FindAsync(filter, options); + return await result.FirstOrDefaultAsync(); + } + catch (Exception ex) + { + Log.Error("Error searching entity by id: {id}. Exception: {ex}", id, ex); + return null; + } + } + + public async Task FindOptionItemById(ObjectId masterId, ObjectId optionId, LocaleEnum locale) + { + try + { + var localeCode = locale == LocaleEnum.Default + ? "default" + : locale.ToString().ToLower(); + + var pipeline = new[] + { + // 1) Filtrar documento + new BsonDocument("$match", new BsonDocument("_id", masterId)), + + // 2) Desanidar opciones + new BsonDocument("$unwind", "$options"), + + // 3) Filtrar la opción concreta + new BsonDocument("$match", new BsonDocument("options._id", optionId)), + + // 4) Aplicar traducciones + new BsonDocument("$project", new BsonDocument + { + { + "_id", "$options._id" // este sí es el id final que queremos + }, + + { + "name", + new BsonDocument("$cond", new BsonArray + { + // IF: locale = default + new BsonDocument("$or", new BsonArray + { + new BsonDocument("$eq", new BsonArray { localeCode, "default" }), + new BsonDocument("$eq", new BsonArray + { + new BsonDocument("$toLower", "$defaultLocale"), + localeCode + }) + }), + + // THEN: nombre principal + "$options.name", + + // ELSE: traducción + new BsonDocument("$ifNull", new BsonArray + { + new BsonDocument("$let", new BsonDocument + { + { + "vars", + new BsonDocument + { + { + "localeItem", + new BsonDocument("$first", + new BsonDocument("$filter", + new BsonDocument + { + { + "input", + new BsonDocument("$map", + new BsonDocument + { + { + "input", + new BsonDocument("$objectToArray", + "$options.localeItems") + }, + { "as", "pair" }, + { + "in", + new BsonDocument("$cond", + new BsonArray + { + new BsonDocument("$eq", + new BsonArray + { "$$pair.k", localeCode }), + "$$pair.v", + BsonNull.Value + }) + } + }) + }, + { "as", "v" }, + { + "cond", + new BsonDocument("$ne", + new BsonArray { "$$v", BsonNull.Value }) + } + }) + ) + } + } + }, + { + "in", + new BsonDocument("$ifNull", + new BsonArray + { + "$$localeItem.name", + "$options.name" + }) + } + }), + + "$options.name" + }) + }) + }, + + { "optionType", "$options.optionType" }, + { "description", "$options.description" }, + { "color", "$options.color" }, + { "bgColor", "$options.bgColor" }, + { "iconDefault", "$options.iconDefault" }, + { "iconCategory", "$options.iconCategory" }, + { "iconColor", "$options.iconColor" }, + { "initDate", "$options.initDate" }, + { "endDate", "$options.endDate" }, + { "isDefault", "$options.isDefault" }, + { "localeItems", "$options.localeItems" } + }) + }; + + var result = await Collection.Aggregate(pipeline).FirstOrDefaultAsync(); + if (result == null) + return null; + + return BsonSerializer.Deserialize(result); + } + catch (Exception ex) + { + Log.Error("Error FindOptionItemById: {ex}", ex); + return null; + } + } + + public async Task FindOptionItemById(ObjectId masterId, ObjectId optionId) + { + try + { + var filter = Builders.Filter.And( + Builders.Filter.Eq("_id", masterId), + Builders.Filter.Eq("options._id", optionId) + ); + + var projection = Builders.Projection.Include("options.$"); + + // Esto devuelve el documento padre pero SOLO con la opción que coincide en el array + var master = await Collection.Find(filter).Project(projection).FirstOrDefaultAsync(); + + // Accedemos a la primera (y única) opción del array filtrado + return master?.Options?.FirstOrDefault(); + } + catch (Exception ex) + { + Log.Error("Error FindOptionItemById: {ex}", ex); + return null; + } + } + + + public async Task FindById(ObjectId id, LocaleEnum? locale) + { + try + { + var localeCode = locale.HasValue && locale.Value != LocaleEnum.Default + ? locale.Value.ToString().ToLower() + : "default"; + + var pipeline = new[] + { + // 1. Filtrar documento + new BsonDocument("$match", new BsonDocument("_id", id)), + + // 2. Desanidar options + new BsonDocument("$unwind", + new BsonDocument + { + { "path", "$options" }, + { "preserveNullAndEmptyArrays", true } + }), + + + // 3. Proyección + new BsonDocument("$project", new BsonDocument + { + { "_id", "$_id" }, + { "name", "$name" }, + { "defaultLocale", "$defaultLocale" }, + { "listType", "$listType" }, + { "canAddElement", "$canAddElement" }, + { "optionListDetails", "$optionListDetails" }, + { "autoObservationName", "$autoObservationName" }, + { "description", "$description" }, + + { + "options", + new BsonDocument + { + { "_id", "$options._id" }, + { "optionType", "$options.optionType" }, + { "description", "$options.description" }, + { "color", "$options.color" }, + { "bgColor", "$options.bgColor" }, + { "iconDefault", "$options.iconDefault" }, + { "iconCategory", "$options.iconCategory" }, + { "iconColor", "$options.iconColor" }, + { "initDate", "$options.initDate" }, + { "endDate", "$options.endDate" }, + { "isDefault", "$options.isDefault" }, + + // ------------------------------------------------------------------ + // NAME traducido con fallback (misma lógica que antes, corregida) + // ------------------------------------------------------------------ + { + "name", + new BsonDocument("$cond", new BsonArray + { + // IF: locale == default OR defaultLocale == localeCode + new BsonDocument("$or", new BsonArray + { + new BsonDocument("$eq", new BsonArray { localeCode, "default" }), + new BsonDocument("$eq", new BsonArray + { + new BsonDocument("$toLower", "$defaultLocale"), + localeCode + }) + }), + + // THEN = nombre original + "$options.name", + + // ELSE = nombre traducido (si existe) sino original + new BsonDocument("$ifNull", new BsonArray + { + new BsonDocument("$let", new BsonDocument + { + { + "vars", + new BsonDocument + { + { + "localeItem", + new BsonDocument("$first", + new BsonDocument("$filter", + new BsonDocument + { + { + "input", + new BsonDocument("$map", + new BsonDocument + { + { + "input", + new BsonDocument("$objectToArray", + "$options.localeItems") + }, + { "as", "pair" }, + { + "in", + new BsonDocument("$cond", + new BsonArray + { + new BsonDocument("$eq", + new BsonArray + { + "$$pair.k", + localeCode + }), + "$$pair.v", + BsonNull.Value + }) + } + }) + }, + { "as", "v" }, + { + "cond", + new BsonDocument("$ne", + new BsonArray { "$$v", BsonNull.Value }) + } + }) + ) + } + } + }, + { + "in", + new BsonDocument("$ifNull", new BsonArray + { + "$$localeItem.name", + "$options.name" + }) + } + }), + + "$options.name" + }) + }) + }, + + // ------------------------------------------------------------------ + // SIEMPRE eliminar localeItems del resultado final + // ------------------------------------------------------------------ + { + "localeItems", + new BsonDocument("$cond", + new BsonArray { true, "$$REMOVE", "$$REMOVE" }) + } + } + } + }), + + + // 4. Agrupar de vuelta + new BsonDocument("$group", new BsonDocument + { + { "_id", "$_id" }, + { "name", new BsonDocument("$first", "$name") }, + { "defaultLocale", new BsonDocument("$first", "$defaultLocale") }, + { "listType", new BsonDocument("$first", "$listType") }, + { "canAddElement", new BsonDocument("$first", "$canAddElement") }, + { "optionListDetails", new BsonDocument("$first", "$optionListDetails") }, + { "autoObservationName", new BsonDocument("$first", "$autoObservationName") }, + { "description", new BsonDocument("$first", "$description") }, + { "options", new BsonDocument("$push", "$options") } + }) + }; + + var result = await Collection.Aggregate(pipeline).FirstOrDefaultAsync(); + return result; + } + catch (Exception ex) + { + Log.Error("Error searching entity by id: {id}. Exception: {ex}", id, ex); + return null; + } + } + + public async Task FindByName(string name) + { + try + { + var filter = Builders.Filter.Eq(p => p.Name, name); + var projection = Builders.Projection.Slice("options", 100); // Limita a 100 elementos en 'options' + + var options = new FindOptions + { + Projection = projection + }; + + var result = await Collection.FindAsync(filter, options); + return await result.FirstOrDefaultAsync(); + } + catch (Exception ex) + { + Log.Error("Error searching entity by name: {name}. Exception: {ex}", name, ex); + return null; + } + } + + public async Task> GetMasterListByIdAndTextSearchContaining(ObjectId id, string? textSearch) + { + return await GetOptionsByTextSearch(textSearch, id); + } + + public IFindFluent GetPaginatedMasterList(PaginationFilter filter) + { + var filterBuilder = Builders.Filter; + var sort = Builders.Sort.Ascending("name"); + var filters = new List>(); + + if (filter.FilteredRequest == null) return CreateFindFluent(filters, sort); + + // Verifica si el nombre contiene el valor de FilteredRequest.Text + if (!string.IsNullOrEmpty(filter.FilteredRequest?.Text)) + { + var textFilter = filter.FilteredRequest.Text; + var escapedTextFilter = Regex.Escape(textFilter); // Escapa caracteres especiales para evitar problemas en la regex + + filters.Add(filterBuilder.Regex(d => d.Name, + new BsonRegularExpression(escapedTextFilter, "i"))); // Case-insensitive regex match for title + } + + return CreateFindFluent(filters, sort); + } + + public async Task> GetPaginatedOptions(PaginationFilter filter, ObjectId listId) + { + //TODO: LOCALE + var masterList = await FindById(listId); + if (masterList == null) return []; + var options = masterList.Options.AsQueryable(); + + if (filter.FilteredRequest != null && !string.IsNullOrEmpty(filter.FilteredRequest.Text)) + { + var textFilter = filter.FilteredRequest.Text.ToLower(); + options = options.Where(o => o.Name.ToLower().Contains(textFilter) + || (o.Description != null && o.Description.ToLower().Contains(textFilter)) + || (o.OptionType != null && o.OptionType.ToLower().Contains(textFilter))); + } + + return options.ToList(); + } + + public async Task AddOptionToMasterList(ObjectId id, FilterOptionListElement opt) + { + var exist = await GetMasterListByIdAndSearchOptions(id, opt); + if (exist.Count > 0) return null; + var currentList = await FindById(id); + var filter = Builders.Filter.Eq(p => p.Id, id); + var localeList = currentList?.DefaultLocale ?? LocaleEnum.Es; + var newLocale = opt.IsFromAdmPanel.HasValue && opt.IsFromAdmPanel.Value + ? opt.LocaleItems + : GetNewItemLocale(localeList, opt.Name!); + var option = new OptionList + { + Id = ObjectId.GenerateNewId(), + Name = opt.Name!, + OptionType = opt.OptionType ?? null, + Description = opt.Description ?? null, + Color = opt.Color ?? null, + BgColor = opt.BgColor ?? null, + IconCategory = opt.IconCategory ?? null, + IconColor = opt.IconColor ?? null, + IconDefault = opt.IconDefault ?? null, + LocaleItems = newLocale + }; + var update = Builders.Update.Push(m => m.Options, option); + + try + { + var result = await Collection.UpdateOneAsync(filter, update); + return result.ModifiedCount > 0 ? option : null; + } + catch (Exception ex) + { + // Manejo de excepciones adecuado + Log.Error("Error ADDING option to master list: {name}. Exception: {ex}", id, ex); + return null; + } + } + + public async Task> GetAll() + { + try + { + var result = await Collection.FindAsync(_ => true); + return await result.ToListAsync(); + } + catch (Exception ex) + { + Log.Error("Error getting all entities. Exception: {ex}", ex); + return []; + } + } + + public async Task> GetAllWithoutOptions() + { + try + { + return await Collection + .Find(FilterDefinition.Empty) + .Project(ml => new MasterListDto + { + Id = ml.Id, + Name = ml.Name, + Description = ml.Description, + ListType = ml.ListType, + Options = ml.Options.Count + }) + .ToListAsync(); + } + catch (Exception ex) + { + Log.Error("Error getting all entities with no options. Exception: {ex}", ex); + return []; + } + } + + public async Task Count() + { + try + { + var result = await Collection.CountDocumentsAsync(_ => true); + return (int)result; + } + catch (Exception ex) + { + Log.Error("Error counting all entities. Exception: {ex}", ex); + return 0; + } + } + + public async Task> GetMasterListByIdAndSearchOptions(ObjectId id, FilterOptionListElement? filters) + { + try + { + if (filters == null) + return []; + + // ----------------------------------------- + // Construcción de las condiciones de búsqueda + // ----------------------------------------- + var conditions = new List(); + + if (!string.IsNullOrWhiteSpace(filters.OptionType)) + conditions.Add($@"{{ $eq: ['$$option.optionType', '{filters.OptionType}'] }}"); + + if (!string.IsNullOrWhiteSpace(filters.Text)) + { + var normalized = filters.Text.ToLowerInvariant(); + var pattern = BuildRegexPattern(normalized); + + conditions.Add($@" + {{ + $or: [ + {{ $regexMatch: {{ input: '$$option.name', regex: '{pattern}', options: 'i' }} }}, + {{ $regexMatch: {{ input: '$$option.description', regex: '{pattern}', options: 'i' }} }} + ] + }}"); + } + else + { + if (!string.IsNullOrWhiteSpace(filters.Name)) + conditions.Add($@"{{ $eq: ['$$option.name', '{filters.Name}'] }}"); + + if (!string.IsNullOrWhiteSpace(filters.Description)) + conditions.Add($@"{{ $eq: ['$$option.description', '{filters.Description}'] }}"); + } + + var filterCondition = conditions.Count == 0 + ? "true" + : "$and: [" + string.Join(",", conditions) + "]"; + + var localeCode = filters.Locale?.ToString().ToLower() ?? "default"; + + // ----------------------------------------- + // Pipeline de agregación + // ----------------------------------------- + var aggregationPipeline = new[] + { + // 0) MATCH + BsonDocument.Parse(@$"{{ $match: {{ ""_id"": ObjectId(""{id}"") }} }}"), + + // 1) Primer $project: construye originalName / translatedName + BsonDocument.Parse($@" + {{ + $project: {{ + defaultLocale: '$defaultLocale', + options: {{ + $map: {{ + input: '$options', + as: 'o', + in: {{ + _id: '$$o._id', + optionType: '$$o.optionType', + description: '$$o.description', + color: '$$o.color', + bgColor: '$$o.bgColor', + iconDefault: '$$o.iconDefault', + iconCategory: '$$o.iconCategory', + iconColor: '$$o.iconColor', + initDate: '$$o.initDate', + endDate: '$$o.endDate', + isDefault: '$$o.isDefault', + + originalName: '$$o.name', + originalDescription: '$$o.description', + + translatedName: {{ + $let: {{ + vars: {{ + li: {{ + $first: {{ + $filter: {{ + input: {{ + $map: {{ + input: {{ $objectToArray: '$$o.localeItems' }}, + as: 'pair', + in: {{ + $cond: [ + {{ $eq: ['$$pair.k', '{localeCode}'] }}, + '$$pair.v', + null + ] + }} + }} + }}, + as: 'v', + cond: {{ $ne: ['$$v', null] }} + }} + }} + }} + }}, + in: {{ $ifNull: ['$$li.name', null] }} + }} + }}, + + translatedDescription: {{ + $let: {{ + vars: {{ + li: {{ + $first: {{ + $filter: {{ + input: {{ + $map: {{ + input: {{ $objectToArray: '$$o.localeItems' }}, + as: 'pair', + in: {{ + $cond: [ + {{ $eq: ['$$pair.k', '{localeCode}'] }}, + '$$pair.v', + null + ] + }} + }} + }}, + as: 'v', + cond: {{ $ne: ['$$v', null] }} + }} + }} + }} + }}, + in: {{ $ifNull: ['$$li.description', null] }} + }} + }} + }} + }} + }} + }} + }}"), + + // 2) Segundo $project: aplica lógica real de locale y sobrescribe name/description + BsonDocument.Parse($@" + {{ + $project: {{ + defaultLocale: '$defaultLocale', + options: {{ + $map: {{ + input: '$options', + as: 'o', + in: {{ + _id: '$$o._id', + optionType: '$$o.optionType', + color: '$$o.color', + bgColor: '$$o.bgColor', + iconDefault: '$$o.iconDefault', + iconCategory: '$$o.iconCategory', + iconColor: '$$o.iconColor', + initDate: '$$o.initDate', + endDate: '$$o.endDate', + isDefault: '$$o.isDefault', + name: {{ + $cond: [ + {{ $eq: ['{localeCode}', '$defaultLocale'] }}, + '$$o.originalName', + {{ $ifNull: ['$$o.translatedName', '$$o.originalName'] }} + ] + }}, + description: {{ + $cond: [ + {{ $eq: ['{localeCode}', '$defaultLocale'] }}, + '$$o.originalDescription', + {{ $ifNull: ['$$o.translatedDescription', '$$o.originalDescription'] }} + ] + }} + }} + }} + }} + }} + }}"), + + // 3) FILTRO FINAL + BsonDocument.Parse($@" + {{ + $project: {{ + options: {{ + $filter: {{ + input: '$options', + as: 'option', + cond: {{ + {filterCondition} + }} + }} + }} + }} + }}") + }; + + // Ejecuta pipeline + var result = await Collection.AggregateAsync(aggregationPipeline); + var documents = await result.ToListAsync(); + + var optionsList = documents.FirstOrDefault()?.Options; + return optionsList?.OrderBy(o => o.Name).ToList() ?? []; + } + catch (Exception ex) + { + Log.Error("Error searching entity by filters: {filters}. Exception: {ex}", filters, ex); + return []; + } + } + + public async Task UpdateMasterListOption(ObjectId id, OptionList newOpt, LocaleEnum locale) + { + // 1. Evitar duplicados + var exist = await GetMasterListByIdAndTextSearch(id, newOpt.Name, locale); + if (exist.Count > 0) + return exist.FirstOrDefault(); + + // 2. Obtener master para reglas + var master = await FindById(id); + var defaultLocale = master?.DefaultLocale ?? LocaleEnum.Es; + + // 3. Filtro + var filter = Builders.Filter.And( + Builders.Filter.Eq("_id", id), + Builders.Filter.ElemMatch("options", Builders.Filter.Eq("_id", newOpt.Id)) + ); + + var updates = new List>(); + + var isDefaultLocale = locale == defaultLocale; + + // ================================================== + // NAME + // ================================================== + if (!string.IsNullOrEmpty(newOpt.Name) && + (master?.OptionListDetails?.Name?.IsRequired ?? false)) + { + if (isDefaultLocale) + { + // actualizar nombre principal + updates.Add(Builders.Update.Set("options.$.name", newOpt.Name)); + } + else + { + // actualizar traducción + var localePath = $"options.$.localeItems.{locale.ToString().ToLower()}"; + updates.Add(Builders.Update.Set($"{localePath}.name", newOpt.Name)); + } + } + + // ================================================== + // OTHER FIELDS + // ================================================== + if (newOpt.IconDefault != null) + updates.Add(Builders.Update.Set("options.$.iconDefault", newOpt.IconDefault)); + + if (newOpt.IconCategory != null) + updates.Add(Builders.Update.Set("options.$.iconCategory", newOpt.IconCategory)); + if (newOpt.IconColor != null) + updates.Add(Builders.Update.Set("options.$.iconColor", newOpt.IconColor)); + + if (newOpt.Color != null) + updates.Add(Builders.Update.Set("options.$.color", newOpt.Color)); + + if (newOpt.BgColor != null) + updates.Add(Builders.Update.Set("options.$.bgColor", newOpt.BgColor)); + + if (newOpt.Description != null) + updates.Add(Builders.Update.Set("options.$.description", newOpt.Description)); + + if (newOpt.InitDate.HasValue) + updates.Add(Builders.Update.Set("options.$.initDate", newOpt.InitDate)); + + if (newOpt.EndDate.HasValue) + updates.Add(Builders.Update.Set("options.$.endDate", newOpt.EndDate)); + + if (newOpt.OptionType != null && + (master?.OptionListDetails?.OptionType?.IsRequired ?? false)) + updates.Add(Builders.Update.Set("options.$.optionType", newOpt.OptionType)); + + if (!updates.Any()) + return null; + + var update = Builders.Update.Combine(updates); + + try + { + // 4. Ejecutar actualización + var result = await Collection.UpdateOneAsync(filter, update); + + if (result.ModifiedCount == 0) + return null; + var updatedOpt = await FindOptionItemById(id, newOpt.Id!.Value, locale); + + return updatedOpt; + } + catch (Exception ex) + { + Log.Error("Error UpdateMasterListOption: {name}. Exception: {ex}", id, ex); + return null; + } + } + + public async Task UpdateMasterListOption(ObjectId id, OptionList newOpt) + { + var master = await FindById(id); + if (master == null) return null; + var filter = Builders.Filter.And( + Builders.Filter.Eq("_id", id), + Builders.Filter.ElemMatch("options", Builders.Filter.Eq("_id", newOpt.Id)) + ); + + var update = Builders.Update.Set("options.$", newOpt); + + try + { + // 4. Ejecutar actualización + var result = await Collection.UpdateOneAsync(filter, update); + + if (result.ModifiedCount == 0) + return null; + var updatedOpt = await FindOptionItemById(id, newOpt.Id!.Value); + + return updatedOpt; + } + catch (Exception ex) + { + Log.Error("Error UpdateMasterListOption: {name}. Exception: {ex}", id, ex); + return null; + } + } + + public async Task DeleteMasterListOption(ObjectId id, ObjectId deleteOptId) + { + // Define el filtro para encontrar el documento por su _id + var filter = Builders.Filter.Eq("_id", id); + + // Define la actualización con $pull para eliminar el objeto correspondiente en el array "options" + var update = Builders.Update.PullFilter("options", Builders.Filter.And( + Builders.Filter.Eq("_id", deleteOptId) + )); + + // Ejecuta la operación de actualización + var result = await Collection.UpdateOneAsync(filter, update); + if (result.ModifiedCount > 0) return true; + + return false; + } + + public async Task UpdateOptionDetailsToMasterList(ObjectId id, + UpdateMasterListDetailsDto opt) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq("_id", id)); + + // Construir el update, especificando cada campo que se debe actualizar + var updateDefinition = new List>(); + if (opt.CanAddElement != null) + updateDefinition.Add(Builders.Update.Set(c => c.CanAddElement, opt.CanAddElement)); + if (opt.OptionListDetails != null) + updateDefinition.Add(Builders.Update.Set(c => c.OptionListDetails, opt.OptionListDetails)); + + var update = Builders.Update.Combine(updateDefinition); + + try + { + var result = await Collection.UpdateOneAsync(filter, update); + if (result.ModifiedCount > 0) return opt; // Retorna el objeto actualizado si la modificación fue exitosa + return null; // Retorna nulo si no se encontró el documento o no se modificó + } + catch (Exception ex) + { + Log.Error("Error UpdateMasterListOption: {name}. Exception: {ex}", id, ex); + return null; + } + } + + public async Task UpdateMasterListName(ObjectId id, string name) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq("_id", id)); + + var update = Builders.Update.Set(c => c.Name, name); + + try + { + var result = await Collection.UpdateOneAsync(filter, update); + return result.ModifiedCount > 0; + } + catch (Exception ex) + { + Log.Error("Error UpdateMasterListOption: {name}. Exception: {ex}", id, ex); + return false; + } + } + + public async Task UpdateMasterListDescription(ObjectId id, string description) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq("_id", id)); + + var update = Builders.Update.Set(c => c.Description, description); + + try + { + var result = await Collection.UpdateOneAsync(filter, update); + return result.ModifiedCount > 0; + } + catch (Exception ex) + { + Log.Error("Error UpdateMasterListDescription {id} : {description}. Exception: {ex}", id, description, ex); + return false; + } + } + + public async Task RemoveMasterListOption(ObjectId id, OptionList oldOpt) + { + var filter = Builders.Filter.Eq("_id", id); + var pull = Builders.Update.PullFilter("options", Builders.Filter.And( + Builders.Filter.Eq("name", oldOpt.Name), + Builders.Filter.Eq("optionType", oldOpt.OptionType), + Builders.Filter.Eq("iconDefault", oldOpt.IconDefault), + Builders.Filter.Eq("iconCategory", oldOpt.IconCategory), + Builders.Filter.Eq("iconColor", oldOpt.IconColor), + Builders.Filter.Eq("color", oldOpt.Color), + Builders.Filter.Eq("bgColor", oldOpt.BgColor), + Builders.Filter.Eq("isDefault", oldOpt.IsDefault), + Builders.Filter.Eq("description", oldOpt.Description) + )); + + try + { + var result = await Collection.UpdateOneAsync(filter, pull); + return result.ModifiedCount > 0; // Retorna true si se modificó el documento (elemento eliminado) + } + catch (Exception ex) + { + Log.Error("Error removing option from MasterList: {name}. Exception: {ex}", id, ex); + return false; // Retorna false si ocurre un error + } + } + + public override async Task CreateIndexes() + { + if (typeof(T) == typeof(DiagnosisList)) + { + var options = new CreateIndexOptions + { Background = true, Unique = false, LanguageOverride = "spanish", DefaultLanguage = "spanish" }; + + var optionsIndex = new CreateIndexModel( + Builders.IndexKeys.Ascending("options.name"), options); + await Collection.Indexes.CreateOneAsync(optionsIndex); + + var optionsIndex2 = new CreateIndexModel( + Builders.IndexKeys.Ascending("options.description"), options); + await Collection.Indexes.CreateOneAsync(optionsIndex2); + var optionsIndex3 = new CreateIndexModel( + Builders.IndexKeys.Ascending("options._id"), options); + await Collection.Indexes.CreateOneAsync(optionsIndex3); + } + } + + public async Task> GetMasterListByTextSearch(string? textSearch) + { + return await GetOptionsByTextSearch(textSearch); + } + + private async Task> GetOptionsByTextSearch(string? textSearch, ObjectId? id = null) + { + try + { + if (string.IsNullOrWhiteSpace(textSearch)) + return []; + + // Normaliza el texto de búsqueda a minúsculas + var normalizedTextSearch = textSearch.ToLowerInvariant(); + // Construye la expresión regular dinámica para tener en cuenta la acentuación en las vocales + var regexPattern = BuildRegexPattern(normalizedTextSearch); + // foreach (var c in normalizedTextSearch) + // { + // switch (c) + // { + // case 'a': + // regexPattern.Append("[aáàäâ]"); + // break; + // case 'e': + // regexPattern.Append("[eéèëê]"); + // break; + // case 'i': + // regexPattern.Append("[iíìïî]"); + // break; + // case 'o': + // regexPattern.Append("[oóòöô]"); + // break; + // case 'u': + // regexPattern.Append("[uúùüû]"); + // break; + // default: + // regexPattern.Append(c); + // break; + // } + // } + + var aggregationPipeline = new[] + { + BsonDocument.Parse($"{{ $match: {{ \"_id\": ObjectId('{id}') }} }}"), + BsonDocument.Parse($@" + {{ + $project: {{ + options: {{ + $filter: {{ + input: '$options', + as: 'option', + cond: {{ + $or: [ + {{ $regexMatch: {{ input: '$$option.name', regex: '{regexPattern}', options: 'i' }} }}, + {{ $regexMatch: {{ input: '$$option.description', regex: '{regexPattern}', options: 'i' }} }} + ] + }} + }} + }} + }} + }}") + }; + var result = await Collection.AggregateAsync(aggregationPipeline); + var documents = await result.ToListAsync(); + + var optionsList = documents.FirstOrDefault()?.Options; + optionsList = optionsList != null ? optionsList.OrderBy(o => o.Name).ToList() : []; + + return optionsList; + } + catch (Exception ex) + { + Log.Error("Error searching entity by textSearch: {name}. Exception: {ex}", textSearch, ex); + return []; + } + } + + private IFindFluent CreateFindFluent(List> filters, SortDefinition sort) + { + var combinedFilter = filters.Any() + ? Builders.Filter.And(filters) + : Builders.Filter.Empty; // Filtra todo si no hay filtros + return Collection.Find(combinedFilter).Sort(sort); + } + + private Locale GetNewItemLocale(LocaleEnum localeList, string opt) + { + var newLocale = new Locale(); + var localeType = typeof(Locale); + var localeEnumType = typeof(LocaleEnum); + + // Recorremos todos los valores del enum excepto Default + foreach (var enumValue in Enum.GetValues(localeEnumType).Cast()) + { + if (enumValue == LocaleEnum.Default) + continue; + + // Si coincide con el locale a omitir → skip + if (enumValue == localeList) + continue; + + // Nombre de la propiedad del objeto Locale (Es, Pt, Ca, Eng, Zh...) + var propertyName = enumValue.ToString(); + + // Buscar la propiedad en el tipo Locale + var prop = localeType.GetProperty(propertyName); + if (prop != null) + // Asignar dinámicamente nuevo LocaleItem + prop.SetValue(newLocale, new LocaleItem + { + Name = opt + }); + } + + return newLocale; + } + + // Método auxiliar para construir patrones regex con soporte de acentos + private static string BuildRegexPattern(string input) + { + var regexPattern = new StringBuilder(); + foreach (var c in input) + switch (c) + { + case 'a': + regexPattern.Append("[aáàäâ]"); + break; + case 'e': + regexPattern.Append("[eéèëê]"); + break; + case 'i': + regexPattern.Append("[iíìïî]"); + break; + case 'o': + regexPattern.Append("[oóòöô]"); + break; + case 'u': + regexPattern.Append("[uúùüû]"); + break; + default: + if (char.IsDigit(c)) // Detectar si es un número + regexPattern.Append($"[{c}]"); // Tratar como un número literal + else + regexPattern.Append(c); // Escapar otros caracteres especiales + break; + } + + return regexPattern.ToString(); + } + + private async Task> GetMasterListByIdAndTextSearch( + ObjectId id, string newOptName, LocaleEnum locale) + { + try + { + var localeCode = locale.ToString().ToLower(); + + // ------------------------------------------------- + // Pipeline: obtener el documento y reconstruir + // name/description según locale + // ------------------------------------------------- + var pipeline = new[] + { + // 1) MATCH id + BsonDocument.Parse($@"{{ $match: {{ ""_id"": ObjectId(""{id}"") }} }}"), + + // 2) Proyectar opciones con nombre traducido + BsonDocument.Parse($@" + {{ + $project: {{ + defaultLocale: '$defaultLocale', + options: {{ + $map: {{ + input: '$options', + as: 'o', + in: {{ + _id: '$$o._id', + optionType: '$$o.optionType', + description: '$$o.description', + color: '$$o.color', + bgColor: '$$o.bgColor', + iconDefault: '$$o.iconDefault', + iconCategory: '$$o.iconCategory', + iconColorIconColor: '$$o.iconColor', + initDate: '$$o.initDate', + endDate: '$$o.endDate', + isDefault: '$$o.isDefault', + + originalName: '$$o.name', + originalDescription: '$$o.description', + + translatedName: {{ + $let: {{ + vars: {{ + li: {{ + $first: {{ + $filter: {{ + input: {{ + $map: {{ + input: {{ $objectToArray: '$$o.localeItems' }}, + as: 'pair', + in: {{ + $cond: [ + {{ $eq: ['$$pair.k', '{localeCode}'] }}, + '$$pair.v', + null + ] + }} + }} + }}, + as: 'v', + cond: {{ $ne: ['$$v', null] }} + }} + }} + }} + }}, + in: {{ $ifNull: ['$$li.name', null] }} + }} + }} + }} + }} + }} + }} + }}"), + + // 3) Segundo project: asignar Name final según locale + BsonDocument.Parse($@" + {{ + $project: {{ + defaultLocale: 1, + options: {{ + $map: {{ + input: '$options', + as: 'o', + in: {{ + _id: '$$o._id', + optionType: '$$o.optionType', + description: '$$o.originalDescription', + color: '$$o.color', + bgColor: '$$o.bgColor', + iconDefault: '$$o.iconDefault', + iconCategory: '$$o.iconCategory', + iconColor: '$$o.iconColor', + initDate: '$$o.initDate', + endDate: '$$o.endDate', + isDefault: '$$o.isDefault', + + name: {{ + $cond: [ + {{ $eq: ['{localeCode}', '$defaultLocale'] }}, + '$$o.originalName', + {{ $ifNull: ['$$o.translatedName', '$$o.originalName'] }} + ] + }} + }} + }} + }} + }} + }}"), + + // 4) Filtro final por nombre resultante + BsonDocument.Parse($@" + {{ + $project: {{ + options: {{ + $filter: {{ + input: '$options', + as: 'option', + cond: {{ + $eq: ['$$option.name', '{newOptName}'] + }} + }} + }} + }} + }}") + }; + + var result = await Collection.AggregateAsync(pipeline); + var docs = await result.ToListAsync(); + + var opts = docs.FirstOrDefault()?.Options; + return opts != null ? opts.OrderBy(o => o.Name).ToList() : []; + } + catch (Exception ex) + { + Log.Error("Error searching entity by textSearch: {name}. Exception: {ex}", newOptName, ex); + return []; + } + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/MedicineRepository.cs b/adas-core.Infrastructure/Repositories/MedicineRepository.cs new file mode 100644 index 00000000..ef4087b8 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/MedicineRepository.cs @@ -0,0 +1,142 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Filter; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using System.Text.RegularExpressions; + +namespace adas_core.Infrastructure.Repositories; + +public class MedicineRepository : MongoRepository, IMedicineRepository +{ + private readonly ApiSettings _apiSettings; + + public MedicineRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + } //For testing + + + + public override string GetCollectionName() + { + return _apiSettings.Medicines ?? "medicines"; + } + + public async Task GetMedicine(string code) + { + var result = await Collection.FindAsync(x => x.Codes.Contains(code) || x.Notes.Contains(code)); + + return await result.FirstOrDefaultAsync(); + } + + + public async Task> GetMedicineByCodeOrNote(List codeNotes) + { + var filter = Builders.Filter.AnyIn("Codes", codeNotes.ToArray()); + var result = await Collection.FindAsync(filter); + + return result.ToList(); + } + + public async Task GetMedicineByName(string name) + { + var filter = Builders.Filter.Eq(p => p.Name, name); + var result = await Collection.FindAsync(filter); + + return await result.FirstOrDefaultAsync(); + } + + public async Task> GetAll() + { + var result = await Collection.FindAsync(_ => true); + + return result.ToList(); + } + + public async Task GetMedicineById(ObjectId medicineId) + { + var filter = Builders.Filter.Eq(p => p.Id, medicineId); + var result = await Collection.FindAsync(filter); + + return await result.FirstOrDefaultAsync(); + } + + public async Task PostMedicine(Medicine medicine) + { + await Collection.InsertOneAsync(medicine); + var result = await Collection.FindAsync(v => v.Name == medicine.Name); + + return await result.FirstOrDefaultAsync(); + } + + public async Task UpdateMedicine(Medicine medicine) + { + await UpdateOneAsync(medicine.Id, medicine); + + return medicine; + } + + public async Task DeleteMedicineById(ObjectId medicineId) + { + var filter = Builders.Filter.Eq(po => po.Id, medicineId); + + await Collection.DeleteOneAsync(filter); + } + + public IFindFluent GetPaginatedMedicines(PaginationFilter filter) + { + // Crear variable con la clase que construye los filtros que necesitamos + var filterBuilder = Builders.Filter; + // Crear una lista de filtros que pueden venir de tu servicio + var filters = new List>(); + // Ordenar los resultados por "time" en orden descendente + var sort = Builders.Sort.Ascending("name"); + if (filter.FilteredRequest != null) + { + var requestFilter = filter.FilteredRequest; + + if (!string.IsNullOrWhiteSpace(requestFilter.MedicineName)) + { + var escapedTextFilter = Regex.Escape(requestFilter.MedicineName); + filters.Add(filterBuilder.Regex(m => m.Name, + new BsonRegularExpression(escapedTextFilter, "i"))); + } + if (!string.IsNullOrWhiteSpace(requestFilter.MedicineCode)) + filters.Add(filterBuilder.AnyEq(m => m.Codes, requestFilter.MedicineCode)); + + if (!string.IsNullOrWhiteSpace(requestFilter.MedicineType)) + filters.Add(filterBuilder.AnyEq(m => m.Type, requestFilter.MedicineType)); + + if (!string.IsNullOrWhiteSpace(requestFilter.MedicineGroup)) + filters.Add(filterBuilder.AnyEq(m => m.Group, requestFilter.MedicineGroup)); + } + + if (filters.Count == 0) + return Collection.Find(_ => true).Sort(sort); + + var combinedFilter = Builders.Filter.And(filters); + + return Collection + .Find(combinedFilter) + .Sort(sort); + } + + public IAggregateFluent GetDistinctFieldDataQuery(string field) + { + return Collection.Aggregate() + .Unwind(field) + .Group(new BsonDocument { { "_id", $"${field}" } }) + .Sort(new BsonDocument { { "_id", 1 } }) + .Project(new BsonDocument { { field, "$_id" }, { "_id", 0 } }); + } + + + public Task> GetAllGroups() + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/MongoRepository.cs b/adas-core.Infrastructure/Repositories/MongoRepository.cs new file mode 100644 index 00000000..a0ad1272 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/MongoRepository.cs @@ -0,0 +1,155 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Infrastructure.Utils; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Serilog; + +namespace adas_core.Infrastructure.Repositories; + +public abstract class MongoRepository : IMongoRepository +{ + protected readonly IMongoDatabase Db; + + private IMongoCollection? _collection; + + // protected MongoRepository(IOptions dbSettings) + // { + // Db = MongoDbHostBuilderExtension.GetMongoDb(dbSettings); + // } + + protected MongoRepository(IMongoDatabase database) + { + Db = database; + } + + public abstract string GetCollectionName(); + + public IMongoCollection Collection + { + get + { + if (_collection == null) + { + var collectionName = GetCollectionName(); + if (!CollectionExists(collectionName)) Db.CreateCollection(collectionName); + _collection = Db.GetCollection(collectionName); + _ = CreateIndexes(); + _ = InsertInitialLoad(); + } + + return _collection; + } + set => _collection = value; + } + + public virtual async Task InsertOneAsync(T obj) + { + try + { + await Collection.InsertOneAsync(obj); + } + catch (Exception ex) + { + Log.Error("An error occurred: {ExMessage}", ex.Message); + } + } + + public async Task UpdateOneAsync(ObjectId id, T obj) + { + try + { + var filter = Builders.Filter.Eq("_id", id); + + await Collection.ReplaceOneAsync(filter, obj, new ReplaceOptions { IsUpsert = true }); + } + catch (Exception ex) + { + Log.Error("Error updating id: {Id}. Exception:{Ex}, stackTrace: {Trace}", id.ToString(), ex.Message, + ex.StackTrace); + } + } + + public async Task DeleteAsync(ObjectId id) + { + try + { + var filter = Builders.Filter.Eq("_id", id); + return await Collection.FindOneAndDeleteAsync(filter); + } + catch (Exception ex) + { + Log.Error("An error occurred: {ExMessage}", ex.Message); + return default; + } + } + + public virtual Task CreateIndexes() + { + return Task.CompletedTask; + } + + public virtual Task InsertInitialLoad() + { + return Task.CompletedTask; + } + + public virtual async Task InsertManyAsync(List obj) + { + try + { + InsertManyOptions options = new() { IsOrdered = false }; + await Collection.InsertManyAsync(obj, options); + } + catch (Exception ex) + { + Log.Error("An error occurred: {ExMessage}", ex.Message); + } + } + + protected async Task UpdateManyObjectIdAsync(string nameId, ObjectId id, ObjectId oldId) + { + try + { + var update = Builders.Update.Set(nameId, id); + var filter = Builders.Filter.Eq(nameId, oldId); + + await Collection.UpdateManyAsync(filter, update); + } + catch (Exception ex) + { + Log.Error("An error occurred: {ExMessage}", ex.Message); + } + } + + protected async Task DeleteAsync(string id) + { + try + { + var filter = Builders.Filter.Eq("_id", id); + return await Collection.FindOneAndDeleteAsync(filter); + } + catch (Exception ex) + { + Log.Error("An error occurred: {ExMessage}", ex.Message); + return default; + } + } + + protected bool CollectionExists(string collectionName) + { + try + { + var filter = new BsonDocument("name", collectionName); + var options = new ListCollectionNamesOptions { Filter = filter }; + + return Db.ListCollectionNames(options).Any(); + } + catch (Exception ex) + { + Log.Error("An error occurred: {ExMessage}", ex.Message); + return false; + } + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/NoticeRepository.cs b/adas-core.Infrastructure/Repositories/NoticeRepository.cs new file mode 100644 index 00000000..3132d391 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/NoticeRepository.cs @@ -0,0 +1,161 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Serilog; +using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; + +namespace adas_core.Infrastructure.Repositories; + +public class NoticeRepository : MongoRepository, INoticeRepository +{ + private readonly ApiSettings _apiSettings; + + public NoticeRepository(IOptions? apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings != null) + _apiSettings = apiSettings.Value; + else + throw new ArgumentNullException(nameof(apiSettings)); + } + + + + public override string GetCollectionName() + { + return _apiSettings.Notices; + } + + public override async Task InsertOneAsync(Notice notice) + { + try + { + notice.NoticeDate = DateTime.UtcNow; + await Collection.InsertOneAsync(notice); + } + catch (Exception e) + { + Log.Warning("Exception trying to insert notice: {notice}. Exception {e}", notice, e); + } + } + + public async Task Delete(ObjectId id) + { + try + { + var filter = Builders.Filter.Eq(x => x.Id, id); + await Collection.DeleteOneAsync(filter, null); + } + catch (Exception e) + { + Log.Error("Exception trying to delete notice: {id}. Exception {e}", id, e); + throw; + } + } + + public async Task Update(Notice notice) + { + try + { + await UpdateOneAsync(notice.Id, notice); + } + catch (Exception e) + { + Log.Error("Exception trying to update notice: {notice}. Exception {e}", notice, e); + throw; + } + } + + public async Task> FindAll() + { + try + { + var result = await Collection.FindAsync(_ => true); + return await result.ToListAsync(); + } + catch (Exception ex) + { + Log.Error("Error getting all notices. Exception: {ex}", ex); + return []; + } + } + + public async Task FindById(ObjectId id) + { + try + { + var filter = Builders.Filter.Eq(p => p.Id, id); + var result = await Collection.FindAsync(filter); + + return await result.FirstOrDefaultAsync(); + } + catch (Exception ex) + { + Log.Error("Error searching notice by id: {id}. Exception: {ex}", id, ex); + return null; + } + } + + public async Task?> FindByDate(DateTime date) + { + try + { + var filter = Builders.Filter.Eq(p => p.NoticeDate, date); + var result = await Collection.FindAsync(filter); + + return result.ToEnumerable(); + } + catch (Exception ex) + { + Log.Error("Error searching notice by date: {date}. Exception: {ex}", date, ex); + return null; + } + } + + public async Task?> FindByType(string type) + { + try + { + var filter = Builders.Filter.Eq(p => p.NoticeType, type); + var result = await Collection.FindAsync(filter); + + return result.ToEnumerable(); + } + catch (Exception ex) + { + Log.Error("Error searching notice by type: {date}. Exception: {ex}", type, ex); + return null; + } + } + + public async Task?> FindByDisplayId(ObjectId displayId) + { + try + { + var filter = Builders.Filter.Eq(p => p.DisplayId, displayId); + var result = await Collection.FindAsync(filter); + + return result.ToEnumerable(); + } + catch (Exception ex) + { + Log.Error("Error searching notices by display id: {id}. Exception: {ex}", displayId, ex); + return null; + } + } + + public override async Task CreateIndexes() + { + var options = new CreateIndexOptions { Background = true, Unique = false }; + + var indexes = new List> + { + new("{ noticeType: 1, noticeDate: -1 }", options), + new("{ noticeDate: -1 }", options) + }; + + await MongoUtils.EnsureIndexes(Collection, indexes); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/ObservationArchiveRepository.cs b/adas-core.Infrastructure/Repositories/ObservationArchiveRepository.cs new file mode 100644 index 00000000..a7053b0c --- /dev/null +++ b/adas-core.Infrastructure/Repositories/ObservationArchiveRepository.cs @@ -0,0 +1,160 @@ +using System.Diagnostics; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Serilog; + +namespace adas_core.Infrastructure.Repositories; + +public class ObservationArchiveRepository : MongoRepository, IObservationArchiveRepository +{ + private readonly ApiSettings _apiSettings; + + public ObservationArchiveRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + } //For testing + + + + + public async Task> AggregatedPatientLastObservations(ObjectId patientId, int num, + DateTime lastDate, List? filterObservations = null) + { + filterObservations = await AggregatePatientObservations(patientId, filterObservations); + var results = new List(); + + foreach (var obs in filterObservations) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(o => o.PatientId, patientId), + Builders.Filter.Eq(o => o.Name, obs), + Builders.Filter.Lte(o => o.Time, lastDate) + ); + + var sort = Builders.Sort.Descending(o => o.Time); + + results.AddRange(Collection.Find(filter).Sort(sort).Limit(num).ToEnumerable()); + } + + return results; + } + + public override string GetCollectionName() + { + return _apiSettings.ArchivePatientsObservations ?? "archive_patients_observations"; + } + + public new async Task InsertOneAsync(PatientObservation patientObservation) + { + const int maxRetries = 2; // Número máximo de reintentos + var retryCount = 0; + + while (true) + try + { + await Collection.InsertOneAsync(patientObservation); + return; + } + catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + retryCount++; + + Log.Warning( + "Duplicate key error encountered. Retrying with new ObjectId. Attempt {attempt} of {maxRetries}", + retryCount, maxRetries); + + patientObservation.Id = new ObjectId(); + Log.Information("Generated ObjectId: {objectId}", patientObservation.Id); + + if (retryCount >= maxRetries) + { + Log.Error("Maximum retry attempts reached. Could not insert document due to duplicate key error."); + throw; // Re-lanzar la excepción después de alcanzar el número máximo de reintentos + } + } + catch (Exception ex) + { + Log.Error("Error inserting patient observation: {exMessage}", ex.Message); + throw; + } + } + + public async Task DeleteBeforeDate(DateTime date) + { + var filter = Builders.Filter.Lt(po => po.Time, date); + await Collection.DeleteManyAsync(filter); + } + + public async Task InsertBatch(IEnumerable observations) + { + var writes = new List>(); + writes.AddRange(observations.Select(d => new InsertOneModel(d))); + + var bulkInsert = await Collection.BulkWriteAsync(writes); + + return bulkInsert.InsertedCount; + } + + public async Task> FindAllFromPatient(ObjectId patientId) + { + var filter = Builders.Filter.Eq(p => p.PatientId, patientId); + var result = await Collection.FindAsync(filter); + + return await result.ToListAsync(); + } + + private async Task> AggregatePatientObservations(ObjectId patientId, + List? filterObservations = null) + { + var matchPatient = new BsonDocument + { + { "patientid", patientId }, + { "name", new BsonDocument { { "$ne", BsonNull.Value } } } + }; + + if (filterObservations == null || !filterObservations.Any()) + { + // GET DISTINCT OBSERVATIONS + var distinctObs = new BsonDocument + { + { + "$group", new BsonDocument + { + { "_id", "1" }, + { + "obs", new BsonDocument + { + { "$addToSet", "$name" } + } + } + } + } + }; + var distinctPipeline = new[] + { + new() + { + { + "$match", matchPatient + } + }, + distinctObs + }; + Debug.WriteLine("AggregatedArchivedPatientLastObservations distinct obs: \n" + distinctPipeline.ToJson()); + var resultList = + await Collection.AggregateAsync(distinctPipeline, + new AggregateOptions { AllowDiskUse = true }); + var result = resultList.ToList().FirstOrDefault(); + + if (result != null && result.Any()) + filterObservations = result.GetValue("obs").AsBsonArray.Select(it => it.AsString).ToList(); + } + + return filterObservations ?? []; + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/ObservationRepository.cs b/adas-core.Infrastructure/Repositories/ObservationRepository.cs new file mode 100644 index 00000000..c604ca86 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/ObservationRepository.cs @@ -0,0 +1,1031 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Repositories.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.GroupedObservations; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Utils; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using System.Diagnostics; +using System.Text.RegularExpressions; +using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; + +namespace adas_core.Infrastructure.Repositories; + +public class ObservationRepository : MongoRepository, IObservationRepository +{ + private readonly ApiSettings _apiSettings; + private readonly ILogger _logger; + + + public ObservationRepository( + IOptions? apiSettings, + IMongoDatabase database, + ILogger logger) : base(database) + { + if (apiSettings != null) + { + _logger = logger; + _apiSettings = apiSettings.Value; + } + else + { + throw new ArgumentNullException(nameof(apiSettings)); + } + } //For testing + + + public override string GetCollectionName() + { + return _apiSettings.PatientsObservations ?? "patients_observations"; + } + + public async Task> FindLastObservations(ObjectId patientId, string codingSystem, + string code, int num = 2) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(ob => ob.PatientId, patientId), + Builders.Filter.Eq(ob => ob.CodingSystem, codingSystem), + Builders.Filter.Eq(ob => ob.Code, code) + ); + + var result = await Collection.FindAsync( + filter, + new FindOptions + { Sort = Builders.Sort.Descending("time"), Limit = num } + ); + + return result.ToEnumerable(); + } + + public async Task> FindLastObservationsByCodingSystem(ObjectId patientId, + string codingSystem, int num = 10) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(ob => ob.PatientId, patientId), + Builders.Filter.Eq(ob => ob.CodingSystem, codingSystem) + ); + + var result = await Collection.FindAsync( + filter, + new FindOptions + { Sort = Builders.Sort.Descending("time"), Limit = num } + ); + + return await result.ToListAsync(); + } + + public async Task> AggregatedPatientLastObservations(ObjectId patientId, int num, + List? filterObservations = null) + { + filterObservations = await AggregatePatientObservations(patientId, filterObservations); + var results = new List(); + foreach (var obs in filterObservations) + { + var builder = Builders.Filter; + var filter = builder.And( + builder.Eq(o => o.PatientId, patientId), + builder.Eq(o => o.Name, obs) + ); + + var cursor = await Collection.FindAsync(filter, + new FindOptions + { + Sort = Builders.Sort.Descending("time").Descending("id"), + Limit = num + }); + + results.AddRange(cursor.ToEnumerable()); + } + + return results; + } + + + public async Task> AggregatedPatientLastObservationsByLastDate(ObjectId patientId, int num, + DateTime lastDate, List? filterObservations = null) + { + filterObservations = await AggregatePatientObservations(patientId, filterObservations); + var results = new List(); + + foreach (var obs in filterObservations) + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(o => o.PatientId, patientId), + Builders.Filter.Eq(o => o.Name, obs), + Builders.Filter.Lte(o => o.Time, lastDate) + ); + + var cursor = await Collection.FindAsync( + filter, + new FindOptions + { Sort = Builders.Sort.Descending("time").Descending("_id"), Limit = num } + ); + + results.AddRange(await cursor.ToListAsync()); + } + + return results; + } + + + public async Task> AggregatedPatientLastObservationsByField(ObjectId patientId, + List? filterObservations) + { + try + { + var results = new List(); + IAsyncCursor? cursor; + var builder = Builders.Filter; + + if (filterObservations != null) + { + foreach (var obs in filterObservations) + { + FilterDefinition filter; + + if (obs is { OnlyExpired: true, Name: not null }) + filter = builder.And( + builder.Eq(o => o.PatientId, patientId), + builder.Eq(o => o.Name, obs.Name), + builder.Eq(o => o.Expired, obs.OnlyExpired) + ); + else + filter = builder.And( + builder.Eq(o => o.PatientId, patientId), + builder.Eq(o => o.Name, obs.Name) + ); + + cursor = await Collection.FindAsync( + filter, + new FindOptions + { + Sort = Builders.Sort.Descending("time").Descending("id"), + Limit = obs.Last + }); + + results.AddRange(cursor.ToEnumerable()); + } + } + else + { + var filter = builder.Eq(o => o.PatientId, patientId); + + cursor = await Collection.FindAsync( + filter, + new FindOptions + { Sort = Builders.Sort.Descending("time").Descending("id") }); + + results.AddRange(cursor.ToEnumerable()); + } + + return results; + } + catch (Exception ex) + { + _logger.LogError("Error aggregated patient last observations by field {exMessage}", ex.Message); + return []; + } + } + + + public async Task> AggregatedPatientGroupedObservations(ObjectId patientId, + GroupedField groupedField) + { + try + { + DateTime? lastObsTime = null; + if (groupedField.Since == GroupedObservationEnum.Since.Last && !string.IsNullOrEmpty(groupedField.Name)) + { + var lastObsList = + await AggregatedPatientLastObservations(patientId, 1, [groupedField.Name]); + var lastObs = lastObsList.FirstOrDefault(); + if (lastObs != null) + { + lastObsTime = lastObs.Time; + lastObsTime = DateTime.SpecifyKind(lastObsTime.Value, DateTimeKind.Utc); + } + } + + + var fields = new BsonArray(); + + var match = new BsonDocument { { "patientid", patientId } }; + if (groupedField.GetNames().Count > 1) + { + groupedField.GetNames().ForEach(name => { fields.Add(new BsonDocument { { "name", name } }); }); + match.Add("$or", fields); + } + else + { + match.Add("name", groupedField.Name); + } + + var projectDate = new BsonDocument + { + { "y", new BsonDocument { { "$year", "$time" } } }, + { "M", new BsonDocument { { "$month", "$time" } } }, + { "d", new BsonDocument { { "$dayOfMonth", "$time" } } } + }; + var projectTimeFromParts = new BsonDocument + { + { "year", "$_id.year" }, + { "month", "$_id.month" }, + { "day", "$_id.day" } + }; + var idDocument = new BsonDocument { { "year", "$y" }, { "month", "$M" }, { "day", "$d" } }; + var fromDate = DateTime.MinValue; + if (groupedField.Regularity == GroupedObservationEnum.Regularity.Day) + { + if (groupedField.Since == GroupedObservationEnum.Since.Last && lastObsTime.HasValue) + fromDate = lastObsTime.Value.AddDays(-1 * groupedField.Max); + else + fromDate = DateTime.UtcNow.AddDays(-1 * groupedField.Max); + } + + if (groupedField.Regularity is GroupedObservationEnum.Regularity.Hour + or GroupedObservationEnum.Regularity.Minute or GroupedObservationEnum.Regularity.Second + or GroupedObservationEnum.Regularity.Times or GroupedObservationEnum.Regularity.Shift) + { + projectDate.Add("h", new BsonDocument { { "$hour", "$time" } }); + idDocument.Add("hour", "$h"); + projectTimeFromParts.Add("hour", "$_id.hour"); + //fromDate = DateTime.UtcNow.AddHours(-1 * groupedField.max); + if (groupedField.Since == GroupedObservationEnum.Since.Last && lastObsTime.HasValue) + fromDate = new DateTime(lastObsTime.Value.Year, lastObsTime.Value.Month, lastObsTime.Value.Day, + lastObsTime.Value.AddHours(1).Hour, 00, 00).AddHours(-1 * groupedField.Max); + else + fromDate = new DateTime(DateTime.UtcNow.Year, DateTime.UtcNow.Month, DateTime.UtcNow.Day, + DateTime.UtcNow.Hour, 00, 00).AddHours(-1 * groupedField.Max); + } + + if (groupedField.Regularity == GroupedObservationEnum.Regularity.Minute || + groupedField.Regularity == GroupedObservationEnum.Regularity.Second || + groupedField.Regularity == GroupedObservationEnum.Regularity.Times + //No queremos los minutos cuando pedimos por turno, en principio. Revisar si en algún caso necesitamos los minutos, quitado de momento por problemas + // a la hora de devolver el last. + //|| groupedField.regularity == Regularity.Shift + ) + { + projectDate.Add("m", new BsonDocument { { "$minute", "$time" } }); + idDocument.Add("minute", "$m"); + projectTimeFromParts.Add("minute", "$_id.minute"); + + if (groupedField.Regularity != GroupedObservationEnum.Regularity.Shift) + { + if (groupedField.Since == GroupedObservationEnum.Since.Last && lastObsTime.HasValue) + fromDate = lastObsTime.Value.AddMinutes(-1 * groupedField.Max); + else + fromDate = DateTime.UtcNow.AddMinutes(-1 * groupedField.Max); + } + } + + if (groupedField.Regularity == GroupedObservationEnum.Regularity.Second || + groupedField.Regularity == GroupedObservationEnum.Regularity.Times) + { + projectDate.Add("s", new BsonDocument { { "$second", "$time" } }); + idDocument.Add("second", "$s"); + projectTimeFromParts.Add("second", "$_id.second"); + if (groupedField.Since == GroupedObservationEnum.Since.Last && lastObsTime.HasValue) + fromDate = lastObsTime.Value.AddSeconds(-1 * groupedField.Max); + else + fromDate = DateTime.UtcNow.AddSeconds(-1 * groupedField.Max); + } + + if (groupedField.Regularity == GroupedObservationEnum.Regularity.Second || + groupedField.Regularity == GroupedObservationEnum.Regularity.Times) + { + projectDate.Add("ms", new BsonDocument { { "$millisecond", "$time" } }); + idDocument.Add("millisecond ", "$ms"); + projectTimeFromParts.Add("millisecond", "$_id.millisecond"); + } + + projectDate.Add("name", "$name"); + projectDate.Add("min", "$min"); + projectDate.Add("max", "$max"); + projectDate.Add("time", "$time"); + projectDate.Add("value", "$value"); + idDocument.Add("name", "$name"); + //No queremos meter el mínimo y el maximo en la agrupación. + //idDocument.Add("min", "$min"); + //idDocument.Add("max", "$max"); + fromDate = DateTime.SpecifyKind(fromDate, DateTimeKind.Utc); + if (groupedField.Max > 0) + { + //Add filter lte for ICCA future observations like hour balance. + if (groupedField.Since == GroupedObservationEnum.Since.Last && lastObsTime.HasValue) + match.Add("time", new BsonDocument("$gte", fromDate).Add("$lte", lastObsTime.Value)); + else + match.Add("time", new BsonDocument("$gte", fromDate).Add("$lte", DateTime.UtcNow)); + } + + + var group = new BsonDocument + { + { "_id", idDocument } + }; + if (groupedField.Result.Count == 0 || groupedField.Result.Contains(GroupedObservationEnum.Result.First)) + group.Add("first", + new BsonDocument + { + { + "$first", + new BsonDocument + { { "time", "$time" }, { "value", "$value" }, { "min", "$min" }, { "max", "$max" } } + } + }); + + if (groupedField.Result.Contains(GroupedObservationEnum.Result.Last)) + group.Add("last", + new BsonDocument + { + { + "$last", + new BsonDocument + { { "time", "$time" }, { "value", "$value" }, { "min", "$min" }, { "max", "$max" } } + } + }); + + if (groupedField.Result.Contains(GroupedObservationEnum.Result.LastFilled)) + group.Add("lastfilled", + new BsonDocument + { + { + "$last", + new BsonDocument + { { "time", "$time" }, { "value", "$value" }, { "min", "$min" }, { "max", "$max" } } + } + }); + + if (groupedField.Result.Contains(GroupedObservationEnum.Result.Min)) + group.Add("min", + new BsonDocument + { + { + "$min", + new BsonDocument + { { "value", "$value" }, { "time", "$time" }, { "min", "$min" }, { "max", "$max" } } + } + }); + + if (groupedField.Result.Contains(GroupedObservationEnum.Result.Max)) + group.Add("max", + new BsonDocument + { + { + "$max", + new BsonDocument + { + { "value", "$value" }, { "time", "$time" }, { "min", "$min" }, { "max", "$max" } + } + } + }); + + if (groupedField.Result.Contains(GroupedObservationEnum.Result.Sum)) + group.Add("sum", new BsonDocument { { "$sum", "$value" } }); + + if (groupedField.Result.Contains(GroupedObservationEnum.Result.Average)) + group.Add("average", new BsonDocument { { "$avg", "$value" } }); + + if (groupedField.Result.Contains(GroupedObservationEnum.Result.Count)) + group.Add("count", new BsonDocument { { "$sum", 1 } }); + + if (groupedField.Result.Contains(GroupedObservationEnum.Result.HalfHour)) + group.Add("all", + new BsonDocument + { + { + "$push", + new BsonDocument + { + { "value", "$value" }, { "time", "$time" }, { "min", "$min" }, { "max", "$max" } + } + } + }); + //group.Add("halfHour", new BsonDocument { { "$unset", new BsonDocument { { "time", "$time" }, { "value", "$value" }, { "min", "$min" }, { "max", "$max" } } } }); + var pipeline = new List + { + new() + { + { + "$match", match + } + }, + new() + { + { + "$sort", new BsonDocument { { "time", 1 } } + } + }, + new() + { + { + "$project", projectDate + } + }, + new() + { + { + "$group", group + } + }, + new() + { + { + "$addFields", + new BsonDocument + { + { "time", new BsonDocument { { "$dateFromParts", projectTimeFromParts } } }, + { "isFilled", false } + } + } + }, + new() + { + { + "$sort", new BsonDocument { { "time", 1 } } + } + } + }; + if (groupedField is { Regularity: GroupedObservationEnum.Regularity.Times, Max: > 0 }) + pipeline.Add(new BsonDocument("$limit", groupedField.Max)); + //System.Diagnostics.Debug.WriteLine("AggregatedPatientGroupedObservations query: \n" + pipeline.ToJson()); + _logger.LogDebug("Executing AggregatedPatientGroupedObservations query: {pipeline}:", pipeline.ToJson()); + var resultsCursor = + await Collection.AggregateAsync(pipeline, new AggregateOptions { AllowDiskUse = true }); + var results = resultsCursor.ToList(); + + return results; + } + catch (Exception ex) + { + _logger.LogError("Error agregated patient grouped observations {exMessage}", ex.Message); + return []; + } + } + + + public new async Task InsertOneAsync(PatientObservation patientObservation) + { + const int maxRetries = 2; // Número máximo de reintentos + var retryCount = 0; + + while (retryCount < maxRetries) + try + { + await Collection.InsertOneAsync(patientObservation); + return; + } + catch (MongoWriteException ex) when (ex.WriteError.Category == ServerErrorCategory.DuplicateKey) + { + retryCount++; + _logger.LogWarning( + "Duplicate key error encountered. Retrying with new ObjectId. Attempt {attempt} of {maxRetries}", + retryCount, maxRetries); + + if (retryCount < maxRetries) continue; + + _logger.LogError( + "Maximum retry attempts reached. Could not insert document due to duplicate key error."); + throw; // Relanzar la excepción después de alcanzar el número máximo de reintentos + } + catch (Exception ex) + { + _logger.LogError("Error inserting patient observation: {exMessage}", ex.Message); + throw; + } + } + + public async Task DeleteByPatientId(ObjectId id) + { + var filter = Builders.Filter.Eq(po => po.PatientId, id); + await Collection.DeleteManyAsync(filter); + } + + public async Task> FindByPatientIdAsync(ObjectId patientId) + { + var filter = Builders.Filter.Eq(ob => ob.PatientId, patientId); + return await Collection.FindAsync(filter, new FindOptions { BatchSize = 100 }); + } + + public async Task> FindByPatientIdAndCodingSystemAsync(ObjectId patientId, + string codingSystem, string name) + { + var filter = + Builders.Filter.And( + Builders.Filter.Eq(ob => ob.PatientId, patientId), + Builders.Filter.Eq(ob => ob.CodingSystem, codingSystem), + Builders.Filter.Eq(ob => ob.Name, name) + ); + + return await Collection.FindAsync(filter, new FindOptions { BatchSize = 100 }); + } + + + public new async Task DeleteAsync(ObjectId id) + { + var filter = Builders.Filter.Eq(obs => obs.Id, id); + await Collection.DeleteOneAsync(filter); + } + + + public async Task> DeleteOlderDaysAsync(string name, int retentionPolicyValue) + { + var dateLimit = DateTime.UtcNow.AddDays(-1 * retentionPolicyValue); + var filter = Builders.Filter.And( + Builders.Filter.Eq(obs => obs.Name, name), + Builders.Filter.Lt(obs => obs.Time, dateLimit) + ); + var documentsToDelete = await Collection.Find(filter).ToListAsync(); + + await Collection.DeleteManyAsync(filter); + + return documentsToDelete; + } + + + public async Task> DeleteOlderSecondsAsync(string name, int retentionPolicyValue) + { + var dateLimit = DateTime.UtcNow.AddSeconds(-1 * retentionPolicyValue); + var filter = Builders.Filter.And( + Builders.Filter.Eq(obs => obs.Name, name), + Builders.Filter.Lt(obs => obs.Time, dateLimit) + ); + var documentsToDelete = await Collection.Find(filter).ToListAsync(); + + await Collection.DeleteManyAsync(filter); + + return documentsToDelete; + } + + + public async Task> DeleteOlderNumberAsync(string name, int retentionPolicyValue) + { + var builder = Builders.Filter; + var idsToDelete = await Collection.Find(obs => obs.Name == name) + .Project(obs => obs.Id) + .Sort("{time: -1}") + .Skip(retentionPolicyValue) + .ToListAsync(); + + List documentsToDelete = []; + + if (idsToDelete == null || !idsToDelete.Any()) return documentsToDelete; + + + var idFilter = builder.In("_id", idsToDelete); + documentsToDelete = await Collection.Find(idFilter).ToListAsync(); + + await Collection.DeleteManyAsync(idFilter); + + return documentsToDelete; + } + + + public async Task ExistBySystemId(ObjectId patientid, string systemId) + { + return await Collection.Find(Builders.Filter.And( + Builders.Filter.Eq(obs => obs.PatientId, patientid), + Builders.Filter.Eq(obs => obs.SystemId, systemId) + )) + .Project("{ _id: true }") + .AnyAsync(); + } + + + public async Task FindLastObservationBeforeDate(ObjectId patientId, string? name, + DateTime date) + { + var builder = Builders.Filter; + var filter = builder.And( + builder.Eq(obs => obs.PatientId, patientId), + builder.Eq(obs => obs.Name, name), + builder.Lt(obs => obs.Time, date) + ); + + var result = await Collection.FindAsync(filter, + new FindOptions + { Sort = Builders.Sort.Descending("time").Descending("_id") }); + + return await result.FirstOrDefaultAsync(); + } + + public async Task?> FindAnyWithSameDate(ObjectId patientId, string? name, DateTime date) + { + var builder = Builders.Filter; + var filter = builder.Eq(obs => obs.PatientId, patientId) + & builder.Eq(obs => obs.Name, name) + & builder.Eq(obs => obs.Time, date); + + var result = await Collection.FindAsync(filter); + return result.ToList(); + } + + + public async Task> FindAnyBeforeDate(ObjectId patientId, DateTime date) + { + var filterBuilder = Builders.Filter; + var patientIdFilter = filterBuilder.Eq(obs => obs.PatientId, patientId); + var dateFilter = filterBuilder.Lt(obs => obs.Time, date); + + var filter = filterBuilder.And(patientIdFilter, dateFilter); + + var result = await Collection.FindAsync(filter); + return result.ToList(); + } + + public async Task> FindLatestUniqueValuesByName(ObjectId patientId, string name, + int? expires) + { + // Crear el filtro base + var matchFilter = new BsonDocument + { + { "patientid", patientId }, + { "name", name } + }; + + // Agregar filtro de expiración si expires no es null + if (expires.HasValue) + { + var expirationDate = DateTime.UtcNow.AddSeconds(-expires.Value); + matchFilter.Add("time", new BsonDocument("$gte", expirationDate)); + } + + var pipeline = new[] + { + // Filtrar por PatientId, name y (opcionalmente) tiempo no expirado + new BsonDocument("$match", matchFilter), + + // Ordenar por tiempo en orden descendente + new BsonDocument("$sort", new BsonDocument("time", -1)), + + // Agrupar por el campo `value` (valores únicos) + new BsonDocument("$group", new BsonDocument + { + { "_id", "$value" }, + { "LatestObservation", new BsonDocument("$first", "$$ROOT") } + }), + + // Proyectar solo los campos originales + new BsonDocument("$replaceRoot", new BsonDocument("newRoot", "$LatestObservation")) + }; + + var result = await Collection.AggregateAsync(pipeline); + return result.ToList(); + } + + + public async Task> AggregatedPatientActiveIntravenousLinesObservations(ObjectId patientId) + { + var results = new List(); + + var cursor = await Collection.FindAsync( + o => o.PatientId == patientId && o.Name == "IntravenousLinesObs", + new FindOptions + { Sort = Builders.Sort.Descending("time").Descending("id") }); + + results.AddRange(cursor.ToEnumerable()); + + + //Group by tpye and location, cant be in same location two of same type. + return results + .GroupBy(obs => new + { + ((PatientIntravenousLinesValue)obs.Value).Location, + ((PatientIntravenousLinesValue)obs.Value).Type + }) + .Select(g => + g.OrderBy(t => t.Time).ThenBy(t => t.Id).LastOrDefault()) + .ToList(); + } + + + public async Task> FindAllLastPatientObservationTime() + { + var group = new BsonDocument + { + { "_id", "$patientid" }, + { "time", new BsonDocument { { "$last", "$time" } } } + }; + + var pipeline = new List + { + new() + { + { + "$sort", new BsonDocument { { "time", 1 } } + } + }, + new() + { + { + "$group", group + } + } + }; + + var cursor = await Collection.AggregateAsync(pipeline, + new AggregateOptions { AllowDiskUse = true, BatchSize = 10 }); + + var result = new Dictionary(); + + while (await cursor.MoveNextAsync()) + cursor.Current + .Where(obs => obs.GetValue("_id").BsonType != BsonType.Null) + .ToList() + .ForEach(obs => + { + if (obs.Get("_id") != null) + result.Add(obs.Get("_id")?.AsObjectId ?? new ObjectId(), + obs.Get("time")?.ToUniversalTime() ?? DateTime.MinValue); + } + ); + return result; + } + + public async Task FindById(ObjectId id) + { + var filter = Builders.Filter.Eq(o => o.Id, id); + + var result = await Collection.FindAsync(filter); + + return await result.FirstOrDefaultAsync(); + } + + public async Task?> FindByPatientId(ObjectId id) + { + var filter = Builders.Filter.Eq(o => o.PatientId, id); + + var result = await Collection.FindAsync(filter); + + return await result.ToListAsync(); + } + + + public async Task Update(PatientObservation observation) + { + await UpdateOneAsync(observation.Id, observation); + } + + public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId) + { + await UpdateManyObjectIdAsync(nameId, id, oldId); + } + + public async Task UpdateExpiredObservations(List expiredObservations) + { + var filter = Builders.Filter + .In(x => x.Id, expiredObservations.Select(c => c.Id)); + //var filter = Builders.Filter.In("Id", expiredObservations); + var update = Builders.Update.Set("Expired", true); + _ = await Collection.UpdateManyAsync(filter, update); + } + + public async Task UpdateMany(IEnumerable patientObservations, + UpdateDefinition update) + { + var filter = Builders.Filter.In(x => x.Id, patientObservations.Select(x => x.Id)); + + await Collection.UpdateManyAsync(filter, update); + } + + public async Task> FindAll() + { + var result = await Collection.FindAsync(_ => true); + + return result.ToEnumerable(); + } + + public async Task ExpireExpiredObservations( + List configObservationsToExpire) + { + try + { + var builder = Builders.Filter; + var filterObs = builder.Empty; + configObservationsToExpire.ForEach(ob => + { + var minsToAdd = ob.Expires.HasValue ? ob.Expires * -1 : 0; + if (filterObs == builder.Empty) + filterObs = builder.Where(o => + o.Name == ob.Name && o.Time < DateTime.Now.AddMinutes(minsToAdd.Value)); + else + filterObs |= builder.Where(o => + o.Name == ob.Name && o.Time < DateTime.Now.AddMinutes(minsToAdd.Value)); + }); + var filterNotExpired = Builders.Filter.Where(o => !o.Expired); + var combineFilter = filterObs & filterNotExpired; + /*var documentSerializer = BsonSerializer.SerializerRegistry.GetSerializer(); + var renderedFilter = combineFilter.Render(documentSerializer, BsonSerializer.SerializerRegistry); + Debug.WriteLine($"Result : {renderedFilter}");*/ + + var updateDefinition = Builders.Update.Set(o => o.Expired, true); + await Collection.UpdateManyAsync(combineFilter, updateDefinition); + } + catch (Exception ex) + { + _logger.LogError("exception expiring observations expired: {exMessage} ", ex); + } + } + + + public async Task> FindNotExpired(List? filterObservations) + { + var filterBuilder = Builders.Filter; + var filters = new List>(); + + if (filterObservations != null) + { + filters.Add(filterBuilder.Not(filterBuilder.Eq(o => o.Name, null))); + filters.Add(filterBuilder.In(o => o.Name, filterObservations)); + } + + filters.Add(filterBuilder.Ne(o => o.Expired, true)); + + var filter = filterBuilder.And(filters); + + var result = await Collection.FindAsync(filter); + + return await result.ToListAsync(); + } + + + public async Task> FindByName(string name, DateTime? fromDate = null) + { + if (string.IsNullOrWhiteSpace(name)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + if (name.Length > 100) + throw new BadRequestException("Name too long"); + + var safeName = Regex.Escape(name); + + var filterBuilder = Builders.Filter; + + var nameFilter = filterBuilder.And( + filterBuilder.Ne(o => o.Name, null), + filterBuilder.Regex(o => o.Name, new BsonRegularExpression(safeName, "i")) + ); + + var dateFilter = fromDate.HasValue + ? filterBuilder.Gt(o => o.Time, fromDate.Value) + : filterBuilder.Empty; + + var filter = filterBuilder.And(nameFilter, dateFilter); + + var result = await Collection.FindAsync(filter); + + return await result.ToListAsync(); + } + + public IFindFluent GetPaginatedObservations(PaginationFilter filter) + { + // Crear variable con la clase que construye los filtros + var filterBuilder = Builders.Filter; + // Crear una lista de filtros + var filters = new List>(); + // Ordenar los resultados por "time" en orden descendente + var sort = Builders.Sort.Descending("time"); + if (filter.FilteredRequest != null) + { + var requestFilter = filter.FilteredRequest; + if (!requestFilter.Observations.IsNullOrEmpty()) + filters.Add(filterBuilder.In(o => o.Name, requestFilter.Observations)); + + if (requestFilter.PatientId != null) + { + var parsed = ObjectId.TryParse(requestFilter.PatientId, out var patientObjectId); + if (parsed) filters.Add(filterBuilder.Eq(o => o.PatientId, patientObjectId)); + } + } + + // Añadir los filtros por defecto en caso de llegar la lista de filtros vacia, si no la consulta a base de datos falla + filters.Add(filterBuilder.Gt(p => p.Time, filter.FilteredRequest?.StartDate ?? DateTime.MinValue)); + filters.Add(filterBuilder.Lt(p => p.Time, filter.FilteredRequest?.EndDate ?? DateTime.MaxValue)); + + // Combinar los filtros en una consulta compuesta con operador "$and" + var combinedFilter = Builders.Filter.And(filters); + + return Collection + .Find(combinedFilter) + .Sort(sort); + } + + public async Task> FindLastNotExpiredObservatonsByPatient(ObjectId patientId, + string name, int? endAfter = null, int? num = null) + { + var sort = Builders.Sort.Descending("time"); + + var filterBuilder = Builders.Filter; + + if (endAfter.HasValue) + { + var now = DateTime.UtcNow; + + var thresholdTime = now.AddSeconds(-endAfter.Value); // Calcular la fecha límite + + var dateFilter = + filterBuilder.Gte(o => o.Time, thresholdTime); // con fecha mayor que la fecha límite calculada + + filterBuilder.And(dateFilter); + } + + + var patientFilter = filterBuilder.Eq(o => o.PatientId, patientId); + + var nameFilter = filterBuilder.Eq(o => o.Name, name); + + var filters = filterBuilder.And(patientFilter, nameFilter); + + var result = await Collection.FindAsync( + filters, + new FindOptions { Sort = sort, Limit = num } + ); + + return result.ToEnumerable(); + } + + public override async Task CreateIndexes() + { + try + { + var options = new CreateIndexOptions { Background = true, Unique = false }; + var indexes = new List> + { + new("{ patientid: 1 }", options), + new("{ patientid: 1, name: 1, codingSystem: 1 }", options), + new("{ patientid: 1, name: 1 }", options), + new("{ name: 1, time: -1 }", options), + new("{ patientid: 1, systemId: 1 }", options), + new("{ name: 1 }", options), + new("{ patientid: 1, name: 1 , time: -1, id: -1}", options), + new("{ patientid: 1, name: 1 , time: 1}", options) + }; + await MongoUtils.EnsureIndexes(Collection, indexes); + } + catch (Exception e) + { + _logger.LogError( + "error creating indexes for observation collection {eMessage} TRACE: {eStackTrace}", e.Message, + e.StackTrace); + throw; + } + } + + + private async Task> AggregatePatientObservations(ObjectId patientId, + List? filterObservations = null) + { + var matchPatient = new BsonDocument + { + { "patientid", patientId }, + { "name", new BsonDocument { { "$ne", BsonNull.Value } } } + }; + + if (filterObservations == null || !filterObservations.Any()) + { + // GET DISTINCT OBSERVATIONS + var distinctObs = new BsonDocument + { + { + "$group", new BsonDocument + { + { "_id", "1" }, + { + "obs", new BsonDocument + { + { "$addToSet", "$name" } + } + } + } + } + }; + var distinctPipeline = new[] + { + new() + { + { + "$match", matchPatient + } + }, + distinctObs + }; + Debug.WriteLine("AggregatedPatientLastObservations distinct obs: \n" + + distinctPipeline.ToJson()); + var resultList = await Collection.AggregateAsync(distinctPipeline, + new AggregateOptions { AllowDiskUse = true }); + var result = resultList.ToList().FirstOrDefault(); + + if (result != null) + filterObservations = result.GetValue("obs").AsBsonArray.Select(it => it.AsString) + .ToList(); + } + + return filterObservations ?? []; + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/POCMappingRepository.cs b/adas-core.Infrastructure/Repositories/POCMappingRepository.cs new file mode 100644 index 00000000..82065244 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/POCMappingRepository.cs @@ -0,0 +1,34 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; +using MongoDB.Driver; + +namespace adas_core.Infrastructure.Repositories; + +public class PoCMappingRepository : MongoRepository, IPoCMappingRepository +{ + private readonly ApiSettings _apiSettings; + + public PoCMappingRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + _apiSettings = apiSettings.Value; + } //For testing + + + + public override string GetCollectionName() + { + return _apiSettings.Mappings; + } + + public async Task FindByKey(string key) + { + var filterBuilder = Builders.Filter; + var filter = filterBuilder.Eq(config => config.Id, key); + + var result = await Collection.FindAsync(filter); + + return await result.FirstOrDefaultAsync(); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/PatientArchiveRepository.cs b/adas-core.Infrastructure/Repositories/PatientArchiveRepository.cs new file mode 100644 index 00000000..7d25d0ff --- /dev/null +++ b/adas-core.Infrastructure/Repositories/PatientArchiveRepository.cs @@ -0,0 +1,131 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Serilog; +using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; + +namespace adas_core.Infrastructure.Repositories; + +public class PatientArchiveRepository : MongoRepository, IPatientArchiveRepository +{ + private readonly ApiSettings _apiSettings; + + + public PatientArchiveRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + } //For testing + + + + + public override string GetCollectionName() + { + return _apiSettings.ArchivePatient ?? "archive_patient"; + } + + public async Task FindByPatientNumber(string patientNumber) + { + if (string.IsNullOrWhiteSpace(patientNumber)) return null; + + var filterBuilder = Builders.Filter; + var filter = filterBuilder.Eq(p => p.PatientNumber, patientNumber); + + var result = await Collection.FindAsync(filter); + + return await result.FirstOrDefaultAsync(); + } + + + public async Task> FindAll() + { + var filterBuilder = Builders.Filter; + var filter = filterBuilder.Empty; + + var result = await Collection.FindAsync(filter); + + return await result.ToListAsync(); + } + + public async Task SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId) + { + if (string.IsNullOrWhiteSpace(patientNumber)) return null; + + // Paciente ubicado en un PoC pero en diferente unidad + var patient = await Collection.Find(Builders.Filter.And( + Builders.Filter.Eq(p => p.PatientNumber, patientNumber) + )).ToListAsync(); + // Si hay mas de una coincidencia devolvemos null por que puede no estar completo el patientNumber + return patient.Count > 1 ? null : patient.FirstOrDefault(); + } + + public override async Task InsertOneAsync(Patient obj) + { + try + { + if (obj.PatientNumber != null) + { + var patient = await FindByPatientNumber(obj.PatientNumber); + if (patient != null) + { + patient.Allergies = obj.Allergies; + patient.Doctors = obj.Doctors; + patient.Procedures?.AddRange(obj.Procedures ?? []); + patient.Tests?.AddRange(obj.Tests ?? []); + patient.Treatment?.AddRange(obj.Treatment ?? []); + patient.Diagnosis = obj.Diagnosis; + patient.DiagnosisAux = obj.DiagnosisAux; + patient.Insulation = obj.Insulation; + patient.Mobility = obj.Mobility; + patient.Origin = obj.Origin; + patient.OriginAux = obj.OriginAux; + patient.Person = patient.Person; + patient.ArchiveDate = DateTime.UtcNow; + patient.Visits = obj.Visits; + patient.AccessControl = obj.AccessControl; + patient.AdmTime = obj.AdmTime; + patient.PointOfCareId = obj.PointOfCareId; + patient.UnitId = obj.UnitId; + patient.TherapeuticCeiling = obj.TherapeuticCeiling; + patient.Altable = obj.Altable; + if (patient.HistoricalLocations == null) + patient.HistoricalLocations = obj.HistoricalLocations; + else if (obj.HistoricalLocations != null) + foreach (var objHistoricalLocation in obj.HistoricalLocations) + if (!patient.HistoricalLocations.Any(c=> c.AdmTime == objHistoricalLocation.AdmTime)) + patient.HistoricalLocations.Add(objHistoricalLocation); + + await UpdateOneAsync(patient.Id, patient); + } + else + { + await base.InsertOneAsync(obj); + } + } + else + { + await base.InsertOneAsync(obj); + } + } + catch (Exception e) + { + Log.Warning("Exception trying to insert archive patient: {obj}. Exception {e}", obj, e); + } + } + + + public override async Task CreateIndexes() + { + var options = new CreateIndexOptions { Background = true, Unique = false }; + var indexes = new List> + { + new("{ patientNumber: 1 }", options) + }; + await MongoUtils.EnsureIndexes(Collection, indexes); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/PatientCarePlanRepository.cs b/adas-core.Infrastructure/Repositories/PatientCarePlanRepository.cs new file mode 100644 index 00000000..f25ce4e2 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/PatientCarePlanRepository.cs @@ -0,0 +1,67 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Infrastructure.Repositories; + +public class PatientCarePlanRepository : MongoRepository, IPatientCarePlanRepository +{ + #region Properties + + private readonly ApiSettings _apiSettings; + + #endregion + + #region Update + + public async Task UpdateManyObjectId(string patientid, ObjectId patientId, ObjectId oldId) + { + await UpdateManyObjectIdAsync(patientid, patientId, oldId); + } + + #endregion + + #region Constructor + + + + public PatientCarePlanRepository( + IOptions apiSettings, + IMongoDatabase database + ) : base(database) + { + _apiSettings = apiSettings.Value; + } + + #endregion + + #region Read + + public override string GetCollectionName() + { + return _apiSettings.PatientCarePlan ?? "patients_care_plan"; + } + + public async Task> FindByPatientId(ObjectId patientId) + { + var result = await Collection.FindAsync(Builders.Filter.Eq(p => p.PatientId, patientId)); + return result.ToList(); + } + + public async Task> FindByUserId(ObjectId userId) + { + var result = await Collection.FindAsync(Builders.Filter.Eq(p => p.UserId, userId)); + return result.ToList(); + } + + public async Task> FindAll() + { + var result = await Collection.Find(Builders.Filter.Empty).ToListAsync(); + return result; + } + + #endregion +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/PatientRepository.cs b/adas-core.Infrastructure/Repositories/PatientRepository.cs new file mode 100644 index 00000000..180d5044 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/PatientRepository.cs @@ -0,0 +1,973 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Serilog; +using System.Text.RegularExpressions; +using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; + +namespace adas_core.Infrastructure.Repositories; + +public class PatientRepository : MongoRepository, IPatientRepository +{ + private readonly ApiSettings _apiSettings; + + public PatientRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + } //For testing + + // public PatientRepository(IOptions apiSettings, IOptions dbSetting) : + // base(dbSetting) + // { + // if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + // _apiSettings = apiSettings.Value; + // } + + public override string GetCollectionName() + { + return _apiSettings.Patients ?? "patients"; + } + + public async Task FindById(ObjectId id) + { + try + { + var filter = Builders.Filter.Eq(p => p.Id, id); + var result = await Collection.FindAsync(filter); + + return await result.FirstOrDefaultAsync(); + } + catch (Exception ex) + { + Log.Error("Error searching patient by id: {id}. Exception: {ex}", id, ex); + return null; + } + } + + public async Task FindByPointOfCareId(ObjectId id) + { + try + { + var filter = Builders.Filter.Eq(p => p.PointOfCareId, id); + var result = await Collection.FindAsync(filter); + + return await result.FirstOrDefaultAsync(); + } + catch (Exception ex) + { + Log.Error("Error searching patient by id: {id}. Exception: {ex}", id, ex); + return null; + } + } + + public async Task FindByUnitAndPocId(ObjectId unit, ObjectId pointOfCare) + { + try + { + var filter = Builders.Filter.And( + Builders.Filter.Eq(p => p.PointOfCareId, pointOfCare), + Builders.Filter.Eq(p => p.UnitId, unit) + ); + var result = await Collection.FindAsync(filter); + + return await result.FirstOrDefaultAsync(); + } + catch (Exception ex) + { + Log.Error("Error searching patient by unitId: {unitid} pointOfCareid: {id}. Exception: {ex}", unit, + pointOfCare, ex); + return null!; + } + } + + //Deprecated + public async Task FindByLocation(PatientLocation? location) + { + if (location == null) + return null; + // Primero encontrar la unidad + + var filterBuilder = Builders.Filter; + var filter = filterBuilder.Empty; + + if (!string.IsNullOrEmpty(location.UnitName) && !string.IsNullOrEmpty(location.Bed)) + filter = filterBuilder.And( + filterBuilder.Eq(p => p.UnitString, location.UnitName), + filterBuilder.Eq(p => p.Bed, location.Bed) + ); + else if (!string.IsNullOrEmpty(location.Bed)) filter = filterBuilder.Eq(p => p.Bed, location.Bed); + + var result = await Collection.Find(filter).Limit(1).FirstOrDefaultAsync(); + + return result; + } + + + public override async Task InsertOneAsync(Patient patient) + { + try + { + patient.CreationDate = DateTime.UtcNow; + await base.InsertOneAsync(patient); + } + catch (Exception e) + { + Console.WriteLine(e); + + var patientAux = await FindByLocation(patient.Location); + if (patientAux != null && patientAux.PatientNumber == patient.PatientNumber) + { + Log.Warning("Exception trying to insert an existing patient: {patient}. Exception {e}", patient, e); + } + else + { + Log.Error("Exception trying to insert patient: {patient}. Exception {e}", patient, e); + throw; + } + } + } + + public async Task Update(Patient patient) + { + patient.UpdateDate = DateTime.UtcNow; + await UpdateOneAsync(patient.Id, patient); + } + + public async Task Delete(ObjectId id) + { + var filter = Builders.Filter.Eq(x => x.Id, id); + await Collection.DeleteOneAsync(filter, null); + } + + public async Task UpdateLocation(ObjectId id, PatientLocation location) + { + var patient = await FindById(id); + if (patient == null) + { + Log.Warning("Patient not found for updating location: {id}", id); + return; + } + + patient.UpdateDate = DateTime.UtcNow; + patient.Location = location; + await UpdateOneAsync(patient.Id, patient); + /*var filterBuilder = Builders.Filter; + var filter = filterBuilder.Eq(p => p.Id, id); + + var update = Builders.Update + .Set(p => p.Bed, location?.Bed) + .Set(p => p.UnitString, location?.UnitName) + .Set(p => p.UpdateDate, DateTime.UtcNow) + .AddToSet(p => p.HistoricalLocations, new KeyValuePair(DateTime.UtcNow.ToString("o"), location)); + + var result = await Collection.UpdateOneAsync(filter, update); + + Log.Debug("Update location result: {result}", result);*/ + } + + public async Task UpdateLocation(ObjectId id, ObjectId location) + { + var patient = await FindById(id); + if (patient == null) + { + Log.Warning("Patient not found for updating location: {id}", id); + return; + } + + patient.UpdateDate = DateTime.UtcNow; + patient.PointOfCareId = location; + await UpdateOneAsync(patient.Id, patient); + } + + public async Task UpdateAttendingDoctor(ObjectId id, Person attendingDoctor) + { + var update = Builders.Update + .Set(p => p.AttendingDoctor, attendingDoctor) + .Set(p => p.UpdateDate, DateTime.UtcNow); + + await Collection.UpdateOneAsync(p => p.Id == id, update); + } + + public async Task UpdatePatientData(ObjectId id, string patientNumber, Person data, bool updatePatientNumber = true) + { + var filterBuilder = Builders.Filter; + var updateBuilder = Builders.Update.Set(p => p.Person, data).Set(p => p.UpdateDate, DateTime.UtcNow); + + var filter = filterBuilder.Eq(p => p.Id, id); + var update = updateBuilder; + + if (updatePatientNumber) update = update.Set(p => p.PatientNumber, patientNumber); + + await Collection.UpdateOneAsync(filter, update); + } + + + public async Task FindByPatientNumber(string patientNumber) + { + if (string.IsNullOrWhiteSpace(patientNumber)) return null; + + //Último paciente admitido + var patient = await Collection.Find(Builders.Filter.And( + Builders.Filter.Eq(p => p.DisTime, null), + Builders.Filter.Eq(p => p.PatientNumber, patientNumber) + )) + .Sort(Builders.Sort.Descending(p => p.AdmTime)) + .Limit(1) + .FirstOrDefaultAsync(); + + + //Último con fecha de alta más reciente + patient ??= await Collection.Find(Builders.Filter.And( + Builders.Filter.Ne(p => p.DisTime, null), + Builders.Filter.Eq(p => p.PatientNumber, patientNumber) + )) + .Sort(Builders.Sort.Descending(p => p.DisTime)) + .Limit(1) + .FirstOrDefaultAsync(); + + return patient; + } + + public async Task SearchByPatientNumberAndDistinctUnit(string patientNumber, ObjectId unitId) + { + try + { + if (string.IsNullOrWhiteSpace(patientNumber)) return null; + + // Paciente ubicado en un PoC pero en diferente unidad + var patient = await Collection.Find(Builders.Filter.And( + Builders.Filter.Eq(p => p.PatientNumber, patientNumber), + Builders.Filter.Ne(p => p.UnitId, unitId) + )).ToListAsync(); + // Si hay mas de una coincidencia devolvemos null por que puede no estar completo el patientNumber + return patient.Count > 1 ? null : patient.FirstOrDefault(); + } + catch (Exception e) + { + Log.Error( + "Error Search By Patient Number And Distinct Unit on patient repository patientNumber: {patientNumber}, unitId: {unitId}, Excepción: {e}", + patientNumber, unitId, e.Message); + return null; + } + } + + public async Task> FindAllPatientWithFinishedProcedures(int archiveProcedureEndDateAfterMinutes) + { + var currentDateTime = DateTime.UtcNow; + + // Filtro para documentos que contienen al menos un procedimiento con opción "procedure" + var filter = Builders.Filter.ElemMatch( + x => x.Procedures, + procedure => + //procedure.OptionType == "procedure" && + procedure.EndDate.HasValue + ); + + // Ejecutar la consulta inicial y traer los documentos + var patientsWithProcedures = await Collection.Find(filter).ToListAsync(); + + // Aplicar el filtro adicional en memoria + var patientsWithFinishedProcedures = patientsWithProcedures.Where(patient => + patient.Procedures != null && + patient.Procedures.Any(procedure => + procedure is + { + //OptionType: "procedure", + EndDate: not null + } && + procedure.EndDate.Value.AddMinutes(archiveProcedureEndDateAfterMinutes) < currentDateTime + ) + ).ToList(); + + return patientsWithFinishedProcedures; + } + + public async Task> FindAllPatientWithFinishedTests(int archiveTestEndDateAfterMinutes) + { + var currentDateTime = DateTime.UtcNow; + + // Filtro para documentos que contienen al menos un procedimiento con opción "procedure" + var filter = Builders.Filter.ElemMatch( + x => x.Tests, + procedure => //procedure.OptionType == "test" && + procedure.EndDate.HasValue + ); + + // Ejecutar la consulta inicial y traer los documentos + var patientsWithTests = await Collection.Find(filter).ToListAsync(); + + // Aplicar el filtro adicional en memoria + var patientsWithFinishedTests = patientsWithTests.Where(patient => + patient.Tests != null && + patient.Tests.Any(procedure => + procedure is + { + //OptionType: "test", + EndDate: not null + } && + procedure.EndDate.Value.AddMinutes(archiveTestEndDateAfterMinutes) < currentDateTime + ) + ).ToList(); + + return patientsWithFinishedTests; + } + + public async Task> FindAllPatientWithFinishedTreatment(int archiveTreatmentEndDateAfterMinutes) + { + var currentDateTime = DateTime.UtcNow; + + // Filtro para documentos que contienen al menos un procedimiento con opción "procedure" + var filter = Builders.Filter.ElemMatch( + x => x.Treatment, + treatment => treatment.EndDate.HasValue + ); + + // Ejecutar la consulta inicial y traer los documentos + var patientsWithTreatment = await Collection.Find(filter).ToListAsync(); + + // Aplicar el filtro adicional en memoria + var patientsWithFinishedTreatments = patientsWithTreatment.Where(patient => + patient.Treatment != null && + patient.Treatment.Any(treatment => + treatment.EndDate.HasValue && + treatment.EndDate.Value.AddMinutes(archiveTreatmentEndDateAfterMinutes) < currentDateTime + ) + ).ToList(); + + return patientsWithFinishedTreatments; + } + + public async Task> UpdateMasterListOption(List unitIds, UpdateOptionMasterListDto opt, + string typeName) + { + var isParsed = Enum.TryParse(typeName, out var parsedTypeName); + if (isParsed) + { + // Tener en cuenta los datos auxiliares ya que pueden ser texto libre o asignarse el establecido en la lista + // originAux / diagnosisAux modificar en caso de ser el mismo que el padre + // Notificar a todos los fronts con el nuevo valor de cada paciente por el id de los poc's afectados(?) + var filterUnit = Builders.Filter.In("unitId", unitIds); + switch (parsedTypeName) + { + case MasterListType.DiagnosisList: + + var diagnosisFilter = Builders.Filter.Eq( + "diagnosis.name", opt.OldOption?.Name + ); + var filterUpdateDiagnosis = Builders.Filter.And(filterUnit, diagnosisFilter); + var updateDiagnosis = Builders.Update + .Set("diagnosis.name", opt.UpdatedOption?.Name) + .Set("diagnosis.description", opt.UpdatedOption?.Description); + await Collection.UpdateManyAsync(filterUpdateDiagnosis, updateDiagnosis); + + await Collection.UpdateManyAsync(Builders.Filter.And(filterUnit, + Builders.Filter.Eq( + "diagnosisAux", opt.OldOption?.Name + )), Builders.Update + .Set("diagnosisAux", opt.UpdatedOption?.Name)); + + // Filtro para devolver los documentos actualizados (OR entre diagnosis.name y diagnosisAux) + var diagnosisFilterToReturn = + Builders.Filter.Eq("diagnosis.name", opt.UpdatedOption?.Name); + var diagnosisAuxFilterToReturn = + Builders.Filter.Eq("diagnosisAux", opt.UpdatedOption?.Name); + var filterToReturn = Builders.Filter.And( + filterUnit, + Builders.Filter.Or(diagnosisFilterToReturn, diagnosisAuxFilterToReturn) + ); + + // Devolver los documentos actualizados + var updatedDocuments = await Collection.Find(filterToReturn).ToListAsync(); + return updatedDocuments; + case MasterListType.DoctorList: + // Filtro para encontrar el elemento en el array `doctors` que coincida con el nombre + var doctorFilter = Builders.Filter.ElemMatch( + "doctors", + Builders.Filter.Eq("name", opt.OldOption?.Name) + ); + var filterUpdateDoctor = Builders.Filter.And(filterUnit, doctorFilter); + + // Actualizar todos los elementos de la lista `doctors` que coincidan + var updateDoctor = Builders.Update.Set( + "doctors.$[nameElem].name", opt.UpdatedOption?.Name + ); + + // Definir filtros únicos para cada campo utilizado + var arrayFilters = new List + { + new BsonDocumentArrayFilterDefinition( + new BsonDocument("nameElem.name", opt.OldOption?.Name)) + }; + + // Agregar filtro para `optionType` solo si es necesario + if (!string.IsNullOrEmpty(opt.OldOption?.OptionType)) + { + updateDoctor = updateDoctor.Set( + "doctors.$[typeElem].optionType", opt.UpdatedOption?.OptionType + ); + + arrayFilters.Add( + new BsonDocumentArrayFilterDefinition( + new BsonDocument("typeElem.optionType", opt.OldOption.OptionType))); + } + + var updateOptions = new UpdateOptions { ArrayFilters = arrayFilters }; + + // Ejecutar la actualización + await Collection.UpdateManyAsync(filterUpdateDoctor, updateDoctor, updateOptions); + + // Devolver los documentos actualizados + var doctorFilterToReturn = Builders.Filter.ElemMatch( + "doctors", + Builders.Filter.Eq("name", opt.UpdatedOption?.Name) + ); + + var filterToReturnDoctor = Builders.Filter.And(filterUnit, doctorFilterToReturn); + + var updatedDoctorDocuments = await Collection.Find(filterToReturnDoctor).ToListAsync(); + + return updatedDoctorDocuments; + case MasterListType.InsulationList: + break; + case MasterListType.OriginList: + var originFilter = Builders.Filter.Eq( + "origin.name", opt.OldOption?.Name + ); + var filterUpdateOrigin = Builders.Filter.And(filterUnit, originFilter); + var updateOrigin = Builders.Update + .Set("origin.name", opt.UpdatedOption?.Name); + await Collection.UpdateManyAsync(filterUpdateOrigin, updateOrigin); + + await Collection.UpdateManyAsync(Builders.Filter.And(filterUnit, + Builders.Filter.Eq( + "originAux", opt.OldOption?.Name + )), Builders.Update + .Set("originAux", opt.UpdatedOption?.Name)); + + // Filtro para devolver los documentos actualizados (OR entre diagnosis.name y diagnosisAux) + var originFilterToReturn = Builders.Filter.Eq("origin.name", opt.UpdatedOption?.Name); + var originAuxFilterToReturn = Builders.Filter.Eq("originAux", opt.UpdatedOption?.Name); + var originfilterToReturn = Builders.Filter.And( + filterUnit, + Builders.Filter.Or(originFilterToReturn, originAuxFilterToReturn) + ); + + // Devolver los documentos actualizados + var updatedDocumentsorigin = await Collection.Find(originfilterToReturn).ToListAsync(); + return updatedDocumentsorigin; + + case MasterListType.DoctorTypeList: + case MasterListType.AllergyList: + case MasterListType.DestinationList: + case MasterListType.ProcedureList: + case MasterListType.ServiceList: + case MasterListType.TreatmentList: + case MasterListType.AltableOptionList: + case MasterListType.DischargeStatusList: + case MasterListType.InternalDestinationList: + case MasterListType.LanguageBarrierList: + case MasterListType.MobilityOptionList: + case MasterListType.PassiveSittingList: + case MasterListType.PatientStatusList: + case MasterListType.TherapeuticCeilingList: + case MasterListType.VisitOptionList: + case MasterListType.AccessControlList: + break; + } + } + + return []; + } + + public async Task> GetPatientsByUnitIds(List unitIds, string typeName) + { + var isParsed = Enum.TryParse(typeName, out _); + if (!isParsed) return []; + + var filterUnit = Builders.Filter.In("unitId", unitIds); + return await Collection.Find(filterUnit).ToListAsync(); + } + + public async Task CountByUnitId(ObjectId unitId) + { + try + { + var filter = Builders.Filter.Eq(p => p.UnitId, unitId); + return await Collection.CountDocumentsAsync(filter); + } + catch (Exception e) + { + Log.Logger.Error(e.Message); + return 0; + } + } + + + public async Task> DeleteMasterListOption(List unitIds, OptionList opt, + string typeName) + { + var isParsed = Enum.TryParse(typeName, out var parsedTypeName); + if (isParsed) + { + // Tener en cuenta los datos auxiliares ya que pueden ser texto libre o asignarse el establecido en la lista + // originAux / diagnosisAux modificar en caso de ser el mismo que el padre + // Notificar a todos los fronts con el nuevo valor de cada paciente por el id de los poc's afectados(?) + var filterUnit = Builders.Filter.In("unitId", unitIds); + switch (parsedTypeName) + { + case MasterListType.DiagnosisList: + + // Filtro para devolver los documentos actualizados (OR entre diagnosis.name y diagnosisAux) + var diagnosisFilterToReturn = Builders.Filter.Eq("diagnosis.name", opt.Name); + var diagnosisAuxFilterToReturn = Builders.Filter.Eq("diagnosisAux", opt.Name); + var filterToReturn = Builders.Filter.And( + filterUnit, + Builders.Filter.Or(diagnosisFilterToReturn, diagnosisAuxFilterToReturn) + ); + + // Devolver los documentos actualizados + var updatedDocuments = await Collection.Find(filterToReturn).ToListAsync(); + + var diagnosisFilter = Builders.Filter.Eq( + "diagnosis.name", opt.Name + ); + var filterUpdateDiagnosis = Builders.Filter.And(filterUnit, diagnosisFilter); + var updateDiagnosis = Builders.Update + .Set(x => x.Diagnosis, null); + await Collection.UpdateManyAsync(filterUpdateDiagnosis, updateDiagnosis); + + await Collection.UpdateManyAsync(Builders.Filter.And(filterUnit, + Builders.Filter.Eq( + "diagnosisAux", opt.Name + )), Builders.Update + .Set(x => x.DiagnosisAux, null)); + + + return updatedDocuments; + case MasterListType.DoctorList: + // Devolvemos los datos + var doctorFilterToReturn = Builders.Filter.ElemMatch( + "doctors", + Builders.Filter.Eq("_id", opt.Id) + ); + var filterToReturnDoctor = Builders.Filter.And(filterUnit, doctorFilterToReturn); + var updatedDoctorDocuments = await Collection.Find(filterToReturnDoctor).ToListAsync(); + + // Filtro para encontrar el elemento en el array `doctors` que coincida con el nombre + var doctorFilter = Builders.Filter.ElemMatch( + "doctors", + Builders.Filter.Eq("_id", opt.Id) + ); + var filterUpdateDoctor = Builders.Filter.And(filterUnit, doctorFilter); + + // Actualización del campo `name` en el array `doctors` + var updateDoctor = Builders.Update.PullFilter( + "doctors", Builders.Filter.And( + Builders.Filter.Eq("_id", opt.Id) + )); + + // Ejecutar la actualización + await Collection.UpdateManyAsync(filterUpdateDoctor, updateDoctor); + + return updatedDoctorDocuments; + case MasterListType.InsulationList: + break; + case MasterListType.OriginList: + // Filtro para devolver los documentos actualizados (OR entre diagnosis.name y diagnosisAux) + var originFilterToReturn = Builders.Filter.Eq("origin.name", opt.Name); + var originAuxFilterToReturn = Builders.Filter.Eq("originAux", opt.Name); + var filterOriginToReturn = Builders.Filter.And( + filterUnit, + Builders.Filter.Or(originFilterToReturn, originAuxFilterToReturn) + ); + + // Devolver los documentos actualizados + var updatedDocumentsOrigin = await Collection.Find(filterOriginToReturn).ToListAsync(); + + var originFilter = Builders.Filter.Eq( + "origin.name", opt.Name + ); + var filterUpdateOrigin = Builders.Filter.And(filterUnit, originFilter); + var updateOrigin = Builders.Update + .Set(x => x.Diagnosis, null); + await Collection.UpdateManyAsync(filterUpdateOrigin, updateOrigin); + + await Collection.UpdateManyAsync(Builders.Filter.And(filterUnit, + Builders.Filter.Eq( + "originAux", opt.Name + )), Builders.Update + .Set(x => x.OriginAux, null)); + + + return updatedDocumentsOrigin; + + case MasterListType.DoctorTypeList: + case MasterListType.AllergyList: + case MasterListType.DestinationList: + case MasterListType.ProcedureList: + case MasterListType.ServiceList: + case MasterListType.TreatmentList: + case MasterListType.AltableOptionList: + case MasterListType.DischargeStatusList: + case MasterListType.InternalDestinationList: + case MasterListType.LanguageBarrierList: + case MasterListType.MobilityOptionList: + case MasterListType.PassiveSittingList: + case MasterListType.PatientStatusList: + case MasterListType.TherapeuticCeilingList: + case MasterListType.VisitOptionList: + case MasterListType.AccessControlList: + break; + } + } + + return new List(); + } + + public async Task FindByPatientId(string patientId) + { + if (string.IsNullOrWhiteSpace(patientId)) return null; + + //Último paciente admitido + var patient = await Collection.Find(p => !p.DisTime.HasValue && patientId == p.PatientId) + .Sort(Builders.Sort.Descending(p => p.AdmTime)).Limit(1).FirstOrDefaultAsync(); + + //Último con fecha de alta más reciente + patient ??= await Collection.Find(p => p.DisTime.HasValue && patientId == p.PatientId) + .Sort(Builders.Sort.Descending(p => p.DisTime)).Limit(1).FirstOrDefaultAsync(); + + return patient; + } + + public async Task FindByPatientId(ObjectId patientId) + { + //Último paciente admitido + var patient = await Collection.Find(p => !p.DisTime.HasValue && patientId == p.Id) + .Sort(Builders.Sort.Descending(p => p.AdmTime)).Limit(1).FirstOrDefaultAsync(); + + //Último con fecha de alta más reciente + patient ??= await Collection.Find(p => p.DisTime.HasValue && patientId == p.Id) + .Sort(Builders.Sort.Descending(p => p.DisTime)).Limit(1).FirstOrDefaultAsync(); + + return patient; + } + + public async Task> FindAll() + { + return (await Collection.FindAsync(Builders.Filter.Empty)).ToList(); + //return (await Collection.FindAsync(_ => true)).ToList(); + } + + public async Task> FindByPointOfCare(string pointOfCare) + { + var filterBuilder = Builders.Filter; + var filter = filterBuilder.Eq(pa => pa.UnitString, pointOfCare); + + var result = await Collection.FindAsync(filter); + + return await result.ToListAsync(); + } + + public async Task> FindByPointOfCare(ObjectId pointOfCare) + { + var filterBuilder = Builders.Filter; + var filter = filterBuilder.Eq(pa => pa.PointOfCareId, pointOfCare); + + var result = await Collection.FindAsync(filter); + + return await result.ToListAsync(); + } + + public async Task> FindInActivePoC() + { + var virtualPointOfCareValues = Enum.GetValues(typeof(VirtualPointOfCare)).Cast() + .Select(p => p.ToString()) + .ToList(); + + //var filterBuilder = Builders.Filter; + + // Define el pipeline de agregación + var pipeline = new[] + { + new BsonDocument("$lookup", new BsonDocument + { + { "from", "pointOfCares" }, // Colección de PointOfCare + { "localField", "pointOfCareId" }, + { "foreignField", "_id" }, + { "as", "pointOfCareInfo" } + }), + new BsonDocument("$unwind", "$pointOfCareInfo"), + new BsonDocument("$match", new BsonDocument + { + { "pointOfCareInfo.bed", new BsonDocument("$nin", new BsonArray(virtualPointOfCareValues)) } + }) + }; + + var result = await Collection.Aggregate(pipeline).ToListAsync(); + return result; + } + + + public async Task> FindInInactivePoC() + { + var virtualPointOfCareValues = Enum.GetValues(typeof(VirtualPointOfCare)).Cast() + .Select(p => p.ToString()) + .ToList(); + + //var filterBuilder = Builders.Filter; + + // Define el pipeline de agregación + var pipeline = new[] + { + new BsonDocument("$lookup", new BsonDocument + { + { "from", "pointOfCares" }, // Colección de PointOfCare + { "localField", "pointOfCareId" }, + { "foreignField", "_id" }, + { "as", "pointOfCareInfo" } + }), + new BsonDocument("$unwind", "$pointOfCareInfo"), + new BsonDocument("$match", new BsonDocument + { + { "pointOfCareInfo.bed", new BsonDocument("$in", new BsonArray(virtualPointOfCareValues)) } + }) + }; + + var result = await Collection.Aggregate(pipeline).ToListAsync(); + return result; + } + + public IFindFluent GetPaginatedPatients(PaginationFilter filter) + { + var filterBuilder = Builders.Filter; + var sort = Builders.Sort.Descending("admTime"); + var filters = new List>(); + + if (filter.FilteredRequest != null) + { + AddDefaultFilters(filters, filter, filterBuilder); + // return CreateFindFluent(filters, sort); + } + + var requestFilter = filter.FilteredRequest; + + if(requestFilter == null) return CreateFindFluent(filters, sort); + + AddTimeFilters(requestFilter, filters, filterBuilder); + + if (!string.IsNullOrEmpty(filter.FilteredRequest?.Text)) + { + var textFilter = filter.FilteredRequest.Text; + var textFilterEscaped = Regex.Escape(textFilter); + + var orFilters = new List> + { + filterBuilder.Regex(p => p.Person!.FirstName, new BsonRegularExpression(textFilterEscaped, "i")), + filterBuilder.Regex(p => p.Person!.SecondName, new BsonRegularExpression(textFilterEscaped, "i")), + filterBuilder.Regex(p => p.Person!.LastName, new BsonRegularExpression(textFilterEscaped, "i")), + filterBuilder.Regex(p => p.PatientNumber, new BsonRegularExpression(textFilterEscaped, "i")) + }; + + if (ObjectId.TryParse(textFilter, out var id)) orFilters.Add(filterBuilder.Eq("_id", id)); + + filters.Add(filterBuilder.Or(orFilters)); + } + + if (filter.FilteredRequest?.UnitId != null && ObjectId.TryParse(filter.FilteredRequest.UnitId, out var unitId)) + filters.Add(filterBuilder.Eq("unitId", unitId)); + if (filter.FilteredRequest?.PocId != null && ObjectId.TryParse(filter.FilteredRequest.PocId, out var pocId)) + filters.Add(filterBuilder.Eq("pointOfCareId", pocId)); + return CreateFindFluent(filters, sort); + } + + + public async Task> FindPatientsNotUpdatedSince(DateTime date) + { + var filter = Builders.Filter.Or( + Builders.Filter.Lt(p => p.UpdateDate, date), + Builders.Filter.Eq(p => p.UpdateDate, null) + ); + return await Collection.Find(filter).ToListAsync(); + + //return await Collection.Find(p => p.UpdateDate < date).ToListAsync(); + } + + public async Task> FindDischargedPatients() + { + try + { + var filter = Builders.Filter.Ne(p => p.DisTime, null); + var cursor = await Collection.FindAsync(filter); + var patients = await cursor.ToListAsync(); + + return patients; + } + catch (Exception ex) + { + Log.Error("Error finding discharged patients {exMessage}", ex.Message); + return []; + } + } + + public async Task UpdateOne(Patient updatedPatient) + { + var filter = Builders.Filter.Eq("_id", updatedPatient.Id); + + var update = Builders.Update + .Set(p => p.PatientNumber, updatedPatient.PatientNumber) + .Set(p => p.Person, updatedPatient.Person) + .Set(p => p.UpdateDate, DateTime.UtcNow); + + return await Collection.FindOneAndUpdateAsync(filter, update, + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); + } + + public async Task UpdatePatientIncomingData(ObjectId patientId, Patient person) + { + var filter = Builders.Filter.Eq("_id", patientId); + var update = Builders.Update + .Set(p => p.OriginAux, person.OriginAux) + .Set(p => p.DiagnosisAux, person.DiagnosisAux) + .Set(p => p.Diagnosis, person.Diagnosis) + .Set(p => p.UpdateDate, DateTime.UtcNow) + .Set(p => p.AdmTime, person.AdmTime) + .Set(p => p.Origin, person.Origin); + + return await Collection.FindOneAndUpdateAsync(filter, update, + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); + } + + public async Task UpdatePatientDemographicData(ObjectId patientId, Patient person) + { + var filter = Builders.Filter.Eq("_id", patientId); + var update = Builders.Update + .Set(p => p.UpdateDate, DateTime.UtcNow) + .Set(p => p.Allergies, person.Allergies) + .Set(p => p.LanguageBarrier, person.LanguageBarrier) + .Set(p => p.DiagnosisAux, person.DiagnosisAux) + .Set(p => p.Diagnosis, person.Diagnosis) + .Set(p => p.Person, person.Person); + + + return await Collection.FindOneAndUpdateAsync(filter, update, + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); + } + + + public override async Task CreateIndexes() + { + var options = new CreateIndexOptions { Background = true, Unique = false }; + var optionsUq = new CreateIndexOptions + { + Background = true, + Unique = true + //PartialFilterExpression = Builders.Filter.Exists(p => p.PointOfCareId) & + // Builders.Filter.Exists(p => p.UnitId) + }; + var indexes = new List> + { + new("{ patientNumber: 1 }", optionsUq), + new("{ admTime: 1 }", options) + //new("{ pointOfCareId: 1, unitId: 1 }", optionsUq), + //new("{ pointOfCareId: 1 }", optionsUq) + }; + + await MongoUtils.EnsureIndexes(Collection, indexes); + } + + private static void AddTimeFilters(FilteredRequest requestFilter, List> filters, + FilterDefinitionBuilder filterBuilder) + { + // Filter by admission time + if (requestFilter.StartAdmTime.HasValue) + filters.Add(filterBuilder.Gte(p => p.AdmTime, requestFilter.StartAdmTime.Value)); + if (requestFilter.EndAdmTime.HasValue) + filters.Add(filterBuilder.Lte(p => p.AdmTime, requestFilter.EndAdmTime.Value)); + + // Filter by discharge time + if (requestFilter.StartDischargeTime.HasValue) + filters.Add(filterBuilder.Gte(p => p.DisTime, requestFilter.StartDischargeTime.Value)); + if (requestFilter.EndDischargeTime.HasValue) + filters.Add(filterBuilder.Lte(p => p.DisTime, requestFilter.EndDischargeTime.Value)); + + // Filter by last observation time + if (requestFilter.StartLastObsTime.HasValue) + filters.Add(filterBuilder.Gte(p => p.LastObservationDate, requestFilter.StartLastObsTime.Value)); + if (requestFilter.EndLastObsTime.HasValue) + filters.Add(filterBuilder.Lte(p => p.LastObservationDate, requestFilter.EndLastObsTime.Value)); + + // Filter by creation date + if (requestFilter.StartCreationDateTime.HasValue) + filters.Add(filterBuilder.Gte(p => p.CreationDate, requestFilter.StartCreationDateTime.Value)); + if (requestFilter.EndCreationDateTime.HasValue) + filters.Add(filterBuilder.Lte(p => p.CreationDate, requestFilter.EndCreationDateTime.Value)); + + // Filter by update date + if (requestFilter.StartUpDateTime.HasValue) + filters.Add(filterBuilder.Gte(p => p.UpdateDate, requestFilter.StartUpDateTime.Value)); + if (requestFilter.EndUpDateTime.HasValue) + filters.Add(filterBuilder.Lte(p => p.UpdateDate, requestFilter.EndUpDateTime.Value)); + + // Filter by birth date (with Person null check) + if (requestFilter.StartBirthDate.HasValue || requestFilter.EndBirthDate.HasValue) + { + // Check if Person is not null + filters.Add(filterBuilder.Exists(p => p.Person)); + + // Apply birth date filters only if Person is not null + if (requestFilter.StartBirthDate.HasValue) + filters.Add(filterBuilder.Gte(p => p.Person!.BirthDate, requestFilter.StartBirthDate.Value)); + if (requestFilter.EndBirthDate.HasValue) + filters.Add(filterBuilder.Lte(p => p.Person!.BirthDate, requestFilter.EndBirthDate.Value)); + } + } + + + public async Task> FindAllByPointOfCareId(ObjectId pointOfCare) + { + var filterBuilder = Builders.Filter; + var filter = filterBuilder.Eq(pa => pa.PointOfCareId, pointOfCare); + + var result = await Collection.FindAsync(filter); + + return await result.ToListAsync(); + } + + private void AddDefaultFilters(List> filters, PaginationFilter filter, + FilterDefinitionBuilder filterBuilder) + { + // Verifica si PatientId tiene valor + if (filter.FilteredRequest?.PatientId != null) + { + if(ObjectId.TryParse(filter.FilteredRequest.PatientId, out var patientId)) + filters.Add( + filterBuilder.Eq(p => p.Id, patientId) + ); + } + + // Verifica si PatientNumber tiene valor + else if (filter.FilteredRequest?.PatientNumber != null) + filters.Add( + filterBuilder.Eq(p => p.PatientNumber, filter.FilteredRequest.PatientNumber) + ); + } + + private IFindFluent CreateFindFluent(List> filters, + SortDefinition sort) + { + var combinedFilter = filters.Any() + ? Builders.Filter.And(filters) + : Builders.Filter.Empty; + + return Collection.Find(combinedFilter).Sort(sort); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/PoCSettingsRepository.cs b/adas-core.Infrastructure/Repositories/PoCSettingsRepository.cs new file mode 100644 index 00000000..3025ffed --- /dev/null +++ b/adas-core.Infrastructure/Repositories/PoCSettingsRepository.cs @@ -0,0 +1,132 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Serilog; +using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; + +namespace adas_core.Infrastructure.Repositories; + +public class PoCSettingsRepository : MongoRepository, IPoCSettingsRepository +{ + private readonly ApiSettings _apiSettings; + + public PoCSettingsRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + } + + + public override string GetCollectionName() + { + return _apiSettings.PoCSettings ?? "poc_settings"; + } + + public async Task Delete(ObjectId id) + { + try + { + var filter = Builders.Filter.Eq(x => x.Id, id); + await Collection.DeleteOneAsync(filter, null); + } + catch (Exception ex) + { + Log.Error("Error deleting PoCSettings by id: {id}. Exception: {ex}", id, ex); + throw; + } + } + + public async Task> FindAll() + { + try + { + return (await Collection.FindAsync(Builders.Filter.Empty)).ToList(); + } + catch (Exception ex) + { + Log.Error("Error searching PoCSettings. Exception: {ex}", ex); + + return []; + } + } + + public async Task FindById(ObjectId id) + { + try + { + var filter = Builders.Filter.Eq(p => p.Id, id); + var result = await Collection.FindAsync(filter); + + return await result.FirstOrDefaultAsync(); + } + catch (Exception ex) + { + Log.Error("Error searching PoCSettings by id: {id}. Exception: {ex}", id, ex); + return null; + } + } + + public async Task FindByLocation(PatientLocation location) + { + try + { + var filterBuilder = Builders.Filter; + var filter = filterBuilder.Ne(p => p.PatientLocation, null); + // TODO + // if (!string.IsNullOrEmpty(location.PointOfCare) && !string.IsNullOrEmpty(location.Bed)) + // { + // filter = filterBuilder.And( + // filter, + // filterBuilder.Eq(p => p.PatientLocation!.PointOfCare, location.PointOfCare), + // filterBuilder.Eq(p => p.PatientLocation!.Bed, location.Bed) + // ); + // } + + var result = await Collection.Find(filter).Limit(1).FirstOrDefaultAsync(); + + return result; + } + catch (Exception ex) + { + Log.Debug("Error searching by location. Exception: {ex}", ex); + throw; + } + } + + + public async Task Update(PoCSettings pocSettings) + { + try + { + await UpdateOneAsync(pocSettings.Id, pocSettings); + } + catch (Exception ex) + { + Log.Debug("Error updating PoC Settings: {pocS}. Exception: {ex}", pocSettings.ToString(), ex); + throw; + } + } + + public override async Task CreateIndexes() + { + var options = new CreateIndexOptions { Background = true, Unique = false }; + //var optionsUq = new CreateIndexOptions() + //{ + // Background = true, + // Unique = true, + // PartialFilterExpression = Builders.Filter.Exists(p => p.PatientLocation) & + // Builders.Filter.Exists(p => p.ManualRelayStatus) + //}; + var indexes = new List> + { + new("{ patientLocation: 1 }", options) + //new("{ relayStatus: 1, bed: 1 }", optionsUq) + }; + + await MongoUtils.EnsureIndexes(Collection, indexes); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/PointOfCareRepository.cs b/adas-core.Infrastructure/Repositories/PointOfCareRepository.cs new file mode 100644 index 00000000..0ded917f --- /dev/null +++ b/adas-core.Infrastructure/Repositories/PointOfCareRepository.cs @@ -0,0 +1,614 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Serilog; +using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; + +namespace adas_core.Infrastructure.Repositories; + +public class PointOfCareRepository : MongoRepository, IPointOfCareRepository +{ + private readonly ApiSettings _apiSettings; + + public PointOfCareRepository(IOptions? apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings != null) + _apiSettings = apiSettings.Value; + else + throw new ArgumentNullException(nameof(apiSettings)); + } + + + + public override string GetCollectionName() + { + return _apiSettings.Locations; + } + + public override async Task InsertOneAsync(PointOfCare pointOfCare) + { + try + { + await base.InsertOneAsync(pointOfCare); + } + catch (Exception e) + { + Console.WriteLine(e); + Log.Error("Exception trying to insert pointOfCare: {pointOfCare}. Exception {e}", pointOfCare, e); + throw; + } + } + + public async Task Delete(ObjectId id) + { + try + { + var filter = Builders.Filter.Eq(x => x.Id, id); + await Collection.DeleteOneAsync(filter, null); + } + catch (Exception e) + { + Log.Error("Exception trying to delete pointOfCare: {id}. Exception {e}", id, e); + throw; + } + } + + public async Task DeleteManyByUnitId(ObjectId unitId) + { + try + { + var filter = Builders.Filter.Where(p => p.UnitId == unitId); + await Collection.DeleteManyAsync(filter); + + return true; + } + catch (Exception e) + { + Log.Error(e.Message); + return false; + } + } + + public async Task Update(PointOfCare pointOfCare) + { + try + { + await UpdateOneAsync(pointOfCare.Id, pointOfCare); + } + catch (Exception e) + { + Log.Error("Exception trying to update pointOfCare: {pointOfCare}. Exception {e}", pointOfCare, e); + throw; + } + } + + public async Task UpdateUnitId(ObjectId id, Unit unit) + { + var filterBuilder = Builders.Filter; + var filter = filterBuilder.Eq(p => p.Id, id); + + var update = Builders.Update + .Set(p => p.UnitName, unit.Name) + .Set(p => p.UnitId, unit.Id); + + await Collection.UpdateOneAsync(filter, update); + } + + public async Task UpdateRelayConfig(ObjectId pocId, List relayConfig) + { + var filterBuilder = Builders.Filter; + var filter = filterBuilder.Eq(p => p.Id, pocId); + var relayIds = relayConfig.Select(r => r.Id).ToList(); + var update = Builders.Update + .Set(p => p.Configuration!.RelayIdList, relayIds); + + await Collection.UpdateOneAsync(filter, update); + } + public async Task UpdateRelayConfig(ObjectId pocId, List relayConfig) + { + var filterBuilder = Builders.Filter; + var filter = filterBuilder.Eq(p => p.Id, pocId); + var update = Builders.Update + .Set(p => p.Configuration!.RelayIdList, relayConfig); + + await Collection.UpdateOneAsync(filter, update); + } + + public async Task UpdateConfiguration(ObjectId id, PointOfCareConfiguration configuration) + { + var filterBuilder = Builders.Filter; + var filter = filterBuilder.Eq(p => p.Id, id); + + var update = Builders.Update + .Set(p => p.Configuration, configuration); + + await Collection.UpdateOneAsync(filter, update); + } + + public async Task FindById(ObjectId id) + { + try + { + var filter = Builders.Filter.Eq(p => p.Id, id); + return await Collection.Find(filter).FirstOrDefaultAsync(); + } + catch (Exception ex) + { + Log.Error("Error searching pointOfCare by id: {id}. Exception: {ex}", id, ex); + return null; + } + } + + + public async Task> FindByUnitAndStatus(ObjectId unitId, StatusEnum.PointOfCare status, + bool excludeVirtual = false) + { + try + { + var filterBuilder = Builders.Filter; + + var filter = filterBuilder.And( + filterBuilder.Eq(p => p.UnitId, unitId), + filterBuilder.Eq(p => p.Status, status) + ); + if (excludeVirtual) + filter = filterBuilder.And( + filter, + filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.Pushed.ToString()), + filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.Unknown.ToString()), + filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.Deleted.ToString()), + filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.NoBed.ToString()), + filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.Cancelled.ToString()), + filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.Recovered.ToString()), + filterBuilder.Ne(p => p.Bed, VirtualPointOfCare.Moved.ToString()) + ); + var result = await Collection.FindAsync(filter); + + return result.ToEnumerable(); + } + catch (Exception ex) + { + Log.Error("Error searching pointOfCares by Unit: {unit} and status: {}. Exception: {ex}", unitId.ToString(), + status.ToString(), ex); + return new List(); + } + } + + public async Task FindByBedAndUnitId(string? bed, ObjectId unitId) + { + try + { + if (string.IsNullOrEmpty(bed)) return null; + var filterBuilder = Builders.Filter; + List> filters = + [ + filterBuilder.Eq(p => p.Bed, bed), + filterBuilder.Eq(p => p.UnitId, unitId) + // Combina todos los filtros en uno solo usando el operador & si hay más de un filtro, de lo contrario, usa un filtro vacío. + ]; + + // Combina todos los filtros en uno solo usando el operador & si hay más de un filtro, de lo contrario, usa un filtro vacío. + var combinedFilter = filters.Count > 0 + ? filters.Aggregate((current, next) => current & next) + : filterBuilder.Empty; + + var result = await Collection.Find(combinedFilter).Limit(1).FirstOrDefaultAsync(); + + return result; + } + catch (Exception ex) + { + Log.Error("Error searching pointOfCares by patient location unitId: {unitId}, bed: {bed}. Exception: {ex}", + unitId, bed, ex); + return null; + } + } + + public async Task?> FindAllByUnitId(ObjectId unit) + { + try + { + var filter = Builders.Filter.Eq(p => p.UnitId, unit); + var result = await Collection.FindAsync(filter); + + return result.ToEnumerable(); + } + catch (Exception ex) + { + Log.Error("Error searching pointOfCares by Unit: {unit}. Exception: {ex}", unit, ex); + return null; + } + } + + public async Task?> FindByRoom(string room) + { + try + { + var filter = Builders.Filter.Eq(p => p.Room, room); + var result = await Collection.FindAsync(filter); + + return result.ToEnumerable(); + } + catch (Exception ex) + { + Log.Error("Error searching pointOfCares by room: {room}. Exception: {ex}", room, ex); + return null; + } + } + + public async Task?> FindByBed(string bed) + { + try + { + var filter = Builders.Filter.Eq(p => p.Bed, bed); + var result = await Collection.FindAsync(filter); + + return result.ToEnumerable(); + } + catch (Exception ex) + { + Log.Error("Error searching pointOfCares by bed: {bed}. Exception: {ex}", bed, ex); + return null; + } + } + + public async Task> FindByFilter(FilterDefinition filter, + ProjectionDefinition? projection = null) + { + if (projection != null) + return await Collection.Find(filter).Project(projection).ToListAsync(); + return await Collection.Find(filter).ToListAsync(); + } + + public async Task CountByUnitId(ObjectId unitId) + { + try + { + var filterBuilder = Builders.Filter; + var excludedBeds = new[] + { + VirtualPointOfCare.Pushed.ToString(), + VirtualPointOfCare.Unknown.ToString(), + VirtualPointOfCare.Deleted.ToString(), + VirtualPointOfCare.NoBed.ToString(), + VirtualPointOfCare.Cancelled.ToString(), + VirtualPointOfCare.UnitData.ToString(), + VirtualPointOfCare.Recovered.ToString(), + VirtualPointOfCare.Moved.ToString() + }; + var filter = filterBuilder.And( + filterBuilder.Eq(p => p.UnitId, unitId), + filterBuilder.Nin(p => p.Bed, excludedBeds) + ); + + return await Collection.CountDocumentsAsync(filter); + } + catch (Exception e) + { + Log.Logger.Error(e.Message); + throw; + } + } + public async Task CountVirtualsByUnitId(ObjectId unitId) + { + try + { + var filterBuilder = Builders.Filter; + + // Lista de estados que NO quieres contar + var excludedBeds = new[] + { + VirtualPointOfCare.Pushed.ToString(), + VirtualPointOfCare.Unknown.ToString(), + VirtualPointOfCare.Deleted.ToString(), + VirtualPointOfCare.NoBed.ToString(), + VirtualPointOfCare.Cancelled.ToString(), + VirtualPointOfCare.Recovered.ToString(), + VirtualPointOfCare.UnitData.ToString(), + VirtualPointOfCare.Moved.ToString() + }; + + // Filtramos por UnitId Y que el Bed NO esté en la lista de excluidos + var filter = filterBuilder.And( + filterBuilder.Eq(p => p.UnitId, unitId), + filterBuilder.In(p => p.Bed, excludedBeds) + ); + + return await Collection.CountDocumentsAsync(filter); + } + catch (Exception e) + { + Log.Logger.Error(e.Message); + throw; + } + } + public async Task GetPoCConfiguration(ObjectId pocId) + { + var filter = Builders.Filter.Eq(p => p.Id, pocId); + + var projection = Builders.Projection + .Include(p => p.Id) + .Include(p => p.Configuration); + + return await Collection.Find(filter).Project(projection).FirstOrDefaultAsync(); + } + + public async Task?> GetAll() + { + var filter = Builders.Filter.Empty; + return await Collection.Find(filter).ToListAsync(); + } + public async Task FindByIdAllConfig(ObjectId id) + { + return await Collection.Aggregate() + .Match(c=>c.Id == id) + .Lookup(_apiSettings.LightBeacons, "configuration.beaconIdList", "_id", "beacons") + .Lookup(_apiSettings.Cameras, "configuration.cameraIdList", "_id", "cameras") + .Lookup(_apiSettings.Relays, "configuration.relayIdList", "_id", "relays") + .Project(new BsonDocument + { + { "_id", 1 }, + { "room", 1 }, + { "bed", 1 }, + { "hall", 1 }, + { "unitId", 1 }, + { "status", 1 }, + { "admissionId", 1 }, + { "configuration", new BsonDocument + { + { "beaconList", "$beacons" }, + { "cameraList", "$cameras" }, + { "relayList", "$relays" }, + { "beaconIdList", "$configuration.beaconIdList" }, + { "cameraIdList", "$configuration.cameraIdList" }, + { "relayIdList", "$configuration.relayIdList" }, + { "type", "$configuration.type" }, + { "id", "$configuration.id" } + } + } + }).FirstOrDefaultAsync(); + } + + /// + /// Obtiene de forma asíncrona todos los identificadores únicos de cámaras que están + /// vinculados a algún PointOfCare. + /// + /// + /// Se retorna un para optimizar la búsqueda de pertenencia (Contains) en el servicio. + /// Mientras que una requiere un tiempo de búsqueda lineal $O(n)$, el HashSet utiliza + /// una tabla hash que permite verificar si una cámara está en uso en tiempo constante $O(1)$. + /// Esto es crítico para mantener el rendimiento al comparar los IDs de la página actual + /// contra el total de cámaras en uso, independientemente del volumen de datos. + /// + /// Un conjunto hash con los de las cámaras en uso. + public async Task> FindAllIdCamerasInUse() + { + var distinctIds = await Collection + .DistinctAsync("configuration.cameraIdList", Builders.Filter.Empty); + + var list = await distinctIds.ToListAsync(); + return new HashSet(list); + } + public async Task> FindAllIdBeaconsInUse() + { + var distinctIds = await Collection + .DistinctAsync("configuration.beaconIdList", Builders.Filter.Empty); + + var list = await distinctIds.ToListAsync(); + return new HashSet(list); + } + + public async Task?> FindAllByUnitIdWithDevices(ObjectId unitId) + { + try + { + var pipeline = Collection.Aggregate() + // 1. Filtramos primero por UnitId (muy importante para rendimiento) + .Match(p => p.UnitId == unitId) + + // 2. Realizamos los Lookups usando las colecciones desde settings + .Lookup(_apiSettings.LightBeacons, "configuration.beaconIdList", "_id", "beacons") + .Lookup(_apiSettings.Cameras, "configuration.cameraIdList", "_id", "cameras") + .Lookup(_apiSettings.Relays, "configuration.relayIdList", "_id", "relays") + + // 3. Proyectamos para que coincida exactamente con tu modelo C# + .Project(new BsonDocument + { + { "_id", 1 }, + { "room", 1 }, + { "bed", 1 }, + { "hall", 1 }, + { "unitId", 1 }, + { "status", 1 }, + { "admissionId", 1 }, + { "configuration", new BsonDocument + { + // Mapeamos los arrays temporales a las propiedades de la clase + { "beaconList", "$beacons" }, + { "cameraList", "$cameras" }, + { "relayList", "$relays" }, + { "beaconIdList", "$configuration.beaconIdList" }, + { "cameraIdList", "$configuration.cameraIdList" }, + { "relayIdList", "$configuration.relayIdList" }, + { "type", "$configuration.type" }, + { "id", "$configuration.id" } + } + } + }); + + return await pipeline.ToListAsync(); + } + catch (Exception ex) + { + Log.Error("Error searching pointOfCares by Unit: {unit}. Exception: {ex}", unitId, ex); + return null; + } + } + + public async Task> FindAllIdRelaysInUse() + { + var distinctIds = await Collection + .DistinctAsync("configuration.relayIdList", Builders.Filter.Empty); + + var list = await distinctIds.ToListAsync(); + return new HashSet(list); + } + + public async Task?> GetAllConfigs() + { + var pipeline = Collection.Aggregate() + .Lookup( + _apiSettings.LightBeacons, + "configuration.beaconIdList", + "_id", + "beacons" + ) + .Lookup( + _apiSettings.Cameras, + "configuration.cameraIdList", + "_id", + "cameras" + ) + .Lookup( + _apiSettings.Relays, + "configuration.relayIdList", + "_id", + "relays" + ) + .Project(new BsonDocument + { + { "_id", 1 }, + { "room", 1 }, + { "bed", 1 }, + { "hall", 1 }, + { "unitId", 1 }, + { "status", 1 }, + { "admissionId", 1 }, + + { "configuration", new BsonDocument + { + { "beaconList", "$beacons" }, + { "cameraList", "$cameras" }, + { "relayList", "$relays" }, + { "type", "$configuration.type" }, + { "id", "$configuration.id" } + } + } + }); + + return await pipeline.ToListAsync(); + } + + public async Task?> GetAllLocationInfo() + { + var filter = Builders.Filter.Empty; + + var projection = Builders.Projection + .Include(p => p.Id) + .Include(p => p.UnitId) + .Include(p => p.Room) + .Include(p => p.Bed); + + var result = await Collection.Find(filter).Project(projection).ToListAsync(); + + return result; + } + + public IFindFluent GetPaginatedPoCs(PaginationFilter filter) + { + var filterBuilder = Builders.Filter; + var sort = Builders.Sort.Descending("_id"); + var filters = new List>(); + + if (filter.FilteredRequest == null) return CreateFindFluent(filters, sort); + + // Verifica UnitName contiene el valor de FilteredRequest.Text + if (!string.IsNullOrEmpty(filter.FilteredRequest?.UnitId) && + ObjectId.TryParse(filter.FilteredRequest.UnitId, out var unitId)) + filters.Add(filterBuilder.Eq(d => d.UnitId, unitId)); + else if (!string.IsNullOrEmpty(filter.FilteredRequest?.UnitName)) + filters.Add(filterBuilder.Eq(d => d.Unit!.Name, filter.FilteredRequest?.UnitName)); + + if (filter.FilteredRequest?.PointOfCareStatus != null) + { + var statusFilter = filter.FilteredRequest.PointOfCareStatus; + + filters.Add(filterBuilder.Eq(p => p.Status, statusFilter)); + } + + return CreateFindFluent(filters, sort); + } + + //Deprecated PatientLocation by UnitName + public async Task FindByPatientLocation(PatientLocation patientLocation) + { + try + { + var filterBuilder = Builders.Filter; + List> filters = []; + + if (!string.IsNullOrEmpty(patientLocation.UnitName)) + filters.Add(filterBuilder.Eq(p => p.UnitName, patientLocation.UnitName)); + + if (!string.IsNullOrEmpty(patientLocation.Bed)) + filters.Add(filterBuilder.Eq(p => p.Bed, patientLocation.Bed)); + + if (!string.IsNullOrEmpty(patientLocation.Room)) + filters.Add(filterBuilder.Eq(p => p.Room, patientLocation.Room)); + + // Combina todos los filtros en uno solo usando el operador & si hay más de un filtro, de lo contrario, usa un filtro vacío. + var combinedFilter = filters.Count > 0 + ? filters.Aggregate((current, next) => current & next) + : filterBuilder.Empty; + + var result = await Collection.Find(combinedFilter).Limit(1).FirstOrDefaultAsync(); + + return result; + } + catch (Exception ex) + { + Log.Error("Error searching pointOfCares by patient location: {bed}. Exception: {ex}", patientLocation, ex); + return null; + } + } + + public override async Task CreateIndexes() + { + var options = new CreateIndexOptions { Background = true, Unique = false }; + var indexes = new List> + { + new("{ unitId: 1 }", options), + new("{ room: 1 }", options), + new("{ bed: 1 }", options) + }; + + await MongoUtils.EnsureIndexes(Collection, indexes); + } + + public async Task UpdateStatus(ObjectId id, StatusEnum.PointOfCare status) + { + var filterBuilder = Builders.Filter; + var filter = filterBuilder.Eq(p => p.Id, id); + + var update = Builders.Update + .Set(p => p.Status, status); + + await Collection.UpdateOneAsync(filter, update); + } + + private IFindFluent CreateFindFluent(List> filters, + SortDefinition sort) + { + var combinedFilter = filters.Any() + ? Builders.Filter.And(filters) + : Builders.Filter.Empty; // Filtra todo si no hay filtros + return Collection.Find(combinedFilter).Sort(sort); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/PumpAlarmEventRepository.cs b/adas-core.Infrastructure/Repositories/PumpAlarmEventRepository.cs new file mode 100644 index 00000000..da0c00d5 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/PumpAlarmEventRepository.cs @@ -0,0 +1,104 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Pumps; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Infrastructure.Repositories; + +public class PumpAlarmEventRepository : MongoRepository, IPumpAlarmEventRepository +{ + private readonly ApiSettings _apiSettings; + + public PumpAlarmEventRepository(IOptions apiSettings, IMongoDatabase database) + : base(database) + { + _apiSettings = apiSettings.Value; + } + + + + public override string GetCollectionName() + { + return _apiSettings.PumpAlarmEvent ?? "pump_alarm_event"; + } + + public override async Task CreateIndexes() + { + var indexModels = new List> + { + // Principal para consultas por bomba y orden temporal + new( + Builders.IndexKeys + .Ascending(x => x.DeviceId) + .Descending(x => x.Time), + new CreateIndexOptions { Name = "ix_deviceId_time" }), + + // Índice temporal + new( + Builders.IndexKeys.Descending(x => x.Time), + new CreateIndexOptions { Name = "ix_time" }), + + // por paciente + new( + Builders.IndexKeys.Ascending(x => x.PatientId), + new CreateIndexOptions { Name = "ix_patientId" }), + + //por tipo de alarma dentro de una bomba + new( + Builders.IndexKeys + .Ascending(x => x.DeviceId) + .Ascending(x => x.AlarmType), + new CreateIndexOptions { Name = "ix_device_alarmType" }) + }; + + await Collection.Indexes.CreateManyAsync(indexModels); + } + + + public async Task InsertAsync(PumpAlarmEvent alarmEvent) + { + await Collection.InsertOneAsync(alarmEvent); + } + + public async Task> FindByDeviceIdAsync(string deviceId, DateTime? from = null, + DateTime? to = null, int? limit = null) + { + var filter = Builders.Filter.Eq(x => x.DeviceId, deviceId); + + if (from.HasValue) + filter &= Builders.Filter.Gte(x => x.Time, from.Value); + + if (to.HasValue) + filter &= Builders.Filter.Lte(x => x.Time, to.Value); + + var find = Collection.Find(filter).SortByDescending(x => x.Time); + if (limit.HasValue) find = find.Limit(limit.Value) as IOrderedFindFluent; + + return await find.ToListAsync(); + } + + public async Task FindLastByDeviceIdAsync(string deviceId) + { + return await Collection + .Find(x => x.DeviceId == deviceId) + .SortByDescending(x => x.Time) + .FirstOrDefaultAsync(); + } + + public async Task DeleteByPatientId(ObjectId patientId) + { + await Collection.DeleteManyAsync(x => x.PatientId == patientId); + } + + + public async Task UpdateManyObjectIdByFiledNameAsync(string fieldName, ObjectId newId, ObjectId oldId) + { + var filter = Builders.Filter.Eq(fieldName, oldId); + var update = Builders.Update.Set(fieldName, newId); + var result = await Collection.UpdateManyAsync(filter, update); + return result.ModifiedCount; + } + +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/PumpAlarmStateRepository.cs b/adas-core.Infrastructure/Repositories/PumpAlarmStateRepository.cs new file mode 100644 index 00000000..57274b31 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/PumpAlarmStateRepository.cs @@ -0,0 +1,120 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Pumps; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Infrastructure.Repositories; + +public class PumpAlarmStateRepository : MongoRepository, IPumpAlarmStateRepository +{ + private readonly ApiSettings _apiSettings; + + public PumpAlarmStateRepository(IOptions apiSettings, IMongoDatabase database) + : base(database) + { + _apiSettings = apiSettings.Value; + } + + + + public override string GetCollectionName() + { + return _apiSettings.PumpAlarmState ?? "pump_alarm_state"; + } + + public override async Task CreateIndexes() + { + var indexModels = new List> + { + // Clave única de alarma activa + new( + Builders.IndexKeys + .Ascending(x => x.DeviceId) + .Ascending(x => x.AlarmType) + .Ascending(x => x.AlarmCodeMdc), + new CreateIndexOptions { Unique = true, Name = "ux_device_alarm" }), + + // Indexado por DeviceId + new( + Builders.IndexKeys.Ascending(x => x.DeviceId), + new CreateIndexOptions { Name = "ix_device" }), + + // indexado por PatientId + new( + Builders.IndexKeys.Ascending(x => x.PatientId), + new CreateIndexOptions { Name = "ix_patientId" }) + }; + + await Collection.Indexes.CreateManyAsync(indexModels); + } + + public async Task FindActiveAsync(string deviceId, PumpEnum.AlarmType? alarmType, string? alarmCodeMdc = null) + { + var filter = Builders.Filter.Eq(x => x.DeviceId, deviceId); + + if (alarmType.HasValue) + filter &= Builders.Filter.Eq(x => x.AlarmType, alarmType); + + if (!string.IsNullOrWhiteSpace(alarmCodeMdc)) + filter &= Builders.Filter.Eq(x => x.AlarmCodeMdc, alarmCodeMdc); + + return await Collection.Find(filter).FirstOrDefaultAsync(); + } + + public async Task UpsertActiveAsync(PumpAlarmState state) + { + var filter = + Builders.Filter.Eq(x => x.DeviceId, state.DeviceId) & + Builders.Filter.Eq(x => x.AlarmType, state.AlarmType) & + Builders.Filter.Eq(x => x.AlarmCodeMdc, state.AlarmCodeMdc); + + // Revisar si ya existe un documento activo con esa combinación + var existing = await Collection.Find(filter).FirstOrDefaultAsync(); + + if (existing != null) + state.Id = existing.Id; + else + if (state.Id == ObjectId.Empty) + state.Id = ObjectId.GenerateNewId(); + + await Collection.ReplaceOneAsync( + filter, + state, + new ReplaceOptions { IsUpsert = true }); + } + + + public async Task RemoveAsync(string? deviceId, PumpEnum.AlarmType? alarmType, string? alarmCodeMdc = null) + { + var filter = Builders.Filter.Eq(x => x.DeviceId, deviceId); + + if (alarmType.HasValue) + filter &= Builders.Filter.Eq(x => x.AlarmType, alarmType); + + if (!string.IsNullOrWhiteSpace(alarmCodeMdc)) + filter &= Builders.Filter.Eq(x => x.AlarmCodeMdc, alarmCodeMdc); + + await Collection.DeleteManyAsync(filter); + } + + public async Task DeleteByPatientId(ObjectId patientId) + { + await Collection.DeleteManyAsync(p => p.PatientId == patientId); + } + + public async Task> FindAllActiveByDeviceAsync(string deviceId) + { + return await Collection.Find(x => x.DeviceId == deviceId).ToListAsync(); + } + + public async Task UpdateManyObjectIdByFieldNameAsync(string fieldName, ObjectId newId, ObjectId oldId) + { + var filter = Builders.Filter.Eq(fieldName, oldId); + var update = Builders.Update.Set(fieldName, newId); + var result = await Collection.UpdateManyAsync(filter, update); + return result.ModifiedCount; + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/PumpArchiveRepository.cs b/adas-core.Infrastructure/Repositories/PumpArchiveRepository.cs new file mode 100644 index 00000000..c64cc83d --- /dev/null +++ b/adas-core.Infrastructure/Repositories/PumpArchiveRepository.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Pumps; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Infrastructure.Repositories +{ + /// + /// Repositorio de archivo para observaciones de bombas. + /// Colección: archive_pumpobservations (configurable por ApiSettings.ArchivePumpObservations). + /// + public class PumpArchiveRepository : MongoRepository, IPumpArchiveRepository + { + private readonly ApiSettings _apiSettings; + + public PumpArchiveRepository(IOptions apiSettings, IMongoDatabase database) + : base(database) + { + _apiSettings = apiSettings.Value; + } + + + public override string GetCollectionName() + { + // Nombre de colección pactado: "archive_pumpobservations" + return _apiSettings.ArchivePatientsPumpobservations ?? "archive_pumpobservations"; + } + + public override async Task CreateIndexes() + { + var indexModels = new List> + { + // Búsquedas por paciente (audit / restauraciones) + new CreateIndexModel( + Builders.IndexKeys.Ascending(x => x.PatientId), + new CreateIndexOptions { Name = "ix_patientId" }), + + // Timeline por dispositivo (útil para auditorías por equipo) + new CreateIndexModel( + Builders.IndexKeys + .Ascending(x => x.DeviceId) + .Descending(x => x.Time), + new CreateIndexOptions { Name = "ix_deviceId_time" }), + + // Orden temporal simple + new CreateIndexModel( + Builders.IndexKeys.Descending(x => x.Time), + new CreateIndexOptions { Name = "ix_time" }) + }; + + await Collection.Indexes.CreateManyAsync(indexModels); + } + + public async Task InsertAsync(PumpObservation obs) + { + await Collection.InsertOneAsync(obs); + } + + public async Task InsertManyAsync(IEnumerable observations) + { + var list = observations as IList ?? observations.ToList(); + if (list.Count == 0) return; + + await Collection.InsertManyAsync(list); + } + + public async Task> FindByPatientIdAsync( + ObjectId patientId, DateTime? from = null, DateTime? to = null, int? limit = null) + { + var filter = Builders.Filter.Eq(x => x.PatientId, patientId); + + if (from.HasValue) + filter &= Builders.Filter.Gte(x => x.Time, from.Value); + + if (to.HasValue) + filter &= Builders.Filter.Lte(x => x.Time, to.Value); + + var query = Collection.Find(filter).SortByDescending(x => x.Time); + + if (limit.HasValue) + query = query.Limit(limit.Value) as IOrderedFindFluent; + + return await query.ToListAsync(); + } + + public async Task DeleteBeforeDate(DateTime addDays) + { + var filter = Builders.Filter.Lt(x => x.Time, addDays); + await Collection.DeleteManyAsync(filter); + } + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/PumpObservationRepository.cs b/adas-core.Infrastructure/Repositories/PumpObservationRepository.cs new file mode 100644 index 00000000..b4e20f71 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/PumpObservationRepository.cs @@ -0,0 +1,297 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Pumps; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Infrastructure.Repositories +{ + public class PumpObservationRepository : MongoRepository, IPumpObservationRepository + { + private readonly ApiSettings _apiSettings; + + + + public PumpObservationRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + _apiSettings = apiSettings.Value; + } + + public override string GetCollectionName() + { + return _apiSettings.PumpObservations ?? "pump_observations"; + } + + public override async Task CreateIndexes() + { + var indexModels = new List> + { + // Timeline por bomba (consulta más frecuente) + new CreateIndexModel( + Builders.IndexKeys + .Ascending(x => x.DeviceId) + .Descending(x => x.Time), + new CreateIndexOptions { Name = "ix_deviceId_time" }), + + // consultas por paciente + new CreateIndexModel( + Builders.IndexKeys.Ascending(x => x.PatientId), + new CreateIndexOptions { Name = "ix_patientId" }), + + + // TTL por antigüedad (si aplica retención directa en Mongo) + // new CreateIndexModel( + // Builders.IndexKeys.Ascending(x => x.Time), + // new CreateIndexOptions { Name = "ttl_time", ExpireAfter = TimeSpan.FromDays(180) }) + }; + + await Collection.Indexes.CreateManyAsync(indexModels); + } + + public async Task InsertAsync(PumpObservation obs) + { + await Collection.InsertOneAsync(obs); + } + + public async Task InsertManyAsync(IEnumerable? observations) + { + if (observations == null) return; + + var list = observations as IList ?? observations.ToList(); + if (list.Count == 0) return; + + await Collection.InsertManyAsync(list); + } + + public async Task> FindByDeviceIdAsync( + string deviceId, + DateTime? from = null, + DateTime? to = null, + int? limit = null) + { + var filter = Builders.Filter.Eq(x => x.DeviceId, deviceId); + + if (from.HasValue) + filter &= Builders.Filter.Gte(x => x.Time, from.Value); + + if (to.HasValue) + filter &= Builders.Filter.Lte(x => x.Time, to.Value); + + var query = Collection.Find(filter) + .SortByDescending(x => x.Time); + + if (limit.HasValue) + query = query.Limit(limit.Value) as IOrderedFindFluent; + + return await query.ToListAsync(); + } + + // histórico por paciente + public async Task> FindByPatientAsync( + ObjectId patientId, + DateTime? from = null, + DateTime? to = null, + int? limit = null) + { + var filter = Builders.Filter.Eq(x => x.PatientId, patientId); + + if (from.HasValue) + filter &= Builders.Filter.Gte(x => x.Time, from.Value); + + if (to.HasValue) + filter &= Builders.Filter.Lte(x => x.Time, to.Value); + + var query = Collection.Find(filter) + .SortByDescending(x => x.Time); + + if (limit.HasValue) + query = query.Limit(limit.Value) as IOrderedFindFluent; + + return await query.ToListAsync(); + } + + public async Task> FindAllLastPatientObservationTimeAsync() + { + // Pipeline: + // 1) Filtrar observaciones con PatientId no nulo + // 2) Agrupar por PatientId + // 3) Obtener el máximo Time + // 4) Devolver diccionario + + var pipeline = new[] + { + new BsonDocument("$match", new BsonDocument + { + { "patientid", new BsonDocument("$ne", BsonNull.Value) } + }), + new BsonDocument("$group", new BsonDocument + { + { "_id", "$patientid" }, + { "LastTime", new BsonDocument("$max", "$time") } + }) + }; + + var docs = await Collection.Aggregate(pipeline).ToListAsync(); + + var result = new Dictionary(); + + foreach (var doc in docs.Where(doc => + doc["_id"].IsObjectId && doc["LastTime"].IsValidDateTime + )) + { + result[doc["_id"].AsObjectId] = doc["LastTime"].ToUniversalTime(); + } + + return result; + } + + public async Task FindLastByDeviceIdAsync(string deviceId) + { + return await Collection + .Find(x => x.DeviceId == deviceId) + .SortByDescending(x => x.Time) + .FirstOrDefaultAsync(); + } + + public async Task> FindByPatientId(ObjectId? patientId) + { + return await Collection.Find(x => x.PatientId == patientId).ToListAsync(); + } + + + public async Task> AggregatedPatientLastObservations(ObjectId patientId, int num = 100) + { + var filterBuilder = Builders.Filter; + var sortBuilder = Builders.Sort; + + var filter = filterBuilder.Eq(o => o.PatientId, patientId); + var sort = sortBuilder.Descending("time"); + var options = new FindOptions { Sort = sort, Limit = num }; + + var result = await Collection.FindAsync(filter, options); + var observations = await result.ToListAsync(); + + var distinctObservations = observations.DistinctBy(m => new { m.Code, m.Name }).ToList(); + var sortedObservations = distinctObservations.OrderByDescending(x => x.Time).ToList(); + + return sortedObservations; + } + + public async Task DeleteByPatientId(ObjectId? patientId) + { + await Collection.DeleteManyAsync(x => x.PatientId == patientId); + } + public async Task DeleteOlderThanDaysAsync(int days, string? name = null) + { + var limitDate = DateTime.UtcNow.AddDays(-days); + + var filter = Builders.Filter.Lt(x => x.Time, limitDate); + if(!string.IsNullOrEmpty(name)) + filter &= Builders.Filter.Eq(x=> x.Name, name); + + return (await Collection.DeleteManyAsync(filter)).DeletedCount; + } + + public async Task DeleteKeepLastNAsync(int maxCount) + { + // Para cada DeviceId: + var deviceIds = await Collection + .Distinct("DeviceId", FilterDefinition.Empty) + .ToListAsync(); + + long totalDeleted = 0; + + foreach (var filter in deviceIds.Select(deviceId => + Builders.Filter.Eq(x => x.DeviceId, deviceId) + )) + { + var all = await Collection.Find(filter) + .SortByDescending(x => x.Time) + .ToListAsync(); + + if (all.Count <= maxCount) + continue; + + var toDelete = all.Skip(maxCount).Select(x => x.Id).ToList(); + + var deleteFilter = Builders.Filter.In(x => x.Id, toDelete); + var result = await Collection.DeleteManyAsync(deleteFilter); + + totalDeleted += result.DeletedCount; + } + + return totalDeleted; + } + + public async Task UpdateManyObjectIdByFieldAsync(string fieldName, ObjectId newId, ObjectId? oldId) + { + if (string.IsNullOrWhiteSpace(fieldName)) + throw new ArgumentException("fieldName can't be null or empty.", nameof(fieldName)); + + // Normalize casing (Mongo is case-sensitive) + // if (fieldName.Equals("patientid", StringComparison.OrdinalIgnoreCase)) + // fieldName = nameof(PumpObservation.PatientId); + + var filter = Builders.Filter.Eq(fieldName, oldId); + var update = Builders.Update.Set(fieldName, newId); + + var result = await Collection.UpdateManyAsync(filter, update); + return result.ModifiedCount; + } + + + public async Task DeleteOlderNumberAsync(string name, int maxCount) + { + if (string.IsNullOrWhiteSpace(name)) + return 0; + + // 1. Filtrar todas las observaciones con ese Name + var filter = Builders.Filter.Eq(x => x.Name, name); + + // 2. Obtenerlas ordenadas por Time DESC (las más recientes primero) + var all = await Collection + .Find(filter) + .SortByDescending(x => x.Time) + .ToListAsync(); + + // 3. Si hay menos o igual al número permitido → no borrar nada + if (all.Count <= maxCount) + return 0; + + // 4. Seleccionar TODAS excepto las maxCount más recientes + var toDeleteIds = all + .Skip(maxCount) + .Select(x => x.Id) + .ToList(); + + if (toDeleteIds.Count == 0) + return 0; + + // 5. Borrar las seleccionadas + var deleteFilter = Builders.Filter.In(x => x.Id, toDeleteIds); + var result = await Collection.DeleteManyAsync(deleteFilter); + + return result.DeletedCount; + } + + + public async Task> FindLastObservations(ObjectId patientId, string name) + { + if (string.IsNullOrWhiteSpace(name)) + return []; + + var filter = Builders.Filter.And( + Builders.Filter.Eq(x => x.PatientId, patientId), + Builders.Filter.Eq(x => x.Name, name) + ); + + return await Collection + .Find(filter) + .SortByDescending(x => x.Time) + .Limit(2) + .ToListAsync(); + } + + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/PumpStateRepository.cs b/adas-core.Infrastructure/Repositories/PumpStateRepository.cs new file mode 100644 index 00000000..c861d03b --- /dev/null +++ b/adas-core.Infrastructure/Repositories/PumpStateRepository.cs @@ -0,0 +1,79 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Pumps; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Infrastructure.Repositories +{ + public class PumpStateRepository : MongoRepository, IPumpStateRepository + { + private readonly ApiSettings _apiSettings; + + public PumpStateRepository(IOptions apiSettings, IMongoDatabase database) + : base(database) + { + _apiSettings = apiSettings.Value; + } + + + public override string GetCollectionName() + { + return _apiSettings.PumpStates ?? "pump_states"; + } + + public override async Task CreateIndexes() + { + var indexModels = new List> + { + // Clave única del snapshot + new CreateIndexModel( + Builders.IndexKeys.Ascending(x => x.DeviceId), + new CreateIndexOptions { Unique = true, Name = "ux_deviceId" }), + + // Consultas por DeviceId + actualización temporal + new CreateIndexModel( + Builders.IndexKeys + .Ascending(x => x.DeviceId) + .Descending(x => x.LastUpdated), + new CreateIndexOptions { Name = "ix_deviceId_lastUpdated" }), + + // indexado por PatientId + new CreateIndexModel( + Builders.IndexKeys.Ascending(x => x.PatientId), + new CreateIndexOptions { Name = "ix_patientId" }) + }; + + await Collection.Indexes.CreateManyAsync(indexModels); + } + + public async Task FindByDeviceIdAsync(string deviceId) + { + return await Collection.Find(x => x.DeviceId == deviceId).FirstOrDefaultAsync(); + } + + public async Task UpsertAsync(PumpState state) + { + var existing = await Collection + .Find(x => x.DeviceId == state.DeviceId) + .FirstOrDefaultAsync(); + + if (existing != null) + state.Id = existing.Id; + else + if (state.Id == ObjectId.Empty) + state.Id = ObjectId.GenerateNewId(); + + await Collection.ReplaceOneAsync( + x => x.DeviceId == state.DeviceId, + state, + new ReplaceOptions { IsUpsert = true }); + } + + public async Task> GetAllAsync() + { + return await Collection.Find(Builders.Filter.Empty).ToListAsync(); + } + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/RecordingAlertArchiveRepository.cs b/adas-core.Infrastructure/Repositories/RecordingAlertArchiveRepository.cs new file mode 100644 index 00000000..ca29109a --- /dev/null +++ b/adas-core.Infrastructure/Repositories/RecordingAlertArchiveRepository.cs @@ -0,0 +1,47 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using Microsoft.Extensions.Options; +using MongoDB.Driver; + +namespace adas_core.Infrastructure.Repositories; + +public class RecordingAlertArchiveRepository : MongoRepository, IRecordingAlertArchiveRepository +{ + private readonly ApiSettings _apiSettings; + + public RecordingAlertArchiveRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + } // To testing + + + + + public override string GetCollectionName() + { + return _apiSettings.ArchivePatientsRecordingalerts ?? "archive_patients_recordingalerts"; + } + + public override async Task InsertOneAsync(PatientRecordingAlert patientRecordingAlert) + { + await Collection.InsertOneAsync(patientRecordingAlert); + } + + public async Task DeleteBeforeDate(DateTime date) + { + var filter = Builders.Filter.Lt(po => po.Time, date); + await Collection.DeleteManyAsync(filter); + } + + public async Task InsertBatch(IEnumerable patientRecordingAlerts) + { + var writes = new List>(); + writes.AddRange(patientRecordingAlerts.Select(d => new InsertOneModel(d))); + + var bulkInsert = await Collection.BulkWriteAsync(writes); + + return bulkInsert.InsertedCount; + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/RecordingAlertRepository.cs b/adas-core.Infrastructure/Repositories/RecordingAlertRepository.cs new file mode 100644 index 00000000..74cf715d --- /dev/null +++ b/adas-core.Infrastructure/Repositories/RecordingAlertRepository.cs @@ -0,0 +1,211 @@ +using System.Diagnostics; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; +using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; + +namespace adas_core.Infrastructure.Repositories; + +public class RecordingAlertRepository : MongoRepository, IRecordingAlertRepository +{ + private readonly ApiSettings _apiSettings; + + public RecordingAlertRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + } //For testing + + + + public override string GetCollectionName() + { + return _apiSettings.PatientsRecordingAlerts ?? "patients_recordingalerts"; + } + + public async Task> AggregatedPatientLastObservations(ObjectId patientId, int num) + { + var match = new BsonDocument + { + { "patientid", patientId } + }; + var pipeline = new BsonDocument[] + { + new() + { + { + "$match", match + } + }, + new() + { + { + "$sort", new BsonDocument + { + { "codingSystem", 1 }, + { "code", 1 }, + { "time", -1 } + } + } + }, + new() + { + { + "$group", new BsonDocument + { + { + "_id", new BsonDocument + { + // { "codingSystem", "$codingSystem" } , + // { "code", "$code" } , + { "name", "$name" } + } + }, + { + "results", new BsonDocument + { + { "$push", "$$ROOT" } + } + } + } + } + }, + new() + { + { + "$project", new BsonDocument + { + { + "results", new BsonDocument + { + { "$slice", new BsonArray { "$results", num } } + } + } + } + } + } + }; + Debug.WriteLine(pipeline.ToJson()); + var result = + await Collection.AggregateAsync(pipeline, new AggregateOptions { AllowDiskUse = true }); + var obs = new List(); + result.ToList().ForEach(it => + { + foreach (var obit in it.GetValue("results").AsBsonArray) + { + var obsit = obit.AsBsonDocument; + var pobs = BsonSerializer.Deserialize(obsit); + pobs.PatientId = patientId; + obs.Add(pobs); + } + }); + return obs; + } + + public new async Task DeleteAsync(ObjectId id) + { + var filter = Builders.Filter.Eq(obs => obs.Id, id); + await Collection.DeleteOneAsync(filter); + } + + + public override async Task InsertOneAsync(PatientRecordingAlert patientRecordingAlert) + { + await Collection.InsertOneAsync(patientRecordingAlert); + } + + public async Task DeleteOlderDaysAsync(string name, int retentionPolicyValue) + { + var filterBuilder = Builders.Filter; + var filter = filterBuilder.And( + filterBuilder.Eq(obs => obs.Name, name), + filterBuilder.Lt(obs => obs.Time, DateTime.UtcNow.AddDays(-1 * retentionPolicyValue)) + ); + + await Collection.DeleteOneAsync(filter); + } + + + public async Task DeleteByPatientId(ObjectId patientId) + { + var filter = Builders.Filter.Eq(po => po.PatientId, patientId); + await Collection.DeleteManyAsync(filter); + } + + + public async Task DeleteOlderNumberAsync(string name, int retentionPolicyValue) + { + var filterBuilder = Builders.Filter; + var sortBuilder = Builders.Sort; + + var filter = filterBuilder.Eq(obs => obs.Name, name); + var projection = Builders.Projection.Include(obs => obs.Id).Include(obs => obs.Time); + var sort = sortBuilder.Descending("time"); + var options = new FindOptions + { + Projection = projection, + Sort = sort, + Skip = retentionPolicyValue + }; + + var result = await Collection.FindAsync(filter, options); + + await result.ForEachAsync(async obs => + { + var idFilter = filterBuilder.Eq(ob => ob.Id, obs.Id); + await Collection.DeleteOneAsync(idFilter); + }); + } + + + public async Task> FindByPatientIdAsync(ObjectId patientId) + { + var filter = Builders.Filter.Eq(ob => ob.PatientId, patientId); + return await Collection.FindAsync(filter); + } + + + public async Task> FindLastObservations(ObjectId patientId, string name, int num = 2) + { + var filterBuilder = Builders.Filter; + var sortBuilder = Builders.Sort; + + var filter = filterBuilder.And( + filterBuilder.Eq(ob => ob.PatientId, patientId), + filterBuilder.Eq(ob => ob.Name, name) + ); + + var sort = sortBuilder.Descending("time"); + + var options = new FindOptions + { + Sort = sort, + Limit = num + }; + + var result = await Collection.FindAsync(filter, options); + + return await result.ToListAsync(); + } + + + public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId) + { + await UpdateManyObjectIdAsync(nameId, id, oldId); + } + + public override async Task CreateIndexes() + { + var options = new CreateIndexOptions { Background = true, Unique = false }; + var indexes = new List> + { + new("{ patientid: 1 }", options) + }; + + await MongoUtils.EnsureIndexes(Collection, indexes); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/RelayRepository.cs b/adas-core.Infrastructure/Repositories/RelayRepository.cs new file mode 100644 index 00000000..ec9e0441 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/RelayRepository.cs @@ -0,0 +1,140 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Newtonsoft.Json; +using Serilog; +using System.Text.RegularExpressions; + +namespace adas_core.Infrastructure.Repositories; + +public class RelayRepository : MongoRepository, IRelayRepository +{ + private readonly ApiSettings _apiSettings; + + + + public RelayRepository(IMongoDatabase database, IOptions apiSettings) : base(database) + { + _apiSettings = apiSettings.Value; + } + + public override string GetCollectionName() + { + return _apiSettings.Relays; + } + + public async Task GetById(ObjectId relayId) + { + try + { + var filter = Builders.Filter.Eq(x => x.Id, relayId); + var result = await Collection.FindAsync(filter, null); + return result.FirstOrDefault(); + } + catch (Exception e) + { + Log.Error("Exception trying to get relay by id: {id}. Exception {e}", relayId, e); + return null; + } + } + + public List GetRelayByTypeInList(List configurationRelayList, RelayEnum.Type type) + { + var filterBuilder = Builders.Filter; + + var filter = filterBuilder.And( + filterBuilder.In(r => r.Id, configurationRelayList), + filterBuilder.Eq(r => r.Type, type) + ); + + return Collection.Find(filter).ToList(); + } + + public List GetRelayInList(List configurationRelayList) + { + var filterBuilder = Builders.Filter; + + var filter = filterBuilder.And( + filterBuilder.In(r => r.Id, configurationRelayList)); + + return Collection.Find(filter).ToList(); + } + + public IFindFluent GetPaginatedRelays(PaginationFilter filter) + { + var filterBuilder = Builders.Filter; + var sort = Builders.Sort.Ascending("relayName"); + var filters = new List>(); + + if (filter.FilteredRequest == null) return CreateFindFluent(filters, sort); + + if (!string.IsNullOrEmpty(filter.FilteredRequest?.Text)) + { + var textFilter = filter.FilteredRequest.Text; + var textFilterEscaped = Regex.Escape(textFilter); + + filters.Add( + filterBuilder.Or( + filterBuilder.Regex(p => p.RelayName, + new BsonRegularExpression(textFilterEscaped, "i")) + ) + ); + } + + return CreateFindFluent(filters, sort); + } + + public async Task InsertOneRelayAsync(Relay request) + { + try + { + await Collection.InsertOneAsync(request); + return await GetById(request.Id); + } + catch (Exception ex) + { + Log.Error("Error inserting relay: {relay}. Exception: {ex}", + JsonConvert.SerializeObject(request, Formatting.Indented), ex); + + return null; + } + } + + public async Task UpdateRelayAsync(ObjectId objectId, Relay relay) + { + var filter = Builders.Filter.Eq("_id", objectId); + var update = Builders.Update + .Set(c => c.Mode, relay.Mode) + .Set(c => c.RelayNumber, relay.RelayNumber) + .Set(c => c.Username, relay.Username) + .Set(c => c.Password, relay.Password) + .Set(c => c.Driver, relay.Driver) + .Set(c => c.Ip, relay.Ip) + .Set(c => c.Port, relay.Port) + .Set(c => c.RelayName, relay.RelayName); + return await Collection.FindOneAndUpdateAsync(filter, update, + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); + } + + public async Task GetByName(string? requestRelayName) + { + var filterBuilder = Builders.Filter; + + var filter = filterBuilder.Eq(r => r.RelayName, requestRelayName); + + return await Collection.Find(filter).FirstOrDefaultAsync(); + } + + private IFindFluent CreateFindFluent(List> filters, SortDefinition sort) + { + var combinedFilter = filters.Any() + ? Builders.Filter.And(filters) + : Builders.Filter.Empty; + return Collection.Find(combinedFilter).Sort(sort); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/SectionRepository.cs b/adas-core.Infrastructure/Repositories/SectionRepository.cs new file mode 100644 index 00000000..0bface48 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/SectionRepository.cs @@ -0,0 +1,107 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; +using MongoDB.Driver; +using Newtonsoft.Json; +using Serilog; + +namespace adas_core.Infrastructure.Repositories; + +public class SectionRepository : MongoRepository
, ISectionRepository +{ + private readonly ApiSettings _apiSettings; + + public SectionRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + } //For testing + + + + + public override string GetCollectionName() + { + return _apiSettings.ConfigSections ?? "config_sections"; + } + + public async Task> GetAll() + { + var result = await Collection.FindAsync(Builders
.Filter.Empty); + + return result.ToList(); + } + + public async Task FindBySection(string section) + { + var result = await Collection.FindAsync(Builders
.Filter.Eq(x => x.SectionTitle, section)); + + return await result.FirstOrDefaultAsync(); + } + + public async Task FindByPointOfCare(string pointOfCare) + { + var result = await Collection.FindAsync(Builders
.Filter.Eq(x => x.PointOfCare, pointOfCare)); + + return await result.FirstOrDefaultAsync(); + } + + public async Task FindById(string id) + { + var result = await Collection.FindAsync(Builders
.Filter.Eq(x => x.Id, id)); + + return await result.FirstOrDefaultAsync(); + } + + public async Task FindById(object id) + { + var result = await Collection.FindAsync(Builders
.Filter.Eq(x => x._id, id)); + return await result.FirstOrDefaultAsync(); + } + + public async Task> FindByLocation(PatientLocation location) + { + //can not filter to Collection the where condition, it throws System.InvalidOperationException: '{}.pointOfCare is not supported.' + var sections = await GetAll(); + + return sections.Where(section => + (section.PointOfCare == location.UnitName && section.Items.Any(item => + item.Boxes.Any(box => box.PointOfCare == null && box.Bed == location.Bed && box.IsActive))) || + section.Items.Any(item => + item.Boxes.Any(box => + box.PointOfCare == location.UnitName && box.Bed == location.Bed && box.IsActive))).ToList(); + } + + public async Task InsertOneSection(Section section) + { + try + { + await Collection.InsertOneAsync(section); + return await FindById(section.Id); + } + catch (Exception ex) + { + Log.Error("Error inserting section: {section}. Exception: {ex}", + JsonConvert.SerializeObject(section, Formatting.Indented), ex); + + return null; + } + } + + + public async Task UpdateSection(Section section) + { + var filter = Builders
.Filter.Eq("Id", section.Id); + var update = Builders
.Update + .Set(c => c.Id, section.Id) + .Set(c => c.PointOfCare, section.PointOfCare) + .Set(c => c.SectionTitle, section.SectionTitle) + .Set(c => c.Configuration, section.Configuration) + .Set(c => c.Items, section.Items); + + return await Collection.FindOneAndUpdateAsync(filter, update, + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/ServiceConfigRepository.cs b/adas-core.Infrastructure/Repositories/ServiceConfigRepository.cs new file mode 100644 index 00000000..78cd989f --- /dev/null +++ b/adas-core.Infrastructure/Repositories/ServiceConfigRepository.cs @@ -0,0 +1,40 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Infrastructure.Repositories; + +public class ServiceConfigRepository : MongoRepository, IServiceConfigRepository +{ + private readonly ApiSettings _apiSettings; + + public ServiceConfigRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + } //For testing + + + + public override string GetCollectionName() + { + return _apiSettings.ServiceConfig ?? "service_config"; + } + + public async Task FindById(string id) + { + var result = await Collection.FindAsync(Builders.Filter.Eq(x => x.StrId, id)); + + return await result.FirstOrDefaultAsync(); + } + + public async Task FindById(ObjectId oid) + { + var result = await Collection.FindAsync(Builders.Filter.Eq(x => x.Id, oid)); + + return await result.FirstOrDefaultAsync(); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/TreatmentArchiveRepository.cs b/adas-core.Infrastructure/Repositories/TreatmentArchiveRepository.cs new file mode 100644 index 00000000..aacd24ad --- /dev/null +++ b/adas-core.Infrastructure/Repositories/TreatmentArchiveRepository.cs @@ -0,0 +1,55 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Infrastructure.Repositories; + +public class TreatmentArchiveRepository : MongoRepository, ITreatmentArchiveRepository +{ + private readonly ApiSettings _apiSettings; + + public TreatmentArchiveRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + } //For testing + + + + public override async Task InsertOneAsync(PatientTreatment patientTreatment) + { + await Collection.InsertOneAsync(patientTreatment); + } + + public async Task DeleteBeforeDate(DateTime date) + { + var filter = Builders.Filter.Lt(po => po.OrderTime, date); + await Collection.DeleteManyAsync(filter); + } + + public async Task InsertBatch(IEnumerable treatments) + { + var writes = new List>(); + writes.AddRange(treatments.Select(d => new InsertOneModel(d))); + + var bulkInsert = await Collection.BulkWriteAsync(writes); + + return bulkInsert.InsertedCount; + } + + public async Task> FindAllFromPatient(ObjectId patientId) + { + var filter = Builders.Filter.Eq(t => t.PatientId, patientId); + var result = await Collection.FindAsync(filter); + + return await result.ToListAsync(); + } + + public override string GetCollectionName() + { + return _apiSettings.ArchivePatientsTreatments ?? "archive_patients_treatments"; + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/TreatmentRepository.cs b/adas-core.Infrastructure/Repositories/TreatmentRepository.cs new file mode 100644 index 00000000..e2df1686 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/TreatmentRepository.cs @@ -0,0 +1,246 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Filter; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Serilog; +using MongoUtils = adas_core.Infrastructure.Utils.MongoUtils; + +namespace adas_core.Infrastructure.Repositories; + +public class TreatmentRepository : MongoRepository, ITreatmentRepository +{ + private readonly ApiSettings _apiSettings; + + + public TreatmentRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + } //For testing + + + public override string GetCollectionName() + { + return _apiSettings.PatientsTreatments ?? "patients_treatments"; + } + + public async Task> GetByPatientId(ObjectId patientId) + { + var filter = Builders.Filter.Eq(ob => ob.PatientId, patientId); + var result = await Collection.FindAsync(filter); + return result.ToEnumerable(); + } + + public async Task> GetById(ObjectId id) + { + var filter = Builders.Filter.Eq(ob => ob.Id, id); + var result = await Collection.FindAsync(filter); + return result; + } + + public override async Task InsertOneAsync(PatientTreatment treatment) + { + treatment.OrderTime ??= DateTime.UtcNow; + await base.InsertOneAsync(treatment); + } + + public new async Task DeleteAsync(ObjectId id) + { + var filter = Builders.Filter.Eq(ob => ob.Id, id); + await Collection.DeleteOneAsync(filter); + } + + + public async Task> FindByPatientIdAsync(ObjectId patientId) + { + var filter = Builders.Filter.Eq(ob => ob.PatientId, patientId); + return await Collection.FindAsync(filter); + } + + + public async Task DeleteByPatientId(ObjectId patientId) + { + try + { + var filter = Builders.Filter.Eq(po => po.PatientId, patientId); + await Collection.DeleteManyAsync(filter); + return true; + } + catch (Exception ex) + { + Log.Error(ex.ToString()); + return false; + } + } + + public async Task Update(PatientTreatment treatment) + { + try + { + await UpdateOneAsync(treatment.Id, treatment); + return true; + } + catch (Exception ex) + { + Log.Error(ex.ToString()); + return false; + } + } + + public async Task> FindBolusTreatments(ObjectId patientId) + { + var builder = Builders.Filter; + var filter = builder.And( + builder.Eq(t => t.PatientId, patientId), + builder.Exists(t => t.RequestedGiveCodesStatus), + builder.SizeGt(t => t.RequestedGiveCodesStatus, 0) + ); + + var result = await Collection.FindAsync(filter); + + return await result.ToListAsync(); + } + + public async Task> GetActiveTreatmentsByPatientIdAndOrder(ObjectId patientId, string order) + { + var builder = Builders.Filter; + var filter = builder.And( + builder.Or( + builder.Eq(t => t.OrderControl, OrderControlType.Nw), + builder.Eq(t => t.OrderControl, OrderControlType.Xo) + ), + builder.Eq(t => t.PatientId, patientId), + builder.And( + builder.Ne(t => t.PlacerOrder, null), // Verifica que no sea nulo + builder.Eq(t => t.PlacerOrder!.EntityIdentifier, order) + ) + ); + + var result = await Collection.FindAsync(filter); + + return await result.ToListAsync(); + } + + + public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId) + { + await UpdateManyObjectIdAsync(nameId, id, oldId); + } + + public async Task> FindByPatientId(ObjectId patientId) + { + var filter = Builders.Filter.Eq(ob => ob.PatientId, patientId); + var result = await Collection.FindAsync(filter); + return result.ToEnumerable(); + } + + public IFindFluent GetPaginatedTreatments(PaginationFilter filter) + { + var filterBuilder = Builders.Filter; + var sort = Builders.Sort.Descending("orderTime"); + var filters = new List>(); + + if (filter.FilteredRequest == null) + { + AddDefaultTimeFilters(filters, filter, filterBuilder); + return CreateFindFluent(filters, sort); + } + + var requestFilter = filter.FilteredRequest; + if (requestFilter.PatientId != null && ObjectId.TryParse(requestFilter.PatientId, out var patientObjectId)) + filters.Add(filterBuilder.Eq(t => t.PatientId, patientObjectId)); + + if (requestFilter.StartDate != null) filters.Add(filterBuilder.Gt(t => t.OrderTime, requestFilter.StartDate)); + + if (requestFilter.EndDate != null) filters.Add(filterBuilder.Lt(t => t.OrderTime, requestFilter.EndDate)); + + if (string.IsNullOrWhiteSpace(requestFilter.Text)) + { + //TODO falta definir la búsqueda por texto + } + + if (requestFilter.ActiveTreatments) + AddActiveTreatmentFilters(filters, filterBuilder); + else + AddDefaultTimeFilters(filters, filter, filterBuilder); + + return CreateFindFluent(filters, sort); + } + + public override async Task CreateIndexes() + { + var options = new CreateIndexOptions { Background = true, Unique = false }; + var indexes = new List> + { + new("{ patientid: 1 }", options) + }; + + await MongoUtils.EnsureIndexes(Collection, indexes); + } + + private static void AddActiveTreatmentFilters(List> filters, + FilterDefinitionBuilder filterBuilder) + { + var currentTime = DateTime.UtcNow; + + filters.Add( + filterBuilder.And( + filterBuilder.Ne(t => t.PlacerOrder, null), + filterBuilder.Ne(t => t.PlacerOrder!.EntityIdentifier, null) + ) + ); + + filters.Add( + filterBuilder.Or( + filterBuilder.Eq(t => t.StartTime, null), + filterBuilder.Lte(t => t.StartTime, currentTime) + ) + ); + + filters.Add( + filterBuilder.Or( + filterBuilder.Eq(t => t.EndTime, null), + filterBuilder.Gte(t => t.EndTime, currentTime) + ) + ); + + filters.Add(filterBuilder.Ne(t => t.OrderControl, OrderControlType.Dc)); + filters.Add( + filterBuilder.Or( + filterBuilder.Eq(t => t.OrderControl, OrderControlType.Nw), + filterBuilder.Eq(t => t.OrderControl, OrderControlType.Xo) + ) + ); + } + + private void AddDefaultTimeFilters(List> filters, PaginationFilter filter, + FilterDefinitionBuilder filterBuilder) + { + filters.Add( + filterBuilder.Or( + filterBuilder.Eq(t => t.StartTime, null), + filterBuilder.Gt(p => p.StartTime, filter.FilteredRequest?.StartDate ?? DateTime.MinValue) + ) + ); + + filters.Add( + filterBuilder.Or( + filterBuilder.Eq(t => t.EndTime, null), + filterBuilder.Lt(p => p.EndTime, filter.FilteredRequest?.EndDate ?? DateTime.MaxValue) + ) + ); + } + + private IFindFluent CreateFindFluent( + List> filters, SortDefinition sort) + { + var combinedFilter = filters.Any() + ? Builders.Filter.And(filters) + : Builders.Filter.Empty; // Filtra todo si no hay filtros + return Collection.Find(combinedFilter).Sort(sort); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/UnitRepository.cs b/adas-core.Infrastructure/Repositories/UnitRepository.cs new file mode 100644 index 00000000..676c5d75 --- /dev/null +++ b/adas-core.Infrastructure/Repositories/UnitRepository.cs @@ -0,0 +1,402 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Newtonsoft.Json; +using Serilog; +using System.Text.RegularExpressions; + +namespace adas_core.Infrastructure.Repositories; + +public class UnitRepository : MongoRepository, IUnitRepository +{ + #region Properties + + private readonly ApiSettings _apiSettings; + + #endregion + + #region Constructor + + + + public UnitRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + _apiSettings = apiSettings.Value; + } + + #endregion + + #region Methods + + #region Create + + public async Task InsertOneUnit(Unit unit) + { + try + { + await Collection.InsertOneAsync(unit); + return await FindById(unit.Id); + } + catch (Exception ex) + { + Log.Error("Error inserting Unit: {unit}. Exception: {ex}", + JsonConvert.SerializeObject(unit, Formatting.Indented), ex); + + return null; + } + } + + #endregion + + #region Read + + public override string GetCollectionName() + { + return _apiSettings.Units ?? "units"; + } + + // public async Task FindByLocation(PatientLocation location) + // { + // var filter = Builders.Filter.ElemMatch(x => x.PointOfCares, poc => poc.Bed == location.Bed && poc.UnitName == location.UnitName); + // var result = await Collection.Find(filter).FirstOrDefaultAsync(); + // + // return result; + // } + + public async Task FindById(object id) + { + var result = await Collection.FindAsync(Builders.Filter.Eq(x => x.Id, id)); + return await result.FirstOrDefaultAsync(); + } + + public Task FindById(string id) + { + throw new NotImplementedException(); + } + + public async Task> FindByMasterListId(ObjectId id, MasterListType masterListType) + { + try + { + var propertyName = $"{masterListType}Id"; // nombre de la propiedad dinámicamente + var filter = Builders.Filter.Eq(propertyName, id); + + + var result = await Collection.Find(filter).ToListAsync(); + + return result; + } + catch (Exception ex) + { + Log.Error("Error searching unit by {masterlisttype} Id {id}. Exception: {ex}", masterListType.ToString(), + id, ex); + + throw; + } + } + + public async Task> FindByMasterListId(ObjectId id) + { + try + { + var filterBuilder = Builders.Filter; + var filters = new List> + { + filterBuilder.Or( + filterBuilder.Eq(p => p.DoctorListId, id), + filterBuilder.Eq(p => p.AllergyListId, id), + filterBuilder.Eq(p => p.DestinationListId, id), + filterBuilder.Eq(p => p.DiagnosisListId, id), + filterBuilder.Eq(p => p.InsulationListId, id), + filterBuilder.Eq(p => p.OriginListId, id), + filterBuilder.Eq(p => p.ProcedureListId, id), + filterBuilder.Eq(p => p.TestListId, id), + filterBuilder.Eq(p => p.ServiceListId, id), + filterBuilder.Eq(p => p.TreatmentListId, id), + filterBuilder.Eq(p => p.LanguageBarrierListId, id), + filterBuilder.Eq(p => p.AltableOptionListId, id), + filterBuilder.Eq(p => p.DischargeStatusListId, id), + filterBuilder.Eq(p => p.DoctorTypeListId, id), + filterBuilder.Eq(p => p.InternalDestinationListId, id), + filterBuilder.Eq(p => p.PassiveSittingListId, id), + filterBuilder.Eq(p => p.GenericListId, id), + filterBuilder.Eq(p => p.VisitOptionListId, id), + filterBuilder.Eq(p => p.AccessControlListId, id), + filterBuilder.Eq(p => p.TherapeuticCeilingListId, id), + filterBuilder.Eq(p => p.MobilityOptionListId, id) + ) + }; + + + var result = await Collection.Find(Builders.Filter.And(filters)).ToListAsync(); + + return result; + } + catch (Exception ex) + { + Log.Error("Error searching unit by masterlist Id {id}. Exception: {ex}", id.ToString(), ex); + + return new List(); + } + } + + public async Task CountUnitsByMasterListId(ObjectId id, MasterListType masterListType) + { + var propertyName = $"{masterListType}Id"; + var filter = Builders.Filter.Eq(propertyName, id); + var result = await Collection.CountDocumentsAsync(filter); + return result; + } + + public async Task FindByName(string unitName) + { + var result = await Collection.FindAsync(Builders.Filter.Eq(x => x.Name, unitName)); + + return await result.FirstOrDefaultAsync(); + } + + // public async Task> FindByPointOfCare(PointOfCare pointOfCare) + // { + // var filter = Builders.Filter.ElemMatch(x => x.PointOfCares, poc => poc.Bed == pointOfCare.Bed && poc.Room == pointOfCare.Room && pointOfCare.unitName == poc.unitName); + // var result = await Collection.Find(filter).ToListAsync(); + // + // return result; + // } + // public async Task FindByPointOfCare(PointOfCare pointOfCare) + // { + // var filter = Builders.Filter.ElemMatch(x => x.PointOfCares, poc => poc.Bed == pointOfCare.Bed && poc.Room == pointOfCare.Room && pointOfCare.UnitName == poc.UnitName); + // var result = await Collection.Find(filter).FirstOrDefaultAsync(); + // + // return result; + // } + // public async Task> FindByUnitName(string unitName) + // { + // var filter = Builders.Filter.ElemMatch(x => x.PointOfCares, poc => poc.UnitName == unitName); + // var result = await Collection.Find(filter).ToListAsync(); + // + // return result; + // } + public async Task> GetAll() + { + var result = await Collection.FindAsync(Builders.Filter.Empty); + + return result.ToList(); + } + + + public IFindFluent GetPaginatedUnits(PaginationFilter filter) + { + var filterBuilder = Builders.Filter; + var sort = Builders.Sort.Ascending("title"); + var filters = new List>(); + + if (filter.FilteredRequest == null) return CreateFindFluent(filters, sort); + + if (!string.IsNullOrEmpty(filter.FilteredRequest?.Text)) + { + var textFilter = filter.FilteredRequest.Text; + var textFilterEscaped = Regex.Escape(textFilter); + + filters.Add( + filterBuilder.Or( + filterBuilder.Regex(p => p.Name, + new BsonRegularExpression(textFilterEscaped, "i")), // Case-insensitive regex match for name + filterBuilder.Regex(p => p.Title, + new BsonRegularExpression(textFilterEscaped, "i")) // Case-insensitive regex match for title + ) + ); + } + + return CreateFindFluent(filters, sort); + } + + private IFindFluent CreateFindFluent(List> filters, SortDefinition sort) + { + var combinedFilter = filters.Any() + ? Builders.Filter.And(filters) + : Builders.Filter.Empty; + return Collection.Find(combinedFilter).Sort(sort); + } + + #endregion + + #region Update + + public async Task UpdateUnit(Unit unit) + { + var filter = Builders.Filter.Eq("_id", unit.Id); + var update = Builders.Update + //.Set(c => c.Id, unit.Id) + .Set(c => c.Title, unit.Title) + .Set(c => c.Name, unit.Name) + .Set(c => c.Configuration, unit.Configuration) + .Set(c => c.AllergyListId, unit.AllergyListId) + .Set(c => c.DestinationListId, unit.DestinationListId) + .Set(c => c.InternalDestinationListId, unit.InternalDestinationListId) + .Set(c => c.DiagnosisListId, unit.DiagnosisListId) + .Set(c => c.DoctorListId, unit.DoctorListId) + .Set(c => c.DoctorTypeListId, unit.DoctorTypeListId) + .Set(c => c.InsulationListId, unit.InsulationListId) + .Set(c => c.MobilityOptionListId, unit.MobilityOptionListId) + .Set(c => c.OriginListId, unit.OriginListId) + .Set(c => c.PatientStatusListId, unit.PatientStatusListId) + .Set(c => c.ProcedureListId, unit.ProcedureListId) + .Set(c => c.TestListId, unit.TestListId) + .Set(c => c.ServiceListId, unit.ServiceListId) + .Set(c => c.TherapeuticCeilingListId, unit.TherapeuticCeilingListId) + .Set(c => c.TreatmentListId, unit.TreatmentListId) + .Set(c => c.VisitOptionListId, unit.VisitOptionListId) + .Set(c => c.AccessControlListId, unit.AccessControlListId) + .Set(c => c.DischargeStatusListId, unit.DischargeStatusListId); + + return await Collection.FindOneAndUpdateAsync(filter, update, + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); + } + + public async Task UpdateUnitInfo(ObjectId unitId, string name, string title) + { + var filter = Builders.Filter.Eq("_id", unitId); + var update = Builders.Update + //.Set(c => c.Id, unit.Id) + .Set(c => c.Title, title) + .Set(c => c.Name, name); + + + return await Collection.FindOneAndUpdateAsync(filter, update, + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); + } + + public async Task UpdateUnitMasterList(UpdateUnitIdListDto updateUnitListDto) + { + var filter = Builders.Filter.Eq("_id", updateUnitListDto.UnitId); + var update = Builders.Update; + var updates = new List>(); + + foreach (var masterListData in updateUnitListDto.MasterListData) + if (masterListData.MasterListType.HasValue) + switch (masterListData.MasterListType.Value) + { + case MasterListType.AltableOptionList: + updates.Add(update.Set(u => u.AltableOptionListId, masterListData.MasterListId)); + break; + case MasterListType.AllergyList: + updates.Add(update.Set(u => u.AllergyListId, masterListData.MasterListId)); + break; + case MasterListType.DestinationList: + updates.Add(update.Set(u => u.DestinationListId, masterListData.MasterListId)); + break; + case MasterListType.DiagnosisList: + updates.Add(update.Set(u => u.DiagnosisListId, masterListData.MasterListId)); + break; + case MasterListType.DischargeStatusList: + updates.Add(update.Set(u => u.DischargeStatusListId, masterListData.MasterListId)); + break; + case MasterListType.DoctorList: + updates.Add(update.Set(u => u.DoctorListId, masterListData.MasterListId)); + break; + case MasterListType.DoctorTypeList: + updates.Add(update.Set(u => u.DoctorTypeListId, masterListData.MasterListId)); + break; + case MasterListType.InternalDestinationList: + updates.Add(update.Set(u => u.InternalDestinationListId, masterListData.MasterListId)); + break; + case MasterListType.InsulationList: + updates.Add(update.Set(u => u.InsulationListId, masterListData.MasterListId)); + break; + case MasterListType.LanguageBarrierList: + updates.Add(update.Set(u => u.LanguageBarrierListId, masterListData.MasterListId)); + break; + case MasterListType.PassiveSittingList: + updates.Add(update.Set(u => u.PassiveSittingListId, masterListData.MasterListId)); + break; + case MasterListType.GenericList: + updates.Add(update.Set(u => u.GenericListId, masterListData.MasterListId)); + break; + case MasterListType.MobilityOptionList: + updates.Add(update.Set(u => u.MobilityOptionListId, masterListData.MasterListId)); + break; + case MasterListType.OriginList: + updates.Add(update.Set(u => u.OriginListId, masterListData.MasterListId)); + break; + case MasterListType.PatientStatusList: + updates.Add(update.Set(u => u.PatientStatusListId, masterListData.MasterListId)); + break; + case MasterListType.ProcedureList: + updates.Add(update.Set(u => u.ProcedureListId, masterListData.MasterListId)); + break; + case MasterListType.TestList: + updates.Add(update.Set(u => u.TestListId, masterListData.MasterListId)); + break; + case MasterListType.ServiceList: + updates.Add(update.Set(u => u.ServiceListId, masterListData.MasterListId)); + break; + case MasterListType.TherapeuticCeilingList: + updates.Add(update.Set(u => u.TherapeuticCeilingListId, masterListData.MasterListId)); + break; + case MasterListType.TreatmentList: + updates.Add(update.Set(u => u.TreatmentListId, masterListData.MasterListId)); + break; + case MasterListType.VisitOptionList: + updates.Add(update.Set(u => u.VisitOptionListId, masterListData.MasterListId)); + break; + case MasterListType.AccessControlList: + updates.Add(update.Set(u => u.AccessControlListId, masterListData.MasterListId)); + break; + } + + if (updates.Any()) + { + var combinedUpdate = update.Combine(updates); + return await Collection.FindOneAndUpdateAsync(filter, combinedUpdate, + new FindOneAndUpdateOptions { ReturnDocument = ReturnDocument.After }); + } + + return null; + } + + public async Task UpdateConfiguration(ObjectId unitIdParsed, UnitConfiguration unitConfiguration) + { + var filter = Builders.Filter.Eq("_id", unitIdParsed); + var update = Builders.Update + .Set(c => c.Configuration, unitConfiguration); + try + { + var result = await Collection.UpdateOneAsync(filter, update); + return result.ModifiedCount > 0; + } + catch (Exception ex) + { + Log.Error("Error UpdateConfiguration from unit: {name}. Exception: {ex}", unitIdParsed, ex); + return false; + } + } + + #endregion + + #region Delete + + public new async Task DeleteAsync(ObjectId id) + { + var filter = Builders.Filter.Eq(unit => unit.Id, id); + + try + { + return await Collection.FindOneAndDeleteAsync(filter); + } + catch (Exception e) + { + Log.Error(e.Message); + return null; + } + } + + #endregion + + #endregion +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Repositories/UserRepository.cs b/adas-core.Infrastructure/Repositories/UserRepository.cs new file mode 100644 index 00000000..1ff4850c --- /dev/null +++ b/adas-core.Infrastructure/Repositories/UserRepository.cs @@ -0,0 +1,240 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using System.Text.RegularExpressions; + +namespace adas_core.Infrastructure.Repositories; + +public class UserRepository : MongoRepository, IUserRepository +{ + private readonly ApiSettings _apiSettings; + + public UserRepository(IOptions apiSettings, IMongoDatabase database) : base(database) + { + if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings)); + _apiSettings = apiSettings.Value; + } + + public override string GetCollectionName() + { + return _apiSettings.Users; + } + + public async Task GetUser(string username, string password) + { + var filter = Builders + .Filter.Eq(p => p.UserName, username) & Builders + .Filter.Eq(p => p.Password, password); + var result = await Collection.FindAsync(filter); + return await result.FirstOrDefaultAsync(); + } + + public async Task GetById(ObjectId id) + { + var filter = Builders + .Filter.Eq(p => p.Id, id); + var result = await Collection.FindAsync(filter); + return await result.FirstOrDefaultAsync(); + } + + public async Task GetByUserName(string name) + { + var filter = Builders + .Filter.Eq(p => p.UserName, name); + var result = await Collection.FindAsync(filter); + return await result.FirstOrDefaultAsync(); + } + + public async Task GetByUserAndAuthoritesName(string name) + { + var matchStage = new BsonDocument("$match", new BsonDocument("userName", name)); + var lookupStage = new BsonDocument("$lookup", new BsonDocument + { + { "from", "authorizations" }, + { "localField", "_id" }, + { "foreignField", "userId" }, + { "as", "Authorization" } + }); + + var projectStage = new BsonDocument("$project", new BsonDocument + { + { "_id", 1 }, + { "userName", 1 }, + { "email", 1 }, + { "name", 1 }, + { "Authorization", "$Authorization" }, + { + "rol", new BsonDocument("$cond", new BsonArray + { + new BsonDocument("$eq", new BsonArray { "$Authorization.rol", "Admin" }), + "$rol", + "Some" + }) + } + }); + + var pipeline = new[] + { + matchStage, + lookupStage, + projectStage + }; + + var options = new AggregateOptions { AllowDiskUse = true }; + var result = await Collection.AggregateAsync(pipeline, options); + var bsonResult = await result.FirstOrDefaultAsync(); + + return bsonResult; + } + + public async Task GetByName(string name) + { + var filter = Builders + .Filter.Eq(p => p.Name, name); + var result = await Collection.FindAsync(filter); + return await result.FirstOrDefaultAsync(); + } + + public async Task UpdateUser(User user, bool updatePass) + { + var filter = Builders + .Filter.Eq(p => p.Id, user.Id); + var update = Builders.Update + .Set(u => u.UserName, user.UserName) + .Set(u => u.Name, user.Name) + .Set(u => u.Email, user.Email) + .Set(u => u.LockExpirationDate, user.LockExpirationDate) + .Set(u => u.LastLogin, user.LastLogin) + .Set(u => u.IsEnabled, user.IsEnabled); + + if (updatePass) update = update.Set(u => u.Password, user.Password); + + await Collection.UpdateOneAsync(filter, update); + var result = await Collection.Find(filter).FirstOrDefaultAsync(); + return result; + } + + public IFindFluent GetPaginatedUsers(PaginationFilter filteredRequest) + { + // Crear variable con la clase que construye los filtros que necesitamos + var filterBuilder = Builders.Filter; + var sort = Builders.Sort.Ascending("userName"); + // Crear una lista de filtros que pueden venir de tu servicio + var filters = new List>(); + if (filteredRequest.FilteredRequest == null) return CreateFindFluent(filters, sort); + var textFilter = filteredRequest.FilteredRequest?.Text; + if (!string.IsNullOrEmpty(textFilter)) + { + var textFilterEscaped = Regex.Escape(textFilter); + var orFilters = new List> + { + filterBuilder.Regex(p => p.Name, new BsonRegularExpression(textFilterEscaped, "i")), + filterBuilder.Regex(p => p.Email, new BsonRegularExpression(textFilterEscaped, "i")), + filterBuilder.Regex(p => p.UserName, new BsonRegularExpression(textFilterEscaped, "i")) + }; + filters.Add(filterBuilder.Or(orFilters)); + } + + var userStatus = filteredRequest.FilteredRequest?.UserStatus; + filters.Add(filterBuilder.And(GetUserStatusFilter(userStatus))); + + + if (Enum.TryParse(filteredRequest.FilteredRequest?.UserType, out UserEnum.Type userType)) + { + GetUserTypeFilter(userType); + filters.Add(filterBuilder.And(GetUserTypeFilter(userType))); + } + + return CreateFindFluent(filters, sort); + } + + + public async Task GetOrCreateSystemUser() + { + var user = await GetByUserName("System"); + if (user == null) + { + var userToInsert = new User + { + UserName = "System", + Name = "System", + Password = "$2a$12$crWa3EN1izcZBXNc81RzmOlfaYW2TPr2NdDQEWI7RzLTnA0Dd68WG" + }; + await Collection.InsertOneAsync(userToInsert); + return userToInsert; + } + + return user; + } + + public sealed override async Task InsertInitialLoad() + { + await GetOrCreateSystemUser(); + } + + private IFindFluent CreateFindFluent(List> filters, SortDefinition sort) + { + var combinedFilter = filters.Any() + ? Builders.Filter.And(filters) + : Builders.Filter.Empty; // Filtra todo si no hay filtros + + return Collection.Find(combinedFilter).Sort(sort); + } + + private List> GetUserTypeFilter(UserEnum.Type? type) + { + var filters = new List>(); + var filterBuilder = Builders.Filter; + + switch (type) + { + case UserEnum.Type.Local: + filters.Add(filterBuilder.Eq(u => u.Type, UserEnum.Type.Local)); + break; + case UserEnum.Type.Ldap: + filters.Add(filterBuilder.Eq(u => u.Type, UserEnum.Type.Ldap)); + break; + } + + return filters; + } + + private List> GetUserStatusFilter(StatusEnum.User? status) + { + var filters = new List>(); + var filterBuilder = Builders.Filter; + switch (status) + { + case StatusEnum.User.Enabled: + filters.Add(filterBuilder.Or( + filterBuilder.Eq(u => u.IsEnabled, true), + filterBuilder.Exists(u => u.IsEnabled, false) + )); + break; + case StatusEnum.User.EnabledUnlocked: + filters.Add(filterBuilder.Or( + filterBuilder.Eq(u => u.IsEnabled, true), + filterBuilder.Exists(u => u.IsEnabled, false) + )); + filters.Add(filterBuilder.Ne(u => u.LockExpirationDate, null)); + break; + case StatusEnum.User.EnabledLocked: + filters.Add(filterBuilder.Or( + filterBuilder.Eq(u => u.IsEnabled, true), + filterBuilder.Exists(u => u.IsEnabled, false) + )); + filters.Add(filterBuilder.Eq(u => u.LockExpirationDate, null)); + break; + case StatusEnum.User.Disabled: + filters.Add(filterBuilder.Eq(u => u.IsEnabled, false)); + break; + } + + return filters; + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Services/PublisherService.cs b/adas-core.Infrastructure/Services/PublisherService.cs new file mode 100644 index 00000000..c02036a3 --- /dev/null +++ b/adas-core.Infrastructure/Services/PublisherService.cs @@ -0,0 +1,126 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models.AppSettings; +using EasyNetQ; +using EasyNetQ.SystemMessages; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace adas_core.Infrastructure.Services; + +public class PublisherService : IPublisherService +{ + private readonly ILogger _logger; + private readonly HashSet _queues = new(); + private readonly IBus? _bus; + + public PublisherService( + IOptions rabbitMqSettings, + ILogger logger) + { + _logger = logger; + + try + { + var connection = rabbitMqSettings.Value.ConnectionString; + + if (string.IsNullOrWhiteSpace(connection)) + { + _logger.LogError("Missing RabbitMQ connection string"); + return; + } + + var services = new ServiceCollection(); + services.AddEasyNetQ(connection); + + var provider = services.BuildServiceProvider(); + _bus = provider.GetRequiredService(); + + _logger.LogInformation("PublisherService initialized"); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error initializing PublisherService"); + } + } + + public Task CreateQueue(string queueName) + { + if (string.IsNullOrWhiteSpace(queueName)) + return Task.FromResult(false); + + if (_queues.Contains(queueName)) + { + _logger.LogDebug("Queue {queueName} already registered", queueName); + return Task.FromResult(true); + } + + _queues.Add(queueName); + + _logger.LogInformation("Queue registered: {queueName}", queueName); + + // EasyNetQ crea colas automáticamente + return Task.FromResult(true); + } + + public async Task SendMessage(string msg, string queueName) + { + try + { + if (_bus == null) + { + _logger.LogError("Bus is null"); + return false; + } + + await CreateQueue(queueName); + + _logger.LogDebug("Sending message to {queueName}", queueName); + + await _bus.SendReceive.SendAsync(queueName, msg); + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending message to {queueName}", queueName); + return false; + } + } + + public async Task SendMessage(object obj, string queueName) + { + var json = JsonConvert.SerializeObject(obj); + return await SendMessage(json, queueName); + } + + public async Task SendMessageError(object obj, string queueName) + { + try + { + if (_bus == null) + { + _logger.LogError("Bus is null - cannot send error message"); + return false; + } + + if (obj is not Message errorMessage) + return false; + + await CreateQueue(queueName); + + _logger.LogDebug("Sending error message to {queueName}", queueName); + + // enviamos solo el Error (no Message) + await _bus.SendReceive.SendAsync(queueName, errorMessage.Body); + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error sending error message"); + return false; + } + } +} diff --git a/adas-core.Infrastructure/Services/ReceiverService.cs b/adas-core.Infrastructure/Services/ReceiverService.cs new file mode 100644 index 00000000..2dc0c223 --- /dev/null +++ b/adas-core.Infrastructure/Services/ReceiverService.cs @@ -0,0 +1,239 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using EasyNetQ; +using EasyNetQ.SystemMessages; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Newtonsoft.Json; + +namespace adas_core.Infrastructure.Services; + +public class ReceiverService +{ + private readonly ILogger _logger; + private readonly RabbitMqSettings _settings; + + private readonly IObservationService _observationService; + private readonly ITreatmentService _treatmentService; + private readonly IPatientService _patientsService; + private readonly IPumpService _pumpService; + private readonly IRecordingAlertService _recordingAlertService; + private readonly IRecordingService _recordingService; + private readonly IAppointmentService _appointmentService; + private readonly IAlarmService _alarmService; + + private readonly List _queues = []; + + private IBus? _bus; + + public ReceiverService( + IObservationService observationService, + ITreatmentService treatmentService, + IPatientService patientsService, + IPumpService pumpService, + IRecordingAlertService recordingAlertService, + IRecordingService recordingService, + IAppointmentService appointmentService, + IAlarmService alarmService, + IOptions settings, + ILogger logger) + { + _logger = logger; + _settings = settings.Value; + + _observationService = observationService; + _treatmentService = treatmentService; + _patientsService = patientsService; + _pumpService = pumpService; + _recordingAlertService = recordingAlertService; + _recordingService = recordingService; + _appointmentService = appointmentService; + _alarmService = alarmService; + + if (!string.IsNullOrWhiteSpace(_settings.ConnectionString)) + { + SetQueues(); + TryToConnect(); + } + else + { + _logger.LogError("RabbitMQ connection string NOT configured"); + } + } + + private void SetQueues() + { + AddQueue(_settings.ObservationsQueue); + AddQueue(_settings.TreatmentsQueue); + AddQueue(_settings.PatientsQueue); + AddQueue(_settings.PumpsQueue); + AddQueue(_settings.AppointmentsQueue); + AddQueue(_settings.RecordingQueue); + AddQueue(_settings.RecordingAlertQueue); + AddQueue(_settings.AlarmObservationQueue); + } + + private void AddQueue(string? queue) + { + if (!string.IsNullOrWhiteSpace(queue)) + _queues.Add(queue!); + } + + private void TryToConnect() + { + try + { + DisposeBus(); + + var services = new ServiceCollection(); + services.AddEasyNetQ(_settings.ConnectionString); + var provider = services.BuildServiceProvider(); + + _bus = provider.GetRequiredService(); + + + foreach (var queue in _queues) + { + RegisterConsumer(queue); + } + + _logger.LogInformation("RabbitMQ connected. Registered {Count} queues", _queues.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "RabbitMQ connection failed. Retrying in 5 seconds..."); + + var timer = new System.Timers.Timer(5000); + timer.Elapsed += (_, _) => + { + timer.Stop(); + TryToConnect(); + }; + timer.Start(); + } + } + + private void DisposeBus() + { + if (_bus is IDisposable disposable) + { + disposable.Dispose(); + } + } + + private void RegisterConsumer(string queue) + { + _bus!.SendReceive.ReceiveAsync(queue, async payload => + { + try + { + _logger.LogDebug("Message received from {queue}", queue); + + var service = ResolveService(queue); + + if (service == null) + { + _logger.LogWarning("No service mapped for queue {queue}", queue); + return; + } + + var message = new Message( + payload, + new MessageProperties() + ); + + await Task.Run(() => TryToParseAndSend(message, service)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing message from {queue}", queue); + throw; // 🔴 importante: activa retry + } + }); + } + + private IApiRequestService? ResolveService(string queue) + { + return queue switch + { + var q when q == _settings.ObservationsQueue => _observationService, + var q when q == _settings.TreatmentsQueue => _treatmentService, + var q when q == _settings.PatientsQueue => _patientsService, + var q when q == _settings.PumpsQueue => _pumpService, + var q when q == _settings.RecordingQueue => _recordingService, + var q when q == _settings.RecordingAlertQueue => _recordingAlertService, + var q when q == _settings.AlarmObservationQueue => _alarmService, + var q when q == _settings.AppointmentsQueue => _appointmentService, + _ => null + }; + } + + public async Task ProcessErrorQueue(string errorQueueName) + { + if (_bus == null) + { + _logger.LogError("Cannot process error queue - bus is null"); + return; + } + + await _bus.SendReceive.ReceiveAsync(errorQueueName, async err => + { + try + { + var queue = err.RoutingKey?.Replace("Key", ""); + + if (string.IsNullOrEmpty(queue)) + return; + + var service = ResolveService(queue); + + if (service == null) + return; + + var message = new Message( + err.Message, + new MessageProperties() + ); + + await Task.Run(() => TryToParseAndSend(message, service)); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error reprocessing message from {queue}", errorQueueName); + throw; + } + }); + } + + /// + /// Sends EasyNetQ messages to a service implementing IApiRequestService + /// Throws exception to trigger retry on failure + /// + private void TryToParseAndSend(IMessage msg, IApiRequestService service) + { + try + { + _logger.LogDebug("Processing message: {msg}", msg.Body); + + var apiRequest = JsonConvert.DeserializeObject(msg.Body); + + if (apiRequest == null) + { + _logger.LogError("Invalid ApiRequest JSON: {msg}", msg.Body); + return; + } + + if (apiRequest.PatientNumber == "\"\"") + apiRequest.PatientNumber = string.Empty; + + service.SaveRequest(apiRequest); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error processing request"); + throw; + } + } +} diff --git a/adas-core.Infrastructure/Services/RelayService.cs b/adas-core.Infrastructure/Services/RelayService.cs new file mode 100644 index 00000000..cdfa24df --- /dev/null +++ b/adas-core.Infrastructure/Services/RelayService.cs @@ -0,0 +1,459 @@ +using System.Collections.Concurrent; +using System.Net; +using System.Text; +using System.Web; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Exceptions; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Responses; +using adas_core.Infrastructure.Utils; +using adas_core.module.Relays.Devices; +using adas_core.module.Relays.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Quartz.Util; + +namespace adas_core.Infrastructure.Services; + +public class RelayService : IRelayService +{ + private readonly List _devices = []; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + private readonly IPointOfCareService _pointOfCareService; + private readonly RelaySettings _relaySettings; + private readonly IRelayRepository _relayRepository; + + private readonly ConcurrentDictionary, RelayEnum.Status> _relayWithStatus = new(); + private readonly string _url; + private UriBuilder? _builder; + + public RelayService( + ILogger logger, + IHttpClientFactory httpClientFactory, + IOptions relaySettings, + IPointOfCareService pointOfCareService, + IRelayRepository relayRepository) + { + _httpClientFactory = httpClientFactory; + _pointOfCareService = pointOfCareService; + _relayRepository = relayRepository; + _logger = logger; + _relaySettings = relaySettings.Value; + _url = _relaySettings.RecordingOrApiUrl ?? string.Empty; + + Task.Run(async () => await InitRelayWithStatus()); + } + + public async Task CheckRelayStatus(Relay relay) + { + var cacheKey = Tuple.Create(relay.Ip, relay.Port, relay.RelayNumber); + try + { + if (_relaySettings.Cache && relay.Cache) + lock (_relayWithStatus) + { + if (_relayWithStatus.TryGetValue(cacheKey, out var status) && + status != RelayEnum.Status.NotInitialized) + return status; + + _logger.LogDebug("Relay status not found in cache: {Dct}", + DictionaryToString(_relayWithStatus)); + } + + RelayDevice? relayDevice = null; + try + { + relayDevice = GetRelayDevice(relay); + } + catch (Exception e) + { + _logger.LogWarning("Error getting relay device: {EMessage} {EStackTrace}", e.Message, e.StackTrace); + } + + if (relayDevice == null && !string.IsNullOrEmpty(_url)) return await GetRelayStatusByOr(relay); + + if (relayDevice != null) return relayDevice.GetStatusRelay(relay.RelayNumber); + } + catch (Exception e) + { + _logger.LogError("Error CheckRelayStatus relay {Relay}: {EMessage} {EStackTrace}", relay, e.Message, + e.StackTrace); + + lock (_relayWithStatus) + { + _relayWithStatus[cacheKey] = RelayEnum.Status.Unknown; + } + } + + return RelayEnum.Status.Unknown; + } + + public async Task CheckRelayStatus(ObjectId relayId) + { + var relay = await _relayRepository.GetById(relayId); + if(relay == null) return RelayEnum.Status.Unknown; + return await CheckRelayStatus(relay); + } + + public async Task PowerOff(Relay relay) + { + var cacheKey = Tuple.Create(relay.Ip, relay.Port, relay.RelayNumber); + if (_relaySettings.Cache && relay.Cache) + lock (_relayWithStatus) + { + if (_relayWithStatus.Any() && + _relayWithStatus[cacheKey] == RelayEnum.Status.Off) + return; + } + + RelayDevice? relayDevice = null; + try + { + relayDevice = GetRelayDevice(relay); + } + catch (Exception e) + { + _logger.LogWarning("Error getting relay device: {EMessage} {EStackTrace}", e.Message, e.StackTrace); + } + + if (relayDevice == null && !string.IsNullOrEmpty(_url)) + { + _builder = new UriBuilder(_url) + { + Path = $"Relay/{relay.RelayNumber}/powerOff" + }; + _logger.LogDebug("Send PowerOff relay {Relay}", relay); + await PowerRelay(relay, _builder); + } + + relayDevice?.PowerOffRelay(relay.RelayNumber); + + if (_relaySettings.Cache && relay.Cache) + lock (_relayWithStatus) + { + _relayWithStatus[cacheKey] = RelayEnum.Status.Off; + _logger.LogDebug("Relay status cached: {Dct}", DictionaryToString(_relayWithStatus)); + } + } + + public async Task PowerOn(Relay relay) + { + if (string.IsNullOrEmpty(relay.Driver) || string.IsNullOrEmpty(relay.Ip)) return; + var cacheKey = Tuple.Create(relay.Ip, relay.Port, relay.RelayNumber); + if (_relaySettings.Cache && relay.Cache) + if (_relaySettings.Cache && relay.Cache) + lock (_relayWithStatus) + { + if (_relayWithStatus.Any() && + _relayWithStatus[cacheKey] == RelayEnum.Status.On) + return; + } + + + RelayDevice? relayDevice = null; + try + { + relayDevice = GetRelayDevice(relay); + } + catch (Exception e) + { + _logger.LogWarning("Error getting relay device: {EMessage} {EStackTrace}", e.Message, e.StackTrace); + } + + if (relayDevice == null && !string.IsNullOrEmpty(_url)) + { + _builder = new UriBuilder(_url) + { + Path = $"Relay/{relay.RelayNumber}/powerOn" + }; + + _logger.LogDebug("Send PowerOn relay {Relay}", relay); + + await PowerRelay(relay, _builder); + } + + relayDevice?.PowerOnRelay(relay.RelayNumber); + + + if (_relaySettings.Cache && relay.Cache) + lock (_relayWithStatus) + { + _relayWithStatus[cacheKey] = RelayEnum.Status.On; + _logger.LogDebug("Relay status cached: {Dct}", DictionaryToString(_relayWithStatus)); + } + } + + public async Task SetManualRelay(RelayEnum.Status status, ObjectId pocId, RelayEnum.Type type) + { + try + { + var poc = await _pointOfCareService.FindById(pocId); + if (poc is { Configuration.RelayIdList: not null }) + { + var relay = _relayRepository.GetRelayByTypeInList(poc.Configuration.RelayIdList, type).FirstOrDefault(); + if (relay != null) relay.ManualRelayStatus = status; + + await _pointOfCareService.UpdateRelayConfig(poc); + _logger.LogDebug("SetManualRelay: {Status}, pocId: {Location}, bed: {Bed}", status, poc.Id, poc.Bed); + } + } + catch (Exception ex) + { + _logger.LogError("Error settings manual relay. Exception: {ex}", ex); + throw; + } + } + + public Task GetById(ObjectId relay) + { + return _relayRepository.GetById(relay); + } + public List GetRelayInList(List? relayList) + { + if(relayList == null) return new List(); + return _relayRepository.GetRelayInList(relayList); + } + + public List GetRelayByTypeInList(List? configurationRelayList, RelayEnum.Type type) + { + if(configurationRelayList == null) return new List(); + return _relayRepository.GetRelayByTypeInList(configurationRelayList, type); + } + + public async Task> GetPaginatedRelays(PaginationFilter filter) + { + var usedRelayIds = await _pointOfCareService.FindAllIdRelaysInUse(); + + var fluentQuery = _relayRepository.GetPaginatedRelays(filter); + + if (filter.FilteredRequest?.InUse != null) + { + bool filterInUse = filter.FilteredRequest.InUse.Value; + var filterBuilder = Builders.Filter; + + var idFilter = filterInUse + ? filterBuilder.In(c => c.Id, usedRelayIds) + : filterBuilder.Not(filterBuilder.In(c => c.Id, usedRelayIds)); + + fluentQuery.Filter = filterBuilder.And(fluentQuery.Filter, idFilter); + } + + var count = await fluentQuery.CountDocumentsAsync(); + var data = await fluentQuery + .Skip((filter.PageNumber - 1) * filter.PageSize) + .Limit(filter.PageSize) + .ToListAsync(); + + if (data == null) return new PaginationResponse([], filter.PageNumber, filter.PageSize, count); + + foreach (var camera in data) + { + if (camera == null) continue; + + bool isInUse = usedRelayIds.Contains(camera.Id); + + // Asignación mediante reflexión para el private set + camera.GetType().GetProperty(nameof(Relay.InUse)) + ?.SetValue(camera, isInUse); + } + + return new PaginationResponse(data, filter.PageNumber, filter.PageSize, count); + } + + public async Task InsertRelay(Relay request) + { + var relayExist = await _relayRepository.GetByName(request.RelayName); + if(relayExist != null) throw new Exception($"Relay with name {request.RelayName} already exists"); + return await _relayRepository.InsertOneRelayAsync(request); + } + + public Task UpdateRelayById(ObjectId objectId, Relay relay) + { + return _relayRepository.UpdateRelayAsync(objectId, relay); + } + + public event EventHandler>? RelayStatusChanged; + + private async Task InitRelayWithStatus() + { + try + { + var pocConfigList = await _pointOfCareService.GetAllConfigs(); + var relayTasks = new List(); + + pocConfigList.ForEach(poc => + { + if (poc.Configuration?.RelayList == null) return; + relayTasks.AddRange(from relayConf in poc.Configuration.RelayList + let statusTask = CheckRelayStatus(relayConf) + select statusTask.ContinueWith(task => + { + if (task.IsCompletedSuccessfully) + { + var cackeKey = Tuple.Create(relayConf.Ip, relayConf.Port, relayConf.RelayNumber); + if (!relayConf.Cache || !relayConf.Cache) return; + lock (_relayWithStatus) + { + _relayWithStatus[cackeKey] = task.Result; + } + } + else if (task.IsFaulted) + { + // Manejar la excepción si la llamada asincrónica falla + _logger.LogError("Error al obtener el estado del relé {RelayParsedIp}: {TaskException}", + relayConf.Ip, task.Exception.Message); + } + })); + }); + + await Task.WhenAll(relayTasks); + + lock (_relayWithStatus) + { + _logger.LogDebug("Relay status cached: {Dct}", DictionaryToString(_relayWithStatus)); + } + } + catch (Exception ex) + { + Console.WriteLine("Exception setting relay with status: " + ex.Message); + } + } + + private RelayDevice? GetRelayDevice(Relay relay) + { + if(relay.Ip.IsNullOrWhiteSpace() || relay.Port == 0) return null; + var device = _devices.FirstOrDefault(d => d.Relay.Ip == relay.Ip && d.Relay.Port == relay.Port); + if (device != null) return device; + + var type = TypesUtils.GetDriver("Relay", relay.Driver ?? string.Empty); + var constructor = type.GetConstructor([typeof(Relay), typeof(RelaySettings)]); + if (constructor == null) throw new AdasException("Constructor not found for relay device"); + var relayDevice = (RelayDevice)constructor.Invoke([relay, _relaySettings]); + if (relayDevice == null) throw new AdasException("Relay device not found"); + relayDevice.RelayStatusChanged += (_, outletId) => + { + var cacheKey = Tuple.Create(relay.Ip, relay.Port, outletId); + var value = relayDevice.GetStatusRelay(outletId); + lock (_relayWithStatus) + { + _relayWithStatus[cacheKey] = value; + } + + _logger.LogDebug("Relay status changed for {Key} with value {Value}", cacheKey, value); + RelayStatusChanged?.Invoke(this, Tuple.Create(relayDevice, outletId)); + }; + _devices.Add(relayDevice); + return relayDevice; + } + + private async Task PowerRelay(Relay relay, UriBuilder builder) + { + var query = HttpUtility.ParseQueryString(builder.Query); + query["driver"] = relay.Driver; + query["host"] = relay.Ip; + query["port"] = relay.Port.ToString(); + query["relayName"] = relay.RelayName; + query["relays"] = relay.Total.ToString(); + query["username"] = relay.Username; + query["password"] = relay.Password; + + builder.Query = query.ToString(); + HttpRequestMessage request = new() + { + RequestUri = builder.Uri, + Method = HttpMethod.Post + }; + + try + { + using var client = _httpClientFactory.CreateClient(); + var httpResponse = await client.SendAsync(request); + + if (!httpResponse.StatusCode.Equals(HttpStatusCode.OK)) + _logger.LogDebug("Send PowerRelay relay {Relay}", relay); + } + catch (Exception e) + { + _logger.LogError("Error PowerRelay relay {Relay}: {EMessage} {EStackTrace}", relay, e.Message, + e.StackTrace); + } + } + + private static string DictionaryToString(ConcurrentDictionary, RelayEnum.Status> dictionary) + { + var builder = new StringBuilder(); + + foreach (var pair in dictionary) + builder.AppendLine($"Device ID: {pair.Key.Item1}, Port: {pair.Key.Item2}, Status: {pair.Value}"); + + return builder.ToString(); + } + + + private async Task GetRelayStatusByOr(Relay relay) + { + var cacheKey = Tuple.Create(relay.Ip, relay.Port, relay.RelayNumber); + _builder = new UriBuilder(_url) + { + Path = $"Relay/{relay.RelayNumber}/status" + }; + + var query = HttpUtility.ParseQueryString(_builder.Query); + query["driver"] = relay.Driver; + query["host"] = relay.Ip; + query["port"] = relay.Port.ToString(); + query["relayName"] = relay.RelayName; + query["relays"] = relay.Total.ToString(); + query["username"] = relay.Username; + query["password"] = relay.Password; + query["mode"] = relay.Mode.ToString(); + query["refreshTime"] = "0"; + + _builder.Query = query.ToString(); + HttpRequestMessage request = new() + { + RequestUri = _builder.Uri, + Method = HttpMethod.Get + }; + + + using var client = _httpClientFactory.CreateClient(); + client.Timeout = new TimeSpan(0, 0, 3); + var httpResponse = await client.SendAsync(request); + + if (httpResponse.StatusCode.Equals(HttpStatusCode.OK)) + { + var responseContent = httpResponse.Content; + var response = await responseContent.ReadAsStringAsync(); + + response = response.Replace("\"", ""); + + var relayStatusParsed = (RelayEnum.Status)Enum.Parse(typeof(RelayEnum.Status), response); + + if (_relaySettings.Cache && relay.Cache) + lock (_relayWithStatus) + { + _relayWithStatus[cacheKey] = relayStatusParsed; + _logger.LogDebug("Relay status cached: {Dct}", DictionaryToString(_relayWithStatus)); + } + + + return relayStatusParsed; + } + + if (_relaySettings.Cache && relay.Cache) + lock (_relayWithStatus) + { + _relayWithStatus[cacheKey] = RelayEnum.Status.Unknown; + _logger.LogDebug("Relay status cached: {Dct}", DictionaryToString(_relayWithStatus)); + } + + return RelayEnum.Status.Unknown; + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Services/SendAlertService.cs b/adas-core.Infrastructure/Services/SendAlertService.cs new file mode 100644 index 00000000..b985e685 --- /dev/null +++ b/adas-core.Infrastructure/Services/SendAlertService.cs @@ -0,0 +1,282 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.SystemAlerts; +using EasyNetQ; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System.Diagnostics; + +namespace adas_core.Infrastructure.Services; + +public class SendAlertService( + IOptions rabbitMqSettings, + ILogger logger) + : ISendAlertService +{ + private readonly string _errorObservationsQueue = $"Error{rabbitMqSettings.Value.ObservationsQueue}"; + private readonly string _errorPatientsQueue = $"Error{rabbitMqSettings.Value.PatientsQueue}"; + private readonly string _errorPumpsQueue = $"Error{rabbitMqSettings.Value.PumpsQueue}"; + private readonly string _errorRecordingQueue = $"Error{rabbitMqSettings.Value.RecordingQueue}"; + private readonly string _errorTreatmetsQueue = $"Error{rabbitMqSettings.Value.TreatmentsQueue}"; + private readonly string? _rabbitConnectionString = rabbitMqSettings.Value.ConnectionString; + + + //readonly PlatformID[] _windowsPlatforms; + + ////TODO new ws private static DefaultWebSocketHandler defaultWebSocketHandler = new (); + + //_windowsPlatforms = new[] + //{ + // PlatformID.Win32NT, + // PlatformID.Win32S, + // PlatformID.Win32Windows, + // PlatformID.WinCE + //}; + + public async Task> GetQueues() + { + if (_rabbitConnectionString != null) return await GetQueuesAsync(); + + logger.LogError("Rabbit connection string not defined in web config"); + + return []; + } + + public List GetPerformance() + { + logger.LogDebug("Starting check performance data from hospitals"); + + var list = new List(); + + try + { + list.Add(GetConsumedCpu()); + list.Add(GetConsumedRam()); + list.AddRange(GetConsumedStorages()); + } + catch (Exception e) + { + logger.LogError("Error check error performance data from hospitals: {eMessage} {eStackTrace}", + e.Message, e.StackTrace); + } + + return list; + } + + public async Task> GetApiClients() + { + return await GetApiClientsAsync(); + } + + private async Task> GetQueuesAsync() + { + logger.LogDebug("Starting check rabbitMQ data from hospitals"); + + var errorQueues = new List(); + var errorQueuesNames = new List(); + + try + { + var services = new ServiceCollection(); + services.AddEasyNetQ(_rabbitConnectionString); + + var provider = services.BuildServiceProvider(); + var bus = provider.GetRequiredService(); + + var advanced = bus.Advanced; + + if (!string.IsNullOrEmpty(_errorRecordingQueue)) + errorQueuesNames.Add(_errorRecordingQueue); + + if (!string.IsNullOrEmpty(_errorPatientsQueue)) + errorQueuesNames.Add(_errorPatientsQueue); + + if (!string.IsNullOrEmpty(_errorTreatmetsQueue)) + errorQueuesNames.Add(_errorTreatmetsQueue); + + if (!string.IsNullOrEmpty(_errorObservationsQueue)) + errorQueuesNames.Add(_errorObservationsQueue); + + if (!string.IsNullOrEmpty(_errorPumpsQueue)) + errorQueuesNames.Add(_errorPumpsQueue); + + foreach (var errorQueueName in errorQueuesNames.Distinct()) + { + var stats = await advanced.GetQueueStatsAsync(errorQueueName); + + errorQueues.Add(new Queue + { + Name = errorQueueName, + Messages = stats.MessagesCount, + Consumers = stats.ConsumersCount + }); + } + } + catch (Exception e) + { + logger.LogError("Error checking RabbitMQ queues: {msg} {stack}", + e.Message, e.StackTrace); + } + + return errorQueues; + } + + public Performance GetConsumedCpu() + { + Performance performance = new(); + + try + { + var currentProcess = Process.GetCurrentProcess(); + var percentage = currentProcess.TotalProcessorTime.TotalMilliseconds / Environment.ProcessorCount / 10; + + performance = new Performance + { + Name = "CPU", + PercentageConsumed = percentage, + ValueTotal = 100, + Unit = "%" + }; + } + catch (Exception e) + { + logger.LogError("Error check CPU performance: {eMessage} {eStackTrace}", e.Message, e.StackTrace); + } + + return performance; + } + + public Performance GetConsumedRam() + { + Performance performance = new(); + try + { + var totalMemoryBytes = GC.GetTotalMemory(false); + var totalMemoryGb = Math.Round((double)totalMemoryBytes / 1024 / 1024 / 1024, 2); + + performance = new Performance + { + Name = "RAM", + ValueTotal = totalMemoryGb, + ValueConsumed = totalMemoryGb, + PercentageConsumed = 100, + Unit = "GB" + }; + } + catch (Exception e) + { + logger.LogError("Error check RAM performance: {eMessage} {eStackTrace}", e.Message, e.StackTrace); + } + + return performance; + } + + + public Performance GetConsumedStorage(DriveInfo drive) + { + Performance performance = new(); + try + { + double percentage = 0; + + //Total storage + double valueTotal = drive.TotalSize; + + //Consumed storage + var value = valueTotal - drive.AvailableFreeSpace; + + if (valueTotal != 0) + { + percentage = Math.Round(value * 100 / valueTotal, 2); //% + + valueTotal = Math.Round(valueTotal / 1024 / 1024 / 1024, 2); //Bytes -> GB + } + + value = Math.Round(value / 1024 / 1024 / 1024, 2); //Bytes -> GB + + performance = new Performance + { + Name = "STORAGE " + drive.Name, + ValueConsumed = value, + ValueTotal = valueTotal, + PercentageConsumed = percentage, + Unit = "GB" + }; + } + catch (Exception e) + { + logger.LogError("Error check Storage performance: {eMessage} {eStackTrace}", e.Message, e.StackTrace); + } + + return performance; + } + + private List GetConsumedStorages() + { + var list = new List(); + try + { + foreach (var drive in DriveInfo.GetDrives()) + { + if (!drive.IsReady) continue; + try + { + var performance = GetConsumedStorage(drive); + list.Add(performance); + } + catch (Exception e) + { + logger.LogError("Error check Storage drive {drive} in performance: {eMessage}", drive.Name, + e.Message); + } + } + } + catch (Exception e) + { + logger.LogError("Error check Storages performance: {eMessage}", e.Message); + } + + return list; + } + + + private async Task> GetApiClientsAsync() + { + logger.LogDebug("Starting check conected clients from hospitals"); + + var clientsList = new List(); + + try + { + var clients = await GetWebSocketClients(); + if (clients != null) + clientsList.Add(clients); + } + catch (Exception e) + { + logger.LogError("Error check error conected clients from hospitals: {eMessage} {eStackTrace}", + e.Message, e.StackTrace); + } + + return clientsList; + } + + private Task GetWebSocketClients() + { + ApiClients apiClients = new(); + + try + { + //TODO new ws apiClients = defaultWebSocketHandler.GetSubscribersConected(); + + //apiClients = webSocketHandler.GetSubscribersConected(); + } + catch (Exception e) + { + logger.LogError("Error check WebSocket Clients: {eMessage} {eStackTrace}", e.Message, e.StackTrace); + } + + return Task.FromResult(apiClients); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/CacheHostBuilderExtension.cs b/adas-core.Infrastructure/Utils/CacheHostBuilderExtension.cs new file mode 100644 index 00000000..37c5b290 --- /dev/null +++ b/adas-core.Infrastructure/Utils/CacheHostBuilderExtension.cs @@ -0,0 +1,50 @@ +using adas_core.Application.Services.Caching; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models.AppSettings; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace adas_core.Infrastructure.Utils +{ + public static class CacheHostBuilderExtension + { + public static IHostBuilder UseCache(this IHostBuilder hostBuilder) + { + return hostBuilder.ConfigureServices((context, services) => + { + services.Configure(context.Configuration.GetSection("CacheSettings")); + services.AddSingleton(sp => sp.GetRequiredService>().Value); + + services.AddSingleton(); + services.AddSingleton(); + + // RedisLockProvider obtiene IDatabase de forma lazy a través de una clausura, + // ya que la conexión se establece de forma asíncrona dentro de RedisService. + services.AddSingleton(sp => { + RedisService? svcRef = null; + var lockProvider = new RedisLockProvider(() => svcRef?.Database); + var lockMgr = new LockManagerService( + sp.GetRequiredService>(), + lockProvider); + svcRef = new RedisService( + sp.GetRequiredService>(), + sp.GetRequiredService>(), + lockMgr); + return svcRef; + }); + + // Inyectamos LockManager con InMemoryLockProvider + services.AddSingleton(sp => { + var lockMgr = new LockManagerService( + sp.GetRequiredService>(), + sp.GetRequiredService()); + return new CacheService(lockMgr); + }); + + services.AddSingleton(); + }); + } + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/CustomPointOfCareConverter.cs b/adas-core.Infrastructure/Utils/CustomPointOfCareConverter.cs new file mode 100644 index 00000000..d6f9598c --- /dev/null +++ b/adas-core.Infrastructure/Utils/CustomPointOfCareConverter.cs @@ -0,0 +1,43 @@ +using adas_core.Domain.Models.MongoModels; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace adas_core.Infrastructure.Utils; + +public class CustomPointOfCareConverter : JsonConverter +{ + public override bool CanConvert(Type objectType) + { + return objectType == typeof(PointOfCare); + } + + public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value == null) + { + writer.WriteNull(); + return; + } + + var jo = JObject.FromObject(value); + // Aquí modificas específicamente las propiedades que necesitas + // Por ejemplo, aplicar StringEnumConverter solo a ciertas propiedades enumeradas + + // Esto es un ejemplo, ajusta según tus necesidades + foreach (var prop in value.GetType().GetProperties()) + if (prop.PropertyType.IsEnum) + { + var enumValue = prop.GetValue(value); + if (enumValue != null) jo[prop.Name] = JToken.FromObject(enumValue.ToString() ?? string.Empty); + } + + jo.WriteTo(writer); + } + + public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, + JsonSerializer serializer) + { + // Implementa la lógica de deserialización si es necesario + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoDbHostBuilderExtension.cs b/adas-core.Infrastructure/Utils/MongoDbHostBuilderExtension.cs new file mode 100644 index 00000000..45b78f57 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoDbHostBuilderExtension.cs @@ -0,0 +1,104 @@ +using System.Reflection; +using adas_core.Domain.Models.AppSettings; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using MongoDB.Bson.Serialization.Conventions; +using MongoDB.Driver; +using MongoMigrations.Core; +using MongoClient = MongoDB.Driver.MongoClient; + +namespace adas_core.Infrastructure.Utils; + +public static class MongoDbHostBuilderExtension +{ + public static IHostBuilder UseMongo(this IHostBuilder hostBuilder) + { + ConfigureMongoDbConventions(); + + ConfigureRegisterMapClass(); + + return hostBuilder; + } + public static IHost RunMongoMigrations(this IHost host) + { + using var scope = host.Services.CreateScope(); + + var database = scope.ServiceProvider.GetRequiredService(); + + var locator = new MigrationLocator(); + locator.LookForMigrationsInAssembly( + typeof(adas_core.Infrastructure.Migrations.MongoMigrations.U_0_1_0_UpdateDataPatien).Assembly + ); + + var runner = new MigrationRunner( + database, + collectionName: "migrations", + migrationLocator: locator + ); + + runner.UpdateToLatest(); + + return host; + } + private static IMongoDatabase ConfigureMongoDbConnection(IOptions dbSettings) + { + var connectionString = dbSettings.Value.ConnectionString; + + var databaseName = dbSettings.Value.DatabaseName; + + if (string.IsNullOrEmpty(connectionString) || string.IsNullOrEmpty(databaseName)) + throw new Exception("DataBase connection string and name is requiered"); + + _mongoClient = new MongoClient(connectionString); + + var mongoDb = _mongoClient.GetDatabase(databaseName); + + if (mongoDb == null) throw new Exception("DataBase doesn't exist"); + + return mongoDb; + } + + public static void ConfigureRegisterMapClass() + { + var assembly = Assembly.GetExecutingAssembly(); + + // Busca todos los tipos que implementan la interfaz + var contributors = assembly.GetTypes() + .Where(t => typeof(IEntityMapContributor).IsAssignableFrom(t) && + t is { IsInterface: false, IsAbstract: false }); + + // Crea una instancia de cada contribuidor y ejecuta su método + foreach (var contributorType in contributors) + { + var contributorInstance = (IEntityMapContributor)Activator.CreateInstance(contributorType)!; + contributorInstance.RegisterMaps(); + } + } + + public static void ConfigureMongoDbConventions() + { + var pack = new ConventionPack + { + new IgnoreExtraElementsConvention(true), + new CamelCaseElementNameConvention() + }; + + ConventionRegistry.Register( + "Ignore Extra Elements Convention", + pack, + _ => true); + + ConventionRegistry.Register( + "Camel Case Convention", + pack, + _ => true); + } + + #region MongoDB + + private static MongoClient? _mongoClient; + + #endregion +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/AlarmMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/AlarmMapContributor.cs new file mode 100644 index 00000000..eb25c1d5 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/AlarmMapContributor.cs @@ -0,0 +1,133 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.SystemAlerts; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class AlarmMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(AlarmConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Name).SetIgnoreIfNull(true); + cm.MapMember(c => c.Enabled).SetDefaultValue(false); + cm.MapMember(c => c.EndAfter).SetIgnoreIfNull(true); + cm.MapMember(c => c.Recording).SetIgnoreIfNull(true); + cm.MapMember(c => c.Beacon).SetIgnoreIfNull(true); + cm.MapMember(c => c.OpenDoor).SetIgnoreIfNull(true); + cm.MapMember(c => c.Priority).SetIgnoreIfNull(true); + cm.MapMember(c => c.Color).SetIgnoreIfNull(true); + cm.MapMember(c => c.AudioConfig).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(AudioConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Type) + .SetDefaultValue(AlarmEnum.AudioAlarmType.Off) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.Path).SetIgnoreIfNull(true); + cm.MapMember(c => c.EndAfter).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(AlarmItem))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Enabled).SetDefaultValue(false); + cm.MapMember(c => c.StartBefore).SetIgnoreIfNull(true); + cm.MapMember(c => c.EndAfter).SetIgnoreIfNull(true); + cm.MapMember(c => c.Color).SetIgnoreIfNull(true); + cm.MapMember(c => c.Severity) + .SetDefaultValue(AlarmEnum.Severity.None) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.BeaconColor) + .SetDefaultValue(AlarmEnum.BeaconColor.None) + .SetSerializer(new EnumSerializer(BsonType.String)); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(PatientObservationAlarm))) + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.InactivationState).SetIgnoreIfNull(true); + cm.MapMember(c => c.EventPhase) + .SetDefaultValue(AlarmEnum.EventPhase.Continue) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.Event).SetIgnoreIfNull(true); + cm.MapMember(c => c.EventId).SetIgnoreIfNull(true); + cm.MapMember(c => c.State) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + cm.MapMember(c => c.Priority) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + cm.MapMember(c => c.PriorityLevel) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer(new Int32Serializer())); + cm.UnmapMember(c => c.AlarmConfig); + cm.MapMember(c => c.Type) + .SetIgnoreIfNull(true) + .SetSerializer( + new NullableSerializer( + new EnumSerializer(BsonType.String))); + cm.MapMember(c => c.AlertColor).SetIgnoreIfNull(true) + .SetSerializer(new StringSerializer(BsonType.String)); + cm.UnmapMember(c => c.MessageTime); + cm.MapMember(c => c.Persist).SetIgnoreIfNull(true); + cm.UnmapMember(c => c.Expired); + cm.MapMember(c => c.Expires).SetIgnoreIfNull(true); + cm.MapMember(c => c.Sources).SetIgnoreIfNull(true); + }); + + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Audio) + .SetIgnoreIfNull(true) + .SetSerializer( + new NullableSerializer( + new EnumSerializer(BsonType.String))); + cm.MapMember(c => c.Visual) + .SetIgnoreIfNull(true) + .SetSerializer( + new NullableSerializer( + new EnumSerializer(BsonType.String))); + cm.MapMember(c => c.Acknowledge).SetIgnoreIfNull(true); + }); + + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Code).SetIgnoreIfNull(true); + cm.MapMember(c => c.OriginalName).SetIgnoreIfNull(true); + cm.MapMember(c => c.CodeSystem).SetIgnoreIfNull(true); + cm.MapMember(c => c.Units).SetIgnoreIfNull(true); + cm.MapMember(c => c.Value).SetIgnoreIfNull(true); + cm.MapMember(c => c.Result).SetIgnoreIfNull(true); + }); + } + + if (!BsonClassMap.IsClassMapRegistered(typeof(PatientRecordingAlert))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.IsRecording); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Performance))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/AppointmentMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/AppointmentMapContributor.cs new file mode 100644 index 00000000..6f083431 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/AppointmentMapContributor.cs @@ -0,0 +1,72 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class AppointmentMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(PatientAppointment))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.PatientId).SetSerializer(new ObjectIdSerializer(BsonType.String)); + cm.MapMember(c => c.Patient).SetIgnoreIfNull(true); + cm.MapMember(c => c.Timings).SetDefaultValue(new List()); + cm.MapMember(c => c.CreateTime).SetIgnoreIfNull(true); + cm.MapMember(c => c.UpdateTime).SetIgnoreIfNull(true); + cm.MapMember(c => c.PlacerOrder).SetIgnoreIfNull(true); + cm.MapMember(c => c.FillerOrder).SetIgnoreIfNull(true); + cm.MapMember(c => c.EventReason).SetIgnoreIfNull(true); + cm.MapMember(c => c.AppointmentReason).SetIgnoreIfNull(true); + cm.MapMember(c => c.AppointmentType).SetIgnoreIfNull(true); + cm.MapMember(c => c.AppointmentOperationType) + .SetIgnoreIfNull(true) + .SetSerializer( + new NullableSerializer(new EnumSerializer(BsonType.String))); + cm.MapMember(c => c.AppointmentStatus) + .SetIgnoreIfNull(true) + .SetSerializer( + new NullableSerializer(new EnumSerializer(BsonType.String))); + cm.MapMember(c => c.Duration).SetIgnoreIfNull(true); + cm.MapMember(c => c.VisitNumber).SetIgnoreIfNull(true); + cm.MapMember(c => c.PatientClass).SetIgnoreIfNull(true); + cm.MapMember(c => c.EpisodeActivation).SetDefaultValue(true); + cm.MapMember(c => c.PlacerContact).SetIgnoreIfNull(true); + cm.MapMember(c => c.FillerContact).SetIgnoreIfNull(true); + cm.MapMember(c => c.ResourceGroups).SetDefaultValue(new List()); + cm.MapMember(c => c.Allergies).SetDefaultValue(new List()); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(PatientAppointmentResourceGroup))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + + cm.MapMember(c => c.Services).SetIgnoreIfNull(true); + cm.MapMember(c => c.Resources).SetIgnoreIfNull(true); + cm.MapMember(c => c.Locations).SetIgnoreIfNull(true); + cm.MapMember(c => c.Personnel).SetIgnoreIfNull(true); + + cm.UnmapMember(c => c.ServicesActions); + cm.UnmapMember(c => c.ResourcesActions); + cm.UnmapMember(c => c.LocationsActions); + cm.UnmapMember(c => c.PersonnelActions); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Allergies))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.AllergenType).SetIgnoreIfNull(true); + cm.MapMember(c => c.Allergen).SetIgnoreIfNull(true); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/AuthMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/AuthMapContributor.cs new file mode 100644 index 00000000..015ba426 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/AuthMapContributor.cs @@ -0,0 +1,37 @@ +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson.Serialization; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class AuthMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(User))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Password) + .SetIgnoreIfNull(true) + .SetShouldSerializeMethod(obj => !string.IsNullOrEmpty(((User)obj).Password)); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.LockExpirationDate) + .SetIgnoreIfNull(true); + cm.MapMember(c => c.Authorization).SetElementName("Authorization") + .SetShouldSerializeMethod(_ => false); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Authorization))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.UnitId).SetIgnoreIfNull(true); + cm.UnmapProperty(c => c.User); + cm.UnmapProperty(c => c.Display); + cm.UnmapProperty(c => c.Unit); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/BannerConfigMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/BannerConfigMapContributor.cs new file mode 100644 index 00000000..3e21a983 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/BannerConfigMapContributor.cs @@ -0,0 +1,62 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class BannerConfigMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(BannerItem))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Type) + .SetIgnoreIfNull(true) + .SetSerializer( + new NullableSerializer( + new EnumSerializer(BsonType.String))); + cm.MapMember(c => c.GrowPriority).SetDefaultValue(1); + cm.MapMember(c => c.Config).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(BannerItemConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.BannerItemDialogTableConfig).SetIgnoreIfNull(true); + cm.MapMember(c => c.BannerItemTableConfig).SetIgnoreIfNull(true); + cm.MapMember(c => c.MedicalStaffConfig).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(BannerItemTableConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Config).SetIgnoreIfNull(true); + cm.MapMember(c => c.BgColor).SetIgnoreIfNull(true); + cm.MapMember(c => c.TextColor).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(HeaderBannerItemTableConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.MapMember(c => c.Type) + .SetIgnoreIfNull(true) + .SetSerializer( + new NullableSerializer( + new EnumSerializer(BsonType.String))); + cm.MapMember(c => c.SubType).SetIgnoreIfNull(true); + cm.MapMember(c => c.Field).SetIgnoreIfNull(true); + cm.MapMember(c => c.GrowPriority).SetIgnoreIfNull(true); + cm.MapMember(c => c.Icon).SetIgnoreIfNull(true); + cm.MapMember(c => c.BgColor).SetIgnoreIfNull(true); + cm.MapMember(c => c.TextColor).SetIgnoreIfNull(true); + cm.MapMember(c => c.Title).SetIgnoreIfNull(true); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/BeaconMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/BeaconMapContributor.cs new file mode 100644 index 00000000..44ad2161 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/BeaconMapContributor.cs @@ -0,0 +1,42 @@ +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using adas_core.module.LightBeacons.Devices; +using MongoDB.Bson.Serialization; +using LightBeacon = adas_core.Domain.Models.MongoModels.LightBeacon; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class BeaconMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(LightBeacon))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.Type).SetDefaultValue(string.Empty); + cm.MapMember(c => c.Name).SetDefaultValue(string.Empty); + cm.MapMember(c => c.Options).SetDefaultValue(new Options()); + cm.MapMember(c => c.InUse).SetIgnoreIfNull(true).SetIsRequired(false); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Options))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Url).SetIgnoreIfNull(true); + cm.MapMember(c => c.Port).SetIgnoreIfNull(true); + cm.MapMember(c => c.Password).SetIgnoreIfNull(true); + cm.MapMember(c => c.Emulate).SetDefaultValue(false); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(LightBeaconAbstract))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Host); + cm.MapMember(c => c.Password); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/BoxConfigMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/BoxConfigMapContributor.cs new file mode 100644 index 00000000..67136d28 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/BoxConfigMapContributor.cs @@ -0,0 +1,48 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.BsonConverters; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using Stream = adas_core.Domain.Models.MongoModels.Stream; + + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class BoxConfigMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(Box))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.UnmapMember(c => c.PointOfCare); //Esto hace que ignora la propiedad en la serialización del Bson + cm.MapMember(c => c.Bed).SetElementName("name"); + cm.UnmapMember(c => c.IsActive); + cm.UnmapMember(c => c.IsVisible); + cm.MapMember(c => c.Configuration) + .SetSerializer(new DictionaryBsonConverter()) + .SetIgnoreIfNull(true); + cm.UnmapMember(c => c.Location); + cm.UnmapMember(c => c.HasPatient); + cm.UnmapMember(c => c.Patientid); + cm.UnmapMember(c => c.Patient); + cm.UnmapMember(c => c.AttendingDoctor); + cm.UnmapMember(c => c.Type); + cm.UnmapMember(c => c.Observations); + cm.UnmapMember(c => c.Medication); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Sensor))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Name).SetIgnoreIfNull(true); + cm.MapMember(c => c.Title).SetIgnoreIfNull(true); + cm.MapMember(c => c.OnlyNumbers).SetDefaultValue(true); + cm.MapMember(c => c.IsGeneral).SetDefaultValue(true); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/CameraContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/CameraContributor.cs new file mode 100644 index 00000000..a57404db --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/CameraContributor.cs @@ -0,0 +1,39 @@ +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson.Serialization; +using Stream = System.IO.Stream; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class CameraContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(Camera))) + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.Streams).SetIgnoreIfNull(true); + cm.MapMember(c => c.Name).SetDefaultValue(string.Empty); + cm.MapMember(c => c.Ptz).SetDefaultValue(false); + cm.MapMember(c => c.Driver).SetIgnoreIfNull(true); + cm.MapMember(c => c.Ip).SetIgnoreIfNull(true); + cm.MapMember(c => c.Username).SetIgnoreIfNull(true); + cm.MapMember(c => c.Password).SetIgnoreIfNull(true); + cm.MapMember(c => c.InUse).SetIgnoreIfNull(true).SetIsRequired(false); + }); + + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Rtsp).SetIgnoreIfNull(true); + cm.MapMember(c => c.Jpeg).SetIgnoreIfNull(true); + cm.MapMember(c => c.WebRtc).SetIgnoreIfNull(true); + cm.MapMember(c => c.Hls).SetIgnoreIfNull(true); + cm.MapMember(c => c.Mp4).SetIgnoreIfNull(true); + }); + } + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/CardMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/CardMapContributor.cs new file mode 100644 index 00000000..ef039819 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/CardMapContributor.cs @@ -0,0 +1,93 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class CardMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(SectionBoxLayout))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Type).SetDefaultValue(DisplayConfigEnums.RowType.Simple) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.Name).SetIgnoreIfNull(true); + cm.MapMember(c => c.Subtitle).SetIgnoreIfNull(true); + cm.MapMember(c => c.Icon).SetIgnoreIfNull(true); + cm.MapMember(c => c.GridColumn).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingTop).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingBottom).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingLeft).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingRight).SetIgnoreIfNull(true); + cm.MapMember(c => c.BorderColor).SetIgnoreIfNull(true); + cm.MapMember(c => c.BorderWidth).SetIgnoreIfNull(true); + cm.MapMember(c => c.BorderStyle).SetIgnoreIfNull(true); + cm.MapMember(c => c.BorderRadius).SetIgnoreIfNull(true); + cm.MapMember(c => c.HProportion).SetIgnoreIfNull(true); + cm.MapMember(c => c.BgColor).SetIgnoreIfNull(true); + cm.MapMember(c => c.SectionUrl).SetIgnoreIfNull(true); + cm.MapMember(c => c.MinHeight).SetIgnoreIfNull(true); + cm.MapMember(c => c.Subtitle).SetIgnoreIfNull(true); + cm.MapMember(c => c.Conditions).SetIgnoreIfNull(true); + cm.MapMember(c => c.Direction).SetDefaultValue(DisplayConfigEnums.DirectionEnum.Row) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.Rows).SetDefaultValue(new List()); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(CardDetailsConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.Header).SetIgnoreIfNull(true); + cm.MapMember(c => c.SmartSections).SetDefaultValue(new List()); + cm.MapMember(c => c.NurseRows).SetDefaultValue(new List()); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(CardRotatingLayout))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.MillisecondsBeforeRotating).SetDefaultValue(3000); + cm.MapMember(c => c.Order).SetIgnoreIfNull(true); + cm.MapMember(c => c.Name).SetIgnoreIfNull(true); + cm.MapMember(c => c.Title).SetIgnoreIfNull(true); + cm.MapMember(c => c.Data) + .SetIgnoreIfNull(true) + .SetIsRequired(false); + cm.MapMember(c => c.Type).SetDefaultValue(DisplayConfigEnums.RotatingLayoutType.HomeSection) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.Mode).SetDefaultValue(DisplayConfigEnums.RotatingLayoutMode.Default) + .SetSerializer(new EnumSerializer(BsonType.String)); + }); + + //standard card rotating layout + if (!BsonClassMap.IsClassMapRegistered(typeof(Step))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Type) + .SetIgnoreIfNull(true) + .SetSerializer( + new NullableSerializer( + new EnumSerializer(BsonType.String))); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(HomeConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.MinColumnSize).SetDefaultValue("250"); + cm.MapMember(c => c.ColumnsPerBreakpoint).SetDefaultValue(1); + cm.MapMember(c => c.BreakpointSize).SetDefaultValue("2000"); + cm.MapMember(c => c.CardAspectRatioHeight).SetDefaultValue("700"); + cm.MapMember(c => c.CardAspectRatioWidth).SetDefaultValue("500"); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/CellMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/CellMapContributor.cs new file mode 100644 index 00000000..3185212c --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/CellMapContributor.cs @@ -0,0 +1,129 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class CellMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(Cell))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Type) + .SetDefaultValue(DisplayConfigEnums.CellType.Default) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.ChartSettings).SetIgnoreIfNull(true); + cm.MapMember(c => c.GrowPriority).SetDefaultValue(1.0); + cm.MapMember(c => c.Name).SetIgnoreIfNull(true); + cm.MapMember(c => c.IconValueList) + .SetIgnoreIfNull(true); + cm.MapMember(c => c.Border).SetIgnoreIfNull(true); + cm.MapMember(c => c.BorderRadius).SetIgnoreIfNull(true); + cm.MapMember(c => c.Direction) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + cm.MapMember(c => c.SubType).SetIgnoreIfNull(true); + cm.MapMember(c => c.Size).SetIgnoreIfNull(true); + cm.MapMember(c => c.HideName).SetIgnoreIfNull(true); + cm.MapMember(c => c.IsVisible).SetDefaultValue(true); + cm.MapMember(c => c.IsColumn).SetIgnoreIfNull(true); + cm.MapMember(c => c.BgColor).SetIgnoreIfNull(true); + cm.MapMember(c => c.Title).SetIgnoreIfNull(true); + cm.MapMember(c => c.ValuePathKey).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingTop).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingBottom).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingLeft).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingRight).SetIgnoreIfNull(true); + cm.MapMember(c => c.Color).SetIgnoreIfNull(true); + cm.MapMember(c => c.BackgroundColor).SetIgnoreIfNull(true); + cm.MapMember(c => c.ShowIcon).SetIgnoreIfNull(true); + cm.MapMember(c => c.FlexBasis).SetIgnoreIfNull(true); + cm.MapMember(c => c.Grow).SetIgnoreIfNull(true); + cm.MapMember(c => c.Shrink).SetIgnoreIfNull(true); + cm.MapMember(c => c.ShowTitle).SetIgnoreIfNull(true); + cm.MapMember(c => c.IndicatorHorizontal).SetIgnoreIfNull(true); + cm.MapMember(c => c.DialogConfig).SetIgnoreIfNull(true); + cm.MapMember(c => c.ShowIndicator).SetIgnoreIfNull(true); + cm.MapMember(c => c.OnlyNumber).SetIgnoreIfNull(true); + cm.MapMember(c => c.ShowArrow).SetIgnoreIfNull(true); + cm.MapMember(c => c.GraphConf).SetIgnoreIfNull(true); + cm.MapMember(c => c.Names).SetIgnoreIfNull(true); + cm.MapMember(c => c.ObservationName).SetIgnoreIfNull(true); + cm.MapMember(c => c.ObservationTitle).SetIgnoreIfNull(true); + cm.MapMember(c => c.GraphConf).SetIgnoreIfNull(true); + cm.MapMember(c => c.ValuePathNested).SetIgnoreIfNull(true); + cm.MapMember(c => c.SubObs).SetIgnoreIfNull(true); + cm.MapMember(c => c.TextRules).SetIgnoreIfNull(true); + cm.MapMember(c => c.IsStatic).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(CellDetails))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.TextRules).SetIgnoreIfNull(true); + cm.MapMember(c => c.Type) + .SetSerializer(new EnumSerializer(BsonType.String)) + .SetDefaultValue(DisplayConfigEnums.CellType.Default); + cm.MapMember(c => c.ChartSettings).SetIgnoreIfNull(true); + cm.MapMember(c => c.SubType).SetDefaultValue(string.Empty); + cm.MapMember(c => c.GrowPriority).SetDefaultValue(1.0); + cm.MapMember(c => c.Cells).SetDefaultValue(new List()); + cm.MapMember(c => c.Name).SetIgnoreIfNull(true); + cm.MapMember(c => c.Title).SetIgnoreIfNull(true); + cm.MapMember(c => c.IsVisible).SetDefaultValue(true); + cm.MapMember(c => c.ValuePathNested).SetIgnoreIfNull(true); + cm.MapMember(c => c.ValuePathKey).SetIgnoreIfNull(true); + cm.MapMember(c => c.IconValueList).SetIgnoreIfNull(true); + cm.MapMember(c => c.ObservationName).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingTop).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingBottom).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingLeft).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingRight).SetIgnoreIfNull(true); + cm.MapMember(c => c.Color).SetIgnoreIfNull(true); + cm.MapMember(c => c.BackgroundColor).SetIgnoreIfNull(true); + cm.MapMember(c => c.ShowIcon).SetIgnoreIfNull(true); + cm.MapMember(c => c.FlexBasis).SetIgnoreIfNull(true); + cm.MapMember(c => c.FlexDirection).SetIgnoreIfNull(true); + cm.MapMember(c => c.Grow).SetIgnoreIfNull(true); + cm.MapMember(c => c.Shrink).SetIgnoreIfNull(true); + cm.MapMember(c => c.ShowTitle).SetIgnoreIfNull(true); + cm.MapMember(c => c.DialogConfig).SetIgnoreIfNull(true); + cm.MapMember(c => c.IsStatic).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(IconValueList))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.MinValue).SetIgnoreIfNull(true); + cm.MapMember(c => c.MaxValue).SetIgnoreIfNull(true); + cm.MapMember(c => c.Width).SetIgnoreIfNull(true); + cm.MapMember(c => c.Height).SetIgnoreIfNull(true); + cm.MapMember(c => c.IconList).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(DialogConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.IsDraggable).SetDefaultValue(false); + cm.MapMember(c => c.MedicalConfig).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(MedicalConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.HasFinalizeTime).SetDefaultValue(true); + cm.MapMember(c => c.HasStartTime).SetDefaultValue(true); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/ColorConfigMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/ColorConfigMapContributor.cs new file mode 100644 index 00000000..3375b562 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/ColorConfigMapContributor.cs @@ -0,0 +1,334 @@ +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson.Serialization; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class ColorConfigMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(ColorConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Level).SetDefaultValue(new ColorConfig.LevelColors + { + Level1 = "#FFFFFF", + Level2 = "#FAFF41", + Level3 = "#F5A623", + Level4 = "#C2510F", + Level5 = "#FF5D6A" + }).SetIgnoreIfNull(true); + cm.MapMember(c => c.Text).SetDefaultValue(new ColorConfig.TextColors + { + Normal = "#FFFFFF", + Warning = "#FFAD26", + Alert = "#FF5D6A", + Improve = "#60D61D", + Expired = "#333333" + }).SetIgnoreIfNull(true); + cm.MapMember(c => c.Arrow).SetDefaultValue(new ColorConfig.ArrowColors + { + Normal = "#FFFFFF", + Warning = "#F5A623", + Alert = "#FF5D6A", + Improve = "#60D61D" + }).SetIgnoreIfNull(true); + cm.MapMember(c => c.Indicator).SetDefaultValue(new ColorConfig.IndicatorColors + { + Empty = "#CCCCCC", + Warning = "#FFAD26", + Normal = "#60D61D", + Alert = "#FF5D6A", + Background = "#000000", + EmptyBackground = "#CCCCCC" + }).SetIgnoreIfNull(true); + cm.MapMember(c => c.Graph).SetDefaultValue(new ColorConfig.GraphColors + { + Alert = "#FF5D6A", + Warning = "#FFAD26", + Normal = "#60D61D" + }).SetIgnoreIfNull(true); + cm.MapMember(c => c.BoxNumber).SetDefaultValue(new ColorConfig.StatusBoxNumberColors + { + Reserved = + new ColorConfig.AppearanceSettings { BackgroundColor = "#FFF84C", TextColor = "#000000" }, + InUse = new ColorConfig.AppearanceSettings { BackgroundColor = "#FFF84C", TextColor = "#000000" }, + Available = new ColorConfig.AppearanceSettings + { BackgroundColor = "#FFF84C", TextColor = "#000000" }, + Locked = new ColorConfig.AppearanceSettings { BackgroundColor = "#FFF84C", TextColor = "#000000" }, + Transferable = new ColorConfig.AppearanceSettings + { BackgroundColor = "#FFF84C", TextColor = "#000000" }, + Exitus = new ColorConfig.AppearanceSettings { BackgroundColor = "#FFF84C", TextColor = "#000000" }, + Altable = new ColorConfig.AppearanceSettings { BackgroundColor = "#FFF84C", TextColor = "#000000" } + }).SetIgnoreIfNull(true); + cm.MapMember(c => c.BoxStatusColor).SetDefaultValue(new ColorConfig.StatusBoxNumberColors + { + Reserved = + new ColorConfig.AppearanceSettings { BackgroundColor = "#FFF84C", TextColor = "#000000" }, + InUse = new ColorConfig.AppearanceSettings { BackgroundColor = "#FFF84C", TextColor = "#000000" }, + Available = new ColorConfig.AppearanceSettings + { BackgroundColor = "#FFF84C", TextColor = "#000000" }, + Locked = new ColorConfig.AppearanceSettings { BackgroundColor = "#FFF84C", TextColor = "#000000" }, + Transferable = new ColorConfig.AppearanceSettings + { BackgroundColor = "#FFF84C", TextColor = "#000000" }, + Exitus = new ColorConfig.AppearanceSettings { BackgroundColor = "#FFF84C", TextColor = "#000000" }, + Altable = new ColorConfig.AppearanceSettings { BackgroundColor = "#FFF84C", TextColor = "#000000" } + }).SetIgnoreIfNull(true); + cm.MapMember(c => c.Therapy).SetDefaultValue(new ColorConfig.TherapyColors + { + Default = new ColorConfig.AppearanceSettings(), + Finished = new ColorConfig.AppearanceSettings(), + Initialized = new ColorConfig.AppearanceSettings(), + InProgress = new ColorConfig.AppearanceSettings() + }).SetIgnoreIfNull(true); + cm.MapMember(c => c.Test).SetDefaultValue(new ColorConfig.TestColors + { + Default = new ColorConfig.AppearanceSettings + { + BackgroundColor = "#00A4E1", + TextColor = "#FFF" + }, + Finished = new ColorConfig.AppearanceSettings + { + BackgroundColor = "#00C49B", + TextColor = "#FFF", + Icon = "assets/icon/light-theme/ic-tick.svg", + IconDefault = "icTick" + }, + Initialized = new ColorConfig.AppearanceSettings + { + BackgroundColor = "#ED4965", + TextColor = "#FFF" + }, + Expired = new ColorConfig.AppearanceSettings + { + BackgroundColor = "#ED4965", + TextColor = "#FFF", + Icon = "assets/icon/light-theme/ic-clock.svg", + IconDefault = "icClock" + } + }).SetIgnoreIfNull(true); + cm.MapMember(c => c.Procedure).SetDefaultValue(new ColorConfig.ProcedureColors + { + Default = new ColorConfig.AppearanceSettings + { + BackgroundColor = "#00A4E1", + TextColor = "#FFF" + }, + Finished = new ColorConfig.AppearanceSettings + { + BackgroundColor = "#00C49B", + TextColor = "#FFF", + Icon = "assets/icon/light-theme/ic-tick.svg", + IconDefault = "icTick" + }, + Initialized = new ColorConfig.AppearanceSettings + { + BackgroundColor = "#ED4965", + TextColor = "#FFF" + }, + Expired = new ColorConfig.AppearanceSettings + { + BackgroundColor = "#ED4965", + TextColor = "#FFF", + Icon = "assets/icon/light-theme/ic-clock.svg", + IconDefault = "icClock" + } + }).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ColorConfig.StatusBoxNumberColors))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Reserved).SetDefaultValue(new ColorConfig.AppearanceSettings + { + BackgroundColor = "#FFF84C", + TextColor = "#000000" + }); + cm.MapMember(c => c.InUse).SetDefaultValue(new ColorConfig.AppearanceSettings()); + cm.MapMember(c => c.Available).SetDefaultValue(new ColorConfig.AppearanceSettings + { + BackgroundColor = "#57B812", + TextColor = "#000000" + }); + cm.MapMember(c => c.Locked).SetDefaultValue(new ColorConfig.AppearanceSettings + { + BackgroundColor = "#ED4965", + TextColor = "#000000" + }); + cm.MapMember(c => c.Transferable).SetDefaultValue(new ColorConfig.AppearanceSettings + { + BackgroundColor = "#57B812", + TextColor = "#000000" + }); + cm.MapMember(c => c.Exitus).SetDefaultValue(new ColorConfig.AppearanceSettings + { + BackgroundColor = "#24BFF9", + TextColor = "#000000" + }); + cm.MapMember(c => c.Altable).SetDefaultValue(new ColorConfig.AppearanceSettings + { + BackgroundColor = "#24BFF9", + TextColor = "#000000" + }); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ColorConfig.TherapyColors))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Default).SetDefaultValue(new ColorConfig.AppearanceSettings + { + BackgroundColor = "#00A4E1", + TextColor = "#FFF", + IconInvertColor = 1 + }); + cm.MapMember(c => c.Initialized).SetDefaultValue(new ColorConfig.AppearanceSettings + { + BackgroundColor = "#FFF", + TextColor = "#000" + }); + cm.MapMember(c => c.Finished).SetDefaultValue(new ColorConfig.AppearanceSettings + { + BackgroundColor = "#FFF", + TextColor = "#000", + Icon = "assets/icon/light-theme/ic-tick.svg", + IconDefault = "icTick" + }); + cm.MapMember(c => c.InProgress).SetDefaultValue(new ColorConfig.AppearanceSettings + { + BackgroundColor = "#FFF", + TextColor = "#000" + }); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ColorConfig.AppearanceSettings))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.TextColor).SetDefaultValue("#000000"); + cm.MapMember(c => c.BackgroundColor).SetDefaultValue("#FFFFFF"); + cm.MapMember(c => c.Icon).SetIgnoreIfNull(true); + cm.MapMember(c => c.IconDefault).SetIgnoreIfNull(true); + cm.MapMember(c => c.IconColor).SetIgnoreIfNull(true); + cm.MapMember(c => c.IconCategory).SetIgnoreIfNull(true); + cm.MapMember(c => c.IconInvertColor).SetDefaultValue(0.0); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ColorConfig.TestColors))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Default).SetDefaultValue(new ColorConfig.AppearanceSettings + { + BackgroundColor = "#00A4E1", + TextColor = "#FFF" + }); + cm.MapMember(c => c.Initialized).SetDefaultValue(new ColorConfig.AppearanceSettings + { + BackgroundColor = "#ED4965", + TextColor = "#FFF" + }); + cm.MapMember(c => c.Finished).SetDefaultValue(new ColorConfig.AppearanceSettings + { + BackgroundColor = "#00C49B", + TextColor = "#FFF", + Icon = "assets/icon/light-theme/ic-tick.svg", + IconDefault = "icTick" + }); + cm.MapMember(c => c.Expired).SetDefaultValue(new ColorConfig.AppearanceSettings + { + BackgroundColor = "#ED4965", + TextColor = "#FFF", + Icon = "assets/icon/light-theme/ic-clock.svg", + IconDefault = "icClock" + }); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ColorConfig.ProcedureColors))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Default).SetDefaultValue(new ColorConfig.AppearanceSettings + { + BackgroundColor = "#00A4E1", + TextColor = "#FFF" + }); + cm.MapMember(c => c.Initialized).SetDefaultValue(new ColorConfig.AppearanceSettings + { + BackgroundColor = "#ED4965", + TextColor = "#FFF" + }); + cm.MapMember(c => c.Finished).SetDefaultValue(new ColorConfig.AppearanceSettings + { + BackgroundColor = "#00C49B", + TextColor = "#FFF", + Icon = "assets/icon/light-theme/ic-tick.svg", + IconDefault = "icTick" + }); + cm.MapMember(c => c.Expired).SetDefaultValue(new ColorConfig.AppearanceSettings + { + BackgroundColor = "#ED4965", + TextColor = "#FFF", + Icon = "assets/icon/light-theme/ic-clock.svg", + IconDefault = "icClock" + }); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ColorConfig.LevelColors))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Level1).SetDefaultValue("#FFFFFF"); + cm.MapMember(c => c.Level2).SetDefaultValue("#FAFF41"); + cm.MapMember(c => c.Level3).SetDefaultValue("#F5A623"); + cm.MapMember(c => c.Level4).SetDefaultValue("#C2510F"); + cm.MapMember(c => c.Level5).SetDefaultValue("#FF5D6A"); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ColorConfig.TextColors))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Normal).SetDefaultValue("#FFFFFF"); + cm.MapMember(c => c.Warning).SetDefaultValue("#FFAD26"); + cm.MapMember(c => c.Alert).SetDefaultValue("#FF5D6A"); + cm.MapMember(c => c.Improve).SetDefaultValue("#60D61D"); + cm.MapMember(c => c.Expired).SetDefaultValue("#333333"); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ColorConfig.ArrowColors))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Normal).SetDefaultValue("#FFFFFF"); + cm.MapMember(c => c.Warning).SetDefaultValue("#F5A623"); + cm.MapMember(c => c.Alert).SetDefaultValue("#FF5D6A"); + cm.MapMember(c => c.Improve).SetDefaultValue("#60D61D"); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ColorConfig.IndicatorColors))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Empty).SetDefaultValue("#CCCCCC"); + cm.MapMember(c => c.Warning).SetDefaultValue("#FFAD26"); + cm.MapMember(c => c.Normal).SetDefaultValue("#60D61D"); + cm.MapMember(c => c.Alert).SetDefaultValue("#FF5D6A"); + cm.MapMember(c => c.Background).SetDefaultValue("#000000"); + cm.MapMember(c => c.EmptyBackground).SetDefaultValue("#CCCCCC"); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ColorConfig.GraphColors))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Normal).SetDefaultValue("#60D61D"); + cm.MapMember(c => c.Warning).SetDefaultValue("#FFAD26"); + cm.MapMember(c => c.Alert).SetDefaultValue("#FF5D6A"); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/ComunicationFlowMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/ComunicationFlowMapContributor.cs new file mode 100644 index 00000000..7e2817dc --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/ComunicationFlowMapContributor.cs @@ -0,0 +1,40 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.Recording; +using adas_core.Domain.Models.SignalR; +using adas_core.Domain.Models.SystemAlerts; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson.Serialization; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class ComunicationFlowMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(Message))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.UnmapMember(c => c.Operation); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Queue))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ApiRequest))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(AdmPanelRequest))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.GetMemberMap(c => c.ObsertationData).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ApiClients))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(VideoDto))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/DeviceMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/DeviceMapContributor.cs new file mode 100644 index 00000000..046b27f1 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/DeviceMapContributor.cs @@ -0,0 +1,81 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Utils; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class DeviceMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(DeviceActionType))) + { + BsonClassMap.RegisterClassMap(cm => cm.AutoMap() ); + } + + if (!BsonClassMap.IsClassMapRegistered(typeof(DeviceAction))) + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Type) + .SetDefaultValue(DeviceActionType.Unknown) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.ConfigObservationId).SetIgnoreIfNull(true); + cm.MapMember(c => c.AlarmName).SetIgnoreIfNull(true); + cm.MapMember(c => c.ValueOnSingleClick) + .SetDefaultValue(new object()) + .SetSerializer(new ComplexObjectValueTypeSerializer()); + cm.MapMember(c => c.ValueOnDoubleClick) + .SetDefaultValue(new object()) + .SetSerializer(new ComplexObjectValueTypeSerializer()); + cm.MapMember(c => c.ValueOnHoldClick) + .SetDefaultValue(new object()) + .SetSerializer(new ComplexObjectValueTypeSerializer()); + } + ); + } + + if (!BsonClassMap.IsClassMapRegistered(typeof(DeviceSettings))) + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Action).SetDefaultValue(new DeviceAction()); + } + ); + } + + if (!BsonClassMap.IsClassMapRegistered(typeof(Device))) + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.DeviceType) + .SetDefaultValue(DeviceType.Unknown) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.Uuid).SetIgnoreIfNull(true); + cm.MapMember(c => c.MacAddr).SetIgnoreIfNull(true); + cm.MapMember(c => c.CreatedAt).SetDefaultValue(DateTime.UtcNow); + cm.MapMember(c => c.UpdatedAt).SetDefaultValue(DateTime.UtcNow); + cm.MapMember(c => c.Key).SetIgnoreIfNull(true); + cm.MapMember(c => c.Color).SetIgnoreIfNull(true); + cm.MapMember(c => c.Battery).SetIgnoreIfNull(true); + cm.MapMember(c => c.Name).SetIgnoreIfNull(true); + cm.MapMember(c => c.SerialNumber).SetIgnoreIfNull(true); + cm.MapMember(c => c.PointOfCareIds).SetDefaultValue(new List()); + cm.MapMember(c => c.Connected).SetDefaultValue(false).SetIgnoreIfNull(true); + cm.MapMember(c => c.Ready).SetDefaultValue(false).SetIgnoreIfNull(true); + cm.MapMember(c => c.DeviceType) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.Settings).SetDefaultValue(new DeviceSettings()); + } + ); + } + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/DisplayHomeConfigContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/DisplayHomeConfigContributor.cs new file mode 100644 index 00000000..4fa31490 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/DisplayHomeConfigContributor.cs @@ -0,0 +1,46 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class DisplayHomeConfigContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(CardConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.Rows).SetDefaultValue(new List()); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(RowCardConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Cells).SetDefaultValue(new List()); + cm.MapMember(c => c.GrowPriority).SetDefaultValue(1); + cm.MapMember(c => c.Border).SetIgnoreIfNull(true); + cm.MapMember(c => c.BorderRadius).SetIgnoreIfNull(true); + cm.MapMember(c => c.Title).SetIgnoreIfNull(true); + cm.MapMember(c => c.Size).SetIgnoreIfNull(true); + cm.MapMember(c => c.MarginTop).SetIgnoreIfNull(true); + cm.MapMember(c => c.MarginBottom).SetIgnoreIfNull(true); + cm.MapMember(c => c.MarginLeft).SetIgnoreIfNull(true); + cm.MapMember(c => c.MarginRight).SetIgnoreIfNull(true); + cm.MapMember(c => c.BgColor).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingTop).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingBottom).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingLeft).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingRight).SetIgnoreIfNull(true); + cm.MapMember(c => c.Type) + .SetDefaultValue(DisplayConfigEnums.CellType.Default) + .SetSerializer(new EnumSerializer(BsonType.String)); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/DisplayMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/DisplayMapContributor.cs new file mode 100644 index 00000000..d5d2f527 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/DisplayMapContributor.cs @@ -0,0 +1,134 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class DisplayMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(Display))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.UnmapProperty(c => c.Unit); + cm.UnmapProperty(c => c.PointOfCares); + cm.UnmapProperty(c => c.DisplayConfig); + cm.MapMember(c => c.Type) + .SetDefaultValue(DisplayConfigEnums.DisplayType.Unknown) + .SetSerializer(new EnumSerializer(BsonType.String)); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(DisplayNurse))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(StandarDisplay))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.SectionConfig).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(SmartDisplay))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(PumpDisplay))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(DisplayConfig))) + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.AddKnownType(typeof(DisplayNurse)); + cm.AddKnownType(typeof(StandarDisplay)); + cm.AddKnownType(typeof(SmartDisplay)); + cm.AddKnownType(typeof(PumpDisplay)); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.Type) + .SetDefaultValue(DisplayConfigEnums.DisplayType.Unknown) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.MediaFolder).SetIgnoreIfNull(true); + cm.MapMember(c => c.CardConfigId).SetIgnoreIfNull(true); + cm.MapMember(c => c.CardConfig) + .SetIgnoreIfNull(true) + .SetIsRequired(false); + cm.MapMember(c => c.DetailConfigId).SetIgnoreIfNull(true); + cm.MapMember(c => c.DetailConfig) + .SetIgnoreIfNull(true) + .SetIsRequired(false); + cm.MapMember(c => c.HomeBanner).SetIgnoreIfNull(true); + cm.MapMember(c => c.FormConfig) + .SetDefaultValue(new FormConfig + { + Admission = new FormItemOverview { Nhc = true }, + Demographic = new FormItemOverview { Nhc = true }, + Discharge = new FormItemOverview { Nhc = true }, + IncomeInfo = new FormItemOverview { Nhc = true } + }); + cm.MapMember(c => c.HasCameras).SetIgnoreIfNull(true); + cm.MapMember(c => c.HasSound).SetIgnoreIfNull(true); + cm.MapMember(c => c.IsRotationEnabled).SetIgnoreIfNull(true); + cm.MapMember(c => c.CanChangeCameraMode).SetIgnoreIfNull(true); + cm.MapMember(c => c.CamerasAreActive).SetIgnoreIfNull(true); + cm.MapMember(c => c.CameraStreamType).SetIgnoreIfNull(true); + cm.MapMember(c => c.SensorList).SetIgnoreIfNull(true); + cm.MapMember(c => c.ObservationForIndicator).SetIgnoreIfNull(true); + cm.MapMember(c => c.AlarmFieldList).SetIgnoreIfNull(true); + cm.MapMember(c => c.RequestGroupedFieldList).SetIgnoreIfNull(true); + cm.MapMember(c => c.Pumps).SetIgnoreIfNull(true); + cm.MapMember(c => c.ChartConfig).SetIgnoreIfNull(true); + cm.MapMember(c => c.GraphLayout).SetIgnoreIfNull(true); + cm.MapMember(c => c.CardRotatingLayout).SetIgnoreIfNull(true); + cm.MapMember(c => c.ChartConfigIdList).SetIgnoreIfNull(true); + cm.MapMember(c => c.HomeConfig).SetIgnoreIfNull(true); + cm.MapMember(c => c.HeaderConfig).SetIgnoreIfNull(true); + cm.MapMember(c => c.DisplaySectionIdList).SetDefaultValue(new List()); + cm.MapMember(c => c.Hospital).SetIgnoreIfNull(true); + cm.MapMember(c => c.ColorConfig).SetDefaultValue(new ColorConfig()); + cm.MapMember(c => c.FieldList).SetDefaultValue(new List()); + cm.MapMember(c => c.GroupedFieldList).SetDefaultValue(new List()); + + cm.UnmapProperty(c => c.DisplaySectionList); + // cm.UnmapProperty(c => c.CardConfig); + }); + + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Name).SetIgnoreIfNull(true); + cm.MapMember(c => c.Names).SetIgnoreIfNull(true); + cm.MapMember(c => c.Group).SetIgnoreIfNull(true); + cm.MapMember(c => c.StartTimeShift).SetIgnoreIfNull(true); + cm.MapMember(c => c.Max); + cm.MapMember(c => c.Regularity) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + cm.MapMember(c => c.Since) + .SetDefaultValue(GroupedObservationEnum.Since.Last) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.Result) + .SetIgnoreIfNull(true); + //.SetSerializer(new EnumSerializer(BsonType.Array)); + cm.MapMember(c => c.LabelList).SetIgnoreIfNull(true); + }); + } + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/FormMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/FormMapContributor.cs new file mode 100644 index 00000000..6becda68 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/FormMapContributor.cs @@ -0,0 +1,49 @@ +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson.Serialization; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class FormMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(FormConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Admission).SetDefaultValue(new FormItemOverview()); + cm.MapMember(c => c.Demographic).SetIgnoreIfNull(true); + cm.MapMember(c => c.Discharge).SetIgnoreIfNull(true); + cm.MapMember(c => c.IncomeInfo).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(FormItemOverview))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Nhc).SetDefaultValue(true); + cm.MapMember(c => c.Bed).SetIgnoreIfNull(true); + cm.MapMember(c => c.Name).SetIgnoreIfNull(true); + cm.MapMember(c => c.LastName).SetIgnoreIfNull(true); + cm.MapMember(c => c.SecondName).SetIgnoreIfNull(true); + cm.MapMember(c => c.Genre).SetIgnoreIfNull(true); + cm.MapMember(c => c.Birthday).SetIgnoreIfNull(true); + cm.MapMember(c => c.Origin).SetIgnoreIfNull(true); + cm.MapMember(c => c.OriginAux).SetIgnoreIfNull(true); + cm.MapMember(c => c.Diagnostic).SetIgnoreIfNull(true); + cm.MapMember(c => c.DiagnosticAux).SetIgnoreIfNull(true); + cm.MapMember(c => c.Allergy).SetIgnoreIfNull(true); + cm.MapMember(c => c.Language).SetIgnoreIfNull(true); + cm.MapMember(c => c.Insulation).SetIgnoreIfNull(true); + cm.MapMember(c => c.Service).SetIgnoreIfNull(true); + cm.MapMember(c => c.Destination).SetIgnoreIfNull(true); + cm.MapMember(c => c.DestinationAux).SetIgnoreIfNull(true); + cm.MapMember(c => c.AdmDischarge).SetIgnoreIfNull(true); + cm.MapMember(c => c.NurseDischarge).SetIgnoreIfNull(true); + cm.MapMember(c => c.MedicalDischarge).SetIgnoreIfNull(true); + cm.MapMember(c => c.IncomingDate).SetIgnoreIfNull(true); + cm.MapMember(c => c.UciDays).SetIgnoreIfNull(true); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/GraphMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/GraphMapContributor.cs new file mode 100644 index 00000000..c7718c8d --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/GraphMapContributor.cs @@ -0,0 +1,265 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class GraphMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(GraphLayout))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Layout).SetIgnoreIfNull(true); + cm.MapMember(c => c.ObservationTitle).SetIgnoreIfNull(true); + cm.MapMember(c => c.Name).SetIgnoreIfNull(true); + cm.MapMember(c => c.GraphConf).SetIgnoreIfNull(true); + cm.MapMember(c => c.ObservationName).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ChartSettings))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.LegendLayoutConfig).SetIgnoreIfNull(true); + cm.MapMember(c => c.LegendGrowPriority).SetIgnoreIfNull(true); + cm.MapMember(c => c.ChartGrowPriority).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ChartConfig))) + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.BaseConfig).SetDefaultValue(new ChartBaseConfig()); + cm.MapMember(c => c.AxesConfig).SetDefaultValue(new List()); + cm.MapMember(c => c.SeriesConfig).SetDefaultValue(new List()); + }); + + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Title).SetDefaultValue(string.Empty); + cm.MapMember(c => c.Top).SetDefaultValue("7%"); + cm.MapMember(c => c.Right).SetDefaultValue("7%"); + cm.MapMember(c => c.Bottom).SetDefaultValue("7%"); + cm.MapMember(c => c.Left).SetDefaultValue("7%"); + cm.MapMember(c => c.Group).SetDefaultValue(string.Empty); + cm.MapMember(c => c.Name).SetDefaultValue(string.Empty); + cm.MapMember(c => c.BorderWidth).SetDefaultValue(1); + cm.MapMember(c => c.BorderColor).SetDefaultValue(string.Empty); + cm.MapMember(c => c.ShowLegend).SetDefaultValue(false); + cm.MapMember(c => c.ShowGrid).SetDefaultValue(false); + cm.MapMember(c => c.NumValues).SetIgnoreIfNull(true); + cm.MapMember(c => c.BaselineOffset).SetIgnoreIfNull(true); + }); + + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(a => a.Type) + .SetSerializer(new EnumSerializer(BsonType.String)) + .SetDefaultValue(DisplayConfigEnums.AxisType.Value); + cm.MapMember(a => a.KeyName).SetIgnoreIfNull(true); + cm.MapMember(a => a.Position) + .SetSerializer(new EnumSerializer(BsonType.String)) + .SetDefaultValue(DisplayConfigEnums.AxisPosition.Left); + cm.MapMember(a => a.Min).SetIgnoreIfNull(true); + cm.MapMember(a => a.Max).SetIgnoreIfNull(true); + cm.MapMember(a => a.AxisLine).SetIgnoreIfNull(true); + cm.MapMember(a => a.AxisTick).SetIgnoreIfNull(true); + cm.MapMember(a => a.AxisLabel).SetIgnoreIfNull(true); + cm.MapMember(a => a.Silent).SetDefaultValue(true); + cm.MapMember(a => a.LabelFormat) + .SetSerializer(new EnumSerializer(BsonType.String)) + .SetDefaultValue(DisplayConfigEnums.LabelFormat.Hour); + cm.MapMember(a => a.Offset).SetDefaultValue(0.0); + cm.MapMember(a => a.CustomLabels).SetDefaultValue(new List()); + cm.MapMember(a => a.SortLabels).SetDefaultValue(false); + cm.MapMember(a => a.Show).SetDefaultValue(true); + cm.MapMember(a => a.Regularity) + .SetSerializer(new EnumSerializer(BsonType.String)) + .SetDefaultValue(GroupedObservationEnum.Regularity.Hour); + }); + + + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(a => a.Show).SetIgnoreIfNull(true); + cm.MapMember(a => a.Interval).SetIgnoreIfNull(true); + cm.MapMember(a => a.Length).SetIgnoreIfNull(true); + }); + + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(a => a.Show).SetIgnoreIfNull(true); + cm.MapMember(a => a.Color).SetIgnoreIfNull(true); + cm.MapMember(a => a.Margin).SetIgnoreIfNull(true); + cm.MapMember(a => a.FontSize).SetIgnoreIfNull(true); + cm.MapMember(a => a.Silent).SetIgnoreIfNull(true); + }); + + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(a => a.Color).SetIgnoreIfNull(true); + }); + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(a => a.Show).SetIgnoreIfNull(true); + cm.MapMember(a => a.LineStyle).SetIgnoreIfNull(true); + }); + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + // cm.SetIsRootClass(true); + // cm.AddKnownType(typeof(CandlestickSeriesConfig)); + cm.MapMember(s => s.Key).SetElementName("key"); + cm.MapMember(s => s.Color).SetElementName("color"); + cm.MapMember(s => s.Type) + .SetDefaultValue(DisplayConfigEnums.SeriesType.Line) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(s => s.SourceType) + .SetDefaultValue(DisplayConfigEnums.SourceType.Obs) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(s => s.AxesNames) + .SetDefaultValue(new List()); + cm.MapMember(s => s.ShowSymbol).SetDefaultValue(false); + cm.MapMember(s => s.ShowColorOnLegend).SetDefaultValue(false); + cm.MapMember(s => s.ShowOnLegend).SetDefaultValue(true); + cm.MapMember(s => s.Values) + .SetDefaultValue(GroupedObservationEnum.Result.Last) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(s => s.MarkerIcon) + .SetDefaultValue(DisplayConfigEnums.MarkerIcon.None) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(s => s.VisualMap).SetIgnoreIfNull(true); + cm.MapMember(l => l.LineWidth).SetDefaultValue(2); + cm.MapMember(l => l.LineStyle).SetDefaultValue("solid"); + cm.MapMember(v => v.Marker) + .SetDefaultValue(DisplayConfigEnums.MarkerIcon.Kangaroo) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(a => a.AboveBaselineColor).SetIgnoreIfDefault(true); + cm.MapMember(a => a.BelowBaselineColor).SetIgnoreIfDefault(true); + cm.MapMember(a => a.CandleKeyList).SetIgnoreIfDefault(true); + cm.MapMember(p => p.LineType).SetIgnoreIfNull(true); + }); + // BsonClassMap.RegisterClassMap(cm => + // { + // cm.AutoMap(); + // cm.MapMember(l => l.LineWidth).SetDefaultValue(2); + // cm.MapMember(l => l.LineStyle).SetDefaultValue("solid"); + // }); + // BsonClassMap.RegisterClassMap(cm => + // { + // cm.AutoMap(); + // cm.MapMember(v => v.Marker) + // .SetDefaultValue(DisplayConfigEnums.MarkerIcon.Kangaroo) + // .SetSerializer(new EnumSerializer(BsonType.String)); + // }); + // BsonClassMap.RegisterClassMap(cm => + // { + // cm.AutoMap(); + // cm.MapMember(a => a.AboveBaselineColor).SetIgnoreIfDefault(true); + // cm.MapMember(a => a.BelowBaselineColor).SetIgnoreIfDefault(true); + // }); + // BsonClassMap.RegisterClassMap(cm => + // { + // cm.AutoMap(); + // cm.MapMember(a => a.CandleKeyList).SetIgnoreIfDefault(true); + // }); + // if (!BsonClassMap.IsClassMapRegistered(typeof(LineSeriesConfig))) + // { + // BsonClassMap.RegisterClassMap(cm => + // { + // cm.AutoMap(); + // cm.MapMember(p => p.LineStyle).SetDefaultValue("solid"); + // cm.MapMember(p => p.LineWidth).SetDefaultValue(2); + // cm.MapMember(p => p.LineType).SetIgnoreIfNull(true); + // }); + // } + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(a => a.Key).SetIgnoreIfDefault(true); + cm.MapMember(a => a.CandleValueType) + .SetSerializer( + new NullableSerializer(new EnumSerializer(BsonType.String))); + }); + + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(v => v.Show).SetDefaultValue(false); + cm.MapMember(v => v.Dimension).SetDefaultValue(0); + cm.MapMember(v => v.SerieKey).SetIgnoreIfNull(true); + cm.MapMember(v => v.Pieces).SetDefaultValue(new List()); + }); + } + + if (!BsonClassMap.IsClassMapRegistered(typeof(LegendLayoutConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Rows).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(LegendLayoutRow))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.GrowPriority).SetIgnoreIfNull(true); + cm.MapMember(c => c.Columns).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(LegendLayoutColumn))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Key).SetIgnoreIfNull(true); + cm.MapMember(c => c.GrowPriority).SetIgnoreIfNull(true); + cm.MapMember(c => c.Label).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(LegendLabel))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Name).SetIgnoreIfNull(true); + cm.MapMember(c => c.ColorLabel).SetIgnoreIfNull(true); + cm.MapMember(c => c.ColorIcon).SetIgnoreIfNull(true); + cm.MapMember(c => c.ShowSymbol).SetIgnoreIfNull(true); + cm.MapMember(c => c.IconType).SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Piece))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(p => p.Opacity).SetIgnoreIfNull(true); + cm.MapMember(p => p.LineType) + .SetDefaultValue(DisplayConfigEnums.LineType.Solid) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(p => p.Color).SetIgnoreIfNull(true); + cm.MapMember(p => p.Symbol).SetIgnoreIfNull(true); + cm.MapMember(p => p.SymbolSize).SetIgnoreIfNull(true); + cm.MapMember(p => p.Eq).SetIgnoreIfNull(true); + cm.MapMember(p => p.Neq).SetIgnoreIfNull(true); + cm.MapMember(p => p.Gt).SetIgnoreIfNull(true); + cm.MapMember(p => p.Lt).SetIgnoreIfNull(true); + cm.MapMember(p => p.Gte).SetIgnoreIfNull(true); + cm.MapMember(p => p.Lte).SetIgnoreIfNull(true); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/HeaderMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/HeaderMapContributor.cs new file mode 100644 index 00000000..0bbade45 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/HeaderMapContributor.cs @@ -0,0 +1,39 @@ +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson.Serialization; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class HeaderMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(HeaderConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.PartnerLogo).SetDefaultValue(new HeaderConfig.HeaderItem()).SetIgnoreIfNull(true); + cm.MapMember(c => c.CompanyLogo).SetDefaultValue(new HeaderConfig.HeaderItem()).SetIgnoreIfNull(true); + cm.MapMember(c => c.CenterLogo).SetDefaultValue(new HeaderConfig.HeaderItem()).SetIgnoreIfNull(true); + cm.MapMember(c => c.MeddisLogo).SetDefaultValue(new HeaderConfig.HeaderItem()).SetIgnoreIfNull(true); + cm.MapMember(c => c.UnitName).SetDefaultValue(new HeaderConfig.HeaderItem()).SetIgnoreIfNull(true); + cm.MapMember(c => c.Cameras).SetDefaultValue(new HeaderConfig.HeaderItem()).SetIgnoreIfNull(true); + cm.MapMember(c => c.Sensors).SetDefaultValue(new HeaderConfig.HeaderItem()).SetIgnoreIfNull(true); + cm.MapMember(c => c.Fullscreen).SetDefaultValue(new HeaderConfig.HeaderItem()).SetIgnoreIfNull(true); + cm.MapMember(c => c.Sounds).SetDefaultValue(new HeaderConfig.HeaderItem()).SetIgnoreIfNull(true); + cm.MapMember(c => c.Sidebar).SetDefaultValue(new HeaderConfig.HeaderItem()).SetIgnoreIfNull(true); + cm.MapMember(c => c.CurrentDateTime).SetDefaultValue(new HeaderConfig.HeaderItem()) + .SetIgnoreIfNull(true); + cm.MapMember(c => c.SectionTitle).SetDefaultValue(new HeaderConfig.HeaderItem()).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(HeaderConfig.HeaderItem))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.LogoUrl).SetDefaultValue(() => null) + .SetIgnoreIfNull(true); + cm.MapMember(c => c.IsVisible).SetDefaultValue(true); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/Interfaz/IEntityMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/Interfaz/IEntityMapContributor.cs new file mode 100644 index 00000000..c1030ee7 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/Interfaz/IEntityMapContributor.cs @@ -0,0 +1,9 @@ +namespace adas_core.Infrastructure.Utils.MongoMaps.Interfaz; + +/// +/// Interfaz para clases que contribuyen al registro de mapas de BSON. +/// +public interface IEntityMapContributor +{ + void RegisterMaps(); +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/MasterListMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/MasterListMapContributor.cs new file mode 100644 index 00000000..13416ed6 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/MasterListMapContributor.cs @@ -0,0 +1,158 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.Masters; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class MasterListMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(Element))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.GetMemberMap(c => c.Title).SetDefaultValue(string.Empty); + cm.GetMemberMap(c => c.IsRequired).SetDefaultValue(false); + cm.GetMemberMap(c => c.IsList).SetDefaultValue(false); + cm.GetMemberMap(c => c.ListName).SetDefaultValue(string.Empty); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(OptionListDetails))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.GetMemberMap(c => c.OptionType).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Name).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.IconDefault).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.IconCategory).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.IconColor).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Color).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.BgColor).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Description).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(MasterList))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.UnmapMember(c => c.ManualObservationName); + cm.UnmapMember(c => c.AutoObservationName); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.GetMemberMap(c => c.DefaultLocale).SetIgnoreIfNull(true).SetSerializer( + new NullableSerializer(new EnumSerializer(BsonType.String)) + ); + cm.GetMemberMap(c => c.Name).SetDefaultValue(string.Empty); + cm.GetMemberMap(c => c.Description).SetDefaultValue(string.Empty); + cm.GetMemberMap(c => c.OptionListDetails).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.CanAddElement).SetDefaultValue(false); + cm.MapMember(c => c.ListType) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.Options).SetDefaultValue(new List()); + }); + if (!BsonClassMap.IsClassMapRegistered(typeof(LocaleItem))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.GetMemberMap(c => c.Name).SetIgnoreIfNull(true); + }); + if (!BsonClassMap.IsClassMapRegistered(typeof(Locale))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.GetMemberMap(c => c.Es).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Pt).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Eng).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Ca).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Zh).SetIgnoreIfNull(true); + }); + if (!BsonClassMap.IsClassMapRegistered(typeof(OptionList))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id").SetSerializer(new NullableSerializer(new ObjectIdSerializer(BsonType.ObjectId)));; + cm.GetMemberMap(c => c.OptionType).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Name).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.LocaleItems).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.IconDefault).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.IconCategory).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.IconColor).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Color).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.BgColor).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Description).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.InitDate).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.EndDate).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.IsDefault).SetIgnoreIfNull(true); + cm.SetDiscriminator(nameof(OptionList)); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(AltableOptionList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(VisitOptionList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(AccessControlList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(MobilityOptionList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(TherapeuticCeilingList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(PassiveSittingList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(GenericList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ProcedureList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(TestList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(DiagnosisList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(OriginList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(DestinationList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(InternalDestinationList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(TreatmentList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(PatientStatusList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ServiceList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(AllergyList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(DoctorTypeList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(DoctorList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(InsulationList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(DischargeStatusList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(LanguageBarrierList))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/NoticeControlMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/NoticeControlMapContributor.cs new file mode 100644 index 00000000..099466ff --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/NoticeControlMapContributor.cs @@ -0,0 +1,28 @@ +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson.Serialization; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class NoticeControlMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(Notice))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(StaffInfo))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(MedicalStaffConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.MapMember(c => c.HasTeams).SetIgnoreIfNull(true); + cm.MapMember(c => c.StaffAmount).SetIgnoreIfNull(true); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/ObsUnitMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/ObsUnitMapContributor.cs new file mode 100644 index 00000000..1f84397d --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/ObsUnitMapContributor.cs @@ -0,0 +1,27 @@ +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson.Serialization; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class ObsUnitMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(ConfigUnits))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id").SetDefaultValue(string.Empty); + cm.MapMember(c => c.Items).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ConfigUnitItem))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.GetMemberMap(c => c.Code).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Value).SetIgnoreIfNull(true); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/ObservationMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/ObservationMapContributor.cs new file mode 100644 index 00000000..8553d8ad --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/ObservationMapContributor.cs @@ -0,0 +1,289 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Observations; +using adas_core.Domain.Utils; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using static adas_core.Domain.Models.GroupedObservation; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class ObservationMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(BasePatientObservation))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.PatientId).SetElementName("patientid"); + cm.MapMember(c => c.UserId).SetIgnoreIfNull(true); + cm.MapMember(c => c.ClinicalEpisode).SetIgnoreIfNull(true); + + cm.UnmapMember(c => c.Patient); + cm.MapMember(c => c.SystemId).SetIgnoreIfNull(true); + cm.MapMember(c => c.Code).SetIgnoreIfNull(true); + cm.MapMember(c => c.CodingSystem).SetIgnoreIfNull(true); + cm.MapMember(c => c.ParentData).SetIgnoreIfNull(true); + + cm.MapMember(c => c.Time); + cm.MapMember(c => c.EndTime).SetIgnoreIfNull(true); + + cm.MapMember(c => c.Name).SetIgnoreIfNull(true); + cm.MapMember(c => c.Units).SetIgnoreIfNull(true); + cm.UnmapMember(c => c.CheckObservations); + + cm.UnmapMember(c => c.CreateObservation); + + + cm.MapMember(c => c.PatientId).SetElementName("patientid"); + cm.MapMember(c => c.EndTime).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(BasePatientObservationValue))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Value) + .SetDefaultValue(new object()) + .SetSerializer(new ComplexObjectValueTypeSerializer()); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(PatientObservation))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Max).SetIgnoreIfNull(true); + cm.MapMember(c => c.Min).SetIgnoreIfNull(true); + cm.MapMember(c => c.MaxWarn).SetIgnoreIfNull(true); + cm.MapMember(c => c.MinWarn).SetIgnoreIfNull(true); + cm.MapMember(c => c.WarnColor).SetIgnoreIfNull(true); + cm.MapMember(c => c.AlertColor).SetIgnoreIfNull(true); + cm.UnmapMember(c => c.ShowOnExpired); + cm.MapMember(c => c.InsertMode) + .SetDefaultValue(ObservationEnum.InsertMode.Auto) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.UnmapMember(c => c.Persist); + cm.MapMember(c => c.ColorOnExpired).SetIgnoreIfNull(true); + cm.UnmapMember(c => c.MessageTime); + + cm.MapMember(c => c.Result).SetIgnoreIfNull(true); + cm.MapMember(c => c.Status) + .SetDefaultValue(StatusEnum.Type.Ok) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.UnmapMember(c => c.Level); + cm.UnmapMember(c => c.Expires); + cm.MapMember(c => c.Expired).SetDefaultValue(false); + cm.UnmapMember(c => c.UiConfiguration); + cm.UnmapMember(c => c.Alarm); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ParentDataClass))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.GetMemberMap(c => c.Code).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.CodingSystem).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Name).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ObservationData))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.CodingSystem).SetIgnoreIfNull(true); + cm.MapMember(c => c.Code).SetIgnoreIfNull(true); + cm.MapMember(c => c.Text).SetIgnoreIfNull(true); + cm.MapMember(c => c.Time).SetIgnoreIfNull(true); + cm.MapMember(c => c.Value).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ObservationsRequest))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.PatientNumber).SetIgnoreIfNull(true); + cm.MapMember(c => c.ObservationNames).SetIgnoreIfNull(true); + cm.MapMember(c => c.StartTime).SetIgnoreIfNull(true); + cm.MapMember(c => c.EndTime).SetIgnoreIfNull(true); + cm.MapMember(c => c.PageNumber).SetDefaultValue(1); + cm.MapMember(c => c.PageSize).SetDefaultValue(10); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(DemoConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.ValueOption) + .SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ConfigObservation))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.DemoConfig).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ConfigObservation))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + + cm.MapMember(c => c.Code).SetIgnoreIfNull(true); + cm.MapMember(c => c.CodingSystem).SetIgnoreIfNull(true); + cm.MapMember(c => c.OriginalName).SetIgnoreIfNull(true); + cm.MapMember(c => c.ParentCode).SetIgnoreIfNull(true); + cm.MapMember(c => c.ParentCodingSystem).SetIgnoreIfNull(true); + cm.MapMember(c => c.ParentName).SetIgnoreIfNull(true); + cm.MapMember(c => c.Name).SetIgnoreIfNull(true); + cm.MapMember(c => c.Units).SetIgnoreIfNull(true); + cm.MapMember(c => c.ArrowType) + .SetSerializer(new EnumSerializer(BsonType.String)) + .SetDefaultValue(ObservationEnum.ArrowType.Default); + cm.MapMember(c => c.ShowArrow).SetDefaultValue(true); + cm.MapMember(c => c.ShowValue).SetDefaultValue(true); + cm.MapMember(c => c.ForceUnits).SetDefaultValue(false); + cm.MapMember(c => c.MinAlert).SetIgnoreIfNull(true); + cm.MapMember(c => c.MaxAlert).SetIgnoreIfNull(true); + cm.MapMember(c => c.MaxWarn).SetIgnoreIfNull(true); + cm.MapMember(c => c.MinWarn).SetIgnoreIfNull(true); + cm.MapMember(c => c.WarnColor).SetIgnoreIfNull(true); + cm.MapMember(c => c.AlertColor).SetIgnoreIfNull(true); + cm.MapMember(c => c.AlertValues).SetIgnoreIfNull(true); + cm.MapMember(c => c.WarningValues).SetIgnoreIfNull(true); + cm.MapMember(c => c.ForceWarn).SetDefaultValue(false); + cm.MapMember(c => c.ForceAlert).SetDefaultValue(false); + + cm.MapMember(c => c.Alert).SetIgnoreIfNull(true); + cm.MapMember(c => c.Expires).SetIgnoreIfNull(true); + cm.MapMember(c => c.ShowOnExpired).SetIgnoreIfDefault(true); + cm.MapMember(c => c.Persist).SetIgnoreIfNull(true); + cm.MapMember(c => c.ColorOnExpired).SetIgnoreIfNull(true); + cm.MapMember(c => c.RetentionPolicy) + .SetIgnoreIfNull(true) + .SetSerializer( + new NullableSerializer(new EnumSerializer(BsonType.String))); + + cm.MapMember(c => c.RetentionPolicyValue).SetIgnoreIfNull(true); + cm.MapMember(c => c.LevelCondition).SetIgnoreIfNull(true); + cm.MapMember(c => c.Grouped).SetIgnoreIfNull(true); + cm.MapMember(c => c.UiConfiguration).SetIgnoreIfNull(true); + cm.MapMember(c => c.Alarm).SetIgnoreIfNull(true); + cm.MapMember(c => c.Description).SetIgnoreIfNull(true); + cm.MapMember(c => c.RequiredValue).SetIgnoreIfNull(true); + cm.MapMember(c => c.Preconditions).SetIgnoreIfNull(true); + cm.MapMember(c => c.CheckObservations).SetDefaultValue(false); + cm.MapMember(c => c.CreateObservation).SetIgnoreIfNull(true); + cm.MapMember(c => c.InsertMode) + .SetDefaultValue(ObservationEnum.InsertMode.Auto) + .SetSerializer(new EnumSerializer(BsonType.String)); + + cm.MapMember(c => c.TimeFromMessageTime).SetIgnoreIfDefault(true); + cm.MapMember(c => c.ColorRanges).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ConfigObservation.ColorRange))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(s => s.ValueType) + .SetIgnoreIfNull(true) + .SetSerializer( + new NullableSerializer( + new EnumSerializer(BsonType.String))); + cm.MapMember(s => s.Min).SetIgnoreIfNull(true); + cm.MapMember(s => s.Max).SetIgnoreIfNull(true); + cm.MapMember(s => s.MatchText).SetIgnoreIfNull(true); + cm.MapMember(s => s.MatchBoolean).SetIgnoreIfNull(true); + cm.MapMember(s => s.MinDate).SetIgnoreIfNull(true); + cm.MapMember(s => s.MaxDate).SetIgnoreIfNull(true); + cm.MapMember(s => s.Color).SetIgnoreIfNull(true); + cm.MapMember(s => s.Label).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(GroupedObservation))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.PatientId); + cm.MapMember(c => c.Name).SetDefaultValue(string.Empty); + cm.MapMember(c => c.Group).SetDefaultValue(string.Empty); + cm.MapMember(c => c.Observations).SetDefaultValue(new List()); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(GroupedObservationObs))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Name).SetDefaultValue(string.Empty); + cm.MapMember(c => c.First).SetIgnoreIfNull(true); + cm.MapMember(c => c.Last).SetIgnoreIfNull(true); + cm.MapMember(c => c.Min).SetIgnoreIfNull(true); + cm.MapMember(c => c.Max).SetIgnoreIfNull(true); + + cm.MapMember(c => c.MinAlert).SetIgnoreIfNull(true); + cm.MapMember(c => c.MaxAlert).SetIgnoreIfNull(true); + cm.MapMember(c => c.Average).SetIgnoreIfNull(true); + cm.MapMember(c => c.Sum).SetIgnoreIfNull(true); + cm.MapMember(c => c.Count).SetIgnoreIfNull(true); + cm.MapMember(c => c.HalfHour).SetIgnoreIfNull(true); + cm.MapMember(c => c.LastFilled).SetIgnoreIfNull(true); + cm.MapMember(c => c.Time); + cm.MapMember(c => c.Shift).SetIgnoreIfNull(true); + cm.MapMember(c => c.ShiftDate); + cm.MapMember(c => c.IsFilled); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(GroupedObservationObsValue))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Type) + .SetDefaultValue(StatusEnum.Type.Ok) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.Value); + cm.MapMember(c => c.Time).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(PredictMedicationObservation))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.MedicineText).SetDefaultValue(string.Empty); + cm.MapMember(c => c.DegreeSimilarity); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ObservationTextRule))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.ValueType) + .SetDefaultValue(ObservationEnum.ValueType.String) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.Value).SetIgnoreIfNull(true); + cm.MapMember(c => c.MatchText).SetIgnoreIfNull(true); + cm.MapMember(c => c.MatchBoolean).SetIgnoreIfNull(true); + cm.MapMember(c => c.MatchNumber).SetIgnoreIfNull(true); + cm.MapMember(c => c.MatchDate).SetIgnoreIfNull(true); + cm.MapMember(c => c.Label).SetIgnoreIfNull(true); + cm.MapMember(c => c.MinDate).SetIgnoreIfNull(true); + cm.MapMember(c => c.MaxDate).SetIgnoreIfNull(true); + cm.MapMember(c => c.MinNum).SetIgnoreIfNull(true); + cm.MapMember(c => c.MaxNum).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ConditionsConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Condition).SetIgnoreIfNull(true); + cm.MapMember(c => c.FieldCondition).SetIgnoreIfNull(true); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/PatientMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/PatientMapContributor.cs new file mode 100644 index 00000000..3ba9f077 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/PatientMapContributor.cs @@ -0,0 +1,245 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Options; +using MongoDB.Bson.Serialization.Serializers; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class PatientMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(HistoricalLocation))) + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.AdmTime); + cm.MapMember(c => c.PatientLocation); + }); + + } + + if (!BsonClassMap.IsClassMapRegistered(typeof(Patient))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.GetMemberMap(c => c.PatientNumber).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.PatientId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.AdmTime).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.DischargeStatus).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Altable).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Origin).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.OriginAux).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Diagnosis).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.DiagnosisAux).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Visits).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.AccessControl).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Insulation).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.PatientStatus).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Mobility).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.TherapeuticCeiling).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Procedures).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Tests).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Treatment).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Doctors).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Allergies).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.LanguageBarrier).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.PassiveSitting).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.DisTime).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Person).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.AttendingDoctor).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.LastObservationDate).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.CreationDate).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.UpdateDate).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.ArchiveDate).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.PointOfCareId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.UnitId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.HistoricalLocations).SetIgnoreIfNull(true); + + // No mapear estos campos + cm.UnmapMember(c => c.Bed); + cm.UnmapMember(c => c.UnitString); + cm.UnmapMember(c => c.Room); + cm.UnmapMember(c => c.PointOfCare); + cm.UnmapMember(c => c.Location); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Person))) + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.LastName).SetIgnoreIfNull(true); + cm.MapMember(c => c.FirstName).SetIgnoreIfNull(true); + cm.MapMember(c => c.SecondName).SetIgnoreIfNull(true); + cm.MapMember(c => c.BirthDate).SetIgnoreIfNull(true); + cm.MapMember(c => c.Language).SetIgnoreIfNull(true); + + cm.MapMember(c => c.Gender) + .SetDefaultValue(PatientEnum.Gender.Unknown) + .SetSerializer(new EnumSerializer(BsonType.String)); + + cm.MapMember(c => c.Ids).SetIgnoreIfNull(true); + + cm.MapMember(c => c.HistoricalIds) + .SetIgnoreIfNull(true); + }); + + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Time).SetIgnoreIfNull(true); + cm.MapMember(c => c.PatientIds) + .SetSerializer( + new DictionaryInterfaceImplementerSerializer, string, string>( + DictionaryRepresentation.Document)) + .SetIgnoreIfNull(true); + }); + } + + + if (!BsonClassMap.IsClassMapRegistered(typeof(PatientLocation))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.UnitName).SetIgnoreIfNull(true); + cm.MapMember(c => c.Bed).SetIgnoreIfNull(true); + cm.MapMember(c => c.Room).SetIgnoreIfNull(true); + }); + + + if (!BsonClassMap.IsClassMapRegistered(typeof(PatientLocationAction))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Location).SetIgnoreIfNull(true); + cm.MapMember(c => c.Action) + .SetIgnoreIfNull(true) + .SetSerializer( + new NullableSerializer( + new EnumSerializer(BsonType.String))); + }); + + + if (!BsonClassMap.IsClassMapRegistered(typeof(PatientIncomeData))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Diagnosis).SetIgnoreIfNull(true); + cm.MapMember(c => c.DiagnosisAux).SetIgnoreIfNull(true); + cm.MapMember(c => c.Origin).SetIgnoreIfNull(true); + cm.MapMember(c => c.OriginAux).SetIgnoreIfNull(true); + cm.MapMember(c => c.AdmTime).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(PatientIcca))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Location).SetDefaultValue(new PatientLocation(string.Empty, string.Empty)); + cm.MapMember(c => c.PatientNumber).SetIgnoreIfNull(true); + cm.MapMember(c => c.PatientId).SetIgnoreIfNull(true); + cm.MapMember(c => c.AdmitTime).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(PatientAllergiesValue))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Notes).SetIgnoreIfNull(true); + cm.MapMember(c => c.Type).SetIgnoreIfNull(true); + cm.MapMember(c => c.Value).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(PatientIntravenousLinesValue))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Type).SetIgnoreIfNull(true); + cm.MapMember(c => c.Duration).SetIgnoreIfNull(true); + cm.MapMember(c => c.Action).SetIgnoreIfNull(true); + cm.MapMember(c => c.Location).SetIgnoreIfNull(true); + cm.MapMember(c => c.InsertTime).SetIgnoreIfNull(true); + cm.MapMember(c => c.RemoveTime).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(PatientDiagnosis))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.SetIgnoreExtraElements(true); + + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.PatientId); + cm.MapMember(c => c.Time); + cm.MapMember(c => c.UpdateDate).SetIgnoreIfNull(true); + cm.UnmapMember(c => c.MessageTime); + cm.MapMember(c => c.CodingSystem).SetIgnoreIfNull(true); + cm.MapMember(c => c.Description).SetIgnoreIfNull(true); + cm.MapMember(c => c.Label).SetIgnoreIfNull(true); + cm.MapMember(c => c.Code).SetIgnoreIfNull(true); + cm.MapMember(c => c.State).SetIgnoreIfNull(true); + cm.MapMember(c => c.Category).SetIgnoreIfNull(true); + cm.MapMember(c => c.StartTime).SetIgnoreIfNull(true); + cm.MapMember(c => c.EndTime).SetIgnoreIfNull(true); + cm.UnmapMember(c => c.DiagnosisCode); + cm.UnmapMember(c => c.DiagnosisSystem); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(PatientDrainagesValue))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Type).SetIgnoreIfNull(true); + cm.MapMember(c => c.Location).SetIgnoreIfNull(true); + cm.MapMember(c => c.Volume).SetIgnoreIfNull(true); + cm.MapMember(c => c.Height).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(PatientCarePlan))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.PatientId); + cm.MapMember(c => c.PointOfCareId).SetIgnoreIfNull(true); + cm.MapMember(c => c.PatientNumber).SetIgnoreIfNull(true); + cm.MapMember(c => c.UserId).SetIgnoreIfNull(true); + cm.MapMember(c => c.CarePlan).SetIgnoreIfNull(true); + cm.MapMember(c => c.Description).SetIgnoreIfNull(true); + cm.MapMember(c => c.Time).SetIgnoreIfNull(true); + + cm.MapMember(c => c.Action) + .SetIgnoreIfNull(true) + .SetSerializer( + new NullableSerializer( + new EnumSerializer(BsonType.String))); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Admission))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.UnmapMember(c => c.PatientLocation); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Discharge))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.UnmapMember(c => c.Patient); + cm.UnmapMember(c => c.PatientLocation); + cm.MapMember(c => c.MedicalDischarge).SetIgnoreIfNull(true); + cm.MapMember(c => c.AdminDischarge).SetIgnoreIfNull(true); + cm.MapMember(c => c.NurseDischarge).SetIgnoreIfNull(true); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/PoCMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/PoCMapContributor.cs new file mode 100644 index 00000000..4dd52099 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/PoCMapContributor.cs @@ -0,0 +1,96 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; +using LightBeacon = adas_core.Domain.Models.MongoModels.LightBeacon; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class PoCMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(PointOfCare))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.Room).SetDefaultValue(string.Empty); + cm.MapMember(c => c.Bed).SetDefaultValue(string.Empty); + cm.MapMember(c => c.Hall).SetIgnoreIfNull(true); + cm.MapMember(c => c.UnitId); + cm.MapMember(c => c.Configuration).SetIgnoreIfNull(true); + cm.MapMember(c => c.Status) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.AdmissionId).SetIgnoreIfNull(true); + cm.MapMember(c => c.IsActive).SetIgnoreIfNull(true); + cm.MapMember(c => c.IsVisible).SetIgnoreIfNull(true); + cm.UnmapMember(c => c.UnitName); + cm.UnmapMember(c => c.Unit); + cm.UnmapMember(c => c.Location); + cm.UnmapMember(c => c.Observations); + cm.UnmapMember(c => c.HasPatient); + cm.UnmapMember(c => c.Patientid); + cm.UnmapMember(c => c.Patient); + cm.UnmapMember(c => c.Admission); + + }); + if (!BsonClassMap.IsClassMapRegistered(typeof(PointOfCareConfiguration))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.BeaconIdList) + .SetDefaultValue(new List()); + cm.MapMember(c => c.CameraIdList) + .SetDefaultValue(new List()); + cm.MapMember(c => c.RelayIdList).SetIgnoreIfNull(true); + cm.MapMember(c => c.Type).SetIgnoreIfNull(true); + cm.MapMember(c => c.Id).SetIgnoreIfNull(true); + + cm.MapMember(c => c.BeaconList) + .SetIgnoreIfNull(true) + .SetIsRequired(false); + + cm.MapMember(c => c.CameraList) + .SetIgnoreIfNull(true) + .SetIsRequired(false); + + cm.MapMember(c => c.RelayList) + .SetIgnoreIfNull(true) + .SetIsRequired(false); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(PoCMapping))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.PointOfCares).SetDefaultValue(new List()); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(PoCMappingItem))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.NewPoC).SetElementName("new"); + cm.MapMember(c => c.OriginalPoC).SetElementName("original"); + cm.MapMember(c => c.Beds).SetDefaultValue(new List>()); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(PoCSettings))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.PatientLocation).SetIgnoreIfNull(true); + cm.MapMember(c => c.ManualRelayStatus) + .SetIgnoreIfNull(true) + .SetSerializer( + new NullableSerializer( + new EnumSerializer(BsonType.String))); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/PumpMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/PumpMapContributor.cs new file mode 100644 index 00000000..5725031e --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/PumpMapContributor.cs @@ -0,0 +1,380 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Pumps; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace adas_core.Infrastructure.Utils.MongoMaps +{ + public class PumpMapContributor : IEntityMapContributor + { + public void RegisterMaps() + { + // ============================================================ + // CommonPumpTypes: PumpValue + // ============================================================ + if (!BsonClassMap.IsClassMapRegistered(typeof(CommonPumpTypes.PumpValue))) + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Value).SetIgnoreIfNull(true); + cm.MapMember(c => c.Units).SetIgnoreIfNull(true); + }); + } + + // ============================================================ + // CommonPumpTypes: SyringeDetails + // ============================================================ + if (!BsonClassMap.IsClassMapRegistered(typeof(CommonPumpTypes.SyringeDetails))) + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Type).SetIgnoreIfNull(true); + cm.MapMember(c => c.Manufacturer).SetIgnoreIfNull(true); + cm.MapMember(c => c.Volume).SetIgnoreIfNull(true); + }); + } + + // ============================================================ + // PatientPumpObservation (nuevo modelo simplificado) + // ============================================================ + if (!BsonClassMap.IsClassMapRegistered(typeof(PumpObservation))) + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + + // Identificadores + cm.MapMember(c => c.DeviceId).SetIgnoreIfNull(true); + cm.MapMember(c => c.RackId).SetIgnoreIfNull(true); + cm.MapMember(c => c.DeviceTypeMdc).SetIgnoreIfNull(true); + cm.MapMember(c => c.InfusionId).SetIgnoreIfNull(true); + + cm.MapMember(c => c.PatientId).SetIgnoreIfNull(true).SetElementName("patientid"); + + + //campos para Alaris + cm.MapMember(c => c.Pressure).SetIgnoreIfNull(true); + cm.MapMember(c => c.DiluentVolume).SetIgnoreIfNull(true); + cm.MapMember(c => c.PatientHeight).SetIgnoreIfNull(true); + cm.MapMember(c => c.BasalRate).SetIgnoreIfNull(true); + cm.MapMember(c => c.TotalAux).SetIgnoreIfNull(true); + cm.MapMember(c => c.GatewayNumber).SetIgnoreIfNull(true); + cm.MapMember(c => c.Number).SetIgnoreIfNull(true); + cm.MapMember(c => c.Total).SetIgnoreIfNull(true); + cm.MapMember(c => c.IsAux).SetIgnoreIfNull(true); + cm.MapMember(c => c.Code).SetIgnoreIfNull(true); + cm.MapMember(c => c.Name).SetIgnoreIfNull(true); + + cm.UnmapMember(c => c.UiConfiguration); + cm.MapMember(c => c.AlarmMode) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + + // Tiempo + cm.MapMember(c => c.Time).SetIgnoreIfNull(true); + + cm.MapMember(c => c.MessageType) + .SetIgnoreIfNull(true) + .SetSerializer(new EnumSerializer(BsonType.String)); + + // Estado de infusión + cm.MapMember(c => c.IsInfusing).SetIgnoreIfNull(true); + + cm.MapMember(c => c.InfusingStatus) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + + cm.MapMember(c => c.Status) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + + cm.MapMember(c => c.PumpMode) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + + cm.MapMember(c => c.ActiveSourceInfo).SetIgnoreIfNull(true); + cm.MapMember(c => c.InfusionModeDetail).SetIgnoreIfNull(true); + cm.MapMember(c => c.NotDeliveringReason).SetIgnoreIfNull(true); + cm.MapMember(c => c.Source).SetIgnoreIfNull(true); + + // Métricas + cm.MapMember(c => c.FlowFluid).SetIgnoreIfNull(true); + cm.MapMember(c => c.Rate).SetIgnoreIfNull(true); + cm.MapMember(c => c.VolumeInfused).SetIgnoreIfNull(true); + cm.MapMember(c => c.FluidDelivTotal).SetIgnoreIfNull(true); + cm.MapMember(c => c.FluidDelivTotalSet).SetIgnoreIfNull(true); + cm.MapMember(c => c.VolumeRemaining).SetIgnoreIfNull(true); + cm.MapMember(c => c.Vtbi).SetIgnoreIfNull(true); + cm.MapMember(c => c.TimeRemaining).SetIgnoreIfNull(true); + cm.MapMember(c => c.TimeProgrammed).SetIgnoreIfNull(true); + + // Medicación + cm.MapMember(c => c.DrugName).SetIgnoreIfNull(true); + cm.MapMember(c => c.DrugId).SetIgnoreIfNull(true); + cm.MapMember(c => c.Concentration).SetIgnoreIfNull(true); + cm.MapMember(c => c.DoseRate).SetIgnoreIfNull(true); + cm.MapMember(c => c.DrugAmount).SetIgnoreIfNull(true); + cm.MapMember(c => c.DrugDoseDelivered).SetIgnoreIfNull(true); + + // Paciente + cm.MapMember(c => c.PatientWeight).SetIgnoreIfNull(true); + cm.MapMember(c => c.Syringe).SetIgnoreIfNull(true); + + // Ubicación + cm.MapMember(c => c.DeviceIp).SetIgnoreIfNull(true); + cm.MapMember(c => c.PillarAssembly).SetIgnoreIfNull(true); + cm.MapMember(c => c.PillarRackSlot).SetIgnoreIfNull(true); + + // ALARMAS + cm.MapMember(c => c.AlarmType) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + + cm.MapMember(c => c.AlarmDescription).SetIgnoreIfNull(true); + cm.MapMember(c => c.AlarmState).SetIgnoreIfNull(true); + cm.MapMember(c => c.AlarmInactivationState).SetIgnoreIfNull(true); + cm.MapMember(c => c.AlarmPriority).SetIgnoreIfNull(true); + cm.MapMember(c => c.AlarmTypeMdc).SetIgnoreIfNull(true); + + cm.MapMember(c => c.EventPhase) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + + cm.MapMember(c => c.AlertSourceMdc).SetIgnoreIfNull(true); + + // EVENTOS (PCD-10) + cm.MapMember(c => c.Event) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + }); + } + + + // ============================================================ + // PumpState + // ============================================================ + if (!BsonClassMap.IsClassMapRegistered(typeof(PumpState))) + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + + cm.MapMember(c => c.DeviceId).SetIgnoreIfNull(true); + cm.MapMember(c => c.RackId).SetIgnoreIfNull(true); + cm.MapMember(c => c.DeviceTypeMdc).SetIgnoreIfNull(true); + cm.MapMember(c => c.DeviceIp).SetIgnoreIfNull(true); + cm.MapMember(c => c.PillarAssembly).SetIgnoreIfNull(true); + cm.MapMember(c => c.PillarRackSlot).SetIgnoreIfNull(true); + cm.MapMember(c => c.InfusionId).SetIgnoreIfNull(true); + + cm.MapMember(c => c.PatientId).SetIgnoreIfNull(true); + + cm.MapMember(c => c.LastUpdated).SetIgnoreIfNull(true); + + // Estado + cm.MapMember(c => c.IsInfusing).SetIgnoreIfNull(true); + + cm.MapMember(c => c.InfusingStatus) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + + cm.MapMember(c => c.Status) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + + cm.MapMember(c => c.PumpMode) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + + cm.MapMember(c => c.InfusionModeDetail).SetIgnoreIfNull(true); + cm.MapMember(c => c.ActiveSourceInfo).SetIgnoreIfNull(true); + cm.MapMember(c => c.NotDeliveringReason).SetIgnoreIfNull(true); + cm.MapMember(c => c.Source).SetIgnoreIfNull(true); + + // Métricas + cm.MapMember(c => c.FlowFluid).SetIgnoreIfNull(true); + cm.MapMember(c => c.Rate).SetIgnoreIfNull(true); + cm.MapMember(c => c.VolumeInfused).SetIgnoreIfNull(true); + cm.MapMember(c => c.FluidDelivTotal).SetIgnoreIfNull(true); + cm.MapMember(c => c.FluidDelivTotalSet).SetIgnoreIfNull(true); + cm.MapMember(c => c.VolumeRemaining).SetIgnoreIfNull(true); + cm.MapMember(c => c.Vtbi).SetIgnoreIfNull(true); + cm.MapMember(c => c.TimeRemaining).SetIgnoreIfNull(true); + cm.MapMember(c => c.TimeProgrammed).SetIgnoreIfNull(true); + + // Medicación + cm.MapMember(c => c.DrugName).SetIgnoreIfNull(true); + cm.MapMember(c => c.DrugId).SetIgnoreIfNull(true); + cm.MapMember(c => c.Concentration).SetIgnoreIfNull(true); + cm.MapMember(c => c.DoseRate).SetIgnoreIfNull(true); + cm.MapMember(c => c.DrugAmount).SetIgnoreIfNull(true); + cm.MapMember(c => c.DrugDoseDelivered).SetIgnoreIfNull(true); + cm.MapMember(c => c.PatientWeight).SetIgnoreIfNull(true); + cm.MapMember(c => c.Syringe).SetIgnoreIfNull(true); + + // Última alarma resumen + cm.MapMember(c => c.AlarmType) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + + cm.MapMember(c => c.AlarmDescription).SetIgnoreIfNull(true); + cm.MapMember(c => c.AlarmState).SetIgnoreIfNull(true); + cm.MapMember(c => c.AlarmInactivationState).SetIgnoreIfNull(true); + cm.MapMember(c => c.AlarmPriority).SetIgnoreIfNull(true); + cm.MapMember(c => c.AlarmCodeMdc).SetIgnoreIfNull(true); + + cm.MapMember(c => c.Event) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + + cm.MapMember(c => c.EventPhase) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + + // Relay / Comm + cm.MapMember(c => c.CommStatus).SetIgnoreIfNull(true); + cm.MapMember(c => c.RelayState).SetIgnoreIfNull(true); + cm.MapMember(c => c.RelayGuid).SetIgnoreIfNull(true); + }); + } + + // ============================================================ + // PumpAlarmEvent + // ============================================================ + if (!BsonClassMap.IsClassMapRegistered(typeof(PumpAlarmEvent))) + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + + cm.MapMember(c => c.DeviceId).SetIgnoreIfNull(true); + cm.MapMember(c => c.RackId).SetIgnoreIfNull(true); + cm.MapMember(c => c.DeviceTypeMdc).SetIgnoreIfNull(true); + cm.MapMember(c => c.InfusionId).SetIgnoreIfNull(true); + + cm.MapMember(c => c.PatientId).SetIgnoreIfNull(true); + + cm.MapMember(c => c.Time).SetIgnoreIfNull(true); + + cm.MapMember(c => c.AlarmType) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + + cm.MapMember(c => c.AlarmDescription).SetIgnoreIfNull(true); + cm.MapMember(c => c.AlarmState).SetIgnoreIfNull(true); + cm.MapMember(c => c.AlarmInactivationState).SetIgnoreIfNull(true); + cm.MapMember(c => c.AlarmPriority).SetIgnoreIfNull(true); + cm.MapMember(c => c.AlarmTypeMdc).SetIgnoreIfNull(true); + + cm.MapMember(c => c.EventPhase) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + + cm.MapMember(c => c.AlertSourceMdc).SetIgnoreIfNull(true); + + cm.MapMember(c => c.DeviceIp).SetIgnoreIfNull(true); + cm.MapMember(c => c.PillarAssembly).SetIgnoreIfNull(true); + cm.MapMember(c => c.PillarRackSlot).SetIgnoreIfNull(true); + }); + } + + // ============================================================ + // PumpAlarmState + // ============================================================ + if (!BsonClassMap.IsClassMapRegistered(typeof(PumpAlarmState))) + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + + cm.MapMember(c => c.DeviceId).SetIgnoreIfNull(true); + + cm.MapMember(c => c.PatientId).SetIgnoreIfNull(true); + + cm.MapMember(c => c.AlarmType) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + + cm.MapMember(c => c.AlarmCodeMdc).SetIgnoreIfNull(true); + cm.MapMember(c => c.AlarmDescription).SetIgnoreIfNull(true); + cm.MapMember(c => c.AlarmPriority).SetIgnoreIfNull(true); + cm.MapMember(c => c.AlarmState).SetIgnoreIfNull(true); + + cm.MapMember(c => c.LastPhase) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + + cm.MapMember(c => c.FirstSeen).SetIgnoreIfNull(true); + cm.MapMember(c => c.LastUpdated).SetIgnoreIfNull(true); + + cm.MapMember(c => c.AlertSourceMdc).SetIgnoreIfNull(true); + cm.MapMember(c => c.InfusionId).SetIgnoreIfNull(true); + }); + } + + // ============================================================ + // ConfigPumps + // ============================================================ + if (!BsonClassMap.IsClassMapRegistered(typeof(ConfigPumps))) + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.Items).SetIgnoreIfNull(true); + + }); + } + + // ============================================================ + // ConfigPumpItem + // ============================================================ + if (!BsonClassMap.IsClassMapRegistered(typeof(ConfigPumpItem))) + { + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.AlarmType) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + cm.MapMember(c => c.UiConfiguration).SetIgnoreIfNull(true); + cm.MapMember(c => c.Type) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + cm.MapMember(c => c.RetentionPolicy).SetIgnoreIfNull(true); + cm.MapMember(c => c.RetentionPolicyValue).SetIgnoreIfNull(true); + }); + } + + + } + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/RelayContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/RelayContributor.cs new file mode 100644 index 00000000..0470f2a1 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/RelayContributor.cs @@ -0,0 +1,48 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class RelayContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(Relay))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.Type) + .SetDefaultValue(RelayEnum.Type.Door) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.Driver).SetIgnoreIfNull(true); + cm.MapMember(c => c.Ip).SetIgnoreIfNull(true); + cm.MapMember(c => c.Port).SetDefaultValue(0); + cm.MapMember(c => c.RelayNumber).SetDefaultValue(1); + cm.MapMember(c => c.RelayName).SetIgnoreIfNull(true); + cm.MapMember(c => c.Total).SetDefaultValue(1); + cm.MapMember(c => c.RefreshTime).SetDefaultValue(60); + cm.MapMember(c => c.Open).SetDefaultValue(false); + cm.MapMember(c => c.Status) + .SetDefaultValue(RelayEnum.Status.NotInitialized) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.Username).SetIgnoreIfNull(true); + cm.MapMember(c => c.Password).SetIgnoreIfNull(true); + cm.MapMember(c => c.Mode) + .SetDefaultValue(RelayEnum.Mode.OpenedOnClosedOff) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.RebootDelay).SetIgnoreIfNull(true); + cm.MapMember(c => c.Cache).SetDefaultValue(true); + cm.MapMember(c => c.InUse).SetIgnoreIfNull(true).SetIsRequired(false); + cm.MapMember(c => c.ManualRelayStatus) + .SetIgnoreIfNull(true) + .SetSerializer( + new NullableSerializer( + new EnumSerializer(BsonType.String))); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/RowMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/RowMapContributor.cs new file mode 100644 index 00000000..9b86a601 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/RowMapContributor.cs @@ -0,0 +1,91 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class RowMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(RowBoxLayout))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.GrowPriority).SetDefaultValue(1D); + cm.MapMember(c => c.Type) + .SetDefaultValue(DisplayConfigEnums.CellType.Default) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.SubType) + .SetDefaultValue(DisplayConfigEnums.WebDisplayCellSubtype.Default) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.BgColor).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingTop).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingBottom).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingLeft).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingRight).SetIgnoreIfNull(true); + cm.MapMember(c => c.Observations) + .SetDefaultValue(new List()); + cm.MapMember(c => c.Direction) + .SetDefaultValue(DisplayConfigEnums.DirectionEnum.Row) + .SetSerializer(new EnumSerializer(BsonType.String)); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(RowDetailsConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Cells).SetDefaultValue(new List()); + cm.MapMember(c => c.Rows).SetDefaultValue(new List()); + cm.MapMember(c => c.GrowPriority).SetDefaultValue(1.0); + cm.MapMember(c => c.Title).SetIgnoreIfNull(true); + cm.MapMember(c => c.DialogConfig).SetIgnoreIfNull(true); + cm.MapMember(c => c.Type).SetDefaultValue(DisplayConfigEnums.RowType.Demographic) + .SetSerializer(new EnumSerializer(BsonType.String)); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ObservationRowBoxLayout))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.TextRules).SetIgnoreIfNull(true); + cm.MapMember(c => c.ChartSettings).SetIgnoreIfNull(true); + cm.MapMember(c => c.ShowTitle).SetIgnoreIfNull(true); + cm.MapMember(c => c.GrowPriority).SetDefaultValue(1D); + cm.MapMember(c => c.Type) + .SetDefaultValue(DisplayConfigEnums.CellType.Default) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.SubType).SetIgnoreIfNull(true); + cm.MapMember(c => c.Name).SetIgnoreIfNull(true); + cm.MapMember(c => c.Icon).SetIgnoreIfNull(true); + cm.MapMember(c => c.Title).SetIgnoreIfNull(true); + cm.MapMember(c => c.GridColumn).SetDefaultValue(1); + cm.MapMember(c => c.IsColumn).SetIgnoreIfNull(true); + cm.MapMember(c => c.IsStatic).SetIgnoreIfNull(true); + cm.MapMember(c => c.GraphConf).SetIgnoreIfNull(true); + cm.MapMember(c => c.Names).SetIgnoreIfNull(true); + cm.MapMember(c => c.ObservationName).SetIgnoreIfNull(true); + cm.MapMember(c => c.ObservationTitle).SetIgnoreIfNull(true); + cm.MapMember(c => c.Observations).SetIgnoreIfNull(true); + cm.MapMember(c => c.BgColor).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingTop).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingBottom).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingLeft).SetIgnoreIfNull(true); + cm.MapMember(c => c.PaddingRight).SetIgnoreIfNull(true); + cm.MapMember(c => c.Border).SetIgnoreIfNull(true); + cm.MapMember(c => c.BorderRadius).SetIgnoreIfNull(true); + cm.MapMember(c => c.ValuePathKey).SetIgnoreIfNull(true); + cm.MapMember(c => c.ValuePathNested).SetIgnoreIfNull(true); + cm.MapMember(c => c.Size).SetDefaultValue(15.0); + cm.MapMember(c => c.Format).SetIgnoreIfNull(true); + cm.MapMember(c => c.Length).SetIgnoreIfNull(true); + cm.MapMember(c => c.Direction) + .SetIgnoreIfNull(true) + .SetSerializer(new NullableSerializer( + new EnumSerializer(BsonType.String))); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/SectionMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/SectionMapContributor.cs new file mode 100644 index 00000000..5c83b7ab --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/SectionMapContributor.cs @@ -0,0 +1,69 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.BsonConverters; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class SectionMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(Section))) + BsonClassMap.RegisterClassMap
(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c._id); + cm.MapMember(c => c.Id) + .SetIsRequired(true) + .SetDefaultValue(string.Empty); + cm.MapMember(c => c.SectionTitle).SetIgnoreIfNull(true); + cm.MapMember(c => c.PointOfCare).SetIgnoreIfNull(true); + cm.MapMember(c => c.LastUpdate).SetIgnoreIfNull(true); + + cm.MapMember(c => c.Configuration) + .SetSerializer(new DictionaryBsonConverter()); + cm.MapMember(c => c.SectionConfig).SetIgnoreIfNull(true); + cm.MapMember(c => c.DesignProperties).SetIgnoreIfNull(true); + cm.MapMember(c => c.PointOfCareList) + .SetDefaultValue(new Dictionary>()); + cm.MapMember(c => c.Items).SetDefaultValue(new List()); + cm.MapMember(c => c.Status) + .SetIgnoreIfNull(true) + .SetSerializer( + new NullableSerializer(new EnumSerializer(BsonType.String))); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Section.SectionItem))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Group).SetIgnoreIfNull(true); + cm.MapMember(c => c.Boxes).SetDefaultValue(new List()); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(SectionConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Columns).SetIgnoreIfNull(true); + cm.MapMember(c => c.Rows).SetIgnoreIfNull(true); + cm.MapMember(c => c.RefreshValues).SetIgnoreIfNull(true); + cm.MapMember(c => c.RefreshConfig).SetIgnoreIfNull(true); + cm.MapMember(c => c.Steps).SetIgnoreIfNull(true); + cm.MapMember(c => c.DesignProperties).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(MinimalDisplaySection))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetDefaultValue(ObjectId.GenerateNewId()); + cm.MapMember(c => c.Name).SetDefaultValue(string.Empty); + cm.MapMember(c => c.IsSelected).SetDefaultValue(false); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/ServiceConfigMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/ServiceConfigMapContributor.cs new file mode 100644 index 00000000..9c8a5628 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/ServiceConfigMapContributor.cs @@ -0,0 +1,60 @@ +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson.Serialization; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class ServiceConfigMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(ServiceConfig))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.StrId).SetElementName("id") + .SetDefaultValue(string.Empty); + cm.MapMember(c => c.Service).SetDefaultValue(new List()); + cm.MapMember(c => c.Theme).SetIgnoreIfNull(true); + cm.MapMember(c => c.BoxObservations).SetIgnoreIfNull(true); + cm.MapMember(c => c.Score).SetDefaultValue(string.Empty); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ServiceConfigService))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Screen).SetDefaultValue(string.Empty); + cm.MapMember(c => c.Sections).SetDefaultValue(new List()); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ServiceConfigServiceSection))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.BoxId).SetDefaultValue(string.Empty); + cm.MapMember(c => c.Box).SetDefaultValue(string.Empty); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ServiceConfigTheme))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.DefaultTheme).SetElementName("default") + .SetDefaultValue(string.Empty); + cm.MapMember(c => c.Timetables).SetDefaultValue(new List()); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(ServiceConfigThemeTimetable))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Theme).SetDefaultValue(string.Empty); + cm.MapMember(c => c.StartDay).SetDefaultValue(string.Empty); + cm.MapMember(c => c.EndDay).SetDefaultValue(string.Empty); + cm.MapMember(c => c.StartHour).SetDefaultValue(string.Empty); + cm.MapMember(c => c.EndHour).SetDefaultValue(string.Empty); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/StandardMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/StandardMapContributor.cs new file mode 100644 index 00000000..5594bc16 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/StandardMapContributor.cs @@ -0,0 +1,45 @@ +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson.Serialization; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class StandardMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(HistoricalConfigChanges))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + //cm.MapMember(c => c.Id) + // .SetSerializer(new ObjectIdSerializer(BsonType.String)); + //cm.GetMemberMap(c => c.ConfigType) + // .SetSerializer(new NullableSerializer(new EnumSerializer(BsonType.String))); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(UiFontData))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.WidthStepBed).SetIgnoreIfNull(true); + cm.MapMember(c => c.HeightStepBed).SetIgnoreIfNull(true); + cm.MapMember(c => c.FontSizeValue).SetIgnoreIfNull(true); + cm.MapMember(c => c.FontSizeMediumValue).SetIgnoreIfNull(true); + cm.MapMember(c => c.FontSizeHeaderValue).SetIgnoreIfNull(true); + cm.MapMember(c => c.FontSizeLittleValue).SetIgnoreIfNull(true); + cm.MapMember(c => c.FontSizeMicroValue).SetIgnoreIfNull(true); + cm.MapMember(c => c.FontSizeNanoValue).SetIgnoreIfNull(true); + cm.MapMember(c => c.FontSizeLittleTextValue).SetIgnoreIfNull(true); + cm.MapMember(c => c.FontSizeName).SetIgnoreIfNull(true); + cm.MapMember(c => c.FontSizeLittleName).SetIgnoreIfNull(true); + cm.MapMember(c => c.FontSizeUnit).SetIgnoreIfNull(true); + cm.MapMember(c => c.BorderColor).SetIgnoreIfNull(true); + cm.MapMember(c => c.BackGroundColor).SetIgnoreIfNull(true); + cm.MapMember(c => c.BoxMarginTop).SetIgnoreIfNull(true); + cm.MapMember(c => c.BoxMarginBot).SetIgnoreIfNull(true); + cm.MapMember(c => c.BoxMarginLeft).SetIgnoreIfNull(true); + cm.MapMember(c => c.BoxMarginRight).SetIgnoreIfNull(true); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/TreatmentMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/TreatmentMapContributor.cs new file mode 100644 index 00000000..8c6c5cc7 --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/TreatmentMapContributor.cs @@ -0,0 +1,116 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.Observations; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class TreatmentMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(PatientTreatment))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.PatientId) + .SetSerializer(new ObjectIdSerializer(BsonType.String)) + .SetIgnoreIfNull(true); + cm.MapMember(c => c.OrderControl) + .SetSerializer(new EnumSerializer(BsonType.String)); + cm.MapMember(c => c.PlacerOrder).SetIgnoreIfNull(true); + cm.MapMember(c => c.FillerOrder).SetIgnoreIfNull(true); + cm.MapMember(c => c.OrderStatus).SetDefaultValue(string.Empty); + cm.MapMember(c => c.OrderTime).SetIgnoreIfNull(true); + cm.MapMember(c => c.StartTime).SetIgnoreIfNull(true); + cm.MapMember(c => c.EndTime).SetIgnoreIfNull(true); + cm.MapMember(c => c.RequestedGiveCodes).SetDefaultValue(new List()); + cm.MapMember(c => c.RequestedGiveTreatment).SetDefaultValue(string.Empty); + cm.MapMember(c => c.RequestedGiveCodesStatus).SetDefaultValue(new List()); + cm.MapMember(c => c.RequestedGiveAmountMinimum).SetIgnoreIfNull(true); + cm.MapMember(c => c.RequestedGiveAmountMaximum).SetIgnoreIfNull(true); + cm.MapMember(c => c.RequestedGiveUnits).SetIgnoreIfNull(true); + cm.MapMember(c => c.RequestedDosageForm).SetIgnoreIfNull(true); + cm.MapMember(c => c.Notes).SetDefaultValue(new List()); + cm.MapMember(c => c.Routes).SetDefaultValue(new List()); + cm.MapMember(c => c.SingleDose).SetDefaultValue(false); + cm.MapMember(c => c.BoloPom).SetDefaultValue(false); + cm.MapMember(c => c.MessageTime); + cm.MapMember(c => c.SystemId).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Entity))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.EntityIdentifier).SetIgnoreIfNull(true); + cm.MapMember(c => c.NamespaceId).SetIgnoreIfNull(true); + cm.MapMember(c => c.UniversalId).SetIgnoreIfNull(true); + cm.MapMember(c => c.UniversalIdType).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Code))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Identifier).SetDefaultValue(string.Empty); + cm.MapMember(c => c.Text).SetDefaultValue(string.Empty); + cm.MapMember(c => c.CodingSystem).SetDefaultValue(string.Empty); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(CodeStatus))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Code).SetIgnoreIfNull(true); + cm.MapMember(c => c.Status).SetIgnoreIfNull(true); + cm.MapMember(c => c.AdministrationTime).SetIgnoreIfNull(true); + cm.MapMember(c => c.EndAdministrationTime).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Note))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.CommentType).SetDefaultValue(string.Empty); + cm.MapMember(c => c.Comment).SetDefaultValue(string.Empty); + cm.MapMember(c => c.EnteredTime).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(TreatmentRoute))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Route).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(Medicine))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id"); + cm.MapMember(c => c.Name).SetIgnoreIfNull(true); + cm.MapMember(c => c.Codes).SetIgnoreIfNull(true); + cm.MapMember(c => c.Notes).SetIgnoreIfNull(true); + cm.MapMember(c => c.Type).SetIgnoreIfNull(true); + cm.MapMember(c => c.Group).SetIgnoreIfNull(true); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(CodeAction))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.Code).SetIsRequired(true); + cm.MapMember(c => c.Action) + .SetSerializer(new EnumSerializer(BsonType.String)); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(PredictMedicationObservation))) + BsonClassMap.RegisterClassMap(cm => { cm.AutoMap(); }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoMaps/UnitMapContributor.cs b/adas-core.Infrastructure/Utils/MongoMaps/UnitMapContributor.cs new file mode 100644 index 00000000..50dbf3cc --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoMaps/UnitMapContributor.cs @@ -0,0 +1,94 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Utils.MongoMaps.Interfaz; +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Bson.Serialization.Serializers; + +namespace adas_core.Infrastructure.Utils.MongoMaps; + +public class UnitMapContributor : IEntityMapContributor +{ + public void RegisterMaps() + { + if (!BsonClassMap.IsClassMapRegistered(typeof(Unit))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapIdMember(c => c.Id).SetElementName("_id").SetIsRequired(true); + cm.GetMemberMap(c => c.Title).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.Name).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.LastUpdate).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.PointOfCareIds).SetIgnoreIfNull(true); + + cm.UnmapMember(c => c.PocCount); + + cm.MapMember(c => c.Status) + .SetDefaultValue(StatusEnum.Type.Ok) + .SetSerializer( + new NullableSerializer(new EnumSerializer(BsonType.String))); + cm.MapMember(c => c.Configuration).SetDefaultValue(new UnitConfiguration()); + + cm.GetMemberMap(c => c.AltableOptionListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.AllergyListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.DestinationListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.InternalDestinationListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.DiagnosisListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.DoctorListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.DoctorTypeListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.InsulationListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.MobilityOptionListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.OriginListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.PatientStatusListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.ProcedureListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.TestListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.ServiceListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.TherapeuticCeilingListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.TreatmentListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.VisitOptionListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.AccessControlListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.LanguageBarrierListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.DischargeStatusListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.PassiveSittingListId).SetIgnoreIfNull(true); + cm.GetMemberMap(c => c.GenericListId).SetIgnoreIfNull(true); + + // No mapear estos campos + cm.UnmapProperty(c => c.AllergyList); + cm.UnmapProperty(c => c.DestinationList); + cm.UnmapProperty(c => c.DiagnosisList); + cm.UnmapProperty(c => c.DoctorList); + cm.UnmapProperty(c => c.DoctorTypeList); + cm.UnmapProperty(c => c.InsulationList); + cm.UnmapProperty(c => c.MobilityOptionList); + cm.UnmapProperty(c => c.OriginList); + cm.UnmapProperty(c => c.PatientStatusList); + cm.UnmapProperty(c => c.ProcedureList); + cm.UnmapProperty(c => c.TestList); + cm.UnmapProperty(c => c.AltableOptionList); + cm.UnmapProperty(c => c.DischargeStatusList); + cm.UnmapProperty(c => c.ServiceList); + cm.UnmapProperty(c => c.TherapeuticCeilingList); + cm.UnmapProperty(c => c.TreatmentList); + cm.UnmapProperty(c => c.VisitOptionList); + cm.UnmapProperty(c => c.PassiveSittingList); + cm.UnmapProperty(c => c.GenericList); + cm.UnmapProperty(c => c.LanguageBarrierList); + cm.UnmapProperty(c => c.PointOfCares); + cm.UnmapProperty(c => c.InternalDestinationList); + }); + + if (!BsonClassMap.IsClassMapRegistered(typeof(UnitConfiguration))) + BsonClassMap.RegisterClassMap(cm => + { + cm.AutoMap(); + cm.MapMember(c => c.AutoAdt).SetDefaultValue(false); + cm.MapMember(c => c.ManualDischarge).SetDefaultValue(true); + cm.MapMember(c => c.ManualAdmit).SetDefaultValue(true); + cm.MapMember(c => c.ManualMove).SetDefaultValue(true); + cm.MapMember(c => c.ManualEdit).SetDefaultValue(true); + cm.MapMember(c => c.PlanDisplayConfiguration).SetIgnoreIfNull(true); + cm.MapMember(c => c.SmartDisplayConfiguration).SetIgnoreIfNull(true); + cm.MapMember(c => c.StandarDisplayConfiguration).SetIgnoreIfNull(true); + }); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/MongoUtils.cs b/adas-core.Infrastructure/Utils/MongoUtils.cs new file mode 100644 index 00000000..9f59994a --- /dev/null +++ b/adas-core.Infrastructure/Utils/MongoUtils.cs @@ -0,0 +1,90 @@ +using MongoDB.Bson; +using MongoDB.Bson.Serialization; +using MongoDB.Driver; + +namespace adas_core.Infrastructure.Utils; + +public class MongoUtils +{ + public static async Task EnsureIndexes(IMongoCollection collection, + List> expectedIndexes) + { + var existingIndexes = await (await collection.Indexes.ListAsync()).ToListAsync(); + var documentSerializer = BsonSerializer.SerializerRegistry.GetSerializer(); + var serializerRegistry = BsonSerializer.SerializerRegistry; + var renderArgs = new RenderArgs(documentSerializer, serializerRegistry); + foreach (var expectedIndexModel in expectedIndexes) + { + var expectedIndexKeys = expectedIndexModel.Keys + .Render(renderArgs).ToString(); + var existingIndexWithSameKeys = existingIndexes.FirstOrDefault(index => + { + var keys = index.Elements.FirstOrDefault(e => e.Name == "key").Value?.ToString(); + return keys == expectedIndexKeys; + }); + + if (existingIndexWithSameKeys != null) + { + // Existe un índice con las mismas claves, verificar las opciones + if (!IndexOptionsMatch(expectedIndexModel, existingIndexWithSameKeys, renderArgs)) + { + // Las opciones no coinciden, eliminar el índice existente y crear el nuevo + var indexName = existingIndexWithSameKeys.Elements.FirstOrDefault(e => e.Name == "name").Value + ?.AsString; + if (!string.IsNullOrEmpty(indexName) && indexName != "_id_") + try + { + await collection.Indexes.DropOneAsync(indexName); + await collection.Indexes.CreateOneAsync(expectedIndexModel); + Console.WriteLine($"Info: Índice con claves '{expectedIndexKeys}' actualizado."); + } + catch (MongoCommandException ex) when (ex.Code == 85) + { + Console.WriteLine( + $"Warning: No se pudo actualizar el índice con claves '{expectedIndexKeys}'. Error: {ex.Message}"); + } + else + try + { + await collection.Indexes.CreateOneAsync(expectedIndexModel); + } + catch (MongoCommandException ex) when (ex.Code == 85) + { + Console.WriteLine( + $"Warning: El índice con claves '{expectedIndexKeys}' ya existe con diferentes opciones y no se pudo actualizar automáticamente."); + } + } + // Si las opciones coinciden, no se hace nada + } + else + { + // No existe un índice con estas claves, crear el nuevo + try + { + await collection.Indexes.CreateOneAsync(expectedIndexModel); + } + catch (MongoCommandException ex) when (ex.Code == 85) + { + Console.WriteLine($"Warning: El índice con claves '{expectedIndexKeys}' ya existe."); + } + } + } + } + + private static bool IndexOptionsMatch(CreateIndexModel expectedIndexModel, + BsonDocument existingIndex, RenderArgs renderArgs) + { + var expectedIndexOptions = expectedIndexModel.Options; + + var unique = existingIndex.Elements.FirstOrDefault(e => e.Name == "unique").Value?.AsBoolean ?? false; + var background = existingIndex.Elements.FirstOrDefault(e => e.Name == "background").Value?.AsBoolean ?? false; + var partialFilter = existingIndex.Elements.FirstOrDefault(e => e.Name == "partialFilterExpression").Value + ?.ToString(); + var expectedPartialFilter = expectedIndexOptions.PartialFilterExpression + ?.Render(renderArgs).ToString(); + + return unique == expectedIndexOptions.Unique && + background == expectedIndexOptions.Background && + partialFilter == expectedPartialFilter; + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/PatientObservationAlarmConverter.cs b/adas-core.Infrastructure/Utils/PatientObservationAlarmConverter.cs new file mode 100644 index 00000000..a0bb8949 --- /dev/null +++ b/adas-core.Infrastructure/Utils/PatientObservationAlarmConverter.cs @@ -0,0 +1,100 @@ +using System.Reflection; +using adas_core.Domain.Models; +using MongoDB.Bson.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; + +namespace adas_core.Infrastructure.Utils; + +public class PatientObservationAlarmConverter : JsonConverter, IBsonSerializer +{ + public object Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + throw new NotImplementedException(); + } + + public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value) + { + throw new NotImplementedException(); + } + + public Type ValueType => typeof(PatientObservationAlarm); + + public override PatientObservationAlarm? ReadJson(JsonReader reader, Type objectType, + PatientObservationAlarm? existingValue, + bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType == JsonToken.Null) + return null; + + // Implement your custom deserialization logic here + var jsonObject = JObject.Load(reader); + // Deserialize properties from jsonObject to PatientObservationAlarm object + + // Example: Deserialize 'Value' property + var value = jsonObject.GetValue("value")?.ToObject(); + + return new PatientObservationAlarm + { + Value = value ?? new object() + // Other property assignments... + }; + } + + public new void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value != null) + { + var jo = JObject.FromObject(value); + + // Add [JsonConverter(typeof(StringEnumConverter))] attribute to specified properties + AddStringEnumConverterAttribute(value, "eventPhase"); + AddStringEnumConverterAttribute(value, "state"); + AddStringEnumConverterAttribute(value, "priority"); + AddStringEnumConverterAttribute(value, "type"); + + jo.Property("messageTime")?.Remove(); + jo.Property("expired")?.Remove(); + jo.WriteTo(writer); + } + } + + private static void AddStringEnumConverterAttribute(object value, string propertyName) + { + var prop = value.GetType().GetProperty(propertyName); + var attr = new JsonConverterAttribute(typeof(StringEnumConverter)); + + if (prop != null) + { + var attrs = prop.GetCustomAttributes(false); + + // Check if the attribute is not already applied + if (Array.Find(attrs, a => a is JsonConverterAttribute) == null) + { + // Create a new array that includes the existing attributes and the new one + var newAttrs = new object[attrs.Length + 1]; + Array.Copy(attrs, newAttrs, attrs.Length); + newAttrs[attrs.Length] = attr; + + // Use reflection to set the new attributes array + var field = typeof(PropertyInfo).GetField("m_customAttributes", + BindingFlags.Instance | BindingFlags.NonPublic); + field?.SetValue(prop, newAttrs); + } + } + } + + public override void WriteJson(JsonWriter writer, PatientObservationAlarm? value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } +} + +/*How to use it: + BsonSerializer.RegisterSerializer(new PatientObservationAlarmConverter()); + JsonConvert.DefaultSettings = () => new JsonSerializerSettings + { + Converters = { new PatientObservationAlarmConverter() } + }; +*/ \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/PatientObservationConverter.cs b/adas-core.Infrastructure/Utils/PatientObservationConverter.cs new file mode 100644 index 00000000..138fe36d --- /dev/null +++ b/adas-core.Infrastructure/Utils/PatientObservationConverter.cs @@ -0,0 +1,68 @@ +using System.Reflection; +using adas_core.Domain.Models; +using MongoDB.Bson.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; + +namespace adas_core.Infrastructure.Utils; + +public class PatientObservationConverter(Type? valueType) : JsonConverter, IBsonSerializer +{ + public object Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + throw new NotImplementedException(); + } + + public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value) + { + throw new NotImplementedException(); + } + + public Type? ValueType { get; } = valueType; + + public override void WriteJson(JsonWriter writer, PatientObservation? value, JsonSerializer serializer) + { + if (value != null) + { + var jo = JObject.FromObject(value); + + // Add [JsonConverter(typeof(StringEnumConverter))] attribute to specified properties + AddStringEnumConverterAttribute(value, "Status"); + + jo.Property("MessageTime")?.Remove(); + jo.WriteTo(writer); + } + } + + private static void AddStringEnumConverterAttribute(object value, string propertyName) + { + var prop = value.GetType().GetProperty(propertyName); + var attr = new JsonConverterAttribute(typeof(StringEnumConverter)); + + if (prop != null) + { + var attrs = prop.GetCustomAttributes(false); + + // Check if the attribute is not already applied + if (Array.Find(attrs, a => a is JsonConverterAttribute) == null) + { + // Create a new array that includes the existing attributes and the new one + var newAttrs = new object[attrs.Length + 1]; + Array.Copy(attrs, newAttrs, attrs.Length); + newAttrs[attrs.Length] = attr; + + // Use reflection to set the new attributes array + var field = typeof(PropertyInfo).GetField("m_customAttributes", + BindingFlags.Instance | BindingFlags.NonPublic); + field?.SetValue(prop, newAttrs); + } + } + } + + public override PatientObservation ReadJson(JsonReader reader, Type objectType, PatientObservation? existingValue, + bool hasExistingValue, JsonSerializer serializer) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/PatientPumpObservationConverter.cs b/adas-core.Infrastructure/Utils/PatientPumpObservationConverter.cs new file mode 100644 index 00000000..516afe12 --- /dev/null +++ b/adas-core.Infrastructure/Utils/PatientPumpObservationConverter.cs @@ -0,0 +1,87 @@ +using System.Reflection; +using adas_core.Domain.Models; +using adas_core.Domain.Models.Pumps; +using MongoDB.Bson.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; +using JsonConverterAttribute = Newtonsoft.Json.JsonConverterAttribute; + +namespace adas_core.Infrastructure.Utils; + +public class PatientPumpObservationConverter : JsonConverter, IBsonSerializer +{ + public object Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + throw new NotImplementedException(); + } + + public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value) + { + throw new NotImplementedException(); + } + + public Type ValueType => typeof(PatientObservationAlarm); + + public override PumpObservation ReadJson(JsonReader reader, Type objectType, + PumpObservation? existingValue, + bool hasExistingValue, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + public new void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) + { + if (value != null) + { + var jo = JObject.FromObject(value); + + // Add [JsonConverter(typeof(StringEnumConverter))] attribute to specified properties + AddStringEnumConverterAttribute(value, "Event"); + AddStringEnumConverterAttribute(value, "Status"); + AddStringEnumConverterAttribute(value, "PumpMode"); + AddStringEnumConverterAttribute(value, "InfusingStatus"); + AddStringEnumConverterAttribute(value, "AlarmMode"); + + jo.WriteTo(writer); + } + } + + public override void WriteJson(JsonWriter writer, PumpObservation? value, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + private static void AddStringEnumConverterAttribute(object value, string propertyName) + { + var prop = value.GetType().GetProperty(propertyName); + var attr = new JsonConverterAttribute(typeof(StringEnumConverter)); + + if (prop != null) + { + var attrs = prop.GetCustomAttributes(false); + + // Check if the attribute is not already applied + if (Array.Find(attrs, a => a is JsonConverterAttribute) == null) + { + // Create a new array that includes the existing attributes and the new one + var newAttrs = new object[attrs.Length + 1]; + Array.Copy(attrs, newAttrs, attrs.Length); + newAttrs[attrs.Length] = attr; + + // Use reflection to set the new attributes array + var field = typeof(PropertyInfo).GetField("m_customAttributes", + BindingFlags.Instance | BindingFlags.NonPublic); + field?.SetValue(prop, newAttrs); + } + } + } +} + +/*How to use it: + BsonSerializer.RegisterSerializer(new PatientObservationAlarmConverter()); + JsonConvert.DefaultSettings = () => new JsonSerializerSettings + { + Converters = { new PatientObservationAlarmConverter() } + }; +*/ \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/PersonConverter.cs b/adas-core.Infrastructure/Utils/PersonConverter.cs new file mode 100644 index 00000000..2a236cbf --- /dev/null +++ b/adas-core.Infrastructure/Utils/PersonConverter.cs @@ -0,0 +1,63 @@ +using System.Reflection; +using adas_core.Domain.Models; +using MongoDB.Bson.Serialization; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Linq; + +namespace adas_core.Infrastructure.Utils; + +public class PersonConverter(Type valueType) : JsonConverter, IBsonSerializer +{ + public object Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args) + { + throw new NotImplementedException(); + } + + public void Serialize(BsonSerializationContext context, BsonSerializationArgs args, object value) + { + throw new NotImplementedException(); + } + + public Type ValueType { get; } = valueType; + + public override void WriteJson(JsonWriter writer, Person? value, JsonSerializer serializer) + { + if (value == null) return; + + var jo = JObject.FromObject(value); + + // Add [JsonConverter(typeof(StringEnumConverter))] attribute to specified properties + AddStringEnumConverterAttribute(value, "Gender"); + + jo.WriteTo(writer); + } + + private static void AddStringEnumConverterAttribute(object value, string propertyName) + { + var prop = value.GetType().GetProperty(propertyName); + var attr = new JsonConverterAttribute(typeof(StringEnumConverter)); + + if (prop == null) return; + var attrs = prop.GetCustomAttributes(false); + + // Check if the attribute is not already applied + if (Array.Find(attrs, a => a is JsonConverterAttribute) != null) return; + + // Create a new array that includes the existing attributes and the new one + var newAttrs = new object[attrs.Length + 1]; + Array.Copy(attrs, newAttrs, attrs.Length); + newAttrs[attrs.Length] = attr; + + // Use reflection to set the new attributes array + var field = typeof(PropertyInfo).GetField("m_customAttributes", + BindingFlags.Instance | BindingFlags.NonPublic); + field?.SetValue(prop, newAttrs); + } + + public override Person ReadJson(JsonReader reader, Type objectType, Person? existingValue, bool hasExistingValue, + JsonSerializer serializer) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/RabbitConsumerErrorStrategy.cs b/adas-core.Infrastructure/Utils/RabbitConsumerErrorStrategy.cs new file mode 100644 index 00000000..897ac2b6 --- /dev/null +++ b/adas-core.Infrastructure/Utils/RabbitConsumerErrorStrategy.cs @@ -0,0 +1,145 @@ +using adas_core.Application.Services.Interfaces; +using EasyNetQ; +using EasyNetQ.SystemMessages; +using Newtonsoft.Json; +using Serilog; +using System.Text; +using ILogger = Serilog.ILogger; + +namespace adas_core.Infrastructure.Utils; + +public class RabbitConsumerErrorHandler(IPublisherService publisherService) +{ + private const int MaxRetries = 2; + private static readonly ILogger Logger = Log.ForContext(); + + public async Task HandleAsync( + Message message, + MessageReceivedInfo receivedInfo, + Func next) + { + try + { + await next(); + } + catch (Exception exception) + { + Logger.Error(exception, "Consumer error {Message}", exception.Message); + + var properties = message.Properties; + + var body = Encoding.UTF8.GetString( + message.Body is byte[] bytes + ? bytes + : Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(message.Body))); + + HandleRetries(receivedInfo, properties, body, exception); + } + } + + private void HandleRetries( + MessageReceivedInfo receivedInfo, + MessageProperties properties, + string body, + Exception exception) + { + try + { + var headers = properties.Headers != null + ? new Dictionary(properties.Headers) + : []; + + var retries = GetRetries(properties); + + // MAX RETRIES → ERROR QUEUE + if (retries > MaxRetries) + { + if (!receivedInfo.Queue.StartsWith("Error")) + { + headers["retries"] = BitConverter.GetBytes(MaxRetries + 1); + + var errorMsg = CreateErrorMessage( + receivedInfo, + properties, + body, + exception, + headers + ); + + _ = publisherService.SendMessageError( + errorMsg, + $"Error{receivedInfo.Queue}"); + } + + return; + } + + // RETRY NORMAL + headers["retries"] = BitConverter.GetBytes(retries + 1); + + var newProps = new MessageProperties + { + DeliveryMode = 2, + Headers = headers, + ContentType = properties.ContentType, + CorrelationId = properties.CorrelationId, + MessageId = properties.MessageId + }; + + var message = new Message(body, newProps); + + var result = publisherService + .SendMessage(message, receivedInfo.Queue) + .GetAwaiter() + .GetResult(); + + if (!result) + throw new Exception("Requeue failed"); + } + catch (Exception e) + { + Logger.Error(e, "Exception processing rabbit message"); + } + } + + private int GetRetries(MessageProperties properties) + { + if (properties.Headers != null && + properties.Headers.TryGetValue("retries", out var retriesObj) && + retriesObj is byte[] bytes) + { + return BitConverter.ToInt32(bytes, 0); + } + + return 0; + } + + private static Message CreateErrorMessage( + MessageReceivedInfo receivedInfo, + MessageProperties originalProperties, + string body, + Exception exception, + Dictionary headers) + { + var props = new MessageProperties + { + Headers = headers, + DeliveryMode = originalProperties.DeliveryMode, + ContentType = originalProperties.ContentType, + CorrelationId = originalProperties.CorrelationId, + MessageId = originalProperties.MessageId + }; + + var error = new Error( + body, + exception.Message, + receivedInfo.Exchange, + receivedInfo.RoutingKey, + receivedInfo.Queue, + DateTime.UtcNow, + props + ); + + return new Message(error, props); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/RabbitIErrorMessageSerializer.cs b/adas-core.Infrastructure/Utils/RabbitIErrorMessageSerializer.cs new file mode 100644 index 00000000..c2180d08 --- /dev/null +++ b/adas-core.Infrastructure/Utils/RabbitIErrorMessageSerializer.cs @@ -0,0 +1,29 @@ +using System.Text; +using EasyNetQ.Consumer; +using Newtonsoft.Json; + +namespace adas_core.Infrastructure.Utils; + +public class RabbitIErrorMessageSerializer : IErrorMessageSerializer +{ + public byte[]? Deserialize(string messageBody) + { + var unescapedJsonString = JsonConvert.DeserializeObject(messageBody); + + return unescapedJsonString != null ? Encoding.UTF8.GetBytes(unescapedJsonString) : null; + } + + public string? Serialize(byte[] messageBody) + { + var stringifiedMsgBody = Encoding.UTF8.GetString(messageBody); + + try + { + return JsonConvert.DeserializeObject(stringifiedMsgBody); + } + catch (Exception) + { + return stringifiedMsgBody; + } + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/Utils/TypesUtils.cs b/adas-core.Infrastructure/Utils/TypesUtils.cs new file mode 100644 index 00000000..2c0d712b --- /dev/null +++ b/adas-core.Infrastructure/Utils/TypesUtils.cs @@ -0,0 +1,40 @@ +using adas_core.Domain.Exceptions; + +namespace adas_core.Infrastructure.Utils; + +public static class TypesUtils +{ + public static Type GetDriver(string deviceType, string device) + { + return deviceType switch + { + "Relay" => GetType("adas-core.module.Relays", device, deviceType), + "LightBeacon" => GetType("adas-core.module.LightBeacons", device, deviceType), + _ => throw new AdasException($"Device type {deviceType} not found") + }; + } + + private static Type GetType(string typeName, string device, string deviceType) + { + var loadedAssemblies = AppDomain.CurrentDomain.GetAssemblies(); + var assembly = loadedAssemblies.FirstOrDefault(a => + { + var name = a.GetName().Name; + return name != null && name.Contains(typeName); + }); + + if (assembly == null) + // El ensamblado no está cargado. Lanzar excepción o manejar el fallo. + throw new AdasException($"Assembly '{typeName}' not loaded."); + + var fullClassName = $"{typeName.Replace('-', '_')}.Devices.{device}{deviceType}"; + + var type = assembly.GetType(fullClassName, + true, + true); + + if (type != null) return type; + + throw new AdasException($"{typeName} driver not found"); + } +} \ No newline at end of file diff --git a/adas-core.Infrastructure/adas-core.Infrastructure.csproj b/adas-core.Infrastructure/adas-core.Infrastructure.csproj new file mode 100644 index 00000000..186f0b92 --- /dev/null +++ b/adas-core.Infrastructure/adas-core.Infrastructure.csproj @@ -0,0 +1,25 @@ + + + + net8.0 + adas_core.Infrastructure + enable + enable + + + + + + + + + + + + + + + + + + diff --git a/adas-core.LdapLogin/Configuration/LDAPModule.cs b/adas-core.LdapLogin/Configuration/LDAPModule.cs new file mode 100644 index 00000000..f5940a8f --- /dev/null +++ b/adas-core.LdapLogin/Configuration/LDAPModule.cs @@ -0,0 +1,15 @@ +using adas_core.Authentication.Interfaces; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace adas_core.LdapLogin.Configuration; + +public class LdapModule : IModule +{ + public static void Configure(IServiceCollection services, IConfiguration configuration) + { + var configSection = configuration.GetSection("Ldap"); + services.Configure(configSection); + services.AddScoped(); + } +} \ No newline at end of file diff --git a/adas-core.LdapLogin/Configuration/LdapConfig.cs b/adas-core.LdapLogin/Configuration/LdapConfig.cs new file mode 100644 index 00000000..008e86a9 --- /dev/null +++ b/adas-core.LdapLogin/Configuration/LdapConfig.cs @@ -0,0 +1,40 @@ +using adas_core.Domain.Enums; + +namespace adas_core.LdapLogin.Configuration; + +public class LdapConfig +{ + public bool Enabled { get; set; } + public string? Server { get; set; } + public int? Port { get; set; } = 389; + public string? UserDomainName { get; set; } + public string? UserNameProperty { get; set; } = "sAMAccountName"; + public string? SearchBase { get; set; } + public string? GroupsProperty { get; set; } = "memberOf"; + public string? LdapUser { get; set; } + public string? LdapPassword { get; set; } + public string? DisplayNameProperty { get; set; } = "displayName"; + public string? FirstNameProperty { get; set; } = "givenName"; + public string? LastNameProperty { get; set; } = "sn"; + + public List AuthoritiesMap { get; set; } = []; + + public List WhiteList { get; set; } = []; +} + +public class UserLdap +{ + public string? Name { get; set; } + public string? Username { get; set; } + public string? Rol { get; set; } + public List Actions { get; set; } = []; + public string? DisplayId { get; set; } +} + +public class AuthorityMap +{ + public string Group { get; set; } = null!; + public string? Rol { get; set; } + public List Actions { get; set; } = []; + public string? DisplayId { get; set; } = null; +} \ No newline at end of file diff --git a/adas-core.LdapLogin/Configuration/LdapConfigValidator.cs b/adas-core.LdapLogin/Configuration/LdapConfigValidator.cs new file mode 100644 index 00000000..2a780f2f --- /dev/null +++ b/adas-core.LdapLogin/Configuration/LdapConfigValidator.cs @@ -0,0 +1,19 @@ +using FluentValidation; + +namespace adas_core.LdapLogin.Configuration; + +public class LdapConfigValidator : AbstractValidator +{ + //public LdapConfigValidator() + //{ + //RuleFor(f => f.Server).NotNull().WithErrorCode("Server is null"); + //RuleFor(f => f.Port).NotNull().WithErrorCode("Port is null"); + //RuleFor(f => f.UserDomainName).NotNull().WithErrorCode("UserDomainName is null"); + //RuleFor(f => f.UserNameProperty).NotNull().WithErrorCode("UserNameProperty is null"); + //RuleFor(f => f.DisplayNameProperty).NotNull().WithErrorCode("DisplayNameProperty is null"); + //RuleFor(f => f.SearchBase).NotNull().WithErrorCode("SearchBase is null"); + //RuleFor(f => f.GroupsProperty).NotNull().WithErrorCode("GroupsProperty is null"); + //RuleFor(f => f.ldapUser).NotNull().WithErrorCode("ldapUser is null"); + //RuleFor(f => f.ldapPassword).NotNull().WithErrorCode("ldapPassword is null"); + //} +} \ No newline at end of file diff --git a/adas-core.LdapLogin/IModule.cs b/adas-core.LdapLogin/IModule.cs new file mode 100644 index 00000000..1622d92b --- /dev/null +++ b/adas-core.LdapLogin/IModule.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace adas_core.LdapLogin; + +public interface IModule +{ + static void Configure(IServiceCollection services, IConfiguration configuration) + { + } +} \ No newline at end of file diff --git a/adas-core.LdapLogin/LdapLoginService.cs b/adas-core.LdapLogin/LdapLoginService.cs new file mode 100644 index 00000000..7876c840 --- /dev/null +++ b/adas-core.LdapLogin/LdapLoginService.cs @@ -0,0 +1,342 @@ +using adas_core.Authentication.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Exceptions; +using adas_core.Domain.Models.MongoModels; +using adas_core.LdapLogin.Configuration; +using FluentValidation; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.HttpResults; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Novell.Directory.Ldap; + +namespace adas_core.LdapLogin; + +public class LdapLoginService : ILoginService +{ + private readonly IAuthorityService _authorityService; + private readonly LdapConfig _ldapConfig; + private readonly ILogger _logger; + private readonly Lazy _userService; + + public LdapLoginService( + IOptions ldapConfig, + IValidator validator, + Lazy userService, + IAuthorityService authorityService, + ILogger logger) + { + _userService = userService; + _authorityService = authorityService; + _logger = logger; + _ldapConfig = ldapConfig.Value; + validator.Validate(_ldapConfig, options => options.ThrowOnFailures()); + } + + public bool AllowPassword => true; + public UserEnum.LoginMethod Method => UserEnum.LoginMethod.Ldap; + + public async Task Login(string username, string password) + { + // Check for LDAP config + if (_ldapConfig.Server == null) throw new LoginServicesException("LDAP Config not found"); + var conn = new LdapConnection(); + try + { + await conn.ConnectAsync(_ldapConfig.Server, _ldapConfig.Port ?? 389); + } + catch (Exception e) + { + _logger.LogError("[LDAP] Error connecting to {server}, port {port}, Exception: {e}", _ldapConfig.Server, + _ldapConfig.Port, e.Message); + throw; + } + + if (_ldapConfig.LdapUser != null) + { + _logger.LogInformation("[LDAP] _ldapConfig.LdapUser is enabled with {LdapUser}", _ldapConfig.LdapUser); + await conn.BindAsync(_ldapConfig.LdapUser, _ldapConfig.LdapPassword); + } + else + { + var ldapUser = (!string.IsNullOrEmpty(_ldapConfig.UserDomainName) + ? _ldapConfig.UserDomainName + @"\" + : "") + username; + + try + { + await conn.BindAsync(ldapUser, password); + } + catch (LdapException e) + { + _logger.LogError("[LDAP] Error binding ldapUser: {LdapUser} and password", ldapUser); + + throw new UserNotFoundException(username, e); + } + } + + var results = await conn.SearchAsync( + _ldapConfig.SearchBase, + LdapConnection.ScopeSub, + $"({_ldapConfig.UserNameProperty}={username})", + null, + false); + + + LdapEntry? entry = null; + + while (await results.HasMoreAsync()) + { + LdapEntry? current = null; + + try + { + current = await results.NextAsync(); + } + catch (LdapException ex) + { + _logger.LogWarning("[LDAP] Skipping invalid entry: {error}", ex.Message); + continue; + } + + if (current != null) + { + entry = current; + break; + } + } + + + if (entry == null) throw new LoginServicesException("LDAP User not found"); + var userEntryLdap = GetUser(entry); + var user = await GetOrCreateUser(userEntryLdap, entry); + _logger.LogInformation("[LDAP] entry is {entry} and user {user}", entry, user); + // user.Authorization.AddRange(GetAuthorities(entry)); + conn.Disconnect(); + return user ?? throw new LoginServicesException("LDAP User not found"); + } + + public Task Login(HttpContext context) + { + throw new LoginServicesException("Not implemented"); + } + + public Task Authenticate(string username, string password) + { + throw new LoginServicesException("Not implemented"); + } + + public Task GetById(ObjectId id) + { + throw new LoginServicesException("Not implemented"); + } + + public Task GetByEmail(string email) + { + throw new LoginServicesException("Not implemented"); + } + + public Task GetByUsername(string username) + { + throw new LoginServicesException("Not implemented"); + } + + public Task> GetAllUsers() + { + throw new LoginServicesException("Not implemented"); + } + + private async Task GetOrCreateUser(User userEntryLdap, LdapEntry entry) + { + var userToReturn = (await _userService.Value.GetUserByUserName(userEntryLdap.UserName) ?? + await _userService.Value.GetUserByName(userEntryLdap.Name)) ?? + await _userService.Value.CreateUser(userEntryLdap); + + if (userToReturn == null) return userToReturn; + userToReturn.Authorization = []; + + var authorities = await CheckAuthorities(userToReturn, entry); + + userToReturn.Authorization.AddRange(authorities); + //foreach (var authorization in userToReturn.Authorization) + //{ + // Enum.TryParse(authorization.Rol, out var compareRole); + // if (compareRole == RolesType.Admin) userToReturn.Rol = RolesType.Admin; + //} + return userToReturn; + } + + private async Task> CheckAuthorities(User user, LdapEntry entry) + { + try + { + var authorizationMap = GetAuthoritiesMap(entry, user); + var authorizationWhiteList = GetAuthoritiesWhiteList(entry, user); + _logger.LogInformation( + "[LDAP] user {user} authorizationMap count is {authorizationMap}, authorizationWhiteList count is {authorizationWhiteList}", + user.UserName, authorizationMap.Count, authorizationWhiteList.Count); + + // Primero, creamos un HashSet con los DisplayID de la lista blanca para búsqueda eficiente + var whiteListDisplayIds = new HashSet(authorizationWhiteList.Select(a => a.DisplayId)); + + // Filtramos los elementos de authorizationMap que no están en la lista blanca, basándonos en DisplayID + var uniqueMapAuthorizations = authorizationMap.Where(a => !whiteListDisplayIds.Contains(a.DisplayId)); + + // Finalmente, combinamos los elementos únicos de authorizationMap con los de authorizationWhiteList + var combinedList = authorizationWhiteList.Concat(uniqueMapAuthorizations).ToList(); + + var userAuthorities = await _authorityService.GetUserAuthorities(user.Id); + foreach (var auth in combinedList) + { + var authFound = userAuthorities.Find(c => c.DisplayId == auth.DisplayId); + if (authFound is { CanUpdate: true }) + { + authFound.Rol = auth.Rol; + await _authorityService.updateOne(authFound); + } + else + { + await _authorityService.InsertOne(auth); + } + } + + return await _authorityService.GetUserAuthorities(user.Id); + } + catch (Exception e) + { + _logger.LogError("[LDAP] CheckAuthorities for user {user} has exception {ex}", user.UserName, e.Message); + + return []; + } + } + + private List GetAuthoritiesWhiteList(LdapEntry entry, User user) + { + try + { + var userWhiteList = new List(); + var userNameProperty = _ldapConfig.UserNameProperty; + + var whiteList = _ldapConfig.WhiteList.FindAll(u => + (u.Name != null && entry.Dn.Contains(u.Name, StringComparison.CurrentCultureIgnoreCase)) || + (u.Username != null && + userNameProperty != null && + entry.GetAttributeSet().TryGetValue(userNameProperty, out var attr) && + attr.StringValue != null && + attr.StringValue.Equals(u.Username, StringComparison.CurrentCultureIgnoreCase)) + ); + + if (whiteList.Count == 0) return userWhiteList; + + foreach (var authorityMap in whiteList) + { + if (!Enum.TryParse(authorityMap.Rol, out _)) + continue; + + userWhiteList.Add(new Authorization + { + UserId = user.Id, + DisplayId = authorityMap.DisplayId, + Rol = authorityMap.Rol + }); + } + + return userWhiteList; + } + catch (Exception e) + { + _logger.LogError("[LDAP] GetAuthoritiesWhiteList for user {user} has exception {ex}", user.UserName, + e.Message); + + return []; + } + } + + private User GetUser(LdapEntry ldapEntry) + { + var attributes = ldapEntry.GetAttributeSet(); + + var user = new User(); + + // UserName + if (!string.IsNullOrWhiteSpace(_ldapConfig.UserNameProperty) && + attributes.TryGetValue(_ldapConfig.UserNameProperty, out var userAttr) && + userAttr?.StringValue != null) + { + user.UserName = userAttr.StringValue; + } + else + { + user.UserName = ""; + } + + _logger.LogInformation("[LDAP] GetUser UserName is {UserName} ", user.UserName); + + // First name + if (!string.IsNullOrWhiteSpace(_ldapConfig.FirstNameProperty) && + attributes.TryGetValue(_ldapConfig.FirstNameProperty, out var firstNameAttr) && + firstNameAttr?.StringValue != null) + { + user.Name = firstNameAttr.StringValue; + } + + // Last name + if (!string.IsNullOrWhiteSpace(_ldapConfig.LastNameProperty) && + attributes.TryGetValue(_ldapConfig.LastNameProperty, out var lastNameAttr) && + lastNameAttr?.StringValue != null) + { + user.Name = string.IsNullOrEmpty(user.Name) + ? lastNameAttr.StringValue + : $"{user.Name} {lastNameAttr.StringValue}"; + } + + return user; + } + + private List GetAuthoritiesMap(LdapEntry ldapEntry, User user) + { + try + { + var authorities = new List(); + + if (!_ldapConfig.AuthoritiesMap.Any()) + return authorities; + + var attributes = ldapEntry.GetAttributeSet(); + + if (!string.IsNullOrWhiteSpace(_ldapConfig.GroupsProperty) || + !attributes.TryGetValue(_ldapConfig.GroupsProperty!, out var groupsAttr) || + groupsAttr?.StringValueArray == null) + { + return authorities; + } + + var groups = groupsAttr.StringValueArray.ToList(); + + foreach (var authorityMap in _ldapConfig.AuthoritiesMap) + { + if (groups.Exists(g => + string.Equals(g, authorityMap.Group, StringComparison.CurrentCultureIgnoreCase))) + { + if (!Enum.TryParse(authorityMap.Rol, out _)) + continue; + + authorities.Add(new Authorization + { + UserId = user.Id, + DisplayId = authorityMap.DisplayId, + Rol = authorityMap.Rol + }); + } + } + + return authorities; + } + catch (Exception e) + { + _logger.LogError("Error getting Authorities Map. Return new empty list. Exception: {e}", e); + return []; + } + } +} \ No newline at end of file diff --git a/adas-core.LdapLogin/RegistrationExtension/LdapRegistration.cs b/adas-core.LdapLogin/RegistrationExtension/LdapRegistration.cs new file mode 100644 index 00000000..cb557c48 --- /dev/null +++ b/adas-core.LdapLogin/RegistrationExtension/LdapRegistration.cs @@ -0,0 +1,15 @@ +using adas_core.LdapLogin.Configuration; +using FluentValidation; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace adas_core.LdapLogin.RegistrationExtension; + +public static class LdapRegistration +{ + public static void AddLdapAuth(this IServiceCollection serviceCollection, IConfigurationRoot configuration) + { + serviceCollection.AddSingleton, LdapConfigValidator>(); + LdapModule.Configure(serviceCollection, configuration); + } +} \ No newline at end of file diff --git a/adas-core.LdapLogin/adas-core.LdapLogin.csproj b/adas-core.LdapLogin/adas-core.LdapLogin.csproj new file mode 100644 index 00000000..7666248e --- /dev/null +++ b/adas-core.LdapLogin/adas-core.LdapLogin.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + adas_core.LdapLogin + enable + enable + + + + + + + + + + + + + diff --git a/adas-core.LocalLogin/ILocalLoginRepository.cs b/adas-core.LocalLogin/ILocalLoginRepository.cs new file mode 100644 index 00000000..f5b0ae15 --- /dev/null +++ b/adas-core.LocalLogin/ILocalLoginRepository.cs @@ -0,0 +1,22 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.LocalLogin; + +public interface ILocalLoginRepository : IMongoRepository +{ + Task> GetAll(); + + Task GetById(ObjectId id); + + Task GetByEmail(string email); + + //User UpdateUser(User user); + //User DeleteUser(ObjectId id); + //User CreateUser(User user); + //IMongoCollection Collection { get; } + Task GetByUsername(string username); + Task UpdateOneUserAsync(ObjectId id, User user); + Task InsertUserOneAsync(User user); +} \ No newline at end of file diff --git a/adas-core.LocalLogin/LocalLoginRepository.cs b/adas-core.LocalLogin/LocalLoginRepository.cs new file mode 100644 index 00000000..7379fb4b --- /dev/null +++ b/adas-core.LocalLogin/LocalLoginRepository.cs @@ -0,0 +1,115 @@ +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Serilog; + +namespace adas_core.LocalLogin; + +public class LocalLoginRepository : MongoRepository, ILocalLoginRepository +{ + private readonly ApiSettings _apiSettings; + // private readonly ILogger _logger; + + public LocalLoginRepository( + IOptions apiSettings, + IMongoDatabase database + ) : base(database) + { + // _logger = logger; + _apiSettings = apiSettings.Value; + } + + public override string GetCollectionName() + { + return _apiSettings.Users; + } + + public async Task InsertUserOneAsync(User user) + { + try + { + await Collection.InsertOneAsync(user); + return true; + } + catch (Exception e) + { + Log.Error("Unable to insert user. Exception: {e}", e.Message); + return false; + } + } + + public async Task UpdateOneUserAsync(ObjectId id, User user) + { + try + { + var filter = Builders.Filter.Eq("Id", user.Id); + var update = Builders.Update + .Set(c => c.Name, user.Name) + .Set(c => c.Email, user.Email) + .Set(c => c.UserName, user.UserName) + .Set(c => c.Password, user.Password) + .Set(c => c.LockExpirationDate, user.LockExpirationDate) + .Set(c => c.LastLogin, user.LastLogin) + .Set(c => c.IsEnabled, user.IsEnabled); + await Collection.UpdateOneAsync(filter, update); + + return true; + } + catch (Exception e) + { + Log.Error("Unable to insert user. Exception: {e}", e.Message); + return false; + } + } + + public async Task GetById(ObjectId id) + { + var filter = Builders.Filter.Eq(p => p.Id, id); + var result = await Collection.Find(filter).ToListAsync(); + return result.FirstOrDefault(); + } + + public async Task GetByEmail(string email) + { + var filter = Builders.Filter.Eq(p => p.Email, email); + var result = await Collection.Find(filter).ToListAsync(); + return result.FirstOrDefault(); + } + + public async Task GetByUsername(string username) + { + var filter = Builders.Filter.Eq(p => p.UserName, username); + var users = await Collection.Find(filter).ToListAsync(); + return users.FirstOrDefault(); + } + + public async Task> GetAll() + { + var result = await Collection.FindAsync(Builders.Filter.Empty); + + return result.ToList(); + } + + public sealed override async Task InsertInitialLoad() + { + await GetOrCreateSystemUser(); + } + + private async Task GetOrCreateSystemUser() + { + var user = await GetByUsername("System"); + if (user == null) + { + var userToInsert = new User + { + UserName = "System", + Name = "System", + Password = "$2a$12$crWa3EN1izcZBXNc81RzmOlfaYW2TPr2NdDQEWI7RzLTnA0Dd68WG" + }; + await Collection.InsertOneAsync(userToInsert); + } + } +} \ No newline at end of file diff --git a/adas-core.LocalLogin/LocalLoginServiceImpl.cs b/adas-core.LocalLogin/LocalLoginServiceImpl.cs new file mode 100644 index 00000000..2f8080f8 --- /dev/null +++ b/adas-core.LocalLogin/LocalLoginServiceImpl.cs @@ -0,0 +1,75 @@ +using adas_core.Application.Exceptions; +using adas_core.Authentication.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Exceptions; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Utils; +using Microsoft.AspNetCore.Http; +using MongoDB.Bson; +using Serilog; + +namespace adas_core.LocalLogin; + +public class LocalLoginServiceImpl( + ILocalLoginRepository localLoginRepository, + IAuthorityService authorityService) + : ILoginService +{ + public bool AllowPassword => true; + public UserEnum.LoginMethod Method => UserEnum.LoginMethod.Local; + + + public async Task Login(string username, string password) + { + var userFound = await localLoginRepository.GetByUsername(username) ?? + throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedInvalidCredentials); + + if (!CryptoAdas.VerifyPassword(password, userFound.Password)) + throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedInvalidCredentials); + + userFound.Authorization = await authorityService.GetUserAuthorities(userFound.Id); + return userFound; + } + + public Task Login(HttpContext context) + { + throw new LoginServicesException("Not implemented"); + } + + public async Task Authenticate(string username, string password) + { + try + { + var userFound = await localLoginRepository.GetByUsername(username); + + if (userFound != null && CryptoAdas.VerifyPassword(password, userFound.Password)) return userFound; + + return new User(); + } + catch (Exception e) + { + Log.Error("Error Authenticate with username-password: {username}: Exception: {exception}", username, e.Message); + throw; + } + } + + public async Task GetById(ObjectId id) + { + return await localLoginRepository.GetById(id); + } + + public async Task GetByEmail(string email) + { + return await localLoginRepository.GetByEmail(email); + } + + public async Task GetByUsername(string username) + { + return await localLoginRepository.GetByUsername(username); + } + + public async Task> GetAllUsers() + { + return await localLoginRepository.GetAll(); + } +} \ No newline at end of file diff --git a/adas-core.LocalLogin/RegistrationExtension/LocalLoginExtension.cs b/adas-core.LocalLogin/RegistrationExtension/LocalLoginExtension.cs new file mode 100644 index 00000000..d754237f --- /dev/null +++ b/adas-core.LocalLogin/RegistrationExtension/LocalLoginExtension.cs @@ -0,0 +1,18 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Authentication; +using adas_core.Authentication.Interfaces; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.DependencyInjection; + +namespace adas_core.LocalLogin.RegistrationExtension; + +public static class LocalLoginExtension +{ + public static void AddLocalLogin(this IServiceCollection serviceCollection) + { + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + } +} \ No newline at end of file diff --git a/adas-core.LocalLogin/adas-core.LocalLogin.csproj b/adas-core.LocalLogin/adas-core.LocalLogin.csproj new file mode 100644 index 00000000..27c0080b --- /dev/null +++ b/adas-core.LocalLogin/adas-core.LocalLogin.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + adas_core.LocalLogin + enable + enable + + + + + + + + + + + + + diff --git a/adas-core.Test/Customizations/H12O/UCIN/CalculatedObservationsTest.cs b/adas-core.Test/Customizations/H12O/UCIN/CalculatedObservationsTest.cs new file mode 100644 index 00000000..d5f47ce7 --- /dev/null +++ b/adas-core.Test/Customizations/H12O/UCIN/CalculatedObservationsTest.cs @@ -0,0 +1,1935 @@ +using adas_core.Application.Customizations.H12O.UCIN; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Moq; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Customizations.H12O.UCIN; + +[TestFixture] +[Order(2)] +public class CalculatedObservationsTest +{ + [SetUp] + public void Setup() + { + _observationServiceMock = new Mock(); + + _configOservationServiceMock = new Mock(); + + _medicineServiceMock = new Mock(); + + _treatmentServiceMock = new Mock(); + + _logger = new Mock>(); + + _optionsApiSettings = Options.Create(_apiSettings); + + _optionsApiSettings.Value.MedicationBolus = ["18851000140100", "21011000140105"]; + _optionsApiSettings.Value.IntravenousLinesCode = ["10546003"]; + _optionsApiSettings.Value.Transcutaneous = ["151756", "151760"]; + _optionsApiSettings.Value.RegionalBrainSaturation = ["194908", "194909"]; + _optionsApiSettings.Value.Electroencephalogram = ["42803009"]; + _optionsApiSettings.Value.Respiratory = ["361110005", "84481000140102"]; + _optionsApiSettings.Value.HighFrequencyVentilation = ["PC-HFO"]; + _optionsApiSettings.Value.InvasiveVentilation = + [ + "A/C (S.I.P.P.V. )", "A/C (S.I.P.P.V.) + V.G.", "PRVC", "PC-SIMV", "SIMV-PRVC", "PC-AC", "SIMV", + "SIMV-PC", "S.I.M.V.", "PC-CMV", "SIPPV", "PC" + ]; + _optionsApiSettings.Value.NonInvasiveVentilation = ["DUOPAP", "NCPAP"]; + _optionsApiSettings.Value.Oni = ["84481000140102", "416362002"]; + _optionsApiSettings.Value.Ecmo = ["705923009"]; + var serviceCollection = new ServiceCollection(); + //serviceCollection.AddSingleton(_observationServiceMock.Object); + serviceCollection.AddSingleton(new Lazy(() => _observationServiceMock.Object)); + + serviceCollection.AddSingleton( + new Lazy(() => _configOservationServiceMock.Object)); + serviceCollection.AddSingleton(_medicineServiceMock.Object); + serviceCollection.AddSingleton(new Lazy(() => _treatmentServiceMock.Object)); + serviceCollection.AddSingleton(_optionsApiSettings); + serviceCollection.AddSingleton(_logger.Object); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + _calculatedObservations = new CalculatedObservations(serviceProvider); + } + + private CalculatedObservations _calculatedObservations; + private Mock _medicineServiceMock; + private Mock _observationServiceMock; + private Mock _configOservationServiceMock; + private Mock _treatmentServiceMock; + private Mock> _logger; + + private readonly ApiSettings _apiSettings = new() + { + ConfigObservation = new ConfigObservationSettings + { + IgnoreUnknownObservation = false + } + }; + + private IOptions _optionsApiSettings; + + [Test] + public async Task Calculate_First_Medicine_Observation_With_Type_Should_Return_1() + { + //Entra un tratamiento por primera vez con una medicina, se crea una observación medication con valor 1 + var patientId = ObjectId.GenerateNewId(); + + var treatment = + new PatientTreatment + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + OrderControl = OrderControlType.Nw, + PlacerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + RequestedGiveCodes = [new Code { Identifier = "12345" }], + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [], + Routes = [] + }; + + var medicine = + new Medicine + { + Codes = ["12345"], + Group = [MedicineEnum.Group.DoubleSignature.ToString()], + Type = [MedicineEnum.Types.MuscleRelaxant.ToString()], + Name = "cisataracurio" + }; + + _medicineServiceMock.Setup(m => m.GetByCodeOrNote(new List { "12345" })) + .ReturnsAsync([medicine]); + _observationServiceMock.Setup(o => o.InsertObservation(It.IsAny(), true, true)); + _observationServiceMock.Setup(o => o.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([]); + _treatmentServiceMock.Setup(t => t.GetActiveTreatmentsByPatient(patientId)) + .ReturnsAsync(new List().AsEnumerable()); + _medicineServiceMock.Setup(m => m.GetMedicinesOfTreatments(It.IsAny>())) + .ReturnsAsync(new List().AsEnumerable()); + + + await _calculatedObservations.Map(treatment); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Medication" && (int)arg.Value == 1 && arg.PatientId == patientId + ), true, false)); + } + + + [Test] + public async Task Calculate_First_Medicine_Observation_With_Out_Type_Should_Return_1() + { + //Entra un tratamiento por primera vez con una medicina, se crea una observación medication con valor 1 + var patientId = ObjectId.GenerateNewId(); + + var treatment = + new PatientTreatment + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + OrderControl = OrderControlType.Nw, + PlacerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + RequestedGiveCodes = [new Code { Identifier = "12345" }], + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [], + Routes = [] + }; + + var medicine = + new Medicine + { + Codes = ["12345"], + Group = [MedicineEnum.Group.DoubleSignature.ToString()], + Name = "cisataracurio" + }; + + _medicineServiceMock.Setup(m => m.GetByCodeOrNote(new List { "12345" })) + .ReturnsAsync([medicine]); + _observationServiceMock.Setup(o => o.InsertObservation(It.IsAny(), true, true)); + _observationServiceMock.Setup(o => o.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([]); + _treatmentServiceMock.Setup(t => t.GetActiveTreatmentsByPatient(patientId)) + .ReturnsAsync(new List().AsEnumerable()); + _medicineServiceMock.Setup(m => m.GetMedicinesOfTreatments(It.IsAny>())) + .ReturnsAsync(new List().AsEnumerable()); + + + await _calculatedObservations.Map(treatment); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Medication" && (int)arg.Value == 0 && arg.PatientId == patientId + ), true, false)); + } + + + [Test] + public async Task Calculate__Medicine_Observation_New_Type_Should_Sum_1() + { + //Entra un tratamiento por primera vez con una medicina, se crea una observación medication con valor 1 + var patientId = ObjectId.GenerateNewId(); + + var treatment = + new PatientTreatment + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + OrderControl = OrderControlType.Nw, + PlacerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + RequestedGiveCodes = [new Code { Identifier = "12345" }], + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [], + Routes = [] + }; + + var medicine = + new Medicine + { + Codes = ["12345"], + Group = [MedicineEnum.Group.DoubleSignature.ToString()], + Name = "cisataracurio", + Type = [MedicineEnum.Types.AdrenalineNoradrenaline.ToString()] + }; + + _treatmentServiceMock.Setup(t => t.GetActiveTreatmentsByPatient(patientId)) + .ReturnsAsync(new List().AsEnumerable()); + _medicineServiceMock.Setup(m => m.GetByCodeOrNote(new List { "12345" })) + .ReturnsAsync([medicine]); + _medicineServiceMock.Setup(m => m.GetMedicinesOfTreatments(It.IsAny>())) + .ReturnsAsync(new List().AsEnumerable()); + _observationServiceMock.Setup(o => o.InsertObservation(It.IsAny(), true, true)); + + + _observationServiceMock.Setup(o => o.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([]); + + await _calculatedObservations.Map(treatment); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Medication" && (int)arg.Value == 1 && arg.PatientId == patientId + ), true, false)); + } + + + [Test] + public async Task Calculate__Medicine_Observation_Type_Already_Exists_Should_Sum_0() + { + //Entra un tratamiento por primera vez con una medicina, se crea una observación medication con valor 1 + var patientId = ObjectId.GenerateNewId(); + + var treatment = + new PatientTreatment + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + OrderControl = OrderControlType.Nw, + PlacerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + RequestedGiveCodes = [new Code { Identifier = "12345" }], + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [], + Routes = [] + }; + + var medicine = + new Medicine + { + Codes = ["12345"], + Group = [MedicineEnum.Group.DoubleSignature.ToString()], + Name = "cisataracurio", + Type = [MedicineEnum.Types.AdrenalineNoradrenaline.ToString()] + }; + + var activeMedicines = new List + { + new() + { + Codes = ["12345"], + Group = [MedicineEnum.Group.DoubleSignature.ToString()], + Name = "Biotin", + Type = [MedicineEnum.Types.AdrenalineNoradrenaline.ToString()] + } + }; + + _treatmentServiceMock.Setup(t => t.GetActiveTreatmentsByPatient(patientId)) + .ReturnsAsync(new List().AsEnumerable()); + _medicineServiceMock.Setup(m => m.GetByCodeOrNote(new List { "12345" })) + .ReturnsAsync([medicine]); + _medicineServiceMock.Setup(m => m.GetMedicinesOfTreatments(It.IsAny>())) + .ReturnsAsync(activeMedicines.AsEnumerable()); + _observationServiceMock.Setup(o => o.InsertObservation(It.IsAny(), true, true)); + _observationServiceMock.Setup(o => o.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([ + new PatientObservation + { + PatientId = patientId, + Value = 0, + Name = "Medication" + } + ]); + + + await _calculatedObservations.Map(treatment); + + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Medication" + && (int)arg.Value == 1 + && arg.PatientId == patientId + ), true, false)); + } + + + [Test] + public async Task Calculate__Medicine_Observation_DC_Type_Already_Exists_Should_Subtract_0() + { + var patientId = ObjectId.GenerateNewId(); + + var treatment = + new PatientTreatment + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + OrderControl = OrderControlType.Dc, + PlacerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + RequestedGiveCodes = [new Code { Identifier = "12345" }], + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [], + Routes = [] + }; + + var medicine = + new Medicine + { + Codes = ["12345"], + Group = [MedicineEnum.Group.DoubleSignature.ToString()], + Name = "cisataracurio", + Type = [MedicineEnum.Types.AdrenalineNoradrenaline.ToString()] + }; + + var activeMedicines = new List + { + new() + { + Codes = ["12345"], + Group = [MedicineEnum.Group.DoubleSignature.ToString()], + Name = "cisataracurio", + Type = [MedicineEnum.Types.AdrenalineNoradrenaline.ToString()] + } + }; + + _treatmentServiceMock.Setup(t => t.GetActiveTreatmentsByPatient(patientId)) + .ReturnsAsync(new List().AsEnumerable()); + _medicineServiceMock.Setup(m => m.GetByCodeOrNote(new List { "12345" })) + .ReturnsAsync([medicine]); + _medicineServiceMock.Setup(m => m.GetMedicinesOfTreatments(It.IsAny>())) + .ReturnsAsync(activeMedicines.AsEnumerable()); + _observationServiceMock.Setup(o => o.InsertObservation(It.IsAny(), true, true)); + _observationServiceMock.Setup(o => o.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([ + new PatientObservation(patientId, 1, "Medication") + ]); + + + await _calculatedObservations.Map(treatment); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "ERMedication" && (int)arg.Value == 0 && arg.PatientId == patientId + ), true, false)); + } + + + [Test] + public async Task Calculate__Medicine_Observation_DC_OF_TYPE_WITH_TWO_MEDICINES_Should_Subtract_0() + { + //Entra un tratamiento por primera vez con una medicina, se crea una observación medication con valor 1 + var patientId = ObjectId.GenerateNewId(); + + var treatment = + new PatientTreatment + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + OrderControl = OrderControlType.Dc, + PlacerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + RequestedGiveCodes = [new Code { Identifier = "12345" }], + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [], + Routes = [] + }; + + var medicine = + new Medicine + { + Codes = ["12345"], + Group = [MedicineEnum.Group.DoubleSignature.ToString()], + Name = "cisataracurio", + Type = [MedicineEnum.Types.AdrenalineNoradrenaline.ToString()] + }; + + var activeMedicines = new List + { + new() + { + Codes = ["12345"], + Name = "cisataracurio", + Group = [MedicineEnum.Group.DoubleSignature.ToString()], + Type = [MedicineEnum.Types.AdrenalineNoradrenaline.ToString()] + }, + + new() + { + Codes = ["12345"], + Name = "Lipoic acid", + Group = [MedicineEnum.Group.DoubleSignature.ToString()], + Type = [MedicineEnum.Types.AdrenalineNoradrenaline.ToString()] + } + }; + + _treatmentServiceMock.Setup(t => t.GetActiveTreatmentsByPatient(patientId)) + .ReturnsAsync(new List().AsEnumerable()); + _medicineServiceMock.Setup(m => m.GetByCodeOrNote(new List { "12345" })) + .ReturnsAsync([medicine]); + _medicineServiceMock.Setup(m => m.GetMedicinesOfTreatments(It.IsAny>())) + .ReturnsAsync(activeMedicines.AsEnumerable()); + _observationServiceMock.Setup(o => o.InsertObservation(It.IsAny(), true, false)); + + _observationServiceMock.Setup(o => o.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([ + new PatientObservation + { + PatientId = patientId, + Value = 2, + Name = "Medication" + } + ]); + + //_observationServiceMock.Setup(o => o.InsertObservation(It.Is(arg => + // (arg.Name == "ERMedication" && (int)arg.Value == 1) + // && arg.PatientId == patientId + //), true, true)); + + + await _calculatedObservations.Map(treatment); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Medication" + && (int)arg.Value == 1 + && arg.PatientId == patientId + ), true, false)); + } + + [Test] + public async Task Calculate_Medicine_Observation_New_Medicine_Without_Type_Should_Insert_Medication_1_point() + { + //Entra un tratamiento por primera vez con una medicina, se crea una observación medication con valor 1 + var patientId = ObjectId.GenerateNewId(); + + var treatment = + new PatientTreatment + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + OrderControl = OrderControlType.Nw, + PlacerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + RequestedGiveCodes = [new Code { Identifier = "12345" }], + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [], + Routes = [] + }; + + var medicine = + new Medicine + { + Codes = ["12345"], + Name = "Betaine" + }; + + var activeMedicines = new List + { + new() + { + Codes = ["12345"], + Name = "cisataracurio", + Type = [MedicineEnum.Types.AdrenalineNoradrenaline.ToString()] + }, + + new() + { + Codes = ["12345"], + Name = "Lipoic acid", + Type = [MedicineEnum.Types.AdrenalineNoradrenaline.ToString()] + } + }; + + + var expectedMedication = new PatientObservation + { + PatientId = patientId, + Value = 0, + Name = "Medication" + }; + + + _treatmentServiceMock.Setup(t => t.GetActiveTreatmentsByPatient(patientId)) + .ReturnsAsync(new List().AsEnumerable()); + _medicineServiceMock.Setup(m => m.GetByCodeOrNote(new List { "12345" })) + .ReturnsAsync([medicine]); + _medicineServiceMock.Setup(m => m.GetMedicinesOfTreatments(It.IsAny>())) + .ReturnsAsync(activeMedicines.AsEnumerable()); + _observationServiceMock.Setup(o => o.InsertObservation(It.IsAny(), true, true)); + _observationServiceMock.Setup(o => o.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([expectedMedication]); + + + await _calculatedObservations.Map(treatment); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Medication" + && (int)arg.Value == 1 + && arg.PatientId == patientId + ), true, false)); + } + + + [Test] + public async Task Calculate_First_Intravenous_Arterial_Observation_Should_Return_5() + { + var patientId = ObjectId.GenerateNewId(); + + var obs = new PatientObservation + { + PatientId = patientId, + Value = new PatientIntravenousLinesValue + { + Action = "Insert", + Duration = "7", + InsertTime = DateTime.Now, + Location = "Rodilla", + Type = "Catéter arterial" + }, + Name = "IntravenousLinesObs" + }; + + + _observationServiceMock.Setup(t => t.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([]); + + _observationServiceMock.Setup(t => t.FindLastIntravenousLinesObservationByLocation(patientId)) + .ReturnsAsync([]); + + await _calculatedObservations.Map(obs, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "IntravenousLines" + && (int)arg.Value == 5 + && arg.PatientId == patientId + ), true, false)); + } + + + [Test] + public async Task Calculate_Intravenous_Obs_Already_Exist_Removed_Should_Return_0() + { + var patientId = ObjectId.GenerateNewId(); + var now = DateTime.Now; + + var obs = new PatientObservation + { + PatientId = patientId, + Value = new PatientIntravenousLinesValue + { + Action = "Insert", + Duration = "7", + InsertTime = now, + Location = "Rodilla", + Type = "Catéter arterial", + RemoveTime = DateTime.Now.AddDays(1) + }, + Name = "IntravenousLinesObs" + }; + + _observationServiceMock.Setup(t => t.FindLastIntravenousLinesObservationByLocation(patientId)) + .ReturnsAsync([]); + + _observationServiceMock.Setup(t => t.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([ + new PatientObservation + { + PatientId = patientId, + Value = new PatientIntravenousLinesValue + { + Action = "Insert", + Duration = "7", + InsertTime = now, + Type = "Catéter arterial", + Location = "Rodilla" + }, + Name = "IntravenousLinesObs" + } + ]); + + await _calculatedObservations.Map(obs, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "IntravenousLines" + && (int)arg.Value == 0 + && arg.PatientId == patientId + ), true, false)); + } + + [Test] + public async Task Calculate_Intravenous_two_observations_of_same_type_and_same_location_should_return_null() + { + var patientId = ObjectId.GenerateNewId(); + var now = DateTime.Now; + + var obs = new PatientObservation + { + PatientId = patientId, + Name = "IntravenousLinesObs", + Value = new PatientIntravenousLinesValue + { + Action = "Insertado", + Duration = "7", + InsertTime = now, + Location = "Rodilla", + Type = "Catéter arterial" + } + }; + + _observationServiceMock.Setup(t => t.FindLastIntravenousLinesObservationByLocation(patientId)) + .ReturnsAsync([ + new PatientObservation + { + PatientId = patientId, + Value = new PatientIntravenousLinesValue + { + Action = "Insertado", + Duration = "7", + InsertTime = now, + Type = "Catéter arterial", + Location = "Rodilla" + }, + Name = "IntravenousLinesObs" + } + ]); + + + await _calculatedObservations.Map(obs, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "IntravenousLines" + && (int)arg.Value == 5 + && arg.PatientId == patientId + ), true, true), Times.Never); + } + + [Test] + public async Task Calculate_Intravenous_two_observations_of_same_type_should_return_10() + { + var patientId = ObjectId.GenerateNewId(); + var now = DateTime.Now; + + var obs = new PatientObservation + { + PatientId = patientId, + Name = "IntravenousLinesObs", + Value = new PatientIntravenousLinesValue + { + Action = "Insertado", + Duration = "7", + InsertTime = now, + Location = "Rodilla", + Type = "Catéter arterial" + } + }; + + _observationServiceMock.Setup(t => t.FindLastIntravenousLinesObservationByLocation(patientId)) + .ReturnsAsync([ + new PatientObservation + { + PatientId = patientId, + Name = "IntravenousLinesObs", + Value = new PatientIntravenousLinesValue + { + Action = "Insertado", + Duration = "7", + InsertTime = now, + Type = "Catéter arterial", + Location = "Codo" + } + } + ]); + + _observationServiceMock.Setup(t => t.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([]); + + await _calculatedObservations.Map(obs, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "IntravenousLines" + && (int)arg.Value == 10 + && arg.PatientId == patientId + ), true, false)); + } + + [Test] + public async Task Calculate_Intravenous_sum_more_than_10_should_limit_to_ten() + { + var patientId = ObjectId.GenerateNewId(); + var now = DateTime.Now; + + var obs = new PatientObservation + { + PatientId = patientId, + Value = new PatientIntravenousLinesValue + { + Action = "Insertado", + Duration = "7", + InsertTime = now, + Location = "Rodilla Izd.", + Type = "Catéter arterial" + }, + Name = "IntravenousLinesObs" + }; + + var patientObservations = new List + { + new() + { + PatientId = patientId, + Name = "IntravenousLinesObs", + Value = new PatientIntravenousLinesValue + { + Action = "Insertado", + Duration = "7", + InsertTime = now, + Type = "Catéter arterial", + Location = "Rodilla Der." + } + }, + new() + { + PatientId = patientId, + Name = "IntravenousLinesObs", + Value = new PatientIntravenousLinesValue + { + Action = "Insertado", + Duration = "7", + InsertTime = now, + Type = "Catéter PICC", + Location = "Brazo" + } + } + }; + + _observationServiceMock.Setup(t => t.FindLastIntravenousLinesObservationByLocation(patientId)) + .ReturnsAsync(patientObservations); + _observationServiceMock.Setup(t => t.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([]); + + await _calculatedObservations.Map(obs, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "IntravenousLines" + && (int)arg.Value == 10 + && arg.PatientId == patientId + ), true, false)); + } + + + [Test] + public async Task Calculate_Intravenous_sum_one_middle_line_and_two_Peripheral_should_return_5() + { + var patientId = ObjectId.GenerateNewId(); + var now = DateTime.Now; + + var obs = new PatientObservation + { + PatientId = patientId, + Value = new PatientIntravenousLinesValue + { + Action = "Insertado", + Duration = "7", + InsertTime = now, + Location = "Rodilla Izd.", + Type = "Catéter línea media" + }, + Name = "IntravenousLinesObs" + }; + + _observationServiceMock.Setup(t => t.FindLastIntravenousLinesObservationByLocation(patientId)) + .ReturnsAsync([ + new PatientObservation + { + PatientId = patientId, + Name = "IntravenousLinesObs", + Value = new PatientIntravenousLinesValue + { + Action = "Insertado", + Duration = "7", + InsertTime = now, + Type = "Catéter EPICUTÁNEO PERIFÉRICO", + Location = "Rodilla Der." + } + }, + + new PatientObservation + { + PatientId = patientId, + Name = "IntravenousLinesObs", + Value = new PatientIntravenousLinesValue + { + Action = "Insertado", + Duration = "7", + InsertTime = now, + Type = "Catéter Venoso PERIFÉRICO", + Location = "Brazo" + } + } + ]); + + _observationServiceMock.Setup(t => t.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([]); + + await _calculatedObservations.Map(obs, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "IntravenousLines" + && (int)arg.Value == 5 + && arg.PatientId == patientId + ), true, false)); + } + + + [Test] + public async Task Calculate_Monitor_Transcuatenous_Regional_Brain_Saturation_and_enter_EEG_should_return_3() + { + var patientId = ObjectId.GenerateNewId(); + + var treatment = new PatientTreatment + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + OrderControl = OrderControlType.Nw, + PlacerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + RequestedGiveCodes = [new Code { Identifier = "42803009" }], + OrderStatus = "NW", + OrderTime = DateTime.Now, + Notes = [], + Routes = [] + }; + + + _treatmentServiceMock.Setup(t => t.GetActiveTreatmentsByPatient(patientId)) + .ReturnsAsync(new List().AsEnumerable()); + _medicineServiceMock.Setup(m => m.GetByCodeOrNote(new List { "42803009" })) + .ReturnsAsync([]); + + _observationServiceMock.Setup(t => t.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([ + new PatientObservation + { + PatientId = patientId, + Code = "151756", + Name = "Transcutaneous", + Time = DateTime.Now + }, + + + new PatientObservation + { + PatientId = patientId, + Code = "151760", + Name = "Transcutaneous", + Time = DateTime.Now + }, + + + new PatientObservation + { + PatientId = patientId, + Code = "194908", + Name = "RegionalBrainSaturation", + Time = DateTime.Now + }, + + new PatientObservation + { + PatientId = patientId, + Code = "194909", + Name = "RegionalBrainSaturation", + Time = DateTime.Now + } + ]); + + await _calculatedObservations.Map(treatment); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Monitor" + && (int)arg.Value == 3 + && arg.PatientId == patientId + ), true, false)); + } + + [Test] + public async Task Calculate_Respiratory_INO_should_return_10() + { + var patientId = ObjectId.GenerateNewId(); + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = "OXIDO NITRICO 800 PPM MOL", + Code = "84481000140102", + Name = "Tipo de ventilación", + CodingSystem = "SNM" + }; + + + _observationServiceMock.Setup(t => t.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([]); + + await _calculatedObservations.Map(observation, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Respiratory" + && (int)arg.Value == 10 + && arg.PatientId == patientId + ), true, false)); + } + + [Test] + public async Task Calculate_Respiratory_VAFO_should_return_5() + { + var patientId = ObjectId.GenerateNewId(); + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = "V.A.F.O.", + Code = "361110005", + Name = "Tipo de ventilación", + CodingSystem = "SNM" + }; + + + _observationServiceMock.Setup(t => t.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([]); + + await _calculatedObservations.Map(observation, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Respiratory" + && (int)arg.Value == 5 + && arg.PatientId == patientId + ), true, false)); + } + + [Test] + public async Task Calculate_Respiratory_VMC_should_return_3() + { + var patientId = ObjectId.GenerateNewId(); + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = "V.M.C.", + Code = "361110005", + Name = "Tipo de ventilación", + CodingSystem = "SNM" + }; + + + _observationServiceMock.Setup(t => t.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([]); + + await _calculatedObservations.Map(observation, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Respiratory" + && (int)arg.Value == 3 + && arg.PatientId == patientId + ), true, false)); + } + + [Test] + public async Task Calculate_Respiratory_VMNI_should_return_2() + { + var patientId = ObjectId.GenerateNewId(); + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = "V.N.I. Ciclada", + Code = "361110005", + Name = "Tipo de ventilación", + CodingSystem = "SNM" + }; + + + _observationServiceMock.Setup(t => t.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([]); + + await _calculatedObservations.Map(observation, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Respiratory" + && (int)arg.Value == 2 + && arg.PatientId == patientId + ), true, false)); + } + + [Test] + public async Task Calculate_Respiratory_Nassal_Canulas_Bajo_Flujo_should_return_2() + { + var patientId = ObjectId.GenerateNewId(); + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = "Bajo Flujo", + Code = "361110005", + Name = "Tipo de ventilación", + CodingSystem = "SNM" + }; + + _observationServiceMock.Setup(t => t.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([]); + + + await _calculatedObservations.Map(observation, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Respiratory" + && (int)arg.Value == 1 + && arg.PatientId == patientId + ), true, false)); + } + + [Test] + public async Task Calculate_Respiratory_Nassal_Canulas_Alto_Flujo_should_return_1() + { + var patientId = ObjectId.GenerateNewId(); + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = "Alto Flujo", + Code = "361110005", + Name = "Tipo de ventilación", + CodingSystem = "SNM" + }; + + + _observationServiceMock.Setup(t => t.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([]); + + await _calculatedObservations.Map(observation, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Respiratory" + && (int)arg.Value == 1 + && arg.PatientId == patientId + ), true, false)); + } + + [Test] + public async Task Calculate_Ventilation_Mode_Returns_INVASIVE() + { + var patientId = ObjectId.GenerateNewId(); + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = "PC-AC", + Name = "Resp_Mode" + }; + + await _calculatedObservations.Map(observation, false); + _observationServiceMock.Verify(o => o.InsertIfChanged("Resp_Type", It.Is(arg => + arg.Name == "Resp_Type" && + arg.CodingSystem == "ADAS" && + arg.Value.ToString() == RespirationType.Invasive.ToString() + ), true, true)); + } + + [Test] + public async Task Calculate_Ventilation_Mode_Returns_NON_INVASIVE() + { + var patientId = ObjectId.GenerateNewId(); + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = "DUOPAP", + Name = "Resp_Mode" + }; + + await _calculatedObservations.Map(observation, false); + _observationServiceMock.Verify(o => o.InsertIfChanged("Resp_Type", It.Is(arg => + arg.Name == "Resp_Type" && + arg.CodingSystem == "ADAS" && + arg.Value.ToString() == RespirationType.NonInvasive.ToString() + ), true, true)); + } + + [Test] + public async Task Calculate_Ventilation_Mode_Returns_HIGH_FREQUENCY() + { + var patientId = ObjectId.GenerateNewId(); + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = "PC-HFO", + Name = "Resp_Mode" + }; + + await _calculatedObservations.Map(observation, false); + _observationServiceMock.Verify(o => o.InsertIfChanged("Resp_Type", It.Is(arg => + arg.Name == "Resp_Type" && + arg.CodingSystem == "ADAS" && + arg.Value.ToString() == RespirationType.HighFrequencyVentilation.ToString() + ), true, true)); + } + + [Test] + public async Task Calculate_Ventilation_Mode_Returns_NONE() + { + var patientId = ObjectId.GenerateNewId(); + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = "VENTAPNEA", + Name = "Resp_Mode" + }; + + await _calculatedObservations.Map(observation, false); + _observationServiceMock.Verify(o => o.InsertIfChanged("Resp_Type", It.Is(arg => + arg.Name == "Resp_Type" && + arg.CodingSystem == "ADAS" && + arg.Value.ToString() == RespirationType.None.ToString() + ), true, true)); + } + + [Test] + public async Task CalculateOxygenationIndex_ShouldInsertCorrectObservation() + { + var patientId = ObjectId.GenerateNewId(); + var observation = new PatientObservation + { + PatientId = patientId, + Name = "FiO2", + Value = 50 + }; + + var observations = new List + { + new() { Name = "AirPressure_Mean", Value = 20, PatientId = patientId }, + new() { Name = "FiO2", Value = 50, PatientId = patientId }, + new() { Name = "PaO2", Value = 80, PatientId = patientId } + }; + + _observationServiceMock.Setup(s => s.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync(observations); + + await _calculatedObservations.CalculateOxygenationIndex(observation); + + + _observationServiceMock.Verify(s => s.InsertObservation( + It.Is(o => + o.Name == "Oxygenation_Index" && o.PatientId == patientId && (int)o.Value == 1250), + It.IsAny(), It.IsAny())); + } + + [Test] + public async Task CalculateAsistResp_ShouldUpdateObservation_WhenConditionsMet() + { + var patientId = ObjectId.GenerateNewId(); + var observation = new PatientObservation + { + PatientId = patientId, + Name = "ONi", + Value = "valor", + Time = DateTime.Now, + Code = "codigo" + }; + + var lastAsistResp = new PatientObservation + { + PatientId = patientId, + Name = "Resp_Mode", + Value = "valor", + Time = DateTime.Now.AddMinutes(-10) + }; + + _observationServiceMock.Setup(s => s.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([lastAsistResp]); + + var result = await _calculatedObservations.CalculateAsistResp(observation); + + _observationServiceMock.Verify(s => s.InsertObservation( + It.Is(o => o.Name == "Respiratory" && o.PatientId == patientId), + It.IsAny(), It.IsAny())); + + Assert.That(result, Is.EqualTo(observation)); + } + + // [TestCase("valorHFV", RespirationType.HIGH_FREQUENCY_VENTILATION)] + // [TestCase("valorInvasive", RespirationType.INVASIVE)] + // [TestCase("valorNonInvasive", RespirationType.NON_INVASIVE)] + // [TestCase("valorNone", RespirationType.NONE)] + public async Task CalculateVentilationMode_ShouldInsertCorrectRespType(string value, + RespirationType expectedRespType) + { + var patientId = ObjectId.GenerateNewId(); + var observation = new PatientObservation + { + PatientId = patientId, + Name = "Resp_Mode", + Time = DateTime.Now, + Value = value switch + { + "valorHFV" => "PC-HFO", + "valorInvasive" => "PRVC", + "valorNonInvasive" => "DUOPAP", + _ => expectedRespType + } + }; + await _calculatedObservations.CalculateVentilationMode(observation); + + _observationServiceMock.Verify(s => s.InsertIfChanged( + "Resp_Type", + It.Is(o => + o.PatientId == patientId && o.Name == "Resp_Type" && o.Value.ToString() == expectedRespType.ToString()), + It.IsAny(), It.IsAny())); + } + + + // [TestCase("TAm", 10, 12, ObservationStatus.Ok)] + // [TestCase("Age_Gestational", 1, 2, ObservationStatus.Alert)] + // [TestCase("Age_Gestational", 1, 12, ObservationStatus.Ok)] + // [TestCase("Age_Gestational_Fixed", 5, 10, ObservationStatus.Ok)] + public async Task CalculateTAmAlert_ShouldUpdateStatusCorrectly(string observationName, int tamValue, + int gestationalAge, StatusEnum.Type expectedType) + { + var patientId = ObjectId.GenerateNewId(); + var observation = new PatientObservation + { + PatientId = patientId, + Name = observationName, + Value = tamValue, + Time = DateTime.Now, + Status = expectedType + }; + List groupedObservations = []; + switch (observationName) + { + case "TAm": + groupedObservations = + [ + new PatientObservation + { + Name = "Age_Gestational", Value = gestationalAge, PatientId = patientId, + Time = DateTime.Now.AddMinutes(-10), Status = expectedType + }, + new PatientObservation + { + Name = "Age_Gestational_Fixed", Value = gestationalAge, PatientId = patientId, + Time = DateTime.Now.AddMinutes(-10), Status = expectedType + } + ]; + break; + case "Age_Gestational": + case "Age_Gestational_Fixed": + groupedObservations = + [ + new PatientObservation + { + Name = "Age_Gestational", Value = gestationalAge, PatientId = patientId, + Time = DateTime.Now.AddMinutes(-10), Status = expectedType + }, + new PatientObservation + { + Name = "TAm", Value = tamValue, PatientId = patientId, Time = DateTime.Now.AddMinutes(-10), + Status = expectedType + } + ]; + break; + } + + _observationServiceMock.Setup(s => s.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync(groupedObservations); + + await _calculatedObservations.CalculateTAmAlert(observation); + + if (observation.Name == "TAm") + Assert.That(observation.Status, Is.EqualTo(expectedType)); + else + _observationServiceMock.Verify(s => s.InsertObservation( + It.Is(o => o.PatientId == patientId && o.Name == "TAm"), + It.IsAny(), It.IsAny())); + } + [Test] + public async Task ParseWeight_WhenValidObservation_ShouldConvertUnitsAndValue() + { + var obs = new PatientObservation + { + Value = 1.23 + }; + + var result = await _calculatedObservations.ParseWeight(obs) as PatientObservation; + + Assert.That(result, Is.Not.Null); + + Action assertions = () => + { + Assert.That(result!.Units, Is.EqualTo("gr")); + Assert.That(result.Value, Is.EqualTo(1230)); + }; + + Assert.Multiple(assertions); + } + + + [Test] + public async Task ParseWeight_WhenValueIsNotNumeric_ShouldHandleGracefullyAndReturnOriginal() + { + var obs = new PatientObservation + { + Value = "string" + }; + var result = await _calculatedObservations.ParseWeight(obs); + Assert.That(result, Is.EqualTo(obs)); + } + + [Test] + public async Task ParseWeight_WhenInvalidObservation_ShouldReturnOriginal() + { + var obs = new BasePatientObservation(); // No es PatientObservation, por lo que no se puede convertir + + + var result = await _calculatedObservations.ParseWeight(obs); + + Assert.That(result, Is.EqualTo(obs)); + } + + [Test] + public async Task CalculateTempGradient_WhenTempPatientAndTempIncubatorAreRecent_ShouldCalculateGradient() + { + var now = DateTime.UtcNow; + var patientId = ObjectId.GenerateNewId(); + var tempPatientObs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "Temp_Patient", + Time = now, + Value = 36.5 + }; + var tempIncubatorObs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "Temp_Incubator", + Time = now.AddMinutes(-10), // 10 minutos antes + Value = 37.0 + }; + + var tempGradientObs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "Temp_Gradient", + Time = now.AddSeconds(1), + Value = 37.0 + }; + _observationServiceMock.Setup(s => s.FindAnyWithSameDate(patientId, now, "Temp_Gradient")) + .ReturnsAsync([tempGradientObs]); + + _observationServiceMock.Setup(s => s.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([tempIncubatorObs]); + + _observationServiceMock.Setup(s => s.InsertObservation( + It.IsAny(), + It.IsAny(), + It.IsAny() + )).Callback((obs, _, _) => + { + Action assertions = () => + { + Assert.That(obs.Name, Is.EqualTo("Temp_Gradient")); + Assert.That(Math.Abs((double)obs.Value - (37.0 - 36.5)), Is.LessThan(0.001)); + }; + }); + + + await _calculatedObservations.CalculateTempGradient(tempPatientObs); + + + _observationServiceMock.Verify(s => s.InsertObservation( + It.IsAny(), + It.IsAny(), + It.IsAny() + ), Times.Once()); + } + + [Test] + public async Task CalculateTempGradient_WhenOneTemperatureIsMissing_ShouldNotCalculateGradient() + { + var patientId = ObjectId.GenerateNewId(); + var tempPatientObs = new PatientObservation + { + PatientId = patientId, + Name = "Temp_Patient", + Time = DateTime.UtcNow, + Value = 36.5 + }; + + // Configuramos el mock para que no devuelva ninguna observación para Temp_Incubator + _observationServiceMock.Setup(s => s.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([]); + + + await _calculatedObservations.CalculateTempGradient(tempPatientObs); + + + _observationServiceMock.Verify(s => s.InsertObservation( + It.IsAny(), + It.IsAny(), + It.IsAny() + ), Times.Never()); // Verificamos que InsertObservation no se llama + } + + [Test] + public async Task CalculateIntravenousLineObservation_ShouldCalculateObservationAndAdjustTimeIfNecessary() + { + var patientId = ObjectId.GenerateNewId(); + var currentTime = DateTime.UtcNow; + var obs = new PatientObservation + { + PatientId = patientId, + Name = "IntravenousLine", + Value = new PatientIntravenousLinesValue(), + Time = currentTime + }; + + _observationServiceMock.Setup(s => s.FindLastIntravenousLinesObservationByLocation(patientId)) + .ReturnsAsync([]); + + PatientObservation? insertedObservation = null; + _observationServiceMock.Setup(s => + s.InsertObservation(It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((ob, _, _) => insertedObservation = ob) + .Returns(Task.CompletedTask); + + _observationServiceMock.Setup(s => s.FindLastObservations(It.IsAny(), 1, It.IsAny>())) + .ReturnsAsync([]); + + await _calculatedObservations.CalculateIntravenousLineObservation(obs); + + Assert.That(insertedObservation, Is.Not.Null); + Action assertions = () => + { + Assert.That(insertedObservation!.PatientId, Is.EqualTo(patientId)); + Assert.That(insertedObservation.Name, Is.EqualTo("IntravenousLines")); + Assert.That(insertedObservation.Time, + Is.GreaterThanOrEqualTo(currentTime)); // Verificar que la hora no sea anterior a la hora actual + }; + } + + [Test] + public async Task CalculateMonitor_WithValidObservationOrTreatment_ShouldCalculateAndInsertObservation() + { + var patientId = ObjectId.GenerateNewId(); + var monitorObservation = new PatientObservation + { + PatientId = patientId + }; + + var activeObservation = new PatientObservation + { + Code = _optionsApiSettings.Value.RegionalBrainSaturation?.FirstOrDefault(), + PatientId = patientId + }; + + var activeTreatment = new PatientTreatment + { + PatientId = patientId + }; + + + _observationServiceMock.Setup(s => + s.FindLastObservations(It.IsAny(), It.IsAny(), It.IsAny>())) + .ReturnsAsync([activeObservation]); + + _treatmentServiceMock.Setup(s => s.GetActiveTreatmentsByPatient(It.IsAny())) + .ReturnsAsync(new List { activeTreatment }); + + _observationServiceMock.Setup(s => s.InsertObservation(It.IsAny(), true, true)) + .Returns(Task.CompletedTask) + .Verifiable(); + + + await _calculatedObservations.CalculateMonitor(monitorObservation); + + + _observationServiceMock.Verify(s => s.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name == "Monitor" && + o.CodingSystem == "ADAS" && + o.Min != null && Math.Abs(o.Min.Value - 0) < 0.01 && + o.Max != null && Math.Abs(o.Max.Value - 3) < 0.01 + ), It.IsAny(), It.IsAny()), + Times.Once()); + } + + [Test] + public async Task CalculateMonitor_WithPatientTreatment_ShouldCalculateAndInsertObservation() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var monitorTreatment = new PatientTreatment + { + PatientId = patientId + }; + + var activeTreatment = new PatientTreatment + { + PatientId = patientId + }; + + var patientObservation = new PatientObservation + { + Code = _optionsApiSettings.Value.Transcutaneous?.First(), + PatientId = patientId, + Expired = false + }; + + // Simular respuestas de los servicios + _treatmentServiceMock.Setup(s => s.GetActiveTreatmentsByPatient(It.IsAny())) + .ReturnsAsync(new List { activeTreatment }); + + _observationServiceMock.Setup(s => s.InsertObservation(It.IsAny(), true, true)) + .Returns(Task.CompletedTask) + .Verifiable(); + _observationServiceMock.Setup(s => s.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([patientObservation]); + // Act + await _calculatedObservations.CalculateMonitor(monitorTreatment); + + // Assert + _observationServiceMock.Verify(s => s.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name == "Monitor" && + o.CodingSystem == "ADAS" && + o.Min != null && Math.Abs(o.Min.Value - 0) < 0.01 && + o.Max != null && Math.Abs(o.Max.Value - 3) < 0.01 + ), It.IsAny(), It.IsAny()), + Times.Once()); + } + + [Test] + public async Task CalculateMonitor_WithIncompleteData_ShouldNotInsertObservation() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var incompleteObservation = new PatientObservation + { + PatientId = patientId + // Proporcionar datos incompletos o irrelevantes + }; + + // Simular respuestas de los servicios + _observationServiceMock.Setup(s => + s.FindLastObservations(It.IsAny(), It.IsAny(), It.IsAny>())) + .ReturnsAsync([]); + + _observationServiceMock.Setup(s => s.InsertObservation(It.IsAny(), true, true)) + .Returns(Task.CompletedTask) + .Verifiable(); + + // Act + await _calculatedObservations.CalculateMonitor(incompleteObservation); + + // Assert + _observationServiceMock.Verify( + s => s.InsertObservation(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never()); + } + + [Test] + public async Task CheckSurgeryExpired_WhenNotExpired_DoesNotInsertObservation() + { + var obs = new PatientObservation + { + Expired = false + }; + + await _calculatedObservations.CheckSurgeryExpired(obs); + + _observationServiceMock.Verify(s => s.InsertObservation(It.IsAny(), true, false), + Times.Never); + } + + [Test] + public async Task CheckSurgeryExpired_WhenExpired_InsertsCorrectObservation() + { + var initialTime = DateTime.Now; + var obs = new PatientObservation + { + Expired = true, + PatientId = ObjectId.GenerateNewId(), + Time = initialTime, + Expires = 30 + }; + + await _calculatedObservations.CheckSurgeryExpired(obs); + + _observationServiceMock.Verify(s => s.InsertObservation(It.Is(o => + o.PatientId == obs.PatientId && + o.Name == "Surgery" && + o.Min != null && Math.Abs(o.Min.Value - 0) < 0.01 && + o.Max != null && Math.Abs(o.Max.Value - 5) < 0.01 && + o.CodingSystem == "ADAS" && + o.Time == initialTime.AddSeconds(1) && + o.Value.Equals(0) && // Asegúrate de que la comparación de 'o.Value' sea apropiada para su tipo + o.Expires == 30 + ), true, false), Times.Once); + } + + [Test] + public async Task CalculateComplexity_NewObservation_NoECMO_AddsComplexity() + { + var patientId = ObjectId.GenerateNewId(); + var patientTreatment = new PatientTreatment + { + Id = patientId + }; + + var patientObservation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "Weight_Current", + Value = 10 + }; + + var complelxity = new PatientObservation + { + Id = patientId, + Value = 1 + }; + + // Mock de datos de tratamiento activo + var activeTreatments = new List { patientTreatment }; + _treatmentServiceMock.Setup(m => m.GetActiveTreatmentsByPatient(It.IsAny())) + .ReturnsAsync(activeTreatments); + + // Mock de datos de observación + var observations = new List { patientObservation }; + _observationServiceMock.Setup(m => m.FindLastObservations(It.IsAny(), 1, It.IsAny>())) + .ReturnsAsync(observations); + + var complexityObservations = new List { complelxity }; + _observationServiceMock.Setup(m => m.FindLastObservations(It.IsAny(), 1, It.IsAny>())) + .ReturnsAsync(complexityObservations); + + var obs = patientObservation as BasePatientObservation; + var result = await _calculatedObservations.CalculateComplexity(obs); + + + // Verifica que el valor de complejidad calculado sea el esperado + Assert.That(result.Name, Is.EqualTo("Weight_Current")); + + // Verifica que se hayan llamado los métodos esperados en los servicios mockeados + _treatmentServiceMock.Verify(m => m.GetActiveTreatmentsByPatient(It.IsAny()), Times.Once); + + _observationServiceMock.Verify(m => m.FindLastObservations(It.IsAny(), 1, It.IsAny>()), + Times.Exactly(3)); + + + _observationServiceMock.Verify(s => s.InsertObservation(It.Is(o => + o.PatientId == obs.PatientId && + o.Name == "Complexity" && + o.CodingSystem == "ADAS" && + Equals(o.Value, 7) + ), true, false), Times.Once); + } + + [Test] + public async Task CalculateComplexity_ExpiredObservation_DoesNotAddToComplexity() + { + // Configuración inicial + var patientId = ObjectId.GenerateNewId(); + var expiredPatientObservation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "Weight_Current", + Value = 10, + Expired = true // Observación expirada + }; + + var observations = new List { expiredPatientObservation }; + + _observationServiceMock.Setup(m => m.FindLastObservations(It.IsAny(), 1, It.IsAny>())) + .ReturnsAsync(observations); + + var obs = expiredPatientObservation as BasePatientObservation; + var result = await _calculatedObservations.CalculateComplexity(obs); + + // Verificaciones + Assert.That(result.Name, Is.EqualTo("Weight_Current")); + + _observationServiceMock.Verify(s => s.InsertObservation(It.Is(o => + o.PatientId == obs.PatientId && + o.Name == "Complexity" && + Equals(o.Value, 0) // Valor de complejidad no debe cambiar + ), true, true), Times.Never); // No se debe llamar InsertObservation + } + + [Test] + public async Task CalculateComplexity_ECMOActive_AdjustsComplexity() + { + var code = new Code { Identifier = "705923009" }; + + // Configuración inicial + var patientId = ObjectId.GenerateNewId(); + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "ECMO", + Value = 10 + }; + + var lastObservation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "Surgery", + Value = 1 + }; + + var patientTreatment = new PatientTreatment + { + Id = patientId + }; + + patientTreatment.RequestedGiveCodes.Add(code); + + // Mock de datos de tratamiento activo + var activeTreatments = new List { patientTreatment }; + _treatmentServiceMock.Setup(m => m.GetActiveTreatmentsByPatient(It.IsAny())) + .ReturnsAsync(activeTreatments); + + var observations = new List { lastObservation }; + + _observationServiceMock.Setup(m => m.FindLastObservations(It.IsAny(), 1, It.IsAny>())) + .ReturnsAsync(observations); + + BasePatientObservation obs = observation; + var result = await _calculatedObservations.CalculateComplexity(obs); + + // Verificaciones + Assert.That(result.Name, Is.EqualTo("ECMO")); + + _observationServiceMock.Verify(s => s.InsertObservation(It.Is(o => + o.PatientId == obs.PatientId && + o.Name == "ECMO" && + Equals(o.Value, 10) // Valor de complejidad no debe cambiar + ), true, true), Times.Once); + + _observationServiceMock.Verify(s => s.InsertObservation(It.Is(o => + o.PatientId == obs.PatientId && + o.Name == "Complexity" && + Equals(o.Value, "1 ECMO") + ), true, false), Times.Once); + } + + [Test] + public async Task CheckObsExistsAndIncrementTime_SameHourObservation_IncrementsTime() + { + // Configuración inicial + var patientId = ObjectId.GenerateNewId(); + var initialTime = DateTime.UtcNow; + var sameHourObservation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Time = initialTime, + Name = "TestObservation" + }; + + var existingObservation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Time = initialTime, // Misma hora que sameHourObservation + Name = "TestObservation" + }; + + // Mock del servicio de observación para que retorne una observación existente en la misma hora + _observationServiceMock.Setup(m => + m.FindLastBeforeDate(It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(existingObservation); + + var obs = sameHourObservation as BasePatientObservation; + var result = await _calculatedObservations.CheckObsExistsAndIncrementTime(obs); + + // Verificaciones + var resultObs = result as PatientObservation; + Assert.That(resultObs, Is.Not.Null); + Assert.That(resultObs!.Time, Is.GreaterThan(initialTime)); // Verifica que el tiempo ha sido incrementado + Assert.That(resultObs.Time, + Is.EqualTo(existingObservation.Time.AddSeconds(1))); // Verifica que el tiempo se incrementó en 1 segundo + } + + + [Test] + public async Task CheckObsWithSameTimeExistsAndIncrementTime_WhenObservationWithSameTimeExists_IncrementsTime() + { + // Configuración inicial + var patientId = ObjectId.GenerateNewId(); + var observationTime = DateTime.UtcNow; + const string observationName = "TestObservation"; + + var newObservation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Time = observationTime, + Name = observationName + }; + + var existingObservation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Time = observationTime, // Misma hora y nombre que newObservation + Name = observationName + }; + + // Mock del servicio de observación para que retorne una observación existente con el mismo tiempo y nombre + _observationServiceMock.Setup(m => m.FindAnyWithSameDate(patientId, observationTime, observationName)) + .ReturnsAsync([existingObservation]); + + BasePatientObservation obs = newObservation; + var result = await _calculatedObservations.CheckObsWithSameTimeExistsAndIncrementTime(obs); + + // Verificaciones + var resultObs = result as PatientObservation; + Assert.That(resultObs, Is.Not.Null); + using (Assert.EnterMultipleScope()) + { + { + Assert.That(resultObs!.Time, + Is.EqualTo(observationTime.AddSeconds(1))); // Verifica que el tiempo se haya incrementado en 1 segundo + Assert.That(existingObservation.Id, + Is.Not.EqualTo(resultObs.Id)); // Verifica que el Id sea diferente al de la observación existente + } + ; + } + } + + [Test] + public async Task CheckTreatmentMedicines_ProcessesTreatmentCorrectly_WithParentalNutritionMedicine() + { + var patientId = ObjectId.GenerateNewId(); + var code = new Code { Identifier = "705923009" }; + + var medicationObs = new PatientObservation { PatientId = patientId, Value = 2 }; + + // Configuración inicial + var treatment = new PatientTreatment + { + PatientId = patientId, + Notes = + [ + new Note { Comment = "NPT", CommentType = "formularybaseformulation" } + ], + RequestedGiveCodes = [code], + RequestedGiveTreatment = "Medicación" + }; + + var medicine = new Medicine { Codes = [code.Identifier] }; + + var lastMedicationObs = new List { medicationObs }; + + var expectedMedicines = new List { medicine }; + + var activeTreatments = new List { treatment }; + + // Mock de medicineService para que devuelva medicinas esperadas + + _medicineServiceMock.Setup(m => m.GetByCodeOrNote(It.IsAny>())) + .ReturnsAsync(expectedMedicines); + + _medicineServiceMock.Setup(m => m.GetMedicinesOfTreatments(It.IsAny>())) + .ReturnsAsync(expectedMedicines); + + _treatmentServiceMock.Setup(m => m.GetActiveTreatmentsByPatient(It.IsAny())) + .ReturnsAsync(activeTreatments); + + _observationServiceMock + .Setup(m => m.FindLastObservations(It.IsAny(), 1, new List { "Medication" })) + .ReturnsAsync(lastMedicationObs); + + _observationServiceMock.Setup(s => s.FindLastObservations(It.IsAny(), 1, It.IsAny>())) + .ReturnsAsync([]); + // Otras configuraciones de mock según sea necesario + + // Llamada al método bajo prueba + await _calculatedObservations.CheckTreatmentMedicines(treatment); + + // Verificaciones + // Verifica que las llamadas a los servicios mockeados son como se esperan + // Verifica que la lógica de procesamiento de medicamentos es correcta + // Puedes usar Assert o Verify de Moq según corresponda + // ... + + _observationServiceMock.Verify(s => s.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name == "Medication" && + o.CodingSystem == "ADAS" && + Equals(o.Value, 1) + ), true, false), Times.Once); + } +} \ No newline at end of file diff --git a/adas-core.Test/Customizations/HPAZ/CalculatedObservationsTest.cs b/adas-core.Test/Customizations/HPAZ/CalculatedObservationsTest.cs new file mode 100644 index 00000000..10231515 --- /dev/null +++ b/adas-core.Test/Customizations/HPAZ/CalculatedObservationsTest.cs @@ -0,0 +1,886 @@ +using adas_core.Application.Customizations.HPAZ; +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.Pumps; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Moq; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Customizations.HPAZ; + +[TestFixture] +public class CalculatedObservationsTest +{ + [SetUp] + public void Setup() + { + _alarmServiceMock = new Mock(); + var alarmServiceLazy = new Lazy(() => _alarmServiceMock.Object); + + + _patientServiceMock = new Mock(); + + _lightBeaconServiceMock = new Mock(); + var balizaServiceLazy = new Lazy(() => _lightBeaconServiceMock.Object); + + _recordingServiceMock = new Mock(); + var recordingServiceLazy = new Lazy(() => _recordingServiceMock.Object); + + _relayServiceMock = new Mock(); + var relayServiceLazy = new Lazy(() => _relayServiceMock.Object); + + _treatmentServiceMock = new Mock(); + var treatmentServiceLazy = new Lazy(() => _treatmentServiceMock.Object); + + _medicineServiceMock = new Mock(); + var medicineServiceLazy = new Lazy(() => _medicineServiceMock.Object); + + _configObservationServiceMock = new Mock(); + + + _observationServiceMock = new Mock(); + var observationServiceLazy = new Lazy(() => _observationServiceMock.Object); + + _logger = new Mock>(); + + _optionsApiSettings = Options.Create(_apiSettings); + + _optionsApiSettings.Value.IgnoreCalcObservationsOlderInMinutesThan = 5; + + _optionsRecordingSettings = Options.Create(_recordingSettings); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(_patientServiceMock.Object); + serviceCollection.AddSingleton(balizaServiceLazy); + serviceCollection.AddSingleton(recordingServiceLazy); + serviceCollection.AddSingleton(relayServiceLazy); + serviceCollection.AddSingleton(treatmentServiceLazy); + serviceCollection.AddSingleton(medicineServiceLazy); + serviceCollection.AddSingleton(_configObservationServiceMock.Object); + serviceCollection.AddSingleton(observationServiceLazy); + serviceCollection.AddSingleton(_optionsApiSettings); + serviceCollection.AddSingleton(_optionsRecordingSettings); + serviceCollection.AddSingleton(_logger.Object); + serviceCollection.AddSingleton(alarmServiceLazy); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + _calculatedObservations = new CalculatedObservations(serviceProvider); + } + + private CalculatedObservations _calculatedObservations; + private Mock _patientServiceMock; + private Mock _lightBeaconServiceMock; + private Mock _recordingServiceMock; + private Mock _relayServiceMock; + private Mock _treatmentServiceMock; + private Mock _medicineServiceMock; + private Mock _configObservationServiceMock; + private Mock _observationServiceMock; + private Mock _alarmServiceMock; + private Mock> _logger; + + private readonly ApiSettings _apiSettings = new() + { + VolumetricAirOcclusionAlarm = ["Occlusion", "Upstream Occlusion"] + }; + + private IOptions _optionsApiSettings; + private readonly RecordingSettings _recordingSettings = new(); + private IOptions _optionsRecordingSettings; + + private readonly List _pressBloodArteryMean = ["TAm"]; + + + private static readonly DateTime Now = DateTime.Now; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + + + [Test] + public async Task Calculate_Ventilation_Mode_PRVC_Returns_VCRP() + { + var patientId = ObjectId.GenerateNewId(); + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = "PRVC", + Name = "Resp_Mode" + }; + + var result = await _calculatedObservations.Map(observation, false); + + Assert.That(result, Is.Not.Null); + Assert.That(result!.Value, Is.EqualTo("VCRP")); + } + + [Test] + public async Task Calculate_Ventilation_Mode_FLUJ_ALTO_Returns_OAF() + { + var patientId = ObjectId.GenerateNewId(); + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = "FLUJ.ALTO", + Name = "Resp_Mode" + }; + + var result = await _calculatedObservations.Map(observation, false); + + Assert.That(result, Is.Not.Null); + Assert.That(result!.Value, Is.EqualTo("OAF")); + } + + [Test] + public async Task Calculate_Ventilation_Mode_Not_PRVC_Not_FLUJ_ALTO_Returns_Other() + { + var patientId = ObjectId.GenerateNewId(); + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = "VNI PC", + Name = "Resp_Mode" + }; + + var result = await _calculatedObservations.Map(observation, false); + + Assert.That(result, Is.Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(result!.Value, Is.Not.EqualTo("VCRP")); + Assert.That(result.Value, Is.Not.EqualTo("OAF")); + } + + } + + + [Test] + public async Task CalculateSF_Sattc_98_Returns_Not_Sattc_FiO2() + { + var patientId = ObjectId.GenerateNewId(); + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "Sattc", + Value = 98 + }; + + var obsLast = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "FiO2", + Value = 34 + }; + + var value = 96.0 / 34.0; + value = Math.Round(value, 4); + + var obsCalculate = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "Sattc_FiO2", + CodingSystem = "ADAS", + Value = value + }; + + _observationServiceMock + .Setup(l => l.FindLastObservations(It.IsAny(), It.IsAny(), It.IsAny>())) + .ReturnsAsync([obsLast]); + + await _calculatedObservations.Map(observation, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == obsCalculate.Name && + Math.Abs(double.Parse(arg.Value.ToString()!) - double.Parse(obsCalculate.Value.ToString()!)) <= 0 && + arg.PatientId == patientId + && arg.CodingSystem == obsCalculate.CodingSystem + ), true, true), Times.Never); + } + + [Test] + public async Task CalculateOxygenationIndex_FiO2_Returns_Oxygenation_Index() + { + var patientId = ObjectId.GenerateNewId(); + + var obsFio2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "FiO2", + Value = 34 + }; + + var obsPVam = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "P_VAM", + Value = 6 + }; + + var obsPaO2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "PaO2_Tidal", + Value = 234 + }; + + const int value = 6 * 34 * 100 / 234; + + var obsCalculate = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "Oxygenation_Index", + CodingSystem = "ADAS", + Value = value + }; + + _observationServiceMock + .Setup(l => l.FindLastObservations(It.IsAny(), It.IsAny(), It.IsAny>())) + .ReturnsAsync([obsPVam, obsPaO2]); + + await _calculatedObservations.Map(obsFio2, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == obsCalculate.Name && arg.Value.ToString() == obsCalculate.Value.ToString() && + arg.PatientId == patientId + && arg.CodingSystem == obsCalculate.CodingSystem + ), true, true)); + } + + [Test] + public async Task CalculateOxygenationIndex_P_VAM_Returns_Oxygenation_Index() + { + var patientId = ObjectId.GenerateNewId(); + + var obsFiO2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "FiO2", + Value = 34 + }; + + var obsPVam = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "P_VAM", + Value = 6 + }; + + var obsPaO2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "PaO2_Tidal", + Value = 234 + }; + + var value = 6 * 34 * 100 / 234; + + var obsCalculate = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "Oxygenation_Index", + CodingSystem = "ADAS", + Value = value + }; + + _observationServiceMock + .Setup(l => l.FindLastObservations(It.IsAny(), It.IsAny(), It.IsAny>())) + .ReturnsAsync([obsFiO2, obsPaO2]); + + await _calculatedObservations.Map(obsPVam, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == obsCalculate.Name && arg.Value.ToString() == obsCalculate.Value.ToString() && + arg.PatientId == patientId + && arg.CodingSystem == obsCalculate.CodingSystem + ), true, true)); + } + + [Test] + public async Task CalculateOxygenationIndex_PaO2_Tidal_Returns_Oxygenation_Index() + { + var patientId = ObjectId.GenerateNewId(); + + var obsFiO2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "FiO2", + Value = 34 + }; + + var obsPVam = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "P_VAM", + Value = 6 + }; + + var obsPaO2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "PaO2_Tidal", + Value = 234 + }; + + var value = 6 * 34 * 100 / 234; + + var obsCalculate = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "Oxygenation_Index", + CodingSystem = "ADAS", + Value = value + }; + + _observationServiceMock + .Setup(l => l.FindLastObservations(It.IsAny(), It.IsAny(), It.IsAny>())) + .ReturnsAsync([obsFiO2, obsPVam]); + + await _calculatedObservations.Map(obsPaO2, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == obsCalculate.Name && arg.Value.ToString() == obsCalculate.Value.ToString() && + arg.PatientId == patientId + && arg.CodingSystem == obsCalculate.CodingSystem + ), true, true)); + } + + [Test] + public async Task CalculateOxygenationIndex_PaO2_Tidal_Not_Returns_Oxygenation_Index() + { + var patientId = ObjectId.GenerateNewId(); + + var obsFiO2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "FiO2", + Value = 34 + }; + + var obsPaO2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "PaO2_Tidal", + Value = 234 + }; + + var obsCalculate = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "Oxygenation_Index", + CodingSystem = "ADAS" + }; + + _observationServiceMock + .Setup(l => l.FindLastObservations(It.IsAny(), It.IsAny(), It.IsAny>())) + .ReturnsAsync([obsFiO2]); + + await _calculatedObservations.Map(obsPaO2, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == obsCalculate.Name && arg.PatientId == patientId + && arg.CodingSystem == obsCalculate.CodingSystem + ), true, true), Times.Never); + } + + [Test] + public async Task CalculateOxygenationIndex_PaO2_Tidal_0_Not_Returns_Oxygenation_Index() + { + var patientId = ObjectId.GenerateNewId(); + + var obsFiO2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "FiO2", + Value = 34 + }; + + var obsPVam = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "P_VAM", + Value = 6 + }; + + var obsPaO2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "PaO2_Tidal", + Value = 0 + }; + + const int value = 6 * 34 * 100 / 234; + + var obsCalculate = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "Oxygenation_Index", + CodingSystem = "ADAS", + Value = value + }; + + _observationServiceMock + .Setup(l => l.FindLastObservations(It.IsAny(), It.IsAny(), It.IsAny>())) + .ReturnsAsync([obsFiO2, obsPVam]); + + await _calculatedObservations.Map(obsPaO2, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == obsCalculate.Name && arg.PatientId == patientId + && arg.CodingSystem == obsCalculate.CodingSystem + ), true, true), Times.Never); + } + + [Test] + public async Task CalculatePF_PaO2_Returns_PaO2_FiO2() + { + var patientId = ObjectId.GenerateNewId(); + + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "PaO2_Tidal", + Value = 234 + }; + + var obsLast = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "FiO2", + Value = 34 + }; + + var value = 234.0 / 34.0; + value = Math.Round(value, 4); + + var obsCalculate = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "PaO2_FiO2", + CodingSystem = "ADAS", + Value = value + }; + + _observationServiceMock + .Setup(l => l.FindLastObservations(It.IsAny(), It.IsAny(), It.IsAny>())) + .ReturnsAsync([obsLast]); + + await _calculatedObservations.Map(observation, false); + + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == obsCalculate.Name && Equals(TryparseDouble(arg.Value), TryparseDouble(obsCalculate.Value)) && + arg.PatientId == patientId + && arg.CodingSystem == obsCalculate.CodingSystem + ), true, true)); + } + + [Test] + public async Task CalculatePF_FiO2_Returns_PaO2_FiO2() + { + var patientId = ObjectId.GenerateNewId(); + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "FiO2", + Value = 34 + }; + + var obsLast = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "PaO2_Tidal", + Value = 234 + }; + + var value = 234.0 / 34.0; + value = Math.Round(value, 4); + + var obsCalculate = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "PaO2_FiO2", + CodingSystem = "ADAS", + Value = value + }; + + _observationServiceMock + .Setup(l => l.FindLastObservations(It.IsAny(), It.IsAny(), It.IsAny>())) + .ReturnsAsync([obsLast]); + + await _calculatedObservations.Map(observation, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == obsCalculate.Name && Equals(TryparseDouble(arg.Value), TryparseDouble(obsCalculate.Value)) && + arg.PatientId == patientId + && arg.CodingSystem == obsCalculate.CodingSystem + ), true, true)); + } + + [Test] + public async Task CalculatePF_FiO2_0_Not_Returns_PaO2_FiO2() + { + var patientId = ObjectId.GenerateNewId(); + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "PaO2_Tidal", + Value = 98 + }; + + var obsLast = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "FiO2", + Value = 0 + }; + + var obsCalculate = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "Sattc_FiO2", + CodingSystem = "ADAS" + }; + + _observationServiceMock + .Setup(l => l.FindLastObservations(It.IsAny(), It.IsAny(), It.IsAny>())) + .ReturnsAsync([obsLast]); + + await _calculatedObservations.Map(observation, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == obsCalculate.Name && arg.PatientId == patientId + && arg.CodingSystem == obsCalculate.CodingSystem + ), true, true), Times.Never); + } + + + [Test] + public async Task + SendAlarm_BlueCode_FC_SpO2_Alarm_True_Returns_SendAlarm_BlueCode() + { + var patientId = ObjectId.GenerateNewId(); + var now = DateTime.Now; + + + var obsSattc = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "Sattc", + Value = 50, + Time = now + }; + + + var obsFc = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = "50", + Name = "FC", + Time = now + }; + + var listObservation = new List + { + obsSattc, obsFc + }; + + await _calculatedObservations.CalculateBlueCodeList(listObservation); + + + _observationServiceMock.Verify(o => o.InsertObservation(It.IsAny(), true, true), + Times.Once); + _alarmServiceMock.Verify( + a => a.SendAlarm(It.IsAny(), "BlueCode", AlarmEnum.Name.Blue, AlarmEnum.Severity.None, + AlarmEnum.Type.Auto), Times.Once); + } + + [Test] + public async Task + SendAlarm_BlueCode_FC_TAm_Alarm_True_Returns_SendAlarm_BlueCode() + { + var patientId = ObjectId.GenerateNewId(); + var now = DateTime.Now; + + + var obsFc = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = "50", + Name = "FC", + Time = now + }; + + var obsTAm1 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "TAm", + Value = 100, + Time = now.AddSeconds(-5) + }; + + var obsTAm2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "TAm", + Value = 50, + Time = now + }; + + var listObservation = new List + { + obsFc, obsTAm2 + }; + + var listTAmObs = new List + { + obsTAm1 + }; + + _observationServiceMock.Setup(l => l.FindLastObservations(patientId, 1, _pressBloodArteryMean)) + .ReturnsAsync(listTAmObs); + + + await _calculatedObservations.CalculateBlueCodeList(listObservation); + + + _observationServiceMock.Verify(o => o.InsertObservation(It.IsAny(), true, true), + Times.Once); + _alarmServiceMock.Verify( + a => a.SendAlarm(It.IsAny(), "BlueCode", AlarmEnum.Name.Blue, AlarmEnum.Severity.None, + AlarmEnum.Type.Auto), Times.Once); + } + + [Test] + public async Task SendAlarm_BlueCode_FC_TAm_Alarm_False() + { + var patientId = ObjectId.GenerateNewId(); + var now = DateTime.Now; + + + var obsFc = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = "50", + Name = "FC", + Time = now + }; + + var obsTAm1 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "TAm", + Value = 100, + Time = now.AddSeconds(-5) + }; + + var obsTAm2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "TAm", + Value = 80, + Time = now + }; + + var listObservation = new List + { + obsFc, obsTAm2 + }; + + var listTAmObs = new List + { + obsTAm1 + }; + + _observationServiceMock.Setup(l => l.FindLastObservations(patientId, 1, _pressBloodArteryMean)) + .ReturnsAsync(listTAmObs); + + + await _calculatedObservations.CalculateBlueCodeList(listObservation); + + + _observationServiceMock.Verify(o => o.InsertObservation(It.IsAny(), true, true), + Times.Never); + _alarmServiceMock.Verify( + a => a.SendAlarm(It.IsAny(), "BlueCode", AlarmEnum.Name.Blue, AlarmEnum.Severity.None, + AlarmEnum.Type.Auto), Times.Never); + } + + private static double? TryparseDouble(object value) + { + return double.TryParse(value.ToString(), out var valueParsed) ? valueParsed : null; + } + + + [Test] + public async Task Map_PumpObservation_DrugName_Null_AlarmType_Occlusion_Return_Insert_Pump_Insert_PositionName() + { + var pumpObservation = new PumpObservation + { + Code = "Tile-6", + Time = Now, + Name = "g-1-pump-Tile-6", + Number = 2, + Total = 7, + TotalAux = 0, + Status = PumpEnum.Status.Alarm, + PumpMode = PumpEnum.Mode.Unknown, + InfusingStatus = PumpEnum.InfusingStatus.Unknown, + Pressure = new CommonPumpTypes.PumpValue(), + //drugName = "Nutrición Parenteral", + Concentration = new CommonPumpTypes.PumpValue(), + DrugAmount = new CommonPumpTypes.PumpValue(), + DiluentVolume = new CommonPumpTypes.PumpValue(), + DoseRate = new CommonPumpTypes.PumpValue(), + Rate = new CommonPumpTypes.PumpValue(), + VolumeInfused = new CommonPumpTypes.PumpValue + { + Value = 0.00001 + }, + VolumeRemaining = new CommonPumpTypes.PumpValue + { + Value = 0.00001 + }, + TimeRemaining = new CommonPumpTypes.PumpValue + { + Value = 1441, + Units = "min" + }, + PatientWeight = new CommonPumpTypes.PumpValue(), + AlarmType = PumpEnum.AlarmType.Occlusion, + AlarmMode = PumpEnum.AlarmMode.Hight, + GatewayNumber = 1, + IsAux = false, + IsInfusing = false, + Expires = 100 + }; + + await _calculatedObservations.Map(pumpObservation); + + + _observationServiceMock.Verify(d => d.InsertObservation(It.Is(arg => + arg.Value.ToString() == "Rack 1 Bomba 2"), true, true)); + } + + [Test] + public async Task + Map_PumpObservation_DrugName_Null_AlarmType_Occlusion_Return_Insert_Pump_Insert_Aux_PositionName() + { + var pumpObservation = new PumpObservation + { + Code = "Tile-6", + Time = Now, + Name = "g-1-pump-Tile-6", + Number = 2, + Total = 7, + TotalAux = 0, + Status = PumpEnum.Status.Alarm, + PumpMode = PumpEnum.Mode.Unknown, + InfusingStatus = PumpEnum.InfusingStatus.Unknown, + Pressure = new CommonPumpTypes.PumpValue(), + //drugName = "Nutrición Parenteral", + Concentration = new CommonPumpTypes.PumpValue(), + DrugAmount = new CommonPumpTypes.PumpValue(), + DiluentVolume = new CommonPumpTypes.PumpValue(), + DoseRate = new CommonPumpTypes.PumpValue(), + Rate = new CommonPumpTypes.PumpValue(), + VolumeInfused = new CommonPumpTypes.PumpValue + { + Value = 0.00001 + }, + VolumeRemaining = new CommonPumpTypes.PumpValue + { + Value = 0.00001 + }, + TimeRemaining = new CommonPumpTypes.PumpValue + { + Value = 1441, + Units = "min" + }, + PatientWeight = new CommonPumpTypes.PumpValue(), + AlarmType = PumpEnum.AlarmType.Occlusion, + AlarmMode = PumpEnum.AlarmMode.Hight, + GatewayNumber = 1, + IsAux = true, + IsInfusing = false, + Expires = 100 + }; + + await _calculatedObservations.Map(pumpObservation); + + + _observationServiceMock.Verify(d => d.InsertObservation(It.Is(arg => + arg.Value.ToString() == "Rack Aux 1 Bomba 2"), true, true)); + } + + [Test] + public async Task Map_PatientObservation_Alarm_Pump_Retur_obs() + { + PatientObservation patientObservation = new() + { + CodingSystem = "ADAS_ALARM", + Code = "Pump_VolumetricAirOclusion", + Name = "Alarm_Pump_VolumetricAirOclusion", + Value = "Rack 1 Bomba 1", + PatientId = PatientId, + Time = Now + }; + + var result = await _calculatedObservations.Map(patientObservation, false); + + Assert.That(result, Is.Not.Null); + Assert.That(result!.Name, Is.EqualTo(patientObservation.Name)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Customizations/HRYC/CalculatedObservationsTest.cs b/adas-core.Test/Customizations/HRYC/CalculatedObservationsTest.cs new file mode 100644 index 00000000..72e724f2 --- /dev/null +++ b/adas-core.Test/Customizations/HRYC/CalculatedObservationsTest.cs @@ -0,0 +1,1400 @@ +using adas_core.Application.Customizations.HRYC; +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.MongoModels; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Moq; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Customizations.HRYC; + +[TestFixture] +public class CalculatedObservationsTest +{ + [SetUp] + public void Setup() + { + _observationServiceMock = new Mock(); + + var observationServiceLazy = new Lazy(() => _observationServiceMock.Object); + + + _patientServiceMock = new Mock(); + var patientServiceLazy = new Lazy(() => _patientServiceMock.Object); + + _lightBeaconServiceMock = new Mock(); + var lightBeaconServiceLazy = new Lazy(() => _lightBeaconServiceMock.Object); + + + var listAlertNewsObsConfig = new List + { + new() + { + Name = "Alarm_NEWS_ALERT", + Alarm = new AlarmConfig { Enabled = true, Beacon = new AlarmItem { Enabled = true, Color = "RED" } } + }, + new() + { + Name = "Alarm_NEWS_OFF", + Alarm = new AlarmConfig { Enabled = true, Beacon = new AlarmItem { Enabled = true, Color = "NONE" } } + }, + new() + { + Name = "Alarm_NEWS_WARNING", + Alarm = new AlarmConfig { Enabled = true, Beacon = new AlarmItem { Enabled = true, Color = "YELLOW" } } + } + }; + _configObservationServiceMock = new Mock(); + _configObservationServiceMock + .Setup(x => x.Get(It.IsAny(), It.IsAny())) + .ReturnsAsync((PatientObservation patientObservation, bool _) => + { + // Busca el elemento en la lista que coincida con la propiedad "Name" recibida como parámetro + var configItem = listAlertNewsObsConfig.FirstOrDefault(c => c.Name == patientObservation.Name); + + // Si se encuentra el elemento, devuélvelo; de lo contrario, devuelve null + return configItem; + }); + _optionsApiSettings = Options.Create(_apiSettings); + + _logger = new Mock>(); + + _optionsApiSettings.Value.HighFrequencyVentilation = _highFrequencyVentilationValues; + _optionsApiSettings.Value.InvasiveVentilation = _invasiveVentilationValues; + _optionsApiSettings.Value.NonInvasiveVentilation = _nonInvasiveVentilationValues; + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(observationServiceLazy); + serviceCollection.AddSingleton(patientServiceLazy); + serviceCollection.AddSingleton(lightBeaconServiceLazy); + serviceCollection.AddSingleton(_configObservationServiceMock.Object); + serviceCollection.AddSingleton(_optionsApiSettings); + serviceCollection.AddSingleton(_logger.Object); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + _calculatedObservations = new CalculatedObservations(serviceProvider); + + _patientId = ObjectId.GenerateNewId(); + _patientPocId = ObjectId.GenerateNewId(); + _patientUnitId = ObjectId.GenerateNewId(); + _patient = new Patient { Id = _patientId, UnitId = _patientUnitId, PointOfCareId = _patientPocId }; + _patientServiceMock.Setup(m => m.FindById(_patientId, It.IsAny())) + .ReturnsAsync(_patient); + } + + private CalculatedObservations _calculatedObservations; + private Mock _observationServiceMock; + private Mock _patientServiceMock; + private Mock _lightBeaconServiceMock; + private Mock _configObservationServiceMock; + private Mock> _logger; + + private readonly ApiSettings _apiSettings = new() + { + ConfigObservation = new ConfigObservationSettings + { + IgnoreUnknownObservation = false + } + }; + + private IOptions _optionsApiSettings; + + private readonly List _highFrequencyVentilationValues = ["HNF"]; + private readonly List _nonInvasiveVentilationValues = ["BIPAP", "CPAP"]; + private readonly List _invasiveVentilationValues = []; + private ObjectId _patientId = ObjectId.GenerateNewId(); + private ObjectId _patientPocId = ObjectId.GenerateNewId(); + private ObjectId _patientUnitId = ObjectId.GenerateNewId(); + private Patient _patient; + + [Test] + public async Task Calculate_CheckBeaconOnNEWS_Should_Send_PowerOff() + { + var observation = new PatientObservation + { + Name = "NEWS", + CodingSystem = "ADAS", + Expires = 15, + Value = 4, + PatientId = _patientId, + Time = DateTime.Now + }; + var obsConfig = new ConfigObservation + { + Name = "Alarm_NewsOff", + Alarm = new AlarmConfig + { + Enabled = true, + Beacon = new AlarmItem + { + Enabled = true, + BeaconColor = AlarmEnum.BeaconColor.None + } + } + }; + + _configObservationServiceMock.Setup(c => c.Get(It.IsAny(), false)) + .ReturnsAsync(obsConfig); + + await _calculatedObservations.Map(observation, false); + _lightBeaconServiceMock.Verify( + x => x.SendColor(It.Is(l => l.Equals(_patient.PointOfCareId)), LightBeaconColor.Off), + Times.Once); + } + + [Test] + public async Task Calculate_CheckBeaconOnNEWS_Should_Send_Yellow() + { + var observation = new PatientObservation + { + Name = "NEWS", + CodingSystem = "ADAS", + Expires = 15, + Value = 6, + PatientId = _patientId, + Time = DateTime.Now + }; + var obsConfig = new ConfigObservation + { + Name = "Alarm_NewsWarning", + Alarm = new AlarmConfig + { + Enabled = true, + Beacon = new AlarmItem + { + Enabled = true, + BeaconColor = AlarmEnum.BeaconColor.Yellow + } + } + }; + + _configObservationServiceMock.Setup(c => c.Get(It.IsAny(), false)) + .ReturnsAsync(obsConfig); + + await _calculatedObservations.Map(observation, false); + _lightBeaconServiceMock.Verify( + x => x.SendColor(It.Is(l => l.Equals(_patient.PointOfCareId)), + LightBeaconColor.Yellow), Times.Once); + } + + [Test] + public async Task Calculate_CheckBeaconOnNEWS_Should_Send_Red() + { + var observation = new PatientObservation + { + Name = "NEWS", + CodingSystem = "ADAS", + Expires = 15, + Value = 8, + PatientId = _patientId, + Time = DateTime.Now + }; + + var obsConfig = new ConfigObservation + { + Name = "Alarm_NewsAlert", + Alarm = new AlarmConfig + { + Enabled = true, + Beacon = new AlarmItem + { + Enabled = true, + BeaconColor = AlarmEnum.BeaconColor.Red + } + } + }; + + _configObservationServiceMock.Setup(c => c.Get(It.IsAny(), false)) + .ReturnsAsync(obsConfig); + + await _calculatedObservations.Map(observation, false); + _lightBeaconServiceMock.Verify( + x => x.SendColor(It.Is(l => l.Equals(_patient.PointOfCareId)), LightBeaconColor.Red), + Times.Once); + } + + [Ignore("old NEWS calc")] + [Test] + public async Task Calculate_FR_Returns_NEWS_EXTR_HI_Resp_Rate() + { + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = _patientId, + Value = 90, + Name = "FR" + }; + + var observationVent = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = _patientId, + Value = 90, + Name = "Vent_Rat" + }; + + var observationSpo2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = _patientId, + Value = 90, + Name = "SpO2" + }; + + var observationFiO2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = _patientId, + Value = 90, + Name = "FiO2" + }; + + _observationServiceMock + .Setup(l => l.FindLastObservations(_patientId, 1, new List { "Vent_Rate" })) + .ReturnsAsync([observationVent]); + _observationServiceMock + .Setup(l => l.FindLastObservations(_patientId, 1, new List { "FiO2", "SpO2" })) + .ReturnsAsync([observationSpo2, observationFiO2]); + + await _calculatedObservations.Map(observation, false); + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "NEWS_EXTR_HI_Resp_Rate" && + arg.CodingSystem == "ADAS" && + arg.Value.ToString() == "90" + ), true, true)); + } + + [Test] + public async Task Calculate_FR_Does_Not_Returns_NEWS_EXTR_HI_Resp_Rate_When_Invalid_Value() + { + var patientId = ObjectId.GenerateNewId(); + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = "90a", + Name = "FR" + }; + + var observationVent = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 90, + Name = "Vent_Rat" + }; + + var observationSpo2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 90, + Name = "SpO2" + }; + + var observationFiO2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 90, + Name = "FiO2" + }; + + _observationServiceMock + .Setup(l => l.FindLastObservations(patientId, 1, new List { "Vent_Rate" })) + .ReturnsAsync([observationVent]); + + _observationServiceMock + .Setup(l => l.FindLastObservations(patientId, 1, new List { "FiO2", "SpO2" })) + .ReturnsAsync([observationSpo2, observationFiO2]); + + Func act = () => _calculatedObservations.Map(observation, false); + + Assert.ThrowsAsync(act); + } + + + [Ignore("old NEWS calc")] + [Test] + public async Task Calculate_FR_Returns_NEWS_HI_Resp_Rate() + { + { + var patientId = ObjectId.GenerateNewId(); + + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 23, + Name = "FR" + }; + + var observationVent = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 90, + Name = "Vent_Rat" + }; + + var observationSpo2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 90, + Name = "SpO2" + }; + + var observationFiO2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 90, + Name = "FiO2" + }; + + _observationServiceMock + .Setup(l => l.FindLastObservations(patientId, 1, new List { "Vent_Rate" })) + .ReturnsAsync([observationVent]); + _observationServiceMock + .Setup(l => l.FindLastObservations(patientId, 1, new List { "FiO2", "SpO2" })) + .ReturnsAsync([observationSpo2, observationFiO2]); + + await _calculatedObservations.Map(observation, false); + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "NEWS_HI_Resp_Rate" && + arg.CodingSystem == "ADAS" && + arg.Value.ToString() == "23" + ), true, true)); + } + } + + [Ignore("old NEWS calc")] + [Test] + public async Task Calculate_FR_Returns_NEWS_EXTR_LO_Resp_Rate() + { + { + var patientId = ObjectId.GenerateNewId(); + + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 7, + Name = "FR" + }; + + var observationVent = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 90, + Name = "Vent_Rat" + }; + + var observationSpo2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 90, + Name = "SpO2" + }; + + var observationFiO2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 90, + Name = "FiO2" + }; + + _observationServiceMock + .Setup(l => l.FindLastObservations(patientId, 1, new List { "Vent_Rate" })) + .ReturnsAsync([observationVent]); + _observationServiceMock + .Setup(l => l.FindLastObservations(patientId, 1, new List { "FiO2", "SpO2" })) + .ReturnsAsync([observationSpo2, observationFiO2]); + + await _calculatedObservations.Map(observation, false); + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "NEWS_EXTR_LO_Resp_Rate" && + arg.CodingSystem == "ADAS" && + arg.Value.ToString() == "7" + ), true, true)); + } + } + + [Ignore("old NEWS calc")] + [Test] + public async Task Calculate_SpO2_Returns_NEWS_EXTR_LO_SpO2() + { + { + var patientId = ObjectId.GenerateNewId(); + + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 90, + Name = "SpO2" + }; + + var observationFr = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 90, + Name = "FR" + }; + + _observationServiceMock + .Setup(l => l.FindLastObservations(patientId, 1, new List { "FiO2", "FR" })) + .ReturnsAsync([observation, observationFr]); + + + await _calculatedObservations.Map(observation, false); + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "NEWS_EXTR_LO_SpO2" && + arg.CodingSystem == "ADAS" && + arg.Value.ToString() == "90" + ), true, true)); + } + } + + [Ignore("old NEWS calc")] + [Test] + public async Task Calculate_SpO2_Returns_NEWS_LO_SpO2() + { + { + var patientId = ObjectId.GenerateNewId(); + + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 93, + Name = "SpO2" + }; + + var observationFr = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 90, + Name = "FR" + }; + + _observationServiceMock + .Setup(l => l.FindLastObservations(patientId, 1, new List { "FiO2", "FR" })) + .ReturnsAsync([observation, observationFr]); + + + await _calculatedObservations.Map(observation, false); + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "NEWS_LO_SpO2" && + arg.CodingSystem == "ADAS" && + arg.Value.ToString() == "93" + ), true, true)); + } + } + + [Ignore("old NEWS calc")] + [Test] + public async Task Calculate_Temperature_Returns_NEWS_EXTR_LO_Temperature() + { + { + var patientId = ObjectId.GenerateNewId(); + + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 30, + Name = "Temperature" + }; + + await _calculatedObservations.Map(observation, false); + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "NEWS_EXTR_LO_Temperature" && + arg.CodingSystem == "ADAS" && + arg.Value.ToString() == "30" + ), true, true)); + } + } + + [Ignore("old NEWS calc")] + [Test] + public async Task Calculate_Temperature_Returns_NEWS_HI_Temperature() + { + { + var patientId = ObjectId.GenerateNewId(); + + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 40, + Name = "Temperature" + }; + + await _calculatedObservations.Map(observation, false); + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "NEWS_HI_Temperature" && + arg.CodingSystem == "ADAS" && + arg.Value.ToString() == "40" + ), true, true)); + } + } + + [Ignore("old NEWS calc")] + [Test] + public async Task Calculate_TAs_Returns_NEWS_EXTR_LO_TAs() + { + { + var patientId = ObjectId.GenerateNewId(); + + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 80, + Name = "TAs" + }; + + await _calculatedObservations.Map(observation, false); + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "NEWS_EXTR_LO_TAs" && + arg.CodingSystem == "ADAS" && + arg.Value.ToString() == "80" + ), true, true)); + } + } + + [Ignore("old NEWS calc")] + [Test] + public async Task Calculate_TAs_Returns_NEWS_LO_TAs() + { + { + var patientId = ObjectId.GenerateNewId(); + + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 95, + Name = "TAs" + }; + + await _calculatedObservations.Map(observation, false); + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "NEWS_LO_TAs" && + arg.CodingSystem == "ADAS" && + arg.Value.ToString() == "95" + ), true, true)); + } + } + + [Ignore("old NEWS calc")] + [Test] + public async Task Calculate_TAs_Returns_NEWS_EXTR_HI_TAs() + { + { + var patientId = ObjectId.GenerateNewId(); + + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 240, + Name = "TAs" + }; + + await _calculatedObservations.Map(observation, false); + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "NEWS_EXTR_HI_TAs" && + arg.CodingSystem == "ADAS" && + arg.Value.ToString() == "240" + ), true, true)); + } + } + + [Ignore("old NEWS calc")] + [Test] + public async Task Calculate_FC_Returns_NEWS_EXTR_LO_FC() + { + { + var patientId = ObjectId.GenerateNewId(); + + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 20, + Name = "FC" + }; + + await _calculatedObservations.Map(observation, false); + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "NEWS_EXTR_LO_FC" && + arg.CodingSystem == "ADAS" && + arg.Value.ToString() == "20" + ), true, true)); + } + } + + [Ignore("old NEWS calc")] + [Test] + public async Task Calculate_FC_Returns_NEWS_EXTR_HI_FC() + { + { + var patientId = ObjectId.GenerateNewId(); + + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 140, + Name = "FC" + }; + + await _calculatedObservations.Map(observation, false); + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "NEWS_EXTR_HI_FC" && + arg.CodingSystem == "ADAS" && + arg.Value.ToString() == "140" + ), true, true)); + } + } + + [Ignore("old NEWS calc")] + [Test] + public async Task Calculate_FC_Returns_NEWS_HI_FC() + { + { + var patientId = ObjectId.GenerateNewId(); + + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 120, + Name = "FC" + }; + + await _calculatedObservations.Map(observation, false); + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "NEWS_HI_FC" && + arg.CodingSystem == "ADAS" && + arg.Value.ToString() == "120" + ), true, true)); + } + } + + [Test] + public async Task Check_MDC_VENT_RESP_RATE_IsInsertedBefore_And_RemoveFromList() + { + var patientId = ObjectId.GenerateNewId(); + var now = DateTime.Now; + + var listObsToOrder = new List + { + new() + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 7, + Code = "151586", + CodingSystem = "MDC", + Name = "MDC_VENT_RESP_RATE", + Time = now + }, + new() + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 113, + ParentData = new ParentDataClass + { + Code = "69965", + CodingSystem = "MDC", + Name = "MDC_DEV_MON_PHYSIO_MULTI_PARAM_MDS" + }, + Code = "151562", + CodingSystem = "MDC", + Name = "MDC_RESP_RATE", + Time = now + }, + new() + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 11, + ParentData = new ParentDataClass + { + Code = "69965", + CodingSystem = "MDC", + Name = "MDC_DEV_MON_PHYSIO_MULTI_PARAM_MDS" + }, + Code = "150456", + CodingSystem = "MDC", + Name = "MDC_PULS_OXIM_SAT_O2", + Time = now + } + }; + + var orderedList = await _calculatedObservations.PreMapList(listObsToOrder); + Assert.That(orderedList.All(obs => obs.Name != "MDC_VENT_RESP_RATE"), Is.True); + _observationServiceMock.Verify(o => o.InsertObservation( + It.Is(obs => + obs.Code != null && + obs.Code == "151586"), true, true), Times.Once); + } + + [Test] + public async Task + Check_Insert_Resp_Rate_Calculated_HasTimePlusOneSecond_And_Resp_Rate_Calculated_HasVentRateAsParent() + { + var patientId = ObjectId.GenerateNewId(); + var now = DateTime.Now; + var nowPlusOneSecond = now.AddSeconds(1); + + var ventRate = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 7, + Code = "151586", + CodingSystem = "MDC", + Name = "Vent_Rate", + Time = now + }; + var fr = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 113, + ParentData = new ParentDataClass + { + Code = "69965", + CodingSystem = "MDC", + Name = "MDC_DEV_MON_PHYSIO_MULTI_PARAM_MDS" + }, + Code = "151562", + CodingSystem = "MDC", + Name = "FR", + Time = now + }; + _observationServiceMock.Setup(o => o.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([ventRate]); + + await _calculatedObservations.Map(ventRate, false); + await _calculatedObservations.Map(fr, false); + + _observationServiceMock.Verify(o => o.InsertObservation( + It.Is(obs => + obs.ParentData != null && + obs.ParentData.Name == "Vent_Rate" && + obs.Time == nowPlusOneSecond), true, true), Times.Once); + } + + [Test] + public async Task Calculate_Ventilation_Mode_Should_Return_INVASIVE() + { + var patientId = ObjectId.GenerateNewId(); + + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = "PC-AC", + Name = "Resp_Mode" + }; + + await _calculatedObservations.Map(observation, false); + _observationServiceMock.Verify(o => o.InsertIfChanged("Resp_Type", It.Is(arg => + arg.Name == "Resp_Type" && + arg.CodingSystem == "ADAS" && + arg.Value.ToString() == RespirationType.Invasive.ToString() + ), true, true)); + } + + [Test] + public async Task Calculate_Ventilation_Mode_Should_Return_NON_INVASIVE() + { + { + var patientId = ObjectId.GenerateNewId(); + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = _nonInvasiveVentilationValues[0].Trim(), + Name = "Resp_Mode" + }; + + await _calculatedObservations.Map(observation, false); + _observationServiceMock.Verify(o => o.InsertIfChanged("Resp_Type", It.Is(arg => + arg.Name == "Resp_Type" && + arg.CodingSystem == "ADAS" && + arg.Value.ToString() == RespirationType.NonInvasive.ToString() + ), true, true)); + } + } + + [Test] + public async Task Calculate_Ventilation_Mode_Should_Return_HIGH_FREQUENCY() + { + { + var patientId = ObjectId.GenerateNewId(); + + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = _highFrequencyVentilationValues[0].Trim(), + Name = "Resp_Mode", + Code = "HNF" + }; + + await _calculatedObservations.Map(observation, false); + _observationServiceMock.Verify(o => o.InsertIfChanged("Resp_Type", It.Is(arg => + arg.Name == "Resp_Type" && + arg.CodingSystem == "ADAS" && + arg.Value.ToString() == RespirationType.HighFrequencyVentilation.ToString() + ), true, true)); + } + } + + //[Test] + //public void CalculateCam_Icu_Received_1_Return_Positivo() + //{ + // var patientId = ObjectId.GenerateNewId(); + // var now = DateTime.Now; + + + // var observation = new PatientObservation + // { + // id = ObjectId.GenerateNewId(), + // patientid = patientId, + // value = 1, + // name = "Cam_Icu" + // }; + + // calculatedObservations.Map(observation, false); + + // observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + // arg.name == "Cam_IcuCalculated" && + // arg.codingSystem == "ADAS" && + // arg.value.ToString() == "Positivo" + // ), true, true)); + + //} + + //[Test] + //public void CalculateCam_Icu_Received_0_Return_Negativo() + //{ + // var patientId = ObjectId.GenerateNewId(); + // var now = DateTime.Now; + + + // var observation = new PatientObservation + // { + // id = ObjectId.GenerateNewId(), + // patientid = patientId, + // value = 0, + // name = "Cam_Icu" + // }; + + // calculatedObservations.Map(observation, false); + + // observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + // arg.name == "Cam_IcuCalculated" && + // arg.codingSystem == "ADAS" && + // arg.value.ToString() == "Negativo" + // ), true, true)); + + //} + + //[Test] + //public void CalculateCam_Icu_Received_different_0_1_Return_Value_null() + //{ + // var patientId = ObjectId.GenerateNewId(); + // var now = DateTime.Now; + + + // var observation = new PatientObservation + // { + // id = ObjectId.GenerateNewId(), + // patientid = patientId, + // value=2, + // name = "Cam_Icu" + // }; + + // calculatedObservations.Map(observation, false); + + // observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + // arg.name == "Cam_IcuCalculated" && + // arg.codingSystem == "ADAS" && + // arg.value==null + // ), true, true)); + + //} + + //[Test] + //public void CalculateCam_Icu_Received_null_Return_Value_null() + //{ + // var patientId = ObjectId.GenerateNewId(); + // var now = DateTime.Now; + + + // var observation = new PatientObservation + // { + // id = ObjectId.GenerateNewId(), + // patientid = patientId, + // name = "Cam_Icu" + // }; + + // calculatedObservations.Map(observation, false); + + // observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + // arg.name == "Cam_IcuCalculated" && + // arg.codingSystem == "ADAS" && + // arg.value == null + // ), true, true)); + + //} + + [Test] + public async Task CalculateDiuresis_Weight_Received_D_3924_W_90_Return_43_6() + { + var patientId = ObjectId.GenerateNewId(); + + var patientObservation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 90, + Name = "Weight_Current" + }; + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 3924, + Name = "Diuresis" + }; + + _observationServiceMock.Setup(o => o.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([patientObservation]); + + await _calculatedObservations.Map(observation, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Diuresis_Weight" && + arg.PatientId == patientId && + Math.Abs((double)arg.Value - 43.6) < 0.1 + ), true, true)); + } + + [Test] + public async Task CalculateDiuresis_Weight_D_3924_Receive_W_90_Return_43_6() + { + var patientId = ObjectId.GenerateNewId(); + + var patientObservation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 3924, + Name = "Diuresis" + }; + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 90, + Name = "Weight_Current" + }; + + _observationServiceMock.Setup(o => o.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([patientObservation]); + + await _calculatedObservations.Map(observation, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Diuresis_Weight" && + arg.PatientId == patientId && + Math.Abs((double)arg.Value - 43.6) <= 0 + ), true, true)); + } + + [Test] + public async Task CalculateDiuresis_Weight_Received_W_90_D_Null_Return_Nothing() + { + var patientId = ObjectId.GenerateNewId(); + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 90, + Name = "Weight_Current" + }; + + _observationServiceMock.Setup(o => o.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([]); + + await _calculatedObservations.Map(observation, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.IsAny(), true, true), + Times.Never); + } + + [Test] + public async Task CalculateDiuresis_Weight_Received_D_3924_W_Null_Return_Nothing() + { + var patientId = ObjectId.GenerateNewId(); + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = 3924, + Name = "Diuresis" + }; + + _observationServiceMock.Setup(s => s.FindLastObservations(It.IsAny(), 1, It.IsAny>())) + .ReturnsAsync([]); + + await _calculatedObservations.Map(observation, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Diuresis_Weight" && + arg.PatientId == patientId + ), true, true), Times.Never); + } + + [Test] + public async Task CalculateAllergiesObservation_Received_AllergiesObs_Emty_Return_Allergies_Emty() + { + var patientId = ObjectId.GenerateNewId(); + + List patientAllergiesValues = []; + + var patientObservation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = patientAllergiesValues, + Name = "AllergiesObs" + }; + + await _calculatedObservations.Map(patientObservation, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Allergies" && + arg.PatientId == patientId && + (string)arg.Value == string.Empty + ), true, true)); + } + + [Test] + public async Task CalculateAllergiesObservation_Received_AllergiesObs_Without_Farmacos_Return_Allergies() + { + var patientId = ObjectId.GenerateNewId(); + + List patientAllergiesValues = + [ + new() + { + Type = "Latex", + Value = "Si", + Notes = "" + }, + + new() + { + Type = "Alergia ambiental", + Value = "Estacional", + Notes = "epitelio de perro, mezcla de gramíneas salvajes," + } + ]; + + + var patientObservation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = patientAllergiesValues, + Name = "AllergiesObs" + }; + + await _calculatedObservations.Map(patientObservation, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Allergies" && + arg.PatientId == patientId && + (string)arg.Value == "LATEX, AMBIENTAL" + ), true, true)); + } + + [Test] + public async Task CalculateAllergiesObservation_Received_AllergiesObs_With_1_Farmacos_Return_Allergies() + { + var patientId = ObjectId.GenerateNewId(); + + List patientAllergiesValues = + [ + new() + { + Type = "Alergia a fármacos", + Value = "METILPREDNISOLONA", + Notes = "Tolera dexametasona y actocortina" + }, + + new() + { + Type = "Latex", + Value = "Si", + Notes = "" + }, + + new() + { + Type = "Alergia ambiental", + Value = "Estacional", + Notes = "epitelio de perro, mezcla de gramíneas salvajes," + } + ]; + + + var patientObservation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = patientAllergiesValues, + Name = "AllergiesObs" + }; + + await _calculatedObservations.Map(patientObservation, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Allergies" && + arg.PatientId == patientId && + (string)arg.Value == "LATEX, AMBIENTAL, FÁRMACOS (METILPREDNISOLONA)" + ), true, true)); + } + + [Test] + public async Task CalculateAllergiesObservation_Received_AllergiesObs_With_2_Farmacos_Return_Allergies() + { + var patientId = ObjectId.GenerateNewId(); + + List patientAllergiesValues = + [ + new() + { + Type = "Alergia a fármacos", + Value = "METILPREDNISOLONA", + Notes = "Tolera dexametasona y actocortina" + }, + + new() + { + Type = "Latex", + Value = "Si", + Notes = "" + }, + + new() + { + Type = "Alergia a fármacos", + Value = "Penicilina/cefalosporinas" + }, + + new() + { + Type = "Alergia ambiental", + Value = "Estacional", + Notes = "epitelio de perro, mezcla de gramíneas salvajes," + } + ]; + + + var patientObservation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Value = patientAllergiesValues, + Name = "AllergiesObs" + }; + + await _calculatedObservations.Map(patientObservation, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Allergies" && + arg.PatientId == patientId && + (string)arg.Value == "LATEX, AMBIENTAL, FÁRMACOS (METILPREDNISOLONA, PENICILINA/CEFALOSPORINAS)" + ), true, true)); + } + + [Test] + public async Task Calculate_Drainages_Received_volume_60_height_8_Return_DVE_height() + { + var patientId = ObjectId.GenerateNewId(); + + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "DrainagesObs", + Value = new PatientDrainagesValue + { + Type = "Drenaje ventricular", + Volume = 60, + Height = 8 + } + }; + + await _calculatedObservations.Map(observation, false); + int valueParsed; + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "DVE" && + arg.CodingSystem == "ADAS" && + arg.PatientId == patientId && + int.TryParse(arg.Value.ToString(), out valueParsed) && valueParsed == 60 + ), true, true)); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Drainage_Height" && + arg.CodingSystem == "ADAS" && + arg.PatientId == patientId && + int.TryParse(arg.Value.ToString(), out valueParsed) && valueParsed == 8 + ), true, true)); + } + + [Test] + public async Task Calculate_Drainages_Received_volume_60_height_null_Return_DVE() + { + var patientId = ObjectId.GenerateNewId(); + + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "DrainagesObs", + Value = new PatientDrainagesValue + { + Type = "Drenaje ventricular", + Volume = 60 + } + }; + + await _calculatedObservations.Map(observation, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "DVE" && + arg.CodingSystem == "ADAS" && + arg.PatientId == patientId && + int.Parse(arg.Value.ToString()!) == 60 + ), true, true)); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Drainage_Height" && + arg.CodingSystem == "ADAS" && + arg.PatientId == patientId + ), true, true), Times.Never); + } + + [Test] + public async Task Calculate_Drainages_Received_volume_null_height_8_Return_height() + { + var patientId = ObjectId.GenerateNewId(); + + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "DrainagesObs", + Value = new PatientDrainagesValue + { + Type = "Drenaje ventricular", + Height = 8 + } + }; + + await _calculatedObservations.Map(observation, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "DVE" && + arg.CodingSystem == "ADAS" && + arg.PatientId == patientId + ), true, true), Times.Never); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Drainage_Height" && + arg.CodingSystem == "ADAS" && + arg.PatientId == patientId && + int.Parse(arg.Value.ToString()!) == 8 + ), true, true)); + } + + [Test] + public async Task Calculate_Drainages_Received_volume_null_height_null_Return_nothing() + { + var patientId = ObjectId.GenerateNewId(); + + + var observation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientId, + Name = "DrainagesObs", + Value = new PatientDrainagesValue + { + Type = "Drenaje ventricular" + } + }; + + await _calculatedObservations.Map(observation, false); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "DVE" && + arg.CodingSystem == "ADAS" && + arg.PatientId == patientId + ), true, true), Times.Never); + + _observationServiceMock.Verify(o => o.InsertObservation(It.Is(arg => + arg.Name == "Drainage_Height" && + arg.CodingSystem == "ADAS" && + arg.PatientId == patientId + ), true, true), Times.Never); + } +} \ No newline at end of file diff --git a/adas-core.Test/Customizations/HUVH/UCIA/CalculatedObservationsTest.cs b/adas-core.Test/Customizations/HUVH/UCIA/CalculatedObservationsTest.cs new file mode 100644 index 00000000..bcdba9d7 --- /dev/null +++ b/adas-core.Test/Customizations/HUVH/UCIA/CalculatedObservationsTest.cs @@ -0,0 +1,744 @@ +using adas_core.Application.Customizations.HUVH.UCIA; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Moq; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Customizations.HUVH.UCIA; + +[TestFixture] +[Order(2)] +public class CalculatedObservationsTest +{ + [SetUp] + public void Setup() + { + _observationServiceMock = new Mock(); + + _medicineServiceMock = new Mock(); + + _treatmentServiceMock = new Mock(); + + _logger = new Mock>(); + + _optionsApiSettings = Options.Create(_apiSettings); + + var serviceCollection = new ServiceCollection(); + //serviceCollection.AddSingleton(_observationServiceMock.Object); + serviceCollection.AddSingleton(new Lazy(() => _observationServiceMock.Object)); + serviceCollection.AddSingleton(new Lazy(() => _medicineServiceMock.Object)); + serviceCollection.AddSingleton(new Lazy(() => _treatmentServiceMock.Object)); + + serviceCollection.AddSingleton(_optionsApiSettings); + serviceCollection.AddSingleton(_logger.Object); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + _calculatedObservations = new CalculatedObservations(serviceProvider); + } + + private CalculatedObservations _calculatedObservations; + private Mock _medicineServiceMock; + private Mock _observationServiceMock; + private Mock _treatmentServiceMock; + private Mock> _logger; + + private readonly ApiSettings _apiSettings = new() + { + ConfigObservation = new ConfigObservationSettings + { + IgnoreUnknownObservation = false + } + }; + + private IOptions _optionsApiSettings; + + //Mandar observación 'SOBRESEDACIÓN' + //cuando: + //(RASS =-4 o RASS =-5) y + //PSI < 25 y + //TS > 5 y + //(Propofol > 3 ó Midazolam > 0,05 ó Isoflorano > 10) + + [Test] + public async Task CalculateOverSedation_Should_Insert_Observation_When_Conditions_Met() + { + var patientId = ObjectId.GenerateNewId(); + var rassObservation = new PatientObservation { PatientId = patientId, Name = "RASS", Value = -4 }; + var psiObservation = new PatientObservation { PatientId = patientId, Name = "PSI", Value = 20 }; + var tsObservation = new PatientObservation { PatientId = patientId, Name = "TS", Value = 6 }; + + _observationServiceMock.Setup(x => x.FindLastObservations(patientId, 1, new List { "PSI", "TS" })) + .ReturnsAsync([psiObservation, tsObservation]); + + _treatmentServiceMock.Setup(x => x.GetActiveTreatmentsByPatient(patientId)) + .ReturnsAsync(new List + { new() { RequestedGiveCodes = [new Code { Text = "Propofol" }], RequestedGiveAmountMinimum = 4 } }); + + await _calculatedObservations.Map(rassObservation); + + _observationServiceMock.Verify(x => x.InsertObservation(It.IsAny(), true, false), + Times.Once); + } + + [Test] + public async Task CalculateOverSedation_Should_Not_Insert_Observation_When_RASS_Is_Not_Negative4_Or_Negative5() + { + var patientId = ObjectId.GenerateNewId(); + var rassObservation = new PatientObservation { PatientId = patientId, Name = "RASS", Value = -3 }; + + await _calculatedObservations.Map(rassObservation); + + _observationServiceMock.Verify(x => x.InsertObservation(It.IsAny(), true, false), + Times.Never); + } + + [Test] + public async Task CalculateOverSedation_Should_Not_Insert_Observation_When_PSI_Is_25_Or_Higher() + { + var patientId = ObjectId.GenerateNewId(); + var rassObservation = new PatientObservation { PatientId = patientId, Name = "RASS", Value = -4 }; + + var psiObservation = new PatientObservation { PatientId = patientId, Name = "PSI", Value = 25 }; + var tsObservation = new PatientObservation { PatientId = patientId, Name = "TS", Value = 6 }; + + _observationServiceMock.Setup(x => x.FindLastObservations(patientId, 1, new List { "PSI", "TS" })) + .ReturnsAsync([psiObservation, tsObservation]); + + await _calculatedObservations.Map(rassObservation); + + _observationServiceMock.Verify(x => x.InsertObservation(It.IsAny(), true, false), + Times.Never); + } + + [Test] + public async Task CalculateOverSedation_Should_Not_Insert_Observation_When_TS_Is_5_Or_Lower() + { + var patientId = ObjectId.GenerateNewId(); + var rassObservation = new PatientObservation { PatientId = patientId, Name = "RASS", Value = -4 }; + + var psiObservation = new PatientObservation { PatientId = patientId, Name = "PSI", Value = 20 }; + var tsObservation = new PatientObservation { PatientId = patientId, Name = "TS", Value = 5 }; + + _observationServiceMock.Setup(x => x.FindLastObservations(patientId, 1, new List { "PSI", "TS" })) + .ReturnsAsync([psiObservation, tsObservation]); + + await _calculatedObservations.Map(rassObservation); + + _observationServiceMock.Verify(x => x.InsertObservation(It.IsAny(), true, false), + Times.Never); + } + + [Test] + public async Task CalculateOverSedation_Should_Not_Insert_Observation_When_No_Treatments_Exceed_Threshold() + { + var patientId = ObjectId.GenerateNewId(); + var rassObservation = new PatientObservation { PatientId = patientId, Name = "RASS", Value = -4 }; + + var psiObservation = new PatientObservation { PatientId = patientId, Name = "PSI", Value = 20 }; + var tsObservation = new PatientObservation { PatientId = patientId, Name = "TS", Value = 6 }; + + _observationServiceMock.Setup(x => x.FindLastObservations(patientId, 1, new List { "PSI", "TS" })) + .ReturnsAsync([psiObservation, tsObservation]); + + _treatmentServiceMock.Setup(x => x.GetActiveTreatmentsByPatient(patientId)) + .ReturnsAsync(new List()); // No hay tratamientos activos que superen el umbral + + await _calculatedObservations.Map(rassObservation); + + _observationServiceMock.Verify(x => x.InsertObservation(It.IsAny(), true, false), + Times.Never); + } + + [Test] + public async Task CalculateOverSedation_Should_Insert_Observation_When_Triggered_By_PSI_Observation() + { + var patientId = ObjectId.GenerateNewId(); + var psiObservation = new PatientObservation { PatientId = patientId, Name = "PSI", Value = 20 }; + + var rassObservation = new PatientObservation { PatientId = patientId, Name = "RASS", Value = -4 }; + var tsObservation = new PatientObservation { PatientId = patientId, Name = "TS", Value = 6 }; + + _observationServiceMock.Setup(x => x.FindLastObservations(patientId, 1, new List { "RASS", "TS" })) + .ReturnsAsync([rassObservation, tsObservation]); + + _treatmentServiceMock.Setup(x => x.GetActiveTreatmentsByPatient(patientId)) + .ReturnsAsync(new List + { new() { RequestedGiveCodes = [new Code { Text = "Propofol" }], RequestedGiveAmountMinimum = 4 } }); + + await _calculatedObservations.Map(psiObservation); + + _observationServiceMock.Verify(x => x.InsertObservation(It.IsAny(), true, false), + Times.Once); + } + + [Test] + public async Task CalculateOverSedation_Should_Insert_Observation_When_Triggered_By_TS_Observation() + { + var patientId = ObjectId.GenerateNewId(); + var tsObservation = new PatientObservation { PatientId = patientId, Name = "TS", Value = 6 }; + + var rassObservation = new PatientObservation { PatientId = patientId, Name = "RASS", Value = -4 }; + var psiObservation = new PatientObservation { PatientId = patientId, Name = "PSI", Value = 20 }; + + _observationServiceMock.Setup(x => x.FindLastObservations(patientId, 1, new List { "RASS", "PSI" })) + .ReturnsAsync([rassObservation, psiObservation]); + + _treatmentServiceMock.Setup(x => x.GetActiveTreatmentsByPatient(patientId)) + .ReturnsAsync(new List + { new() { RequestedGiveCodes = [new Code { Text = "Propofol" }], RequestedGiveAmountMinimum = 4 } }); + + await _calculatedObservations.Map(tsObservation); + + _observationServiceMock.Verify(x => x.InsertObservation(It.IsAny(), true, false), + Times.Once); + } + + //- Si llega valor de PI pero NO COMPL, el tipo de ventilación es VMNI + //- Si llega valor de PS pero no llega COMPL, el tipo de ventilación es VMNI + //- Si llega COMPL, el tipo de ventilación es VMI + //- Si llega Flujo, leer el tipo de ventilación de CCC(CNAF o CTAF) + + + [Test] + public async Task CalculateVentilationMode_Should_Set_VMNI_When_PI_PS_Arrives_Without_COMPL() + { + var patientId = ObjectId.GenerateNewId(); + var piObservation = new PatientObservation { PatientId = patientId, Name = "Inspiratory_Pressure" }; + + _observationServiceMock.Setup(x => x.FindLastObservations(patientId, 1, new List { "Compliancia" })) + .ReturnsAsync([]); // No hay COMPL + + await _calculatedObservations.Map(piObservation); + + _observationServiceMock.Verify( + x => x.InsertObservation(It.Is(o => o.Value.ToString() == "VMNI"), true, false), + Times.Once); + } + + + [Test] + public async Task CalculateVentilationMode_Should_Set_VMI_When_COMPL_Arrives() + { + var patientId = ObjectId.GenerateNewId(); + var complObservation = new PatientObservation { PatientId = patientId, Name = "Compliancia" }; + + await _calculatedObservations.Map(complObservation); + + _observationServiceMock.Verify( + x => x.InsertObservation(It.Is(o => o.Value.ToString() == "VMI"), true, false), + Times.Once); + } + + [Test] + public async Task CalculateVentilationMode_Should_Set_CCC_Mode_When_AirFlow_Arrives_And_CCC_Has_Mode() + { + var patientId = ObjectId.GenerateNewId(); + var airFlowObservation = new PatientObservation { PatientId = patientId, Name = "Air_Flow" }; + + var mockCursor = new Mock>(); + mockCursor.Setup(c => c.Current).Returns(new List { new() { Value = "CNAF" } }); + mockCursor.SetupSequence(c => c.MoveNext(It.IsAny())).Returns(true).Returns(false); + mockCursor.SetupSequence(c => c.MoveNextAsync(It.IsAny())).ReturnsAsync(true) + .ReturnsAsync(false); + + _observationServiceMock.Setup(x => x.FindByPatientIdAndCodingSystemAsync(patientId, "CCC", "Ventilation_Mode")) + .ReturnsAsync(mockCursor.Object); + + + await _calculatedObservations.Map(airFlowObservation); + + _observationServiceMock.Verify( + x => x.InsertObservation(It.Is(o => o.Value.ToString() == "CNAF"), true, false), + Times.Once); + } + + [Test] + public async Task CalculateVentilationMode_Should_Insert_When_AirFlow_Arrives_And_CCC_Mode() + { + var patientId = ObjectId.GenerateNewId(); + var airFlowObservation = new PatientObservation { PatientId = patientId, Name = "Air_Flow" }; + + var mockCursor = new Mock>(); + + // Configura la lista de observaciones devueltas por el cursor + var observations = new List { new() { Value = "CNAF" } }; + mockCursor.Setup(c => c.Current).Returns(observations); + + // Configura el comportamiento de MoveNext y MoveNextAsync para simular la enumeración + mockCursor.SetupSequence(c => c.MoveNext(It.IsAny())) + .Returns(true) // Primera llamada: hay datos + .Returns(false); // Segunda llamada: no hay más datos + + mockCursor.SetupSequence(c => c.MoveNextAsync(It.IsAny())) + .ReturnsAsync(true) // Primera llamada asíncrona: hay datos + .ReturnsAsync(false); // Segunda llamada: no hay más datos + + // Configura el mock del servicio para devolver el cursor simulado + _observationServiceMock.Setup(x => x.FindByPatientIdAndCodingSystemAsync(patientId, "CCC", "Ventilation_Mode")) + .ReturnsAsync(mockCursor.Object); + + + await _calculatedObservations.Map(airFlowObservation); + + _observationServiceMock.Verify(x => x.InsertObservation(It.IsAny(), true, false), + Times.Once); + } + + [Test] + public async Task CalculateVentilationMode_Should_Not_Insert_When_PI_Arrives_And_COMPL_Already_Exists() + { + var patientId = ObjectId.GenerateNewId(); + var piObservation = new PatientObservation { PatientId = patientId, Name = "PI" }; + + _observationServiceMock.Setup(x => x.FindLastObservations(patientId, 1, new List { "COMPL" })) + .ReturnsAsync([new PatientObservation { Name = "COMPL" }]); // Existe COMPL + + await _calculatedObservations.Map(piObservation); + + _observationServiceMock.Verify(x => x.InsertObservation(It.IsAny(), true, false), + Times.Never); + } + + [Test] + public async Task CalculateVentilationMode_Should_Not_Insert_When_PS_Arrives_And_COMPL_Already_Exists() + { + var patientId = ObjectId.GenerateNewId(); + var psObservation = new PatientObservation { PatientId = patientId, Name = "PS" }; + + _observationServiceMock.Setup(x => x.FindLastObservations(patientId, 1, new List { "COMPL" })) + .ReturnsAsync([new PatientObservation { Name = "COMPL" }]); // Existe COMPL + + await _calculatedObservations.Map(psObservation); + + _observationServiceMock.Verify(x => x.InsertObservation(It.IsAny(), true, false), + Times.Never); + } + + //Las opciones pueden ser VV, VA o ECMO. + //Cuando llegue VV se muestra VV. + //Cuando llegue VA se muestra VA. + //Cuando llegue los siguientes parámetros, se debe mostrar ECMO: ECCO2r, VVDL, VA+V, VVDL+V, VV+V, VVA + + [Test] + public async Task CalculateEcmoLocation_Should_Return_VV_When_Value_Is_VV() + { + var observation = new PatientObservation { Name = "ECMO_Location", Value = "VV" }; + var result = await _calculatedObservations.Map(observation); + Assert.That(result, Is.Not.Null); + Assert.That(result.Value, Is.EqualTo("VV")); + } + + [Test] + public async Task CalculateEcmoLocation_Should_Return_VA_When_Value_Is_VA() + { + var observation = new PatientObservation { Name = "ECMO_Location", Value = "VA" }; + var result = await _calculatedObservations.Map(observation); + Assert.That(result, Is.Not.Null); + Assert.That(result.Value, Is.EqualTo("VA")); + } + + [Test] + [TestCase("ECCO2r")] + [TestCase("VVDL")] + [TestCase("VA+V")] + [TestCase("VVDL+V")] + [TestCase("VV+V")] + [TestCase("VVA")] + public async Task CalculateEcmoLocation_Should_Return_ECMO_When_Value_Is_In_Ecmo_List(string inputValue) + { + var observation = new PatientObservation { Name = "ECMO_Location", Value = inputValue }; + var result = await _calculatedObservations.Map(observation); + Assert.That(result, Is.Not.Null); + Assert.That(result.Value, Is.EqualTo("ECMO")); + } + + [Test] + public async Task CalculateEcmoLocation_Should_Not_Change_Value_When_Not_In_Ecmo_List() + { + var observation = new PatientObservation { Name = "ECMO_Location", Value = "OTHER" }; + var result = await _calculatedObservations.Map(observation); + Assert.That(result, Is.Not.Null); + Assert.That(result.Value, Is.EqualTo("OTHER")); + } + + + [Test] + public async Task CalculateEcmoLocation_Should_Return_Original_Observation_When_Not_PatientObservation() + { + var observation = new PatientObservation(); + var result = await _calculatedObservations.Map(observation); + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.EqualTo(observation)); + } + + // Mandar observación 'SOBREANALGESIA' + // 0 <= EVN <= 3 o ESCID = 3 o ANI > 70 + // y + // Perfusiones de morfina, remifentanilo 0 fentanilo sostenidas > 24h. + + [Test] + public async Task CalculateOverAnalgesia_Should_Create_Observation_When_Conditions_Are_Met() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var obs = new PatientObservation { Name = "EVN", Value = "3", PatientId = patientId }; + var morfina = new PatientTreatment + { RequestedGiveCodes = [new Code { Text = "Morfina" }], StartTime = DateTime.UtcNow.AddHours(-25) }; + var remifentanilo = new PatientTreatment + { RequestedGiveCodes = [new Code { Text = "Remifentanilo" }], StartTime = DateTime.UtcNow.AddHours(-25) }; + //var fentanilo = new PatientTreatment + // { RequestedGiveCodes = [new Code { Text = "Fentanilo" }], StartTime = DateTime.UtcNow.AddHours(-25) }; + + _observationServiceMock + .Setup(s => s.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([new PatientObservation { Name = "ANI", Value = "80" }]); + + _treatmentServiceMock + .Setup(s => s.GetActiveTreatmentsByPatient(patientId)) + .ReturnsAsync(new List + { morfina, remifentanilo /*, fentanilo*/ }); // No hay tratamientos de analgesia activos + + _observationServiceMock + .Setup(s => s.InsertObservation(It.IsAny(), true, false)) + .Returns(Task.CompletedTask); + + // Act + await _calculatedObservations.Map(obs); + + // Assert + _observationServiceMock.Verify( + x => x.InsertObservation( + It.Is(o => + o.PatientId == patientId && o.Name == "Over_Analgesia" && o.Value.ToString() == "True"), + true, false), + Times.Once + ); + } + + [Test] + public async Task CalculateOverAnalgesia_Should_Not_Create_Observation_When_Conditions_Are_Not_Met() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var obs = new PatientObservation { Name = "EVN", Value = "5", PatientId = patientId }; // No cumple la condición + + _observationServiceMock + .Setup(s => s.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([new PatientObservation { Name = "ANI", Value = "60" }]); + + _treatmentServiceMock + .Setup(s => s.GetActiveTreatmentsByPatient(patientId)) + .ReturnsAsync(new List()); // No hay tratamientos activos + + // Act + await _calculatedObservations.Map(obs); + + // Assert + _observationServiceMock.Verify( + x => x.InsertObservation(It.IsAny(), true, false), + Times.Never + ); + } + + //Diferencia entre la Pmeset y la PEEP + //Calculo Valor = Pmeset - PEEP + + [Test] + public async Task CalculateDrivingPressure_Should_Calculate_Correctly_When_Both_Observations_Are_Present() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var obs = new PatientObservation { Name = "Pmeset", Value = "25", PatientId = patientId }; + + _observationServiceMock + .Setup(s => s.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([new PatientObservation { Name = "PEEP", Value = "10" }]); + + _observationServiceMock + .Setup(s => s.InsertObservation(It.IsAny(), true, false)) + .Returns(Task.CompletedTask); + + // Act + await _calculatedObservations.Map(obs); + + // Assert + _observationServiceMock.Verify( + x => x.InsertObservation( + It.Is(o => + o.PatientId == patientId && o.Name == "Driving_Pressure" && o.Value.ToString() == "15"), + true, false), + Times.Once + ); + } + + [Test] + public async Task CalculateDrivingPressure_Should_Not_Calculate_If_Observation_Is_Missing() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var obs = new PatientObservation { Name = "Pmeset", Value = "25", PatientId = patientId }; + + _observationServiceMock + .Setup(s => s.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([]); // No hay observación complementaria + + // Act + await _calculatedObservations.Map(obs); + + // Assert + _observationServiceMock.Verify( + x => x.InsertObservation(It.IsAny(), true, false), + Times.Never + ); + } + + // Cálculo dividiendo el ratio SpO2/FiO2 entre la FR + // Valor = (SpO2_FiO2_Ratio) / FR + + [Test] + public async Task CalculateRoxIndex_Should_Calculate_Correctly_When_Both_Observations_Are_Present() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var obs = new PatientObservation { Name = "SpO2_FiO2_Ratio", Value = "200", PatientId = patientId }; + + _observationServiceMock + .Setup(s => s.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([new PatientObservation { Name = "FR", Value = "25" }]); + + _observationServiceMock + .Setup(s => s.InsertObservation(It.IsAny(), true, false)) + .Returns(Task.CompletedTask); + + // Act + await _calculatedObservations.Map(obs); + + // Assert + _observationServiceMock.Verify( + x => x.InsertObservation( + It.Is(o => + o.PatientId == patientId && o.Name == "Rox_Index" && o.Value.ToString() == (200 / 25).ToString()), + true, false), + Times.Once + ); + } + + [Test] + public async Task CalculateRoxIndex_Should_Not_Calculate_If_Observation_Is_Missing() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var obs = new PatientObservation { Name = "SpO2_FiO2_Ratio", Value = "200", PatientId = patientId }; + + _observationServiceMock + .Setup(s => s.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([]); // No hay observación complementaria + + // Act + await _calculatedObservations.Map(obs); + + // Assert + _observationServiceMock.Verify( + x => x.InsertObservation(It.IsAny(), true, false), + Times.Never + ); + } + + [Test] + public async Task CalculateRoxIndex_Should_Not_Calculate_When_FR_Is_Zero_To_Avoid_Division_By_Zero() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var obs = new PatientObservation { Name = "SpO2_FiO2_Ratio", Value = "200", PatientId = patientId }; + + _observationServiceMock + .Setup(s => s.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([new PatientObservation { Name = "FR", Value = "0" }]); + + // Act + await _calculatedObservations.Map(obs); + + // Assert + _observationServiceMock.Verify( + x => x.InsertObservation(It.IsAny(), true, false), + Times.Never + ); + } + + // Calculado como el sumatorio de la Diuresis de las últimas 6h + // entre el último peso medido del paciente y entre 6. Medido en ml/kg/h + // Valor = sum(Diuresis últimas 6 horas) / Weight / 6 + + [Test] + public async Task CalculateDiuresis_Should_Calculate_Correctly_When_Observations_Are_Available() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var obs = new PatientObservation { Name = "Diuresis", Value = "100", PatientId = patientId }; + + _observationServiceMock + .Setup(s => s.FindLastObservations(patientId, 2, It.IsAny>())) + .ReturnsAsync([ + new PatientObservation { Name = "Diuresis", Value = "120" }, + new PatientObservation { Name = "Diuresis", Value = "110" } + ]); + + _observationServiceMock + .Setup(s => s.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([new PatientObservation { Name = "Weight", Value = "70" }]); + + _observationServiceMock + .Setup(s => s.InsertObservation(It.IsAny(), true, false)) + .Returns(Task.CompletedTask); + + // Act + await _calculatedObservations.Map(obs); + + // Assert + const double expectedValue = (100 + 120 + 110) / 70.0 / 6; + _observationServiceMock.Verify( + x => x.InsertObservation( + It.Is(o => + o.PatientId == patientId && o.Name == "Calculated_Diuresis" && + Math.Abs((double)o.Value - expectedValue) < 0.1), + true, false), + Times.Once + ); + } + + [Test] + public async Task CalculateDiuresis_Should_Not_Calculate_If_Diuresis_Observations_Are_Missing() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var obs = new PatientObservation { Name = "Diuresis", Value = "100", PatientId = patientId }; + + _observationServiceMock + .Setup(s => s.FindLastObservations(patientId, 2, It.IsAny>())) + .ReturnsAsync([]); // No hay diuresis + + // Act + await _calculatedObservations.Map(obs); + + // Assert + _observationServiceMock.Verify( + x => x.InsertObservation(It.IsAny(), true, false), + Times.Never + ); + } + + [Test] + public async Task CalculateDiuresis_Should_Not_Calculate_If_Weight_Is_Invalid() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var obs = new PatientObservation { Name = "Diuresis", Value = "100", PatientId = patientId }; + + _observationServiceMock + .Setup(s => s.FindLastObservations(patientId, 2, It.IsAny>())) + .ReturnsAsync([ + new PatientObservation { Name = "Diuresis", Value = "120" }, + new PatientObservation { Name = "Diuresis", Value = "110" } + ]); + + _observationServiceMock + .Setup(s => s.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([]); // No hay peso + + // Act + await _calculatedObservations.Map(obs); + + // Assert + _observationServiceMock.Verify( + x => x.InsertObservation(It.IsAny(), true, false), + Times.Never + ); + } + + [Test] + public async Task CalculateRehabilitationAlarm_Should_Set_Alert_When_Condition_Is_Met() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var obs = new PatientObservation { Name = "Rehabilitation", Value = "2", PatientId = patientId }; + + _observationServiceMock + .Setup(s => s.FindLastObservations(patientId, 6, It.IsAny>())) + .ReturnsAsync([ + new PatientObservation { Name = "Rehabilitation", Value = "0" }, + new PatientObservation { Name = "Rehabilitation", Value = "1" }, + new PatientObservation { Name = "Rehabilitation", Value = "2" }, + new PatientObservation { Name = "Rehabilitation", Value = "0" }, + new PatientObservation { Name = "Rehabilitation", Value = "1" }, + new PatientObservation { Name = "Rehabilitation", Value = "2" } + ]); + + // Act + var result = await _calculatedObservations.Map(obs); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Status, Is.EqualTo(StatusEnum.Type.Alert)); + } + + [Test] + public async Task CalculateRehabilitationAlarm_Should_Not_Set_Alert_When_Condition_Is_Not_Met() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var obs = new PatientObservation { Name = "Rehabilitation", Value = "2", PatientId = patientId }; + + _observationServiceMock + .Setup(s => s.FindLastObservations(patientId, 6, It.IsAny>())) + .ReturnsAsync([ + new PatientObservation { Name = "Rehabilitation", Value = "0" }, + new PatientObservation { Name = "Rehabilitation", Value = "1" }, + new PatientObservation { Name = "Rehabilitation", Value = "2" }, + new PatientObservation { Name = "Rehabilitation", Value = "1" }, + new PatientObservation { Name = "Rehabilitation", Value = "3" }, //<--fuera de rango + new PatientObservation { Name = "Rehabilitation", Value = "0" } + ]); + + // Act + var result = await _calculatedObservations.Map(obs); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Status, Is.Not.EqualTo(StatusEnum.Type.Alert)); + } + + [Test] + public async Task CalculateRehabilitationAlarm_Should_Not_Set_Alert_When_Value_Is_Greater_Than_2() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var obs = new PatientObservation { Name = "Rehabilitation", Value = "3", PatientId = patientId }; + + // Act + var result = await _calculatedObservations.Map(obs); + + // Assert + Assert.That(result, Is.SameAs(obs)); + } + + [Test] + public async Task CalculateRehabilitationAlarm_Should_Return_Same_Observation_If_Not_PatientObservation() + { + // Arrange + var obs = new BasePatientObservation(); // No es un PatientObservation + + // Act + var result = await _calculatedObservations.Map(obs); + + // Assert + Assert.That(result, Is.SameAs(obs)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Customizations/HUVH/UCIN/CalculatedObservationsTest.cs b/adas-core.Test/Customizations/HUVH/UCIN/CalculatedObservationsTest.cs new file mode 100644 index 00000000..8954e6ee --- /dev/null +++ b/adas-core.Test/Customizations/HUVH/UCIN/CalculatedObservationsTest.cs @@ -0,0 +1,2401 @@ +using adas_core.Application.Customizations.HUVH.UCIN; +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.Utils; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Moq; +using Newtonsoft.Json; + +namespace adas_core.Test.Customizations.HUVH.UCIN; + +[TestFixture] +public class CalculatedObservationsTest +{ + [SetUp] + public void Setup() + { + _mockObservationService = new Mock(); + _medicineServiceMock = new Mock(); + _treatmentServiceMock = new Mock(); + _logger = new Mock>(); + + //JSON data + const string mappingInterventionsJson = @"[ + { + ""Type"": ""Respiratory_Device"", + ""Category"": ""Intervention"", + ""Group"": ""not defined"", + ""Name"": ""Oxigeno (O2)/Alto flujo"", + ""Value"": [ + { + ""InitialValue"": 1.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 204.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Respiratory_Device"", + ""Category"": ""Intervention"", + ""Group"": ""not defined"", + ""Name"": ""CPAP "", + ""Value"": [ + { + ""InitialValue"": 2.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 202.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Respiratory_Device"", + ""Category"": ""Intervention"", + ""Group"": ""not defined"", + ""Name"": ""Ventilaci\u00f3n mec\u00e1nica Invasiva"", + ""Value"": [ + { + ""InitialValue"": 3.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 201.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Respiratory_Device"", + ""Category"": ""Intervention"", + ""Group"": ""not defined"", + ""Name"": ""VAFO Ventilaci\u00f3n Oscilatoria de alta frecuencia"", + ""Value"": [ + { + ""InitialValue"": 4.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 200.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Intravenous_Routes"", + ""Category"": ""Intervention"", + ""Group"": ""not defined"", + ""Name"": ""VV perfif\u00e9rica"", + ""Value"": [ + { + ""InitialValue"": 1.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 103.1, + ""FinalValue"": 103.5 + } + ] + }, + { + ""Type"": ""Intravenous_Routes"", + ""Category"": ""Intervention"", + ""Group"": ""not defined"", + ""Name"": ""VV epicut\u00e1neo cava"", + ""Value"": [ + { + ""InitialValue"": 2.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 102.1, + ""FinalValue"": 102.5 + } + ] + }, + { + ""Type"": ""Intravenous_Routes"", + ""Category"": ""Intervention"", + ""Group"": ""not defined"", + ""Name"": ""VV umbilical"", + ""Value"": [ + { + ""InitialValue"": 2.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 101.1, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Intravenous_Routes"", + ""Category"": ""Intervention"", + ""Group"": ""not defined"", + ""Name"": ""VV central"", + ""Value"": [ + { + ""InitialValue"": 2.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 104.1, + ""FinalValue"": 104.3 + } + ] + }, + { + ""Type"": ""Intravenous_Routes"", + ""Category"": ""Intervention"", + ""Group"": ""not defined"", + ""Name"": ""VV PICC"", + ""Value"": [ + { + ""InitialValue"": 2.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 108.1, + ""FinalValue"": 108.2 + } + ] + }, + { + ""Type"": ""Intravenous_Routes"", + ""Category"": ""Intervention"", + ""Group"": ""not defined"", + ""Name"": ""VA umbilical"", + ""Value"": [ + { + ""InitialValue"": 3.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 101.2, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Intravenous_Routes"", + ""Category"": ""Intervention"", + ""Group"": ""not defined"", + ""Name"": ""VA"", + ""Value"": [ + { + ""InitialValue"": 3.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 106.1, + ""FinalValue"": 106.3 + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Antiarr\u00edtmicos"", + ""Name"": ""Digoxina ev"", + ""Value"": [ + { + ""InitialValue"": 1.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 118.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Antiarr\u00edtmicos"", + ""Name"": ""Digoxina vo"", + ""Value"": [ + { + ""InitialValue"": 1.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 1753.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Antiarr\u00edtmicos"", + ""Name"": ""Amiodarona ev"", + ""Value"": [ + { + ""InitialValue"": 1.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 169.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Antiarr\u00edtmicos"", + ""Name"": ""Amiodarona vo"", + ""Value"": [ + { + ""InitialValue"": 1.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 16323.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Antiarr\u00edtmicos"", + ""Name"": ""Atropina"", + ""Value"": [ + { + ""InitialValue"": 1.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 186.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Antiarr\u00edtmicos"", + ""Name"": ""Esmolol"", + ""Value"": [ + { + ""InitialValue"": 1.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 853.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Antiarr\u00edtmicos"", + ""Name"": ""Propranolol"", + ""Value"": [ + { + ""InitialValue"": 1.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 7122.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Antiarr\u00edtmicos"", + ""Name"": ""Isoprenalina"", + ""Value"": [ + { + ""InitialValue"": 1.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 283.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Antiarr\u00edtmicos"", + ""Name"": ""Flecainida ev"", + ""Value"": [ + { + ""InitialValue"": 1.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 1247.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Antiarr\u00edtmicos"", + ""Name"": ""Flecainida vo"", + ""Value"": [ + { + ""InitialValue"": 1.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 7055.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Drogas vasoactivas"", + ""Name"": ""Adrenalina"", + ""Value"": [ + { + ""InitialValue"": 2.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 281.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Drogas vasoactivas"", + ""Name"": ""Noradrenalina"", + ""Value"": [ + { + ""InitialValue"": 2.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 100000.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Drogas vasoactivas"", + ""Name"": ""Milrinona"", + ""Value"": [ + { + ""InitialValue"": 2.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 618.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Drogas vasoactivas"", + ""Name"": ""Levosimendan"", + ""Value"": [ + { + ""InitialValue"": 2.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 2073.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Drogas vasoactivas"", + ""Name"": ""Dopamina"", + ""Value"": [ + { + ""InitialValue"": 2.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 149.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Drogas vasoactivas"", + ""Name"": ""Debutamina"", + ""Value"": [ + { + ""InitialValue"": 2.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 1968.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Drogas vasoactivas"", + ""Name"": ""Fenilefrina"", + ""Value"": [ + { + ""InitialValue"": 2.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 12319.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Sedaci\u00f3n"", + ""Name"": ""Morfina ev"", + ""Value"": [ + { + ""InitialValue"": 0.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 472.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Sedaci\u00f3n"", + ""Name"": ""Morfina ov"", + ""Value"": [ + { + ""InitialValue"": 0.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 7081.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Sedaci\u00f3n"", + ""Name"": ""Fentanil"", + ""Value"": [ + { + ""InitialValue"": 0.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 434.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Sedaci\u00f3n"", + ""Name"": ""Midazolam"", + ""Value"": [ + { + ""InitialValue"": 0.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 756.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Sedaci\u00f3n"", + ""Name"": ""Dexmedetomidina"", + ""Value"": [ + { + ""InitialValue"": 0.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 15910.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Sedaci\u00f3n"", + ""Name"": ""Cisatraculi"", + ""Value"": [ + { + ""InitialValue"": 0.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 15066.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Sedaci\u00f3n"", + ""Name"": ""Propofol"", + ""Value"": [ + { + ""InitialValue"": 0.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 314.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Medication"", + ""Category"": ""Treatment"", + ""Group"": ""Sedaci\u00f3n"", + ""Name"": ""Ketamina"", + ""Value"": [ + { + ""InitialValue"": 0.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 374.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""System"", + ""Category"": ""Intervention"", + ""Group"": ""not defined"", + ""Name"": ""Hipotermia"", + ""Value"": [ + { + ""InitialValue"": 2.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 100001.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""System"", + ""Category"": ""Intervention"", + ""Group"": ""not defined"", + ""Name"": ""aEEG"", + ""Value"": [ + { + ""InitialValue"": 2.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 100002.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""System"", + ""Category"": ""Intervention"", + ""Group"": ""not defined"", + ""Name"": ""Drenaje ventricular (DVE)"", + ""Value"": [ + { + ""InitialValue"": 2.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 315.1, + ""FinalValue"": null + }, + { + ""InitialValue"": 315.2, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""System"", + ""Category"": ""Intervention"", + ""Group"": ""not defined"", + ""Name"": ""Drenaje toracico"", + ""Value"": [ + { + ""InitialValue"": 2.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 311.0, + ""FinalValue"": 314.0 + } + ] + }, + { + ""Type"": ""System"", + ""Category"": ""Intervention"", + ""Group"": ""not defined"", + ""Name"": ""Dialisis peritoneal - abdominal"", + ""Value"": [ + { + ""InitialValue"": 2.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 306.0, + ""FinalValue"": 310.0 + } + ] + }, + { + ""Type"": ""Newborn_Weight"", + ""Category"": ""Observation"", + ""Group"": ""not defined"", + ""Name"": ""Newborn_Weight"", + ""Value"": [ + { + ""InitialValue"": 0.0, + ""FinalValue"": 500.0, + ""valueContributed"": 5 + }, + { + ""InitialValue"": 501.0, + ""FinalValue"": 1000.0, + ""valueContributed"": 4 + }, + { + ""InitialValue"": 1001.0, + ""FinalValue"": 1500.0, + ""valueContributed"": 3 + }, + { + ""InitialValue"": 1501.0, + ""FinalValue"": 2000.0, + ""valueContributed"": 2 + }, + { + ""InitialValue"": 2000.0, + ""FinalValue"": 2500.0, + ""valueContributed"": 1 + } + ], + ""Codes"": [ + { + ""InitialValue"": 10000465.0, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""surgery"", + ""Category"": ""Observation"", + ""Group"": ""not defined"", + ""Name"": ""Cardiaca"", + ""Value"": [ + { + ""InitialValue"": 4.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 14000070.1, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""surgery"", + ""Category"": ""Observation"", + ""Group"": ""not defined"", + ""Name"": ""Digestiva"", + ""Value"": [ + { + ""InitialValue"": 3.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 14000070.2, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""surgery"", + ""Category"": ""Observation"", + ""Group"": ""not defined"", + ""Name"": ""Neurologica "", + ""Value"": [ + { + ""InitialValue"": 3.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 14000070.3, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""surgery"", + ""Category"": ""Observation"", + ""Group"": ""not defined"", + ""Name"": ""Toracica"", + ""Value"": [ + { + ""InitialValue"": 3.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 14000070.4, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""surgery"", + ""Category"": ""Observation"", + ""Group"": ""not defined"", + ""Name"": ""ORL/Maxil\u00b7lo"", + ""Value"": [ + { + ""InitialValue"": 2.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 14000070.5, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""surgery"", + ""Category"": ""Observation"", + ""Group"": ""not defined"", + ""Name"": ""Oftalmo"", + ""Value"": [ + { + ""InitialValue"": 1.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 14000070.6, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""surgery"", + ""Category"": ""Observation"", + ""Group"": ""not defined"", + ""Name"": ""Ortopedia"", + ""Value"": [ + { + ""InitialValue"": 1.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 14000070.7, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""surgery"", + ""Category"": ""Observation"", + ""Group"": ""not defined"", + ""Name"": ""Altres"", + ""Value"": [ + { + ""InitialValue"": 1.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 14000070.8, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""surgery"", + ""Category"": ""Observation"", + ""Group"": ""not defined"", + ""Name"": ""No"", + ""Value"": [ + { + ""InitialValue"": 0.0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": 14000070.9, + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Nutrition"", + ""Category"": ""Treatment"", + ""Group"": ""LM"", + ""Name"": ""Llet materna"", + ""Value"": [ + { + ""InitialValue"": 0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": ""BIB586"", + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Nutrition"", + ""Category"": ""Treatment"", + ""Group"": ""LM"", + ""Name"": ""Llet materna de banc"", + ""Value"": [ + { + ""InitialValue"": 0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": ""BIB000589"", + ""FinalValue"": null + }, + { + ""InitialValue"": ""BIB000587"", + ""FinalValue"": null + }, + { + ""InitialValue"": ""BIB000588"", + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Nutrition"", + ""Category"": ""Treatment"", + ""Group"": ""LM"", + ""Name"": ""Llet materna a demanda"", + ""Value"": [ + { + ""InitialValue"": 0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": ""NO_EST2"", + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Nutrition"", + ""Category"": ""Treatment"", + ""Group"": ""LM descremada"", + ""Name"": ""LM descremada"", + ""Value"": [ + { + ""InitialValue"": 0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": ""NO_EST3"", + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Nutrition"", + ""Category"": ""Treatment"", + ""Group"": ""LA"", + ""Name"": ""Formula de soja"", + ""Value"": [ + { + ""InitialValue"": 0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": ""BIBCOM003"", + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Nutrition"", + ""Category"": ""Treatment"", + ""Group"": ""LA"", + ""Name"": ""Formula hidrolizada (Alfare)"", + ""Value"": [ + { + ""InitialValue"": 0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": ""BIBCOM002"", + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Nutrition"", + ""Category"": ""Treatment"", + ""Group"": ""LA"", + ""Name"": ""Formula inici"", + ""Value"": [ + { + ""InitialValue"": 0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": ""BIBNOR003"", + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Nutrition"", + ""Category"": ""Treatment"", + ""Group"": ""LA"", + ""Name"": ""Formula prematurs 14,4%"", + ""Value"": [ + { + ""InitialValue"": 0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": ""BIBCOM017"", + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Nutrition"", + ""Category"": ""Treatment"", + ""Group"": ""LA"", + ""Name"": ""Formula prematurs 15,9%"", + ""Value"": [ + { + ""InitialValue"": 0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": ""BIBCOM018"", + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Nutrition"", + ""Category"": ""Treatment"", + ""Group"": ""LA"", + ""Name"": ""Formula sense lactosa"", + ""Value"": [ + { + ""InitialValue"": 0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": ""BIBCOM01"", + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Nutrition"", + ""Category"": ""Treatment"", + ""Group"": ""LA"", + ""Name"": ""Monogen"", + ""Value"": [ + { + ""InitialValue"": 0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": ""BIBCOM009"", + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Nutrition"", + ""Category"": ""Treatment"", + ""Group"": ""LA"", + ""Name"": ""Infatrini"", + ""Value"": [ + { + ""InitialValue"": 0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": ""BIBNOR001"", + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Nutrition"", + ""Category"": ""Treatment"", + ""Group"": ""LA"", + ""Name"": ""Infatini Peptisorb"", + ""Value"": [ + { + ""InitialValue"": 0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": ""BIBNOR0013"", + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Nutrition"", + ""Category"": ""Treatment"", + ""Group"": ""LA"", + ""Name"": ""Neocate 13,8%"", + ""Value"": [ + { + ""InitialValue"": 0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": ""BIBBAS019"", + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Nutrition"", + ""Category"": ""Treatment"", + ""Group"": ""LA"", + ""Name"": ""Neocate Advance 25%"", + ""Value"": [ + { + ""InitialValue"": 0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": ""NO_EST4"", + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Nutrition"", + ""Category"": ""Treatment"", + ""Group"": ""LA"", + ""Name"": ""Energivit"", + ""Value"": [ + { + ""InitialValue"": 0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": ""BIBBAS021"", + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Nutrition"", + ""Category"": ""Treatment"", + ""Group"": ""LA"", + ""Name"": ""Bibero especial"", + ""Value"": [ + { + ""InitialValue"": 0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": ""ALTESP001"", + ""FinalValue"": null + } + ] + }, + { + ""Type"": ""Nutrition"", + ""Category"": ""Treatment"", + ""Group"": ""Absoluta"", + ""Name"": ""Absoluta"", + ""Value"": [ + { + ""InitialValue"": 0, + ""FinalValue"": null + } + ], + ""Codes"": [ + { + ""InitialValue"": ""15002284"", + ""FinalValue"": null + } + ] + } + ]"; + + const string complexityValuesJson = @"{ + ""Oxigeno (O2)/Alto flujo"": 1, + ""CPAP "": 2, + ""Ventilación mecánica Invasiva"": 3, + ""VAFO Ventilación Oscilatoria de alta frecuencia"": 4, + ""VV perfiférica"": 1, + ""VV epicutáneo cava"": 2, + ""VV umbilical"": 2, + ""VV central"": 2, + ""VV PICC"": 2, + ""VA umbilical"": 3, + ""VA"": 3, + ""Digoxina ev"": 1, + ""Digoxina vo"": 1, + ""Amiodarona ev"": 1, + ""Amiodarona vo"": 1, + ""Atropina"": 1, + ""Esmolol"": 1, + ""Propranolol"": 1, + ""Isoprenalina"": 1, + ""Flecainida ev"": 1, + ""Flecainida vo"": 1, + ""Adrenalina"": 2, + ""Noradrenalina"": 2, + ""Milrinona"": 2, + ""Levosimendan"": 2, + ""Dopamina"": 2, + ""Debutamina"": 2, + ""Fenilefrina"": 2, + ""Hipotermia"": 2, + ""aEEG"": 2, + ""Drenaje ventricular (DVE)"": 2, + ""Drenaje toracico"": 2, + ""Dialisis peritoneal - abdominal"": 2, + ""Cardiaca"": 4, + ""Digestiva"": 3, + ""Neurologica "": 3, + ""Toracica"": 3, + ""ORL/Maxil·lo"": 2, + ""Oftalmo"": 1, + ""Ortopedia"": 1, + ""Altres"": 1, + ""No"": 0 + }"; + + // Deserialize the JSON + var apiSettings = JsonConvert.DeserializeObject( + $$""" + { + "MappingInterventions": {{mappingInterventionsJson}}, + "ComplexityMappingObservation": {{complexityValuesJson}} + } + """ + ); + + if (apiSettings == null) return; + + // Mock IOptions to return our populated ApiSettings + _mockApiSettings = new Mock>(); + _mockApiSettings.Setup(s => s.Value).Returns(apiSettings); + + // Create an instance of MappingUtils with the mocked IOptions + _mappingUtils = new MappingUtils(_mockApiSettings.Object); + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddSingleton(new Lazy(() => _mockObservationService.Object)); + serviceCollection.AddSingleton(new Lazy(() => _medicineServiceMock.Object)); + serviceCollection.AddSingleton(new Lazy(() => _treatmentServiceMock.Object)); + serviceCollection.AddSingleton(_mockApiSettings.Object); + serviceCollection.AddSingleton(_logger.Object); + serviceCollection.AddSingleton(_mappingUtils); // Register the real instance + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + _calculatedObservations = new CalculatedObservations(serviceProvider); + } + + private Mock _mockObservationService; + private Mock _medicineServiceMock; + private Mock _treatmentServiceMock; + private Mock> _logger; + private Mock> _mockApiSettings; + private MappingUtils _mappingUtils; + private CalculatedObservations _calculatedObservations; + + [Test] + public async Task Map_WithInterventionCodeInObservationValue_CallsCalculateInterventionMultiValueObservation() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var observation = new PatientObservation + { + Name = "Intervention_In", PatientId = patientId, Value = "204 Oxigeno (O2)/Alto flujo" + }; // Code 204 exists in data + + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.IsAny>())) + .ReturnsAsync([]); // Simulate no existing multivalue observation + + // Act + await _calculatedObservations.Map(observation); + + // Assert + + _mockObservationService.Verify( + x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name != null && + o.Name.Equals("Respiratory_Device_Multivalue") && + o.Value.GetType() == typeof(string[]) && + ((string[])o.Value).Count() == 1 && + ((string[])o.Value)[0].ToString() == "Oxigeno (O2)/Alto flujo"), true, true), + Times.Once); + } + + [Test] + public async Task Map_WithInterventionOut_UpdatesExistingMultiValueObservation() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var interventionCode = "204"; + var interventionDescription = "Oxigeno (O2)/Alto flujo"; + var observationOutValue = $"{interventionCode} {interventionDescription}"; + var observationOut = new PatientObservation + { Name = "Intervention_Out", PatientId = patientId, Value = observationOutValue }; + + // Simulate an existing multi-value observation that is active + var existingMultiValueObservation = new PatientObservation + { + PatientId = patientId, + Name = "Respiratory_Device_Multivalue", + Value = new[] { interventionDescription }, + Time = DateTime.UtcNow.AddMinutes(-30) // Simulate it started 30 minutes ago + }; + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Respiratory_Device_Multivalue")))) // Find the active multi-value observation + .ReturnsAsync([existingMultiValueObservation]); + + // Act + await _calculatedObservations.Map(observationOut); + + // Assert + // Verify that the existing multi-value observation was updated to set an EndTime + _mockObservationService.Verify( + x => x.UpdateObservation(It.Is(o => + o.PatientId == patientId && + o.Name != null && + o.Name.Equals("Respiratory_Device_Multivalue") + )), + Times.Once); + + // Optionally, verify that a new observation was NOT inserted + _mockObservationService.Verify( + x => x.InsertObservation(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [Test] + public async Task Map_WithNewInterventionNoExistingLastIntervention_NewMultiValueObservation() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + + // Simular una intervención activa existente + //var existingInterventionCode = "204"; + const string existingInterventionDescription = "Oxigeno (O2)/Alto flujo"; + var existingMultiValueObservation = new PatientObservation + { + PatientId = patientId, + Name = "Respiratory_Device_Multivalue", + Value = new[] { existingInterventionDescription }, + Time = DateTime.UtcNow.AddMinutes(-30), + Expired = true + }; + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Respiratory_Device_Multivalue")))) + .ReturnsAsync([existingMultiValueObservation]); + + // Nuevo evento de "Intervention_In" con una intervención diferente + const string newInterventionCode = "202"; + const string newInterventionDescription = "CPAP"; + const string newObservationInValue = $"{newInterventionCode} {newInterventionDescription}"; + var newObservationIn = new PatientObservation + { Name = "Intervention_In", PatientId = patientId, Value = newObservationInValue }; + + // Act + await _calculatedObservations.Map(newObservationIn); + + // Assert + + // Verificar que NO se insertó una nueva observación (se actualizó la existente) + _mockObservationService.Verify( + x => x.UpdateObservation(It.IsAny()), + Times.Never); + + // Verificar que la observación multi-valor existente se actualizó añadiendo la nueva intervención + _mockObservationService.Verify( + x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name != null && + o.Name.Equals("Respiratory_Device_Multivalue") && + o.Value.GetType() == typeof(string[]) && + ((string[])o.Value).Length == 1 && + ((string[])o.Value)[0] != existingInterventionDescription && + ((string[])o.Value)[0] != newInterventionDescription), true, true), + Times.Once); + } + + [Test] + public async Task Map_WithNewInterventionAddsToExistingMultiValueObservation() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + + // Simular una intervención activa existente + //var existingInterventionCode = "204"; + const string existingInterventionDescription = "Oxigeno (O2)/Alto flujo"; + var existingMultiValueObservation = new PatientObservation + { + PatientId = patientId, + Name = "Respiratory_Device_Multivalue", + Value = new[] { existingInterventionDescription }, + Time = DateTime.UtcNow.AddMinutes(-30) + }; + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Respiratory_Device_Multivalue")))) + .ReturnsAsync([existingMultiValueObservation]); + + // Nuevo evento de "Intervention_In" con una intervención diferente + const string newInterventionCode = "202"; + const string newInterventionDescription = "CPAP"; + const string newObservationInValue = $"{newInterventionCode} {newInterventionDescription}"; + var newObservationIn = new PatientObservation + { Name = "Intervention_In", PatientId = patientId, Value = newObservationInValue }; + + // Act + await _calculatedObservations.Map(newObservationIn); + + // Assert + // Verificar que la observación multi-valor existente se actualizó añadiendo la nueva intervención + _mockObservationService.Verify( + x => x.UpdateObservation(It.Is(o => + o.PatientId == patientId && + o.Name != null && + o.Name.Equals("Respiratory_Device_Multivalue") && + o.Value.GetType() == typeof(string[]) && + ((string[])o.Value).Length == 1 && + ((string[])o.Value)[0] == existingInterventionDescription && + o.Expired)), + Times.Once); + + // Verificar que NO se insertó una nueva observación (se actualizó la existente) + _mockObservationService.Verify( + x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name != null && + o.Name.Equals("Respiratory_Device_Multivalue") && + o.Value.GetType() == typeof(string[]) && + ((string[])o.Value).Length == 2 && + ((string[])o.Value)[0] == existingInterventionDescription && + ((string[])o.Value)[1] == newInterventionDescription + ), It.IsAny(), It.IsAny()), + Times.Never); + } + + [Test] + public async Task Map_WithSurgeryObservation_InsertsNewSurgeryMultiValueObservation() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string surgeryName = "Surgery"; + const string surgeryValue = "Appendectomy"; + var surgeryObservation = new PatientObservation + { Name = surgeryName, PatientId = patientId, Value = surgeryValue }; + const string expectedMultiValueObservationName = $"{surgeryName}_Multivalue"; + + // Simulate no existing Surgery_Multivalue observation + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains(expectedMultiValueObservationName)))) + .ReturnsAsync([]); + + // Act + await _calculatedObservations.Map(surgeryObservation); + + // Assert + // Verify that a new Surgery_Multivalue observation was inserted with the correct value + _mockObservationService.Verify( + x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name != null && + o.Name.Equals(expectedMultiValueObservationName) && + o.Value.GetType() == typeof(string[]) && + ((string[])o.Value).Length == 1 && + ((string[])o.Value)[0].ToString() == surgeryValue), + true, true), + Times.Once); + + // Verify that UpdateObservation was not called + _mockObservationService.Verify( + x => x.UpdateObservation(It.IsAny()), + Times.Never); + } + + [Test] + public async Task Map_WithSurgeryObservation_UpdatesExistingSurgeryMultiValueObservation() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string surgeryName = "Surgery"; + const string newSurgeryValue = "Cholecystectomy"; + var surgeryObservation = new PatientObservation + { Name = surgeryName, PatientId = patientId, Value = newSurgeryValue }; + const string expectedMultiValueObservationName = $"{surgeryName}_Multivalue"; + const string existingSurgeryValue = "Appendectomy"; + var existingMultiValueObservation = new PatientObservation + { + PatientId = patientId, + Name = expectedMultiValueObservationName, + Value = new[] { existingSurgeryValue }, + Time = DateTime.UtcNow.AddMinutes(-30), + Expired = false + }; + + // Simulate an existing Surgery_Multivalue observation + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains(expectedMultiValueObservationName)))) + .ReturnsAsync([existingMultiValueObservation]); + + // Act + await _calculatedObservations.Map(surgeryObservation); + + // Assert + // Verify that the existing Surgery_Multivalue observation was expired + _mockObservationService.Verify( + x => x.UpdateObservation(It.Is(o => + o.PatientId == patientId && + o.Name != null && + o.Name.Equals(expectedMultiValueObservationName) && + o.Expired)), + Times.Once); + + // Verify that a new Surgery_Multivalue observation was inserted with the new value + _mockObservationService.Verify( + x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name != null && + o.Name.Equals(expectedMultiValueObservationName) && + o.Value.GetType() == typeof(string[]) && + ((string[])o.Value).Length == 2 && + ((string[])o.Value)[0].ToString() == existingSurgeryValue && + ((string[])o.Value)[1].ToString() == newSurgeryValue), + true, true), + Times.Once); + } + + [Test] + public async Task Map_WithSurgeryObservation_UpdatesExistingSurgeryMultiValueObservation_RemovesDuplicate() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string surgeryName = "Surgery"; + const string duplicateSurgeryValue = "Appendectomy"; + var surgeryObservation = new PatientObservation + { Name = surgeryName, PatientId = patientId, Value = duplicateSurgeryValue }; + const string expectedMultiValueObservationName = $"{surgeryName}_Multivalue"; + var existingMultiValueObservation = new PatientObservation + { + PatientId = patientId, + Name = expectedMultiValueObservationName, + Value = new[] { "Appendectomy", "Tonsillectomy" }, + Time = DateTime.UtcNow.AddMinutes(-30), + Expired = false + }; + + // Simulate an existing Surgery_Multivalue observation with a duplicate value + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains(expectedMultiValueObservationName)))) + .ReturnsAsync([existingMultiValueObservation]); + + // Act + await _calculatedObservations.Map(surgeryObservation); + + // Assert + // Verify that the existing Surgery_Multivalue observation was expired + _mockObservationService.Verify( + x => x.UpdateObservation(It.Is(o => + o.PatientId == patientId && + o.Name != null && + o.Name.Equals(expectedMultiValueObservationName) && + o.Expired)), + Times.Once); + + // Verify that a new Surgery_Multivalue observation was inserted with the duplicate removed + _mockObservationService.Verify( + x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name != null && + o.Name.Equals(expectedMultiValueObservationName) && + o.Value.GetType() == typeof(string[]) && + ((string[])o.Value).Length == 2 && + ((string[])o.Value)[1].ToString() == "Appendectomy" && + ((string[])o.Value)[0].ToString() == "Tonsillectomy"), + true, true), + Times.Once); + } + + [Test] + public async Task Map_WithSurgeryObservation_HandlesNonStringValue() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string surgeryName = "Surgery"; + var surgeryObservation = new PatientObservation + { Name = surgeryName, PatientId = patientId, Value = 123 }; // Non-string value + + // Act + await _calculatedObservations.Map(surgeryObservation); + + // Assert + // Verify that no new observation was inserted + _mockObservationService.Verify( + x => x.InsertObservation(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + + // Verify that no existing observation was updated + _mockObservationService.Verify( + x => x.UpdateObservation(It.IsAny()), + Times.Never); + } + + [Test] + public async Task Map_WithRespiratoryDeviceMultiValueObservation_CallsCalculateComplexity_Return1() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string observationName = "Respiratory_Device_Multivalue"; + var observationValue = new[] { "Oxigeno (O2)/Alto flujo" }; + var observation = new PatientObservation + { Name = observationName, PatientId = patientId, Value = observationValue }; //value 1 + + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains(observationName)))) + .ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Intravenous_Routes_Multivalue")))) + .ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Medication_Multivalue")))) + .ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("System_Multivalue")))) + .ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Newborn_Weight")))) + .ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Surgery_Multivalue")))) + .ReturnsAsync([]); + + + // Act + await _calculatedObservations.Map(observation); + + // Assert + _mockObservationService.Verify( + x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name != null && + o.Name.Equals("Complexity") && + o.Value is int && + (int)o.Value == 1), + true, false), + Times.Once); + } + + [Test] + public async Task Map_WithIntravenousRoutesAndRespiratoryDeviceMultiValueObservation_CalculatesComplexity_Return5() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string observationName = "Intravenous_Routes_Multivalue"; + var observationValue = new[] { "VV central" }; + var observation = new PatientObservation + { Name = observationName, PatientId = patientId, Value = observationValue }; // value 2 + + var ventilationObs = new PatientObservation + { + Name = "Respiratory_Device_Multivalue", PatientId = patientId, + Value = new[] { "Ventilación mecánica Invasiva" } + }; //value 3 + + + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains(observationName)))).ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Respiratory_Device_Multivalue")))).ReturnsAsync([ventilationObs]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Medication_Multivalue")))).ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("System_Multivalue")))).ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Newborn_Weight")))).ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Surgery_Multivalue")))).ReturnsAsync([]); + + // Act + await _calculatedObservations.Map(observation); + + // Assert + _mockObservationService.Verify( + x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name != null && + o.Name.Equals("Complexity") && + o.Value is int && + (int)o.Value == + 5), // Esperamos 2 para "VV central" y 3 de "Ventilación mecánica Invasiva" según ComplexityValuesJson + true, false), + Times.Once); + } + + [Test] + public async Task Map_WithTwoMedicationMultiValueObservation_CalculatesComplexity_Return4() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string observationName = "Medication_Multivalue"; + var observationValue = new[] { "Adrenalina", "Dopamina" }; + var observation = new PatientObservation + { Name = observationName, PatientId = patientId, Value = observationValue }; + + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains(observationName)))).ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Respiratory_Device_Multivalue")))).ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Intravenous_Routes_Multivalue")))).ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("System_Multivalue")))).ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Newborn_Weight")))).ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Surgery_Multivalue")))).ReturnsAsync([]); + + // Act + await _calculatedObservations.Map(observation); + + // Assert + _mockObservationService.Verify( + x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name != null && + o.Name.Equals("Complexity") && + o.Value is int && + (int)o.Value == 4), // Esperamos 2 para "Adrenalina" y 2 para Dopamina + true, false), + Times.Once); + } + + [Test] + public async Task Map_WithSystemMedicationAndNewbornWeightMultiValueObservation_CalculatesComplexity_Return9() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string observationName = "System_Multivalue"; + var observationValue = new[] { "Hipotermia" }; + var observation = new PatientObservation + { Name = observationName, PatientId = patientId, Value = observationValue }; //value 2 + var medicationObs = new PatientObservation + { + Name = "Medication_Multivalue", PatientId = patientId, Value = new[] { "Isoprenalina", "Milrinona" } + }; //value 1 + 2 + var newbornWeight = new PatientObservation + { Name = "Newborn_Weight", PatientId = patientId, Value = 800 }; //value 4 + + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains(observationName)))).ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Respiratory_Device_Multivalue")))).ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Intravenous_Routes_Multivalue")))).ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Medication_Multivalue")))).ReturnsAsync([medicationObs]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Newborn_Weight")))).ReturnsAsync([newbornWeight]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Surgery_Multivalue")))).ReturnsAsync([]); + + // Act + await _calculatedObservations.Map(observation); + + // Assert + _mockObservationService.Verify( + x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name != null && + o.Name.Equals("Complexity") && + o.Value is int && + (int)o.Value == 9), // Esperamos 9 + true, false), + Times.Once); + } + + [Test] + public async Task Map_WithNewbornWeightObservation_CalculatesComplexity_Return4() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string observationName = "Newborn_Weight"; + const string observationValue = "600"; // Ejemplo de peso + var observation = new PatientObservation + { Name = observationName, PatientId = patientId, Value = observationValue }; + + _mockObservationService.Setup(x => + x.FindLastObservations(patientId, 1, It.Is>(list => list.Contains(observationName)))) + .ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, + It.Is>(list => list.Contains("Respiratory_Device_Multivalue")))).ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, + It.Is>(list => list.Contains("Intravenous_Routes_Multivalue")))).ReturnsAsync([]); + _mockObservationService.Setup(x => + x.FindLastObservations(patientId, 1, + It.Is>(list => list.Contains("Medication_Multivalue")))) + .ReturnsAsync([]); + _mockObservationService.Setup(x => + x.FindLastObservations(patientId, 1, It.Is>(list => list.Contains("System_Multivalue")))) + .ReturnsAsync([]); + _mockObservationService.Setup(x => + x.FindLastObservations(patientId, 1, It.Is>(list => list.Contains("Surgery_Multivalue")))) + .ReturnsAsync([]); + + // Act + await _calculatedObservations.Map(observation); + + // Assert + _mockObservationService.Verify( + x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name != null && + o.Name.Equals("Complexity") && + o.Value is int && + (int)o.Value == 4), // Esperamos 4 para un peso entre 501 y 1000 según ComplexityValuesJson + true, false), + Times.Once); + } + + [Test] + public async Task Map_WithSurgeryMedicationMewbornWeightMultiValueObservation_CalculatesComplexity_Returns13() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string observationName = "Surgery_Multivalue"; + var observationValue = new[] { "Cardiaca", "Toracica", "Ortopedia" }; + var observation = new PatientObservation + { Name = observationName, PatientId = patientId, Value = observationValue }; //value 4 + 3 + 1 + var medicationObs = new PatientObservation + { + Name = "Medication_Multivalue", PatientId = patientId, Value = new[] { "Isoprenalina", "Milrinona" } + }; //value 1 + 2 + var newbornWeight = new PatientObservation + { Name = "Newborn_Weight", PatientId = patientId, Value = 1800 }; //value 2 + + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains(observationName)))).ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Respiratory_Device_Multivalue")))).ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Intravenous_Routes_Multivalue")))).ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Medication_Multivalue")))).ReturnsAsync([medicationObs]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("System_Multivalue")))).ReturnsAsync([]); + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains("Newborn_Weight")))).ReturnsAsync([newbornWeight]); + + // Act + await _calculatedObservations.Map(observation); + + // Assert + _mockObservationService.Verify( + x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name != null && + o.Name.Equals("Complexity") && + o.Value is int && + (int)o.Value == 13), // Esperamos 13 + true, false), + Times.Once); + } + + [Test] + public async Task CalculateSaturation_DiffObservation_WhenPreObservationExistsAndPostIsFound_InsertsDiffAndPrePost() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string preObservationName = "SpO2"; + const string postObservationName = "SpO2_Post"; + var currentTime = DateTime.UtcNow; + + var preObservation = new PatientObservation + { + PatientId = patientId, + Name = preObservationName, + Value = 98, + Time = currentTime.AddMinutes(-5) + }; + + var postObservation = new PatientObservation + { + PatientId = patientId, + Name = postObservationName, + Value = 95, + Time = currentTime + }; + + // Simula que FindLastObservations devuelve la observación post + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains(preObservationName) && list.Contains(postObservationName)))) + .ReturnsAsync([postObservation]); + + // Act + await _calculatedObservations.Map(preObservation); + + // Assert + // Verifica que se insertaron dos observaciones + _mockObservationService.Verify(x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name == "SpO2_Diff" && + o.Value.Equals(Math.Round(98.0 - 95.0, 2))), // Verifica la diferencia calculada + true, true), Times.Once); + + _mockObservationService.Verify(x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name == "SpO2_Pre_Post" && + o.Value.Equals("98/95")), true, true), + Times.Once); + } + + [Test] + public async Task CalculateSaturation_DiffObservation_WhenPostObservationExistsAndPreIsFound_InsertsDiffAndPrePost() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string preObservationName = "SpO2"; + const string postObservationName = "SpO2_Post"; + var currentTime = DateTime.UtcNow; + + var preObservation = new PatientObservation + { + PatientId = patientId, + Name = preObservationName, + Value = 98, + Time = currentTime.AddMinutes(-5) + }; + + var postObservation = new PatientObservation + { + PatientId = patientId, + Name = postObservationName, + Value = 95, + Time = currentTime + }; + + // Simula que FindLastObservations devuelve la observación pre + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains(preObservationName) && list.Contains(postObservationName)))) + .ReturnsAsync([preObservation]); + + // Act + await _calculatedObservations.Map(postObservation); + + // Assert + // Verifica que se insertaron dos observaciones + _mockObservationService.Verify(x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name == "SpO2_Diff" && + o.Value.Equals(Math.Round(98.0 - 95.0, 2))), + true, true), Times.Once); + + _mockObservationService.Verify(x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name == "SpO2_Pre_Post" && + o.Value.Equals("98/95")), true, true), + Times.Once); + } + + [Test] + public async Task CalculateSaturation_DiffObservation_WhenOnlyPreObservationExists_DoesNotInsertNewObservations() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string preObservationName = "SpO2_Pre"; + const string postObservationName = "SpO2_Post"; + var currentTime = DateTime.UtcNow; + + var preObservation = new PatientObservation + { + PatientId = patientId, + Name = preObservationName, + Value = 98, + Time = currentTime.AddMinutes(-5) + }; + + // Simula que FindLastObservations devuelve solo la observación pre + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains(preObservationName) && list.Contains(postObservationName)))) + .ReturnsAsync([preObservation]); + + // Act + await _calculatedObservations.Map(preObservation); + + // Assert + // Verifica que no se insertaron nuevas observaciones + _mockObservationService.Verify( + x => x.InsertObservation(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Test] + public async Task CalculateSaturation_DiffObservation_WhenOnlyPostObservationExists_DoesNotInsertNewObservations() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string preObservationName = "SpO2_Pre"; + const string postObservationName = "SpO2_Post"; + var currentTime = DateTime.UtcNow; + + var postObservation = new PatientObservation + { + PatientId = patientId, + Name = postObservationName, + Value = 95, + Time = currentTime + }; + + // Simula que FindLastObservations devuelve solo la observación post + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains(preObservationName) && list.Contains(postObservationName)))) + .ReturnsAsync([postObservation]); + + // Act + await _calculatedObservations.Map(postObservation); + + // Assert + // Verifica que no se insertaron nuevas observaciones + _mockObservationService.Verify( + x => x.InsertObservation(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Test] + public async Task CalculateSaturation_DiffObservation_WithDifferentPrePostValues_CalculatesCorrectDifference() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string preObservationName = "SpO2"; + const string postObservationName = "SpO2_Post"; + var currentTime = DateTime.UtcNow; + + var preObservation = new PatientObservation + { + PatientId = patientId, + Name = preObservationName, + Value = 99.5, + Time = currentTime.AddMinutes(-5) + }; + + var postObservation = new PatientObservation + { + PatientId = patientId, + Name = postObservationName, + Value = 92.1, + Time = currentTime + }; + + // Simula que FindLastObservations devuelve la observación post + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains(preObservationName) && list.Contains(postObservationName)))) + .ReturnsAsync([postObservation]); + + // Act + await _calculatedObservations.Map(preObservation); + + // Assert + // Verifica la diferencia calculada correctamente + _mockObservationService.Verify(x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name == "SpO2_Diff" && + o.Value.Equals(7.4)), + true, true), Times.Once); + + _mockObservationService.Verify(x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name == "SpO2_Pre_Post" && + o.Value.Equals("99,5/92,1")), true, true), + Times.Once); + } + + [Test] + public async Task CalculateSaturation_DiffObservation_WithNonNumericPreValue_TreatsAsZero() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string preObservationName = "SpO2"; + const string postObservationName = "SpO2_Post"; + var currentTime = DateTime.UtcNow; + + var preObservation = new PatientObservation + { + PatientId = patientId, + Name = preObservationName, + Value = "TextoNoNumerico", + Time = currentTime.AddMinutes(-5) + }; + + var postObservation = new PatientObservation + { + PatientId = patientId, + Name = postObservationName, + Value = "95", + Time = currentTime + }; + + // Simula que FindLastObservations devuelve la observación post + _mockObservationService.Setup(x => x.FindLastObservations(patientId, 1, It.Is>(list => + list.Contains(preObservationName) && list.Contains(postObservationName)))) + .ReturnsAsync([postObservation]); + + // Act + await _calculatedObservations.Map(preObservation); + + // Assert + // Verifica que la diferencia se calcula como 0 - 95 = -95 + _mockObservationService.Verify(x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name == "SpO2_Diff" && + o.Value.Equals(Math.Round(0 - 95.0, 2))), + true, true), Times.Once); + + _mockObservationService.Verify(x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name == "SpO2_Pre_Post" && + o.Value.Equals("TextoNoNumerico/95")), true, true), + Times.Once); + } + + [Test] + public async Task Map_TreatmentWithNoPlacerOrder_ReturnsOriginalTreatment() + { + // Arrange + var treatment = new PatientTreatment { PlacerOrder = null }; + + // Act + var result = await _calculatedObservations.Map(treatment); + + // Assert + Assert.That(result, Is.SameAs(treatment)); + Assert.That(treatment.PlacerOrder, Is.Null); + } + + [Test] + public async Task Map_TreatmentWithActiveOlderTreatments_SetsOrderControlToXO() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string orderIdentifier = "ORD123"; + var treatment = new PatientTreatment + { PatientId = patientId, PlacerOrder = new Entity { EntityIdentifier = orderIdentifier } }; + var oldTreatments = new List { new() }; + + _treatmentServiceMock.Setup(x => x.GetActiveTreatmentsByPatient(patientId)).ReturnsAsync(oldTreatments); + + // Act + var result = await _calculatedObservations.Map(treatment); + + // Assert + Assert.That(result.OrderControl, Is.EqualTo(OrderControlType.Xo)); + + _mockObservationService.Verify( + x => x.InsertObservation(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Test] + public async Task Map_TreatmentWithNoActiveOlderTreatments_SetsOrderControlToNW() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string orderIdentifier = "ORD123"; + var treatment = new PatientTreatment + { PatientId = patientId, PlacerOrder = new Entity { EntityIdentifier = orderIdentifier } }; + + _treatmentServiceMock.Setup(x => x.GetActiveTreatmentsByPatient(patientId)).ReturnsAsync([]); + + // Act + var result = await _calculatedObservations.Map(treatment); + + // Assert + Assert.That(result.OrderControl, Is.EqualTo(OrderControlType.Nw)); + + _mockObservationService.Verify( + x => x.InsertObservation(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Test] + public async Task Map_TreatmentWithEndTimeSet_SetsOrderControlToDC() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string orderIdentifier = "ORD123"; + var treatment = new PatientTreatment + { + PatientId = patientId, + PlacerOrder = new Entity { EntityIdentifier = orderIdentifier }, + EndTime = DateTime.UtcNow.AddHours(1) + }; + + _treatmentServiceMock.Setup(x => x.GetActiveTreatmentsByPatient(patientId)).ReturnsAsync([]); + + // Act + var result = await _calculatedObservations.Map(treatment); + + // Assert + Assert.That(result.OrderControl, Is.EqualTo(OrderControlType.Dc)); + _mockObservationService.Verify( + x => x.InsertObservation(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + + [Test] + public async Task Map_TreatmentWithEndTimeSet_OverridesOrderControlFromOldTreatments() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string orderIdentifier = "ORD123"; + var treatment = new PatientTreatment + { + PatientId = patientId, + PlacerOrder = new Entity { EntityIdentifier = orderIdentifier }, + EndTime = DateTime.UtcNow.AddHours(1) + }; + var oldTreatments = new List { new() }; + + _treatmentServiceMock.Setup(x => x.GetActiveTreatmentsByPatient(patientId)).ReturnsAsync(oldTreatments); + + // Act + var result = await _calculatedObservations.Map(treatment); + + // Assert + // Aunque había tratamientos antiguos, EndTime debería priorizar OrderControlType.DC + Assert.That(result.OrderControl, Is.EqualTo(OrderControlType.Dc)); + } + + [Test] + public async Task Map_TreatmentWithMedication_InsertObservation() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + const string orderIdentifier = "ORD123"; + var code = new Code { Identifier = "16323", CodingSystem = "CCC", Text = "Amiodarona vo" }; + var medication = new Medicine + { + Name = code.Text, + Codes = [code.Identifier], + Type = ["Medication"], + Group = ["Antiarrítmicos"] + }; + var treatment = new PatientTreatment + { + PatientId = patientId, + PlacerOrder = new Entity { EntityIdentifier = orderIdentifier }, + RequestedGiveCodes = [code] + }; + var oldTreatments = new List { new() }; + + _treatmentServiceMock.Setup(x => x.GetActiveTreatmentsByPatient(patientId)).ReturnsAsync(oldTreatments); + + _medicineServiceMock.Setup(m => m.GetByCode(code.Identifier)).ReturnsAsync(medication); + + _mockObservationService.Setup(s => s.FindLastObservations(It.IsAny(), 1, It.IsAny>())) + .ReturnsAsync([]); + // Act + var result = await _calculatedObservations.Map(treatment); + + // Assert + //Assert.That(result,Is.Null); + Assert.That(result.OrderControl, Is.EqualTo(OrderControlType.Xo)); + + _mockObservationService.Verify( + x => x.InsertObservation(It.Is(o => + o.PatientId == patientId && + o.Name != null && + o.Name.Equals("Medication_Multivalue") && + o.Value.GetType() == typeof(string[]) && + ((string[])o.Value).Length == 1 && + ((string[])o.Value)[0].ToString() == "Amiodarona vo" + ), + true, true), + Times.Once); + } +} \ No newline at end of file diff --git a/adas-core.Test/Models/FakeLogger.cs b/adas-core.Test/Models/FakeLogger.cs new file mode 100644 index 00000000..28fb0d85 --- /dev/null +++ b/adas-core.Test/Models/FakeLogger.cs @@ -0,0 +1,70 @@ +using Serilog; +using Serilog.Core; +using Serilog.Events; + +namespace adas_core.Test.Models; + +public class FakeLogger : ILogger +{ + public List Messages { get; } = []; + + public ILogger ForContext(IEnumerable enrichers) + { + return this; + } + + public ILogger ForContext(string propertyName, object? value, bool destructureObjects = false) + { + return this; + } + + public ILogger ForContext() + { + return this; + } + + public ILogger ForContext(Type source) + { + return this; + } + + public void Write(LogEvent logEvent) + { + Messages.Add(logEvent.RenderMessage()); + } + + public void Write(LogEventLevel level, string messageTemplate, params object?[]? propertyValues) + { + // Manejar la situación donde propertyValues es nulo, si es necesario + Messages.Add(propertyValues == null + ? messageTemplate + : string.Format(messageTemplate, propertyValues)); // Por ejemplo, solo añadir el mensaje sin formato + } + + + public void Write(LogEventLevel level, string messageTemplate, T propertyValue) + { + Messages.Add(string.Format(messageTemplate, propertyValue)); + } + + public void Write(LogEventLevel level, Exception? exception, string messageTemplate, + params object?[]? propertyValues) + { + Messages.Add(propertyValues == null ? messageTemplate : string.Format(messageTemplate, propertyValues)); + + if (exception != null) Messages.Add($"Exception: {exception.Message}"); + } + + + public bool IsEnabled(LogEventLevel level) + { + return true; + } + + public void Write(LogEventLevel level, Exception? exception, string messageTemplate, T propertyValue) + { + Messages.Add(string.Format(messageTemplate, propertyValue)); + if (exception != null) + Messages.Add($"Exception: {exception.Message}"); // You might want to include the exception here. + } +} \ No newline at end of file diff --git a/adas-core.Test/Models/SignalR/SubscriberGroupedTest.cs b/adas-core.Test/Models/SignalR/SubscriberGroupedTest.cs new file mode 100644 index 00000000..5375b765 --- /dev/null +++ b/adas-core.Test/Models/SignalR/SubscriberGroupedTest.cs @@ -0,0 +1,238 @@ +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.GroupedObservations; +using MongoDB.Bson; +using Moq; +using static adas_core.Domain.Models.GroupedObservation; + +namespace adas_core.Test.Models.SignalR; + +[TestFixture] +public class SubscriberGroupedTest +{ + [SetUp] + public void Setup() + { + // Set up GroupedObservation + _go = new GroupedObservation + { + PatientId = _patient1, + Name = "GroupedObservationTest1", + Group = "GroupedObservationTest1", + Observations = [] + }; + _go2 = new GroupedObservation + { + PatientId = _patient2, + Name = "GroupedObservationTest2", + Group = "GroupedObservationTest2", + Observations = [] + }; + + // Set up GroupedField + _gf1 = new GroupedField + { + Name = "GroupedObservationTest1", + Group = "GroupedFieldTest1", + StartTimeShift = ["00:59", "04:59", "08:59", "12:59", "16:59", "20:59"], + Max = 24, + Regularity = GroupedObservationEnum.Regularity.Shift, + Result = [GroupedObservationEnum.Result.Last] + }; + _gf2 = new GroupedField + { + Names = ["GroupedObservationTest2", "GroupedObservationTest1"], + Group = "GroupedFieldTest2", + Max = 24, + Regularity = GroupedObservationEnum.Regularity.Hour, + Result = [GroupedObservationEnum.Result.Last, GroupedObservationEnum.Result.LastFilled] + }; + + // Set up WebSocketSubscriber + _ws1 = new WsSubscriber("1234") + { + Id = "1234", + Box = "Box1", + Section = "Section1", + Version = "1.0.0", + GroupedFields = [], + SubscriptionType = SubscriptionEnum.WsType.Box + }; + _ws2 = new WsSubscriber("1234") + { + Id = "5321", + Box = "Box2", + Section = "Section1", + Version = "1.0.0", + GroupedFields = [], + SubscriptionType = SubscriptionEnum.WsType.Box + }; + + // Set up groupedFields for subscribers + _ws1.GroupedFields = [_gf1]; + _ws2.GroupedFields = [_gf1, _gf2]; + + // Set up WsSubscriberGrouped + _wsBase = new WsSubscriberGrouped(_patient1, _ws1.Id, "Romance Standard Time", _gf1, _go, delegate { }); + _wsBase2 = new WsSubscriberGrouped(_patient2, _ws2.Id, "Romance Standard Time", _gf2, _go2, delegate { }); + } + + private GroupedObservation _go; + private GroupedObservation _go2; + + private GroupedField _gf1; + private GroupedField _gf2; + + private WsSubscriber _ws1; + private WsSubscriber _ws2; + + private WsSubscriberGrouped _wsBase; + private WsSubscriberGrouped _wsBase2; + + private readonly ObjectId _patient1 = ObjectId.GenerateNewId(); + private readonly ObjectId _patient2 = ObjectId.GenerateNewId(); + + [Test] + public void GetGroupeds_ReturnsExpectedResult() + { + var mockSubscriberGrouped = new List + { + new(_patient1, _ws1.Id, "Romance Standard Time", _gf1, _go, delegate { }), + new(_patient2, _ws1.Id, "Romance Standard Time", _gf1, _go, delegate { }) + }; + + var mockSubscriberGroupedService = new Mock(); + mockSubscriberGroupedService.Setup(x => x.GetGrouped()).Returns(mockSubscriberGrouped); + + + var result = mockSubscriberGroupedService.Object.GetGrouped(); + + Assert.That(result, Is.EqualTo(mockSubscriberGrouped)); + } + + [Test] + public void Compare_ExtensionResult() + { + using (Assert.EnterMultipleScope()) + { + Assert.That(_wsBase.Compare(_gf1, _go.PatientId), Is.True); + Assert.That(_wsBase.Compare(_gf1, _go2.PatientId), Is.False); + Assert.That(_wsBase.Compare(_gf2, _go.PatientId), Is.False); + Assert.That(_wsBase2.Compare(_gf2, _go2.PatientId), Is.True); + }; + } + + [Test] + public void Observation_is_rrelevant_because_is_the_first_in_hour() + { + var obs = new PatientObservation + { + Time = DateTime.UtcNow, + Value = 5 + }; + + + Assert.That(_wsBase.IsNewObservationRelevantForGroup(obs), Is.True); + } + + [Test] + public void Grouped_Observation_Already_Observations_In_Hour_Should_Be_Relevant_By_Max_Result() + { + var patientId = ObjectId.GenerateNewId(); + var grouped = new GroupedObservation + { + PatientId = patientId, + Name = "GroupedObservationTest1", + Group = "GroupedObservationTest1", + Observations = + [ + new GroupedObservationObs + { + Max = new GroupedObservationObsValue(5, DateTime.UtcNow) + } + ] + }; + + var wsSubscriber = new WsSubscriber("1234") + { + Id = "1234", + Box = "Box1", + Section = "Section1", + Version = "1.0.0", + GroupedFields = [], + SubscriptionType = SubscriptionEnum.WsType.Box + }; + + var groupedField = new GroupedField + { + Names = ["GroupedObservationTest2", "GroupedObservationTest1"], + Group = "GroupedFieldTest2", + Max = 24, + Regularity = GroupedObservationEnum.Regularity.Hour, + Result = [GroupedObservationEnum.Result.Max] + }; + + var newObservation = new PatientObservation + { + PatientId = patientId, + Time = DateTime.UtcNow, + Value = 8 + }; + + var wsGrouped = new WsSubscriberGrouped(patientId, wsSubscriber.Id, "", groupedField, grouped, delegate { }); + //for utc.now two observations, last was 5 and now is 8 result is by Max, should be relevant + Assert.That(wsGrouped.IsNewObservationRelevantForGroup(newObservation), Is.True); + } + + [Test] + public void Grouped_Observation_Already_Observations_In_Hour_Should_Be_IRelevant_By_Max_Result() + { + var patientId = ObjectId.GenerateNewId(); + var grouped = new GroupedObservation + { + PatientId = patientId, + Name = "GroupedObservationTest1", + Group = "GroupedObservationTest1", + Observations = + [ + new GroupedObservationObs + { + Max = new GroupedObservationObsValue(5, DateTime.UtcNow), + Time = DateTime.UtcNow + } + ] + }; + + var wsSubscriber = new WsSubscriber("1234") + { + Id = "1234", + Box = "Box1", + Section = "Section1", + Version = "1.0.0", + GroupedFields = [], + SubscriptionType = SubscriptionEnum.WsType.Box + }; + + var groupedField = new GroupedField + { + Names = ["GroupedObservationTest2", "GroupedObservationTest1"], + Group = "GroupedFieldTest2", + Max = 24, + Regularity = GroupedObservationEnum.Regularity.Hour, + Result = [GroupedObservationEnum.Result.Max] + }; + + var newObservation = new PatientObservation + { + PatientId = patientId, + Time = DateTime.UtcNow, + Value = 4 + }; + + var wsGrouped = new WsSubscriberGrouped(patientId, wsSubscriber.Id, "", groupedField, grouped, delegate { }); + //for utc.now two observations, last was 5 and now is 4 result is by Max, should be Irelevant + Assert.That(wsGrouped.IsNewObservationRelevantForGroup(newObservation), Is.False); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/AdmissionRepositoryTest.cs b/adas-core.Test/Repositories/AdmissionRepositoryTest.cs new file mode 100644 index 00000000..862d91dc --- /dev/null +++ b/adas-core.Test/Repositories/AdmissionRepositoryTest.cs @@ -0,0 +1,390 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Repositories; +using adas_core.Test.Utilities; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class AdmissionRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + + await IntegrationDb.Database.DropCollectionAsync("admissions"); + await IntegrationDb.Database.CreateCollectionAsync("admissions"); + + _repository = new AdmissionRepository(_optionsApiSettings, IntegrationDb.Database); + + var admissions = new List + { + TestUtilities.CreateValidAdmission(), + TestUtilities.CreateValidAdmission(), + TestUtilities.CreateValidAdmission() + }; + + foreach (var admission in admissions) await _repository.InsertOneAsync(admission); + } + + private AdmissionRepository _repository; + private IOptions _optionsApiSettings; + + private readonly ApiSettings _apiSettings = new() + { + Admissions = "admissions" + }; + + [Test] + public async Task InsertOneAsync_Success() + { + // Arrange + var admission = TestUtilities.CreateValidAdmission(); + + // Act + await _repository.InsertOneAsync(admission); + + // Assert + var result = await _repository.FindById(admission.Id); + + Assert.That(result, Is.Not.Null); + Assert.That(result?.Id, Is.EqualTo(admission.Id)); + } + + [Test] + public async Task Delete_Success() + { + // Arrange + var admission = TestUtilities.CreateValidAdmission(); + await _repository.InsertOneAsync(admission); + + // Act + await _repository.Delete(admission.Id); + + // Assert + var result = await _repository.FindById(admission.Id); + Assert.That(result, Is.Null); + } + + [Test] + public async Task Update_Success() + { + // Arrange + var admission = TestUtilities.CreateValidAdmission(); + await _repository.InsertOneAsync(admission); + + // Modify the admission + admission.DiagnosisAux = "Updated value"; + + // Act + await _repository.Update(admission); + + // Assert + var updatedAdmission = await _repository.FindById(admission.Id); + Assert.That(updatedAdmission, Is.Not.Null); + Assert.That(updatedAdmission?.DiagnosisAux, Is.EqualTo(admission.DiagnosisAux)); + } + + [Test] + public async Task UpdateLocation_Success() + { + // Arrange + var admission = TestUtilities.CreateValidAdmission(); + var originalLocationId = ObjectId.GenerateNewId(); + var newLocationId = ObjectId.GenerateNewId(); + + admission.PointOfCareId = originalLocationId; + await _repository.InsertOneAsync(admission); + + // Act + await _repository.UpdateLocation(admission.Id, newLocationId); + + // Assert + var updatedAdmission = await _repository.FindById(admission.Id); + Assert.That(updatedAdmission, Is.Not.Null); + Assert.That(newLocationId, Is.EqualTo(updatedAdmission?.PointOfCareId)); + } + + [Test] + public async Task UpdatePatient_Success() + { + // Arrange + var admission = TestUtilities.CreateValidAdmission(); + var newPatient = new Person + { + LastName = "Doe", + FirstName = "John", + BirthDate = new DateTime(1990, 1, 1), + Gender = PatientEnum.Gender.Male + }; + + await _repository.InsertOneAsync(admission); + + // Act + await _repository.UpdatePatient(admission.Id, newPatient); + + // Assert + var updatedAdmission = await _repository.FindById(admission.Id); + Assert.That(updatedAdmission, Is.Not.Null); + Assert.That(updatedAdmission?.Person, Is.Not.Null); + Assert.That(updatedAdmission?.Person?.FirstName, Is.EqualTo(newPatient.FirstName)); + } + + [Test] + public async Task FindAll_Success() + { + // Arrange + + + // Act + var result = await _repository.FindAll(); + + // Assert + var admissions = result as Admission[] ?? result.ToArray(); + Assert.That(admissions, Is.Not.Null); + Assert.That(admissions, Is.Not.Empty); + Assert.That(admissions.Count(), Is.EqualTo(3)); + } + + [Test] + public async Task FindById_Success() + { + // Arrange + var admission = TestUtilities.CreateValidAdmission(); + await _repository.InsertOneAsync(admission); + + // Act + var result = await _repository.FindById(admission.Id); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(admission.Id, Is.EqualTo(result?.Id)); + } + + [Test] + public async Task FindById_NotFound() + { + // Arrange + var nonExistingId = ObjectId.GenerateNewId(); + + // Act + var result = await _repository.FindById(nonExistingId); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public async Task FindByNhc_Success() + { + // Arrange + var admission = TestUtilities.CreateValidAdmission(); + await _repository.InsertOneAsync(admission); + + // Act + var result = await _repository.FindByNhc(admission.Nhc); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(admission.Nhc, Is.EqualTo(result?.Nhc)); + } + + [Test] + public async Task FindByNhc_NotFound() + { + // Arrange + var nonExistingNhc = "non-existing-nhc"; + + // Act + var result = await _repository.FindByNhc(nonExistingNhc); + + // Assert + Assert.That(result, Is.Null); + } + + // [Test] + // public async Task FindByLocation_Success() + // { + // // Arrange + // var location = new PatientLocation + // { + // UnitName = "TestUnit", + // Bed = "TestBed", + // Room = "TestRoom" + // }; + // + // var admission1 = TestUtilities.CreateValidAdmission(); + // admission1.PatientLocation = location; + // + // var admission2 = TestUtilities.CreateValidAdmission(); + // admission2.PatientLocation = location; + // + // await _repository.InsertOneAsync(admission1); + // await _repository.InsertOneAsync(admission2); + // + // // Act + // var result = await _repository.FindByLocation(location); + // + // // Assert + // Assert.That(result, Is.Not.Null); + // Assert.That(result, Is.Not.Empty); + // Assert.That(result.Count, Is.EqualTo(2)); + // + // foreach (var admission in result) + // { + // Assert.That(admission.PatientLocation, Is.Not.Null); + // Assert.That(location.UnitName, Is.EqualTo(admission?.PatientLocation?.UnitName)); + // Assert.That(location.Bed, Is.EqualTo(admission?.PatientLocation?.Bed)); + // Assert.That(location.Room, Is.EqualTo(admission?.PatientLocation?.Room)); + // } + // } + + // [Test] + // public async Task FindByOrigin_Success() + // { + // // Arrange + // var origin = "TestOrigin"; + // var admission1 = TestUtilities.CreateValidAdmission(); + // admission1.Origin = TestUtilities.CreateValidOptionList(); + // + // var admission2 = TestUtilities.CreateValidAdmission(); + // admission2.Origin = TestUtilities.CreateValidOptionList(); + // + // await _repository.InsertOneAsync(admission1); + // await _repository.InsertOneAsync(admission2); + // + // // Act + // var result = await _repository.FindByOrigin(origin); + // var resultList = result?.ToList(); + // + // // Assert + // Assert.That(resultList, Is.Not.Null); + // Assert.That(resultList, Is.Not.Empty); + // Assert.That(resultList?.Count(), Is.EqualTo(2)); + // } + + [Test] + public async Task InsertOneAsyncAndReturn_Success() + { + // Arrange + var admission = TestUtilities.CreateValidAdmission(); + + // Act + var result = await _repository.InsertOneAsyncAndReturn(admission); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.EqualTo(admission)); + var retrievedAdmission = await _repository.FindById(admission.Id); + Assert.That(retrievedAdmission, Is.Not.Null); + Assert.That(retrievedAdmission?.Id, Is.EqualTo(admission.Id)); + } + + + [Test] + public async Task GetAdmissionByUnitIdWithOutPoC_Success() + { + // Arrange + var unitId = ObjectId.GenerateNewId(); + var admission1 = TestUtilities.CreateValidAdmission(); + admission1.UnitId = unitId; + admission1.PointOfCareId = null; + + var admission2 = TestUtilities.CreateValidAdmission(); + admission2.UnitId = ObjectId.GenerateNewId(); + admission2.PointOfCareId = ObjectId.GenerateNewId(); + + await _repository.InsertOneAsync(admission1); + await _repository.InsertOneAsync(admission2); + + // Act + var result = await _repository.GetAdmissionByUnitIdWithOutPoC(unitId); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result[0].UnitId, Is.EqualTo(unitId)); + Assert.That(result[0].PointOfCareId, Is.Null); + } + + [Test] + public async Task GetAdmissionByUnitIdWithOutPoC_NoMatch() + { + // Arrange + var unitId = ObjectId.GenerateNewId(); + + // Act + var result = await _repository.GetAdmissionByUnitIdWithOutPoC(unitId); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.Empty); + } + + [Test] + public async Task FindByPointOfCareId_Success() + { + var pocId = ObjectId.GenerateNewId(); + var admission1 = TestUtilities.CreateValidAdmission(); + admission1.PointOfCareId = pocId; + + var admission2 = TestUtilities.CreateValidAdmission(); + admission2.PointOfCareId = ObjectId.GenerateNewId(); + + await _repository.InsertOneAsync(admission1); + await _repository.InsertOneAsync(admission2); + + var result = await _repository.FindByPointOfCareId(pocId); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result[0].PointOfCareId, Is.EqualTo(pocId)); + } + + [Test] + public async Task FindByPointOfCareId_NoMatch() + { + var pocId = ObjectId.GenerateNewId(); + + var result = await _repository.FindByPointOfCareId(pocId); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.Empty); + } + + [Test] + public async Task FindByDiagnosis_Success() + { + var diagnosis = "SomeDiagnosis"; + var admission1 = TestUtilities.CreateValidAdmission(); + admission1.Diagnosis = TestUtilities.CreateValidOptionList(); + + var admission2 = TestUtilities.CreateValidAdmission(); + admission2.Diagnosis = TestUtilities.CreateValidOptionList(); + + await _repository.InsertOneAsync(admission1); + await _repository.InsertOneAsync(admission2); + + var result = await _repository.FindByDiagnosis(diagnosis); + + Assert.That(result, Is.Not.Null); + } + + [Test] + public async Task FindByDiagnosis_NoMatch() + { + var diagnosis = "NonExistentDiagnosis"; + + var result = await _repository.FindByDiagnosis(diagnosis); + + Assert.That(result, Is.Empty); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/AlarmRepositoryTest.cs b/adas-core.Test/Repositories/AlarmRepositoryTest.cs new file mode 100644 index 00000000..c6c55352 --- /dev/null +++ b/adas-core.Test/Repositories/AlarmRepositoryTest.cs @@ -0,0 +1,233 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Moq; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Order(1)] +[Category("Integration")] +public class AlarmRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + + var pObsAlarm1 = new PatientObservationAlarm + { + Id = Id, + PatientId = PatientId, + Code = "code1", + CodingSystem = "SNM", + Name = "name1", + Time = Now, + Value = "value1", + SystemId = "systemId1" + }; + + var pObsAlarm2 = new PatientObservationAlarm + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + Code = "code2", + CodingSystem = "SNM", + Name = "name2", + Time = Now, + Value = "value2", + SystemId = "systemId2" + }; + + await IntegrationDb.Database.DropCollectionAsync("patients_alarms"); + await IntegrationDb.Database.CreateCollectionAsync("patients_alarms"); + + _logger = new Mock>(); + + _repository = new AlarmRepository(_optionsApiSettings, IntegrationDb.Database, _logger.Object); + + await _repository.InsertOneAsync(pObsAlarm1); + await _repository.InsertOneAsync(pObsAlarm2); + } + + private AlarmRepository _repository; + + private readonly ApiSettings _apiSettings = new() + { + PatientsAlarms = "patients_alarms" + }; + + private IOptions? _optionsApiSettings; + private Mock> _logger; + + private static readonly DateTime Now = DateTime.UtcNow; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + private static readonly ObjectId Id = ObjectId.GenerateNewId(); + + [Test] + public async Task AggregatedPatientLastObservationsByField_Not_Found_Returns_Empty_List() + { + var filter = new List + { + new() + { + Name = "name100", + Last = 2, + OnlyExpired = false + } + }; + + var result = await _repository.AggregatedPatientLastObservationsByField(PatientId, filter); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.Empty); + } + + [Test] + public async Task AggregatedPatientLastObservationsByField_Found_Returns_1() + { + var filter = new List + { + new() + { + Name = "name1", + Last = 2, + OnlyExpired = false + } + }; + + var result = await _repository.AggregatedPatientLastObservationsByField(PatientId, filter); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Count(), Is.EqualTo(1)); + } + + [Test] + public async Task AggregatedPatientLastObservationsByField_Found_Returns_2() + { + var filter = new List + { + new() + { + Name = "name1", + Last = 1, + OnlyExpired = false + }, + new() + { + Name = "name2", + Last = 1, + OnlyExpired = false + } + }; + + var result = await _repository.AggregatedPatientLastObservationsByField(PatientId, filter); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Count(), Is.EqualTo(2)); + } + + [Test] + public async Task AggregatedPatientLastObservationsByField_AllExpired_Returns_Empty_List() + { + var expiredTime = Now.AddDays(-1); // Set a time in the past to simulate expiration + + var expiredObs = new PatientObservationAlarm + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + Code = "code3", + CodingSystem = "SNM", + Name = "name3", + Time = expiredTime, + Value = "value3", + SystemId = "systemId3", + Expired = true + }; + + await _repository.InsertOneAsync(expiredObs); + + var filter = new List + { + new() + { + Name = "name3", + Last = 2, + OnlyExpired = true + } + }; + + var result = await _repository.AggregatedPatientLastObservationsByField(PatientId, filter); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.Empty); + } + + [Test] + public async Task AggregatedPatientLastObservationsByField_MultipleObservations_Returns_Most_Recent() + { + var oldTime = Now.AddMinutes(-10); // Set an older time + + var oldObs = new PatientObservationAlarm + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + Code = "code4", + CodingSystem = "SNM", + Name = "name4", + Time = oldTime, + Value = "value4", + SystemId = "systemId4" + }; + + var newObs = new PatientObservationAlarm + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + Code = "code4", + CodingSystem = "SNM", + Name = "name4", + Time = Now, + Value = "value5", + SystemId = "systemId5" + }; + + await _repository.InsertOneAsync(oldObs); + await _repository.InsertOneAsync(newObs); + + var filter = new List + { + new() + { + Name = "name4", + Last = 1, + OnlyExpired = false + } + }; + + var result = await _repository.AggregatedPatientLastObservationsByField(PatientId, filter); + + Assert.That(result, Is.Not.Null); + using (Assert.EnterMultipleScope()) + { + + Assert.That(result, Has.Count.EqualTo(1)); + Assert.That(result.First().Value, Is.EqualTo("value5")); // Ensure the most recent observation is returned + }; + } + + [Test] + public async Task AggregatedPatientLastObservationsByField_FilterObservations_Null_Returns_All() + { + var result = await _repository.AggregatedPatientLastObservationsByField(PatientId); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.Not.Empty); // Ensure it returns all observations for the patient + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/AppointmentArchiveRepositoryTest.cs b/adas-core.Test/Repositories/AppointmentArchiveRepositoryTest.cs new file mode 100644 index 00000000..81b846d9 --- /dev/null +++ b/adas-core.Test/Repositories/AppointmentArchiveRepositoryTest.cs @@ -0,0 +1,93 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class AppointmentArchiveRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + var patient = new Person + { + FirstName = "firstName", + SecondName = "secondName", + BirthDate = Now.AddYears(-10), + Gender = PatientEnum.Gender.Female, + Ids = new Dictionary { { "MR", "123456" }, { "VN", "654321" } } + }; + + PatientAppointment testPatientAppointment = new() + { + PatientId = PatientId, + Patient = patient, + CreateTime = Now.AddHours(-2), + UpdateTime = Now.AddHours(-2) + }; + + PatientAppointment testPatientAppointment2 = new() + { + PatientId = PatientId, + Patient = patient, + CreateTime = Now.AddHours(-3), + UpdateTime = Now.AddHours(-3), + VisitNumber = "654321", + AppointmentReason = "appointmentReason", + ResourceGroups = + [ + new PatientAppointmentResourceGroup + { + Locations = [new PatientLocation("UCI5C", "Box4")] + } + ] + }; + + await IntegrationDb.Database.DropCollectionAsync("patients_appointments"); + await IntegrationDb.Database.CreateCollectionAsync("patients_appointments"); + + _repository = new AppointmentArchiveRepository(_optionsApiSettings, IntegrationDb.Database); + + await _repository.InsertOneAsync(testPatientAppointment); + await _repository.InsertOneAsync(testPatientAppointment2); + } + + private AppointmentArchiveRepository _repository; + + private readonly ApiSettings _apiSettings = new() + { + ArchivePatientsAppointments = "archive_patients_appointments" + }; + + private IOptions _optionsApiSettings; + //Mock> logger; + + private static readonly DateTime Now = DateTime.Now; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + + + [Test] + public async Task DeleteBeforeDate() + { + var result = await _repository.Collection.FindAsync(p => p.PatientId == PatientId); + + Assert.That(result, Is.Not.Null); + Assert.That(result.ToList(), Has.Count.GreaterThan(1)); + + await _repository.DeleteBeforeDate(Now.AddHours(-2)); + + var resultDelete = await _repository.Collection.FindAsync(p => p.PatientId == PatientId); + + Assert.That(resultDelete, Is.Not.Null); + Assert.That(resultDelete.ToList(), Has.Count.EqualTo(1)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/AppointmentRepositoryTest.cs b/adas-core.Test/Repositories/AppointmentRepositoryTest.cs new file mode 100644 index 00000000..5714ed22 --- /dev/null +++ b/adas-core.Test/Repositories/AppointmentRepositoryTest.cs @@ -0,0 +1,148 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class AppointmentRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + var patient = new Person + { + FirstName = "firstName", + SecondName = "secondName", + BirthDate = Now.AddYears(-10), + Gender = PatientEnum.Gender.Female, + Ids = new Dictionary { { "MR", "123456" }, { "VN", "654321" } } + }; + + var testPatientAppointment = new PatientAppointment + { + PatientId = PatientId, + Patient = patient, + CreateTime = Now.AddHours(-2), + UpdateTime = Now.AddHours(-2) + }; + + var testPatientAppointment2 = new PatientAppointment + { + PatientId = PatientId, + Patient = patient, + CreateTime = Now.AddHours(-3), + UpdateTime = Now.AddHours(-3), + VisitNumber = "654321", + AppointmentReason = "appointmentReason", + ResourceGroups = + [ + new PatientAppointmentResourceGroup + { + Locations = [new PatientLocation("UCI5C", "Box4")] + } + ] + }; + + await IntegrationDb.Database.DropCollectionAsync("patients_appointments"); + await IntegrationDb.Database.CreateCollectionAsync("patients_appointments"); + + _repository = new AppointmentRepository(_optionsApiSettings, IntegrationDb.Database); + + await _repository.InsertOneAsync(testPatientAppointment); + await _repository.InsertOneAsync(testPatientAppointment2); + } + + private AppointmentRepository _repository; + + private readonly ApiSettings _apiSettings = new() + { + PatientsAppointments = "patients_appointments" + }; + + private IOptions _optionsApiSettings; + + private static readonly DateTime Now = DateTime.Now; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + + [Test] + public async Task GetByPatient_Find_Patient_Return_List() + { + var result = await _repository.GetByPatient(PatientId); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.Not.Empty); + } + + [Test] + public async Task GetByPatient_not_Find_Patient_Return_Empty_List() + { + var result = await _repository.GetByPatient(ObjectId.GenerateNewId()); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.Empty); + } + + [Test] + public async Task FindByPatientAndVisitNumber_Not_Find_Patient_Return_Null() + { + var result = await _repository.FindByPatientAndVisitNumber(PatientId, "visitNumber"); + + Assert.That(result, Is.Null); + } + + [Test] + public async Task FindByPatientAndVisitNumber_Find_Patient_Return_Appointment() + { + var result = await _repository.FindByPatientAndVisitNumber(PatientId, "654321"); + + Assert.That(result, Is.Not.Null); + Assert.That(result!.VisitNumber, Is.EqualTo("654321")); + } + + [Test] + public async Task FindByPatientAndReason_Not_Find_Patient_Return_Null() + { + var result = await _repository.FindByPatientAndReason(PatientId, "NotappointmentReason"); + + Assert.That(result, Is.Null); + } + + [Test] + public async Task FindByPatientAndReason_Find_Patient_Return_Appointment() + { + var result = await _repository.FindByPatientAndReason(PatientId, "appointmentReason"); + + Assert.That(result, Is.Not.Null); + Assert.That(result!.AppointmentReason, Is.EqualTo("appointmentReason")); + } + + [Test] + public async Task FindByLocation_Not_Find_Patient_Return_Null() + { + var location = new PatientLocation("UCI5C", "Box3"); + + var result = await _repository.FindByLocation(location); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Count(), Is.EqualTo(0)); + } + + [Test] + public async Task FindByLocation_Find_Patient_Return_Appointment() + { + var location = new PatientLocation("UCI5C", "Box4"); + + var result = await _repository.FindByLocation(location); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Count(), Is.EqualTo(1)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/ConfigObservationRepositoryTest.cs b/adas-core.Test/Repositories/ConfigObservationRepositoryTest.cs new file mode 100644 index 00000000..81d05bad --- /dev/null +++ b/adas-core.Test/Repositories/ConfigObservationRepositoryTest.cs @@ -0,0 +1,190 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Moq; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class ConfigObservationRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _id = ObjectId.GenerateNewId(); + _optionsApiSettings = Options.Create(_apiSettings); + _masterListMock = new Mock(); + + var testConfigObservation = new List + { + new() + { + Id = _id, + Name = "Hemoglobina", + Code = "12345", + CodingSystem = "SNM" + }, + + new() + { + Id = ObjectId.GenerateNewId(), + Name = "Glucemia", + Code = "54321", + CodingSystem = "SNM", + OriginalName = "GLU" + }, + + new() + { + Id = ObjectId.GenerateNewId(), + Name = "Sodio", + OriginalName = "Sodio" + }, + + new() + { + Id = ObjectId.GenerateNewId(), + Name = "ph", + Code = "555", + CodingSystem = "MG4", + ParentCode = "3333", + ParentCodingSystem = "SNM" + }, + + + new() + { + Id = ObjectId.GenerateNewId(), + Name = "SOFA", + Code = "278061009", + OriginalName = "SOFA", + CodingSystem = "SNM", + MinAlert = 10, + MinWarn = 14 + }, + + + new() + { + Id = ObjectId.GenerateNewId(), + Name = "FC", + Code = "147842", + //originalName = "MDC_ECG_CARD_BEAT_RATE", + CodingSystem = "MDC", + ParentCode = "69965", + ParentName = "MDC_DEV_MON_PHYSIO_MULTI_PARAM_MDS", + MinAlert = 60, + MaxAlert = 100, + ForceAlert = true + } + }; + + + await IntegrationDb.Database.DropCollectionAsync("config_observations"); + await IntegrationDb.Database.CreateCollectionAsync("config_observations"); + + _repository = + new ConfigObservationRepository(_optionsApiSettings, IntegrationDb.Database, _masterListMock.Object); + + await _repository.InsertManyAsync(testConfigObservation); + } + + private ConfigObservationRepository _repository; + private IMock _masterListMock; + private ObjectId _id; + + private readonly ApiSettings _apiSettings = new() + { + ConfigObservations = "config_observations" + }; + + private IOptions _optionsApiSettings; + + + [Test] + public async Task FindById_Find_Id_Return_Configs() + { + var result = await _repository.FindById(_id); + + Assert.That(result, Is.Not.Null); + } + + [Test] + public async Task FindById_Not_Find_Id_Return_Empty() + { + var result = await _repository.FindById(ObjectId.GenerateNewId()); + + Assert.That(result, Is.Null); + } + + [Test] + public async Task Update_Find_Id_Return_Update_Configs() + { + var updateConfigObservation = new ConfigObservation + { + Id = _id, + Name = "Hemoglobina 2", + Code = "12345", + CodingSystem = "SNM" + }; + + var result = await _repository.FindById(_id); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Name, Is.EqualTo("Hemoglobina")); + + var resultUpdate = await _repository.Update(updateConfigObservation); + + Assert.That(resultUpdate, Is.Not.Null); + Assert.That(resultUpdate.Name, Is.EqualTo("Hemoglobina 2")); + } + + [Test] + public async Task Update_Not_Find_Id_Create_It() + { + var id2 = ObjectId.GenerateNewId(); + var updateConfigObservation = new ConfigObservation + { + Id = id2, + Name = "Hemoglobina", + Code = "12345", + CodingSystem = "SNM" + }; + + await _repository.Update(updateConfigObservation); + var resultfind = await _repository.FindById(id2); + Assert.That(resultfind, Is.Not.Null); + } + + + [Test] + public async Task Delete_With_Base_DeleteAsync() + { + var iid = ObjectId.GenerateNewId(); + var deleteConfigObservation = new ConfigObservation + { + Id = iid, + Name = "Hemoglobina3", + Code = "123455", + CodingSystem = "SNMM" + }; + + var result = await _repository.FindById(iid); + Assert.That(result, Is.Null); + + await _repository.InsertOneAsync(deleteConfigObservation); + + var resultInsert = await _repository.FindById(iid); + Assert.That(resultInsert, Is.Not.Null); + + await _repository.Delete(iid); + + var resultDelete = await _repository.FindById(iid); + Assert.That(resultDelete, Is.Null); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/ConfigPumpsRepositoryTest.cs b/adas-core.Test/Repositories/ConfigPumpsRepositoryTest.cs new file mode 100644 index 00000000..4d2c7a35 --- /dev/null +++ b/adas-core.Test/Repositories/ConfigPumpsRepositoryTest.cs @@ -0,0 +1,88 @@ +using System.Diagnostics; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class ConfigPumpsRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + var configPumpItem1 = new ConfigPumpItem(); + var configPumpItem2 = new ConfigPumpItem(); + + var configPumps = new ConfigPumps + { + Id = "PV1", + Items = [configPumpItem1, configPumpItem2] + }; + + await IntegrationDb.Database.DropCollectionAsync("config_pumps"); + await IntegrationDb.Database.CreateCollectionAsync("config_pumps"); + + _repository = new ConfigPumpsRepository(_optionsApiSettings, IntegrationDb.Database); + + await _repository.InsertOneAsync(configPumps); + } + + private ConfigPumpsRepository _repository; + + private readonly ApiSettings _apiSettings = new() + { + ConfigPumps = "config_pumps" + }; + + private IOptions _optionsApiSettings; + + + [Test] + public async Task FindById_Find_id_Return_List() + { + var result = await _repository.FindById("PV1"); + + Assert.That(result, Is.Not.Null); + } + + [Test] + public async Task FindAsync_not_Find_id_Return_Empty_List() + { + var result = await _repository.FindById("PV2"); + + Assert.That(result, Is.Null); + } + + [Test] + public async Task Insert_PumpConfig_returns2() + { + var configPumpItem1 = new ConfigPumpItem(); + var configPumpItem2 = new ConfigPumpItem(); + + var newPumpConfig = new ConfigPumps + { + Id = "PV2", + Items = [configPumpItem1, configPumpItem2] + }; + + await _repository.InsertOneAsync(newPumpConfig); + + var result = await _repository.GetAllConfigs(); + + Assert.That(result, Has.Count.EqualTo(2)); + + var pumpCfgInserted = await _repository.FindById(newPumpConfig.Id); + + Assert.That(pumpCfgInserted, Is.Not.EqualTo(null)); + + Debug.Assert(pumpCfgInserted != null, nameof(pumpCfgInserted) + " != null"); + + await _repository.DeleteConfig(pumpCfgInserted); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/ConfigUnitsRepositoryTest.cs b/adas-core.Test/Repositories/ConfigUnitsRepositoryTest.cs new file mode 100644 index 00000000..722e2edf --- /dev/null +++ b/adas-core.Test/Repositories/ConfigUnitsRepositoryTest.cs @@ -0,0 +1,60 @@ +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class ConfigUnitsRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + var configUnitItem1 = new ConfigUnitItem(); + var configUnitItem2 = new ConfigUnitItem(); + + var configUnits = new ConfigUnits + { + Id = "PV1", + Items = [configUnitItem1, configUnitItem2] + }; + + await IntegrationDb.Database.DropCollectionAsync("config_units"); + await IntegrationDb.Database.CreateCollectionAsync("config_units"); + + _repository = new ConfigUnitsRepository(_optionsApiSettings, IntegrationDb.Database); + + await _repository.InsertOneAsync(configUnits); + } + + private ConfigUnitsRepository _repository; + + private readonly ApiSettings _apiSettings = new() + { + ConfigUnits = "config_units" + }; + + private IOptions _optionsApiSettings; + + + [Test] + public async Task FindById_Find_id_Return_List() + { + var result = await _repository.FindById("PV1"); + + Assert.That(result, Is.Not.Null); + } + + [Test] + public async Task FindAsync_not_Find_id_Return_Empty_List() + { + var result = await _repository.FindById("PV2"); + + Assert.That(result, Is.Null); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/DiagnosisArchiveRepositoryTest.cs b/adas-core.Test/Repositories/DiagnosisArchiveRepositoryTest.cs new file mode 100644 index 00000000..63f0afd3 --- /dev/null +++ b/adas-core.Test/Repositories/DiagnosisArchiveRepositoryTest.cs @@ -0,0 +1,86 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class DiagnosisArchiveRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + var patientDiagnosis1 = new PatientDiagnosis + { + Id = Id, + PatientId = PatientId, + Time = Now.AddDays(-1), + Code = "code1", + DiagnosisSystem = "diagnosisSystem" + }; + + var patientDiagnosis2 = new PatientDiagnosis + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + Time = Now.AddDays(-2), + Code = "code2", + DiagnosisSystem = "diagnosisSystem" + }; + + var patientDiagnosis3 = new PatientDiagnosis + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + Time = Now.AddDays(-3), + Code = "code3", + DiagnosisSystem = "diagnosisSystem" + }; + + await IntegrationDb.Database.DropCollectionAsync("archive_patients_diagnosis"); + await IntegrationDb.Database.CreateCollectionAsync("archive_patients_diagnosis"); + + _repository = new DiagnosisArchiveRepository(_optionsApiSettings, IntegrationDb.Database); + + await _repository.InsertOneAsync(patientDiagnosis1); + await _repository.InsertOneAsync(patientDiagnosis2); + await _repository.InsertOneAsync(patientDiagnosis3); + } + + private DiagnosisArchiveRepository _repository; + + private readonly ApiSettings _apiSettings = new() + { + ArchivePatientsDiagnosis = "archive_patients_diagnosis" + }; + + private IOptions _optionsApiSettings; + + private static readonly DateTime Now = DateTime.Now; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + private static readonly ObjectId Id = ObjectId.GenerateNewId(); + + + [Test] + public async Task DeleteBeforeDate() + { + var result = await _repository.Collection.FindAsync(p => p.PatientId == PatientId); + + Assert.That(result, Is.Not.Null); + Assert.That(result.ToList(), Has.Count.GreaterThan(1)); + + await _repository.DeleteBeforeDate(Now.AddDays(-1)); + + var resultDelete = await _repository.Collection.FindAsync(p => p.PatientId == PatientId); + + Assert.That(resultDelete, Is.Not.Null); + Assert.That(resultDelete.ToList(), Has.Count.EqualTo(1)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/DiagnosisRepositoryTest.cs b/adas-core.Test/Repositories/DiagnosisRepositoryTest.cs new file mode 100644 index 00000000..dbec4bd0 --- /dev/null +++ b/adas-core.Test/Repositories/DiagnosisRepositoryTest.cs @@ -0,0 +1,197 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class DiagnosisRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + var patientDiagnosis1 = new PatientDiagnosis + { + Id = Id, + PatientId = PatientId, + Time = Now, + Code = "code1", + DiagnosisSystem = "diagnosisSystem" + }; + + var patientDiagnosis2 = new PatientDiagnosis + { + Id = ObjectId.GenerateNewId(), + PatientId = ObjectId.GenerateNewId(), + Time = Now, + Code = "code2", + DiagnosisSystem = "diagnosisSystem" + }; + + var patientDiagnosis3 = new PatientDiagnosis + { + Id = ObjectId.GenerateNewId(), + PatientId = ObjectId.GenerateNewId(), + Time = Now, + Code = "code3", + DiagnosisSystem = "diagnosisSystem" + }; + + await IntegrationDb.Database.DropCollectionAsync("patients_diagnosis"); + await IntegrationDb.Database.CreateCollectionAsync("patients_diagnosis"); + + _repository = new DiagnosisRepository(_optionsApiSettings, IntegrationDb.Database); + + await _repository.InsertOneAsync(patientDiagnosis1); + await _repository.InsertOneAsync(patientDiagnosis2); + await _repository.InsertOneAsync(patientDiagnosis3); + } + + private DiagnosisRepository _repository; + + private readonly ApiSettings _apiSettings = new() + { + PatientsDiagnosis = "patients_diagnosis" + }; + + private IOptions _optionsApiSettings; + + private static readonly DateTime Now = DateTime.Now; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + private static readonly ObjectId Id = ObjectId.GenerateNewId(); + + + [Test] + public async Task GetByPatient_Find_Patient_Return_List() + { + var result = await _repository.GetByPatient(PatientId); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.Not.Empty); + } + + [Test] + public async Task GetByPatient_not_Find_Patient_Return_Empty_List() + { + var result = await _repository.GetByPatient(ObjectId.GenerateNewId()); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.Empty); + } + + [Test] + public async Task FindByPatientIdAndCode_Find_Patient_Return_Diagnois() + { + var result = await _repository.FindByPatientIdAndCode(PatientId, "code1", "diagnosisSystem"); + + Assert.That(result, Is.Not.Null); + } + + [Test] + public async Task FindByPatientIdAndCode_not_Find_Patient_Return_null() + { + var result = await _repository.FindByPatientIdAndCode(ObjectId.GenerateNewId(), "code4", "diagnosisSystem"); + + Assert.That(result, Is.Null); + } + + [Test] + public async Task FindByPatientIdAsync_Find_Patient_Return_List() + { + var result = await _repository.FindByPatientIdAsync(PatientId); + + Assert.That(result, Is.Not.Null); + Assert.That(result.ToList(), Is.Not.Empty); + } + + [Test] + public async Task FindByPatientIdAsync_not_Find_Patient_Return_Empty_List() + { + var result = await _repository.FindByPatientIdAsync(ObjectId.GenerateNewId()); + + Assert.That(result, Is.Not.Null); + Assert.That(result.ToList(), Is.Empty); + } + + [Test] + public async Task DeleteAsync_Find_id() + { + var patientDiagnosis = new PatientDiagnosis + { + Id = ObjectId.GenerateNewId(), + PatientId = ObjectId.GenerateNewId(), + Time = Now, + Code = "code1", + DiagnosisSystem = "diagnosisSystem" + }; + + await _repository.InsertOneAsync(patientDiagnosis); + + var result = await _repository.FindByPatientIdAsync(patientDiagnosis.PatientId); + Assert.That(result.ToList(), Has.Count.EqualTo(1)); + + await _repository.DeleteAsync(patientDiagnosis.Id); + + var resultDelete = await _repository.FindByPatientIdAsync(patientDiagnosis.PatientId); + Assert.That(resultDelete.ToList(), Is.Empty); + } + + [Test] + public async Task DeleteByPatientId_Find_patientId() + { + var patientDiagnosis = new PatientDiagnosis + { + Id = ObjectId.GenerateNewId(), + PatientId = ObjectId.GenerateNewId(), + Time = Now, + Code = "code1", + DiagnosisSystem = "diagnosisSystem" + }; + + await _repository.InsertOneAsync(patientDiagnosis); + + var result = await _repository.FindByPatientIdAsync(patientDiagnosis.PatientId); + Assert.That(result.ToList(), Has.Count.EqualTo(1)); + + await _repository.DeleteByPatientId(patientDiagnosis.PatientId); + + var resultDelete = await _repository.FindByPatientIdAsync(patientDiagnosis.PatientId); + Assert.That(resultDelete.ToList(), Is.Empty); + } + + [Test] + public async Task UpdateManyObjectId_With_base_UpdateManyObjectIdAsync() + { + var patientDiagnosis = new PatientDiagnosis + { + Id = ObjectId.GenerateNewId(), + PatientId = ObjectId.GenerateNewId(), + Time = Now, + Code = "code1", + DiagnosisSystem = "diagnosisSystem" + }; + + await _repository.InsertOneAsync(patientDiagnosis); + + var resultInsert = await _repository.FindByPatientIdAsync(patientDiagnosis.PatientId); + + var resultList = resultInsert.ToList(); + + Assert.That(resultList, Has.Count.EqualTo(1)); + + var newPatientId = ObjectId.GenerateNewId(); + + await _repository.UpdateManyObjectId("patientId", newPatientId, patientDiagnosis.PatientId); + + var resultUpdate = await _repository.FindByPatientIdAsync(newPatientId); + Assert.That(resultUpdate, Is.Not.Null); + Assert.That(resultUpdate.ToList().Count(f => f.PatientId == newPatientId), Is.EqualTo(1)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/DischargeRepositoryTest.cs b/adas-core.Test/Repositories/DischargeRepositoryTest.cs new file mode 100644 index 00000000..ab3264c7 --- /dev/null +++ b/adas-core.Test/Repositories/DischargeRepositoryTest.cs @@ -0,0 +1,373 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Repositories; +using adas_core.Test.Utilities; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class DischargeRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + // Initialize options and repository + _optionsApiSettings = Options.Create(_apiSettings); + + await IntegrationDb.Database.DropCollectionAsync("discharge"); + await IntegrationDb.Database.CreateCollectionAsync("discharge"); + + _dischargeRepository = new DischargeRepository(_optionsApiSettings, IntegrationDb.Database); + } + + private DischargeRepository _dischargeRepository; + private readonly ApiSettings _apiSettings = new(); + private IOptions _optionsApiSettings; + + [Test] + public async Task InsertOneAsync_ShouldInsertDischarge() + { + // Arrange + var discharge = TestUtilities.CreateValidDischarge(); + + // Act + await _dischargeRepository.InsertOneAsync(discharge); + + var result = await _dischargeRepository.FindById(discharge.Id); + Assert.That(result, Is.Not.Null); + } + + [Test] + public async Task Delete_ShouldDeleteDischarge() + { + // Arrange + var discharge = TestUtilities.CreateValidDischarge(); + await _dischargeRepository.InsertOneAsync(discharge); + + // Act + await _dischargeRepository.Delete(discharge.Id); + + var result = await _dischargeRepository.FindById(discharge.Id); + Assert.That(result, Is.Null); + } + + [Test] + public async Task Update_DischargeSuccessfullyUpdated() + { + // Arrange + var pocId = new ObjectId(); + var discharge = TestUtilities.CreateValidDischarge(); + discharge.PointOfCareId = pocId; + await _dischargeRepository.InsertOneAsync(discharge); + + // Act + discharge.Service = "UpdatedService"; + await _dischargeRepository.Update(discharge); + + // Retrieve the discharge from the database + var updatedDischarge = await _dischargeRepository.GetDischargeByPointOfCareId(pocId); + + // Assert + Assert.That(updatedDischarge, Is.Not.Null); + Assert.That(updatedDischarge?.Service, Is.EqualTo("UpdatedService")); + } + + // [Test] + // public async Task UpdateUnit_UnitNameSuccessfullyUpdated() + // { + // // Arrange + // var discharge = TestUtilities.CreateValidDischarge(); + // await _dischargeRepository.InsertOneAsync(discharge); + // + // // Act + // var newUnitName = "NewUnitName"; + // await _dischargeRepository.UpdateUnit(discharge.Id, newUnitName); + // + // // Retrieve the discharge from the database + // var updatedDischarge = await _dischargeRepository.FindById(discharge.Id); + // + // // Assert + // Assert.That(updatedDischarge, Is.Not.Null); + // Assert.That(updatedDischarge?.PatientLocation, Is.Not.Null); + // Assert.That(updatedDischarge?.PatientLocation?.UnitName, Is.EqualTo(newUnitName)); + // } + + // [Test] + // public async Task UpdatePatient_PatientSuccessfullyUpdated() + // { + // // Arrange + // var discharge = TestUtilities.CreateValidDischarge(); + // await _dischargeRepository.InsertOneAsync(discharge); + // + // // Create a new patient object with updated information + // var updatedPatient = TestUtilities.CreateValidPatient(); + // + // // Act + // await _dischargeRepository.UpdatePatient(discharge.Id, updatedPatient); + // + // // Retrieve the discharge from the database + // var updatedDischarge = await _dischargeRepository.FindById(discharge.Id); + // + // // Assert + // Assert.That(updatedDischarge, Is.Not.Null); + // Assert.That(updatedDischarge?.Patient, Is.Not.Null); + // Assert.That(updatedDischarge?.Patient?.Id, Is.EqualTo(updatedPatient.Id)); + // } + + [Test] + public async Task FindAll_ReturnsAllDischarges() + { + // Arrange: Prepare test data + var expectedDischarges = new List + { + new() { Id = ObjectId.GenerateNewId(), DischargeDate = DateTime.UtcNow }, + new() { Id = ObjectId.GenerateNewId(), DischargeDate = DateTime.UtcNow }, + new() { Id = ObjectId.GenerateNewId(), DischargeDate = DateTime.UtcNow } + }; + + await _dischargeRepository.InsertManyAsync(expectedDischarges); + + // Act: Call the method under test + var actualDischarges = await _dischargeRepository.FindAll(); + + var discharges = actualDischarges.ToList(); + Assert.That(discharges, Is.Not.Null); + Assert.That(discharges, Is.Not.Empty); + Assert.That(discharges.Count(), Is.EqualTo(expectedDischarges.Count())); + } + + [Test] + public async Task FindById_ExistingId_ReturnsDischarge() + { + var id = ObjectId.GenerateNewId(); + var expectedDischarge = new Discharge { Id = id, DischargeDate = DateTime.UtcNow }; + await _dischargeRepository.InsertOneAsync(expectedDischarge); + + var actualDischarge = await _dischargeRepository.FindById(id); + + Assert.That(actualDischarge, Is.Not.Null); + Assert.That(actualDischarge?.Id, Is.EqualTo(expectedDischarge.Id)); + } + + [Test] + public async Task FindById_NonExistentId_ReturnsNull() + { + var id = ObjectId.GenerateNewId(); + + var actualDischarge = await _dischargeRepository.FindById(id); + + Assert.That(actualDischarge, Is.Null); + } + + // [Test] + // public async Task FindByUnit_ExistingUnit_ReturnsDischarges() + // { + // // Arrange: Prepare test data + // var unitName = "TestUnit"; + // var expectedDischarges = + // new Discharge + // { Id = ObjectId.GenerateNewId(), PatientLocation = new PatientLocation { UnitName = unitName } }; + // await _dischargeRepository.InsertOneAsync(expectedDischarges); + // + // // Act: Call the method under test + // var actualDischarges = await _dischargeRepository.FindByUnit(unitName); + // + // // Assert: Verify the result + // Assert.That(actualDischarges, Is.Not.Null); + // Assert.That(actualDischarges?.ToList().First().PatientLocation?.UnitName, Is.EquivalentTo(expectedDischarges.PatientLocation.UnitName)); + // } + + [Test] + public async Task FindByDestination_ExistingDestination_ReturnsDischarges() + { + // Arrange: Prepare test data + var destination = "TestDestination"; + var expectedDischarges = + new Discharge + { Id = ObjectId.GenerateNewId(), Destination = destination }; + await _dischargeRepository.InsertOneAsync(expectedDischarges); + + // Act: Call the method under test + var actualDischarges = await _dischargeRepository.FindByDestination(destination); + + // Assert: Verify the result + var discharges = actualDischarges?.ToList() ?? []; + Assert.That(discharges, Is.Not.Null); + Assert.That(discharges.First().Id, Is.EqualTo(expectedDischarges.Id)); + } + + [Test] + public async Task FindByDestination_NonExistentDestination_ReturnsEmptyList() + { + // Arrange: Use a non-existent destination + var destination = "NonExistentDestination"; + + // Act: Call the method under test + var actualDischarges = await _dischargeRepository.FindByDestination(destination); + + // Assert: Verify that the result is an empty list + Assert.That(actualDischarges, Is.Empty); + } + + [Test] + public async Task FindByPoCId_ExistingPoCId_ReturnsDischarges() + { + // Arrange: Prepare test data + var pocId = ObjectId.GenerateNewId(); + var expectedDischarges = + new Discharge + { Id = ObjectId.GenerateNewId(), PointOfCareId = pocId }; + await _dischargeRepository.InsertOneAsync(expectedDischarges); + + // Act: Call the method under test + var actualDischarges = await _dischargeRepository.FindByPoCId(pocId); + + // Assert: Verify the result + var discharges = actualDischarges?.ToList() ?? []; + Assert.That(discharges, Is.Not.Null); + Assert.That(discharges.First().Id, Is.EqualTo(expectedDischarges.Id)); + } + + [Test] + public async Task FindByPoCId_NonExistentPoCId_ReturnsEmptyList() + { + // Arrange: Use a non-existent PoCId + var pocId = ObjectId.GenerateNewId(); + + // Act: Call the method under test + var actualDischarges = await _dischargeRepository.FindByPoCId(pocId); + + // Assert: Verify that the result is an empty list + Assert.That(actualDischarges, Is.Empty); + } + + [Test] + public async Task FindByService_ExistingService_ReturnsDischarges() + { + // Arrange: Prepare test data + var service = "TestService"; + var expectedDischarges = + new Discharge + { Id = ObjectId.GenerateNewId(), Service = service }; + await _dischargeRepository.InsertOneAsync(expectedDischarges); + + // Act: Call the method under test + var actualDischarges = await _dischargeRepository.FindByService(service); + + // Assert: Verify the result + var discharges = actualDischarges?.ToList() ?? []; + Assert.That(discharges, Is.Not.Null); + Assert.That(discharges.First().Id, Is.EqualTo(expectedDischarges.Id)); + } + + [Test] + public async Task FindByService_NonExistentService_ReturnsEmptyList() + { + // Arrange: Use a non-existent service + var service = "NonExistentService"; + + // Act: Call the method under test + var actualDischarges = await _dischargeRepository.FindByService(service); + + // Assert: Verify that the result is an empty list + Assert.That(actualDischarges, Is.Empty); + } + + // [Test] + // public async Task GetDischargeByLocation_ExistingLocation_ReturnsDischarge() + // { + // // Arrange: Prepare test data + // var location = new PatientLocation("TestUnit", "TestBed", "TestRoom"); + // var expectedDischarges = + // new Discharge + // { Id = ObjectId.GenerateNewId(), PatientLocation = location}; + // await _dischargeRepository.InsertOneAsync(expectedDischarges); + // + // // Act: Call the method under test + // var actualDischarge = await _dischargeRepository.GetDischargeByLocation(location); + // + // // Assert: Verify the result + // Assert.That(actualDischarge, Is.Not.Null); + // Assert.That(actualDischarge?.Id, Is.EqualTo(expectedDischarges.Id)); + // } + + [Test] + public async Task GetDischargeByLocation_NonExistentLocation_ReturnsNull() + { + // Arrange: Use a non-existent location + var location = new PatientLocation("NonExistentUnit", "NonExistentBed", "NonExistentRoom"); + + // Act: Call the method under test + var actualDischarge = await _dischargeRepository.GetDischargeByLocation(location); + + // Assert: Verify that the result is null + Assert.That(actualDischarge, Is.Null); + } + + [Test] + public async Task GetDischargeByPointOfCareId_ExistingId_ReturnsDischarge() + { + // Arrange: Prepare test data + var expectedId = ObjectId.GenerateNewId(); + var expectedDischarge = + new Discharge + { Id = ObjectId.GenerateNewId(), PointOfCareId = expectedId }; + await _dischargeRepository.InsertOneAsync(expectedDischarge); + + // Act: Call the method under test + var actualDischarge = await _dischargeRepository.GetDischargeByPointOfCareId(expectedId); + + // Assert: Verify the result + Assert.That(actualDischarge, Is.Not.Null); + Assert.That(actualDischarge?.Id, Is.EqualTo(expectedDischarge.Id)); + } + + [Test] + public async Task GetDischargeByPointOfCareId_NonExistentId_ReturnsNull() + { + // Arrange: Use a non-existent ID + var nonExistentId = ObjectId.GenerateNewId(); + + // Act: Call the method under test + var actualDischarge = await _dischargeRepository.GetDischargeByPointOfCareId(nonExistentId); + + // Assert: Verify that the result is null + Assert.That(actualDischarge, Is.Null); + } + + [Test] + public async Task GetByPatientId_ExistingPatientId_ReturnsDischarge() + { + // Arrange: Prepare test data + var expectedId = ObjectId.GenerateNewId(); + var expectedDischarge = + new Discharge + { Id = ObjectId.GenerateNewId(), PatientId = expectedId }; + await _dischargeRepository.InsertOneAsync(expectedDischarge); + + // Act: Call the method under test + var actualDischarge = await _dischargeRepository.GetByPatientId(expectedId); + + // Assert: Verify the result + Assert.That(actualDischarge, Is.Not.Null); + Assert.That(actualDischarge?.Id, Is.EqualTo(expectedDischarge.Id)); + } + + [Test] + public async Task GetByPatientId_NonExistentPatientId_ReturnsNull() + { + // Arrange: Use a non-existent patient ID + var nonExistentId = ObjectId.GenerateNewId(); + + // Act: Call the method under test + var actualDischarge = await _dischargeRepository.GetByPatientId(nonExistentId); + + // Assert: Verify that the result is null + Assert.That(actualDischarge, Is.Null); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/DisplayConfigTest.cs b/adas-core.Test/Repositories/DisplayConfigTest.cs new file mode 100644 index 00000000..76a406d2 --- /dev/null +++ b/adas-core.Test/Repositories/DisplayConfigTest.cs @@ -0,0 +1,79 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Infrastructure.Repositories; +using adas_core.Test.Utilities; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Moq; + +namespace adas_core.Test.Repositories; + +[TestFixture] +public class DisplayConfigTest +{ + [OneTimeSetUp] + public async Task SetUp() + { + Options.Create(new ApiSettings()); + _unitRepository = new Mock(); + _displayCardConfigRepository = + new DisplayCardConfigRepository( + IntegrationDb.Database, + new ApiSettings(), + new Mock>().Object + ); + _displayConfigRepository = + new DisplayConfigRepository( + IntegrationDb.Database, + new ApiSettings(), + new Mock>().Object, + _unitRepository.Object + ); + _displayDetailConfigRepository = + new DisplayDetailConfigRepository( + IntegrationDb.Database, + new ApiSettings(), + new Mock>().Object + ); + + await _displayDetailConfigRepository.InsertOneAsync( + TestUtilities.CreateValidCardDetailsNurseConfig(_configDisplayCardDetailsId)); + await _displayCardConfigRepository.InsertOneAsync(TestUtilities.CreateValidCardConfig(_configDisplayCardId)); + await _displayConfigRepository.InsertOneAsync(TestUtilities.CreateValidDisplayConfig( + DisplayConfigEnums.DisplayType.DisplayNurse, _configDisplayId, _configDisplayCardId, + _configDisplayCardDetailsId)); + } + + [OneTimeTearDown] + public async Task Cleanup() + { + // Clean up database after tests + await IntegrationDb.Database.DropCollectionAsync("config_displays"); + await IntegrationDb.Database.DropCollectionAsync("config_displays_card"); + await IntegrationDb.Database.DropCollectionAsync("config_displays_detail"); + } + + private DisplayConfigRepository _displayConfigRepository; + private DisplayCardConfigRepository _displayCardConfigRepository; + private DisplayDetailConfigRepository _displayDetailConfigRepository; + private Mock _unitRepository; + private readonly ObjectId _configDisplayId = ObjectId.GenerateNewId(); + private readonly ObjectId _configDisplayCardId = ObjectId.GenerateNewId(); + private readonly ObjectId _configDisplayCardDetailsId = ObjectId.GenerateNewId(); + + [Test] + public async Task GetByIdShouldReturnAllConfigsAssociated() + { + var result = await _displayConfigRepository.GetById(_configDisplayId); + Assert.That(result, Is.Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(result.CardConfigId, Is.Not.Null); + Assert.That(result.CardConfig, Is.Not.Null); + Assert.That(result.DetailConfigId, Is.Not.Null); + Assert.That(result.DetailConfig, Is.Not.Null); + }; + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/DisplayRepisitoryTest.cs b/adas-core.Test/Repositories/DisplayRepisitoryTest.cs new file mode 100644 index 00000000..3ce41e34 --- /dev/null +++ b/adas-core.Test/Repositories/DisplayRepisitoryTest.cs @@ -0,0 +1,18 @@ +namespace adas_core.Test.Repositories; + +[TestFixture] +public class DisplayRepisitoryTest +{ + //private Mock> _mockLogger ; + //private Mock> _mockOptionsApiSettings ; + + [OneTimeSetUp] + public async Task OneTimeSetUp() + { + //_mockLogger = new Mock>(); + //_mockOptionsApiSettings = new Mock>(); + + await IntegrationDb.Database.DropCollectionAsync("displays"); + await IntegrationDb.Database.CreateCollectionAsync("displays"); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/HistoricalConfigChangesRepositoryTest.cs b/adas-core.Test/Repositories/HistoricalConfigChangesRepositoryTest.cs new file mode 100644 index 00000000..b886b309 --- /dev/null +++ b/adas-core.Test/Repositories/HistoricalConfigChangesRepositoryTest.cs @@ -0,0 +1,212 @@ +using System.Diagnostics; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Moq; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class HistoricalConfigChangesRepositoryTest +{ + [OneTimeSetUp] + public void Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + _logger = new Mock>(); + + + _repository = + new HistoricalConfigChangesRepository(_optionsApiSettings, IntegrationDb.Database, _logger.Object); + } + + [TearDown] + public async Task Cleanup() + { + await IntegrationDb.Database.DropCollectionAsync("historicalConfigChanges"); + await IntegrationDb.Database.CreateCollectionAsync("historicalConfigChanges"); + + await InitializeData(); + } + + private HistoricalConfigChangesRepository _repository; + + private readonly ApiSettings _apiSettings = new() + { + HistoricalConfigChanges = "historicalConfigChanges" + }; + + private IOptions _optionsApiSettings; + + private Mock> _logger; + + private async Task InitializeData() + { + var id = ObjectId.GenerateNewId(); + HistoricalConfigChanges historicalConfigChanges1 = new() + { + //Id = ObjectId.GenerateNewId(), + ConfigType = DisplayConfigEnums.ConfigTypes.ObservationCfg, + Time = DateTime.UtcNow, + Username = "adas", + OldConfig = new ConfigObservation + { + Id = id, + Name = "Hemoglobina", + Code = "54321", + CodingSystem = "SNM" + }.ToJson(), + NewConfig = new ConfigObservation + { + Id = id, + Name = "Hemoglobina", + Code = "xxxxx", + CodingSystem = "SNM" + }.ToJson() + }; + + await _repository.InsertOneAsync(historicalConfigChanges1); + + HistoricalConfigChanges historicalConfigChanges2 = new() + { + //Id = ObjectId.GenerateNewId(), + ConfigType = DisplayConfigEnums.ConfigTypes.ObservationCfg, + Time = DateTime.UtcNow, + Username = "adas", + OldConfig = new ConfigObservation + { + Id = id, + Name = "Hemoglobina", + Code = "xxxxx", + CodingSystem = "SNM" + }.ToJson(), + NewConfig = new ConfigObservation + { + Id = id, + Name = "Hemoglobina", + Code = "11111", + CodingSystem = "SNM" + }.ToJson() + }; + + await _repository.InsertOneAsync(historicalConfigChanges2); + + HistoricalConfigChanges historicalConfigChanges3 = new() + { + //Id = ObjectId.GenerateNewId(), + ConfigType = DisplayConfigEnums.ConfigTypes.ObservationCfg, + Time = DateTime.UtcNow, + Username = "adas", + OldConfig = new ConfigObservation + { + Id = id, + Name = "Hemoglobina", + Code = "111111", + CodingSystem = "SNM" + }.ToJson(), + NewConfig = new ConfigObservation + { + Id = id, + Name = "Hemoglobina", + Code = "99999", + CodingSystem = "SNM" + }.ToJson() + }; + + await _repository.InsertOneAsync(historicalConfigChanges3); + } + + + [Test] + public async Task Get_HistoricalConfigChanges_returns_count_3() + { + await Cleanup(); + + var result = await _repository.FindAll(); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Count.EqualTo(3)); + } + + + [Test] + public async Task HistoricalConfigChanges_returns_diferent_document() + { + await Cleanup(); + var id = ObjectId.GenerateNewId(); + var resultList = await _repository.FindAll(); + var result = resultList.FirstOrDefault(); + + Assert.That(result, Is.Not.Null); + + Debug.Assert(result != null, nameof(result) + " != null"); + HistoricalConfigChanges updatedHistoricalConfigChanges = new() + { + Id = result.Id, + ConfigType = DisplayConfigEnums.ConfigTypes.ObservationCfg, + Time = DateTime.UtcNow, + Username = "admin", + OldConfig = new ConfigObservation + { + Id = id, + Name = "Hemoglobina", + Code = "12345", + CodingSystem = "SNM" + }.ToJson(), + NewConfig = new ConfigObservation + { + Id = id, + Name = "Hemoglobina", + Code = "54321", + CodingSystem = "SNM" + }.ToJson() + }; + + + var updated = await _repository.Update(updatedHistoricalConfigChanges); + using (Assert.EnterMultipleScope()) + { + Assert.That(updated, Is.Not.Null); + Assert.That(result.Time, Is.EqualTo(updated?.Time)); + Assert.That(result.Id, Is.EqualTo(updated?.Id)); + Assert.That(result.Username, Is.EqualTo(updated?.Username)); + Assert.That(result.ConfigType, Is.EqualTo(updated?.ConfigType)); + Assert.That(result.OldConfig, Is.EqualTo(updated?.OldConfig)); + Assert.That(result.NewConfig, Is.EqualTo(updated?.NewConfig)); + }; + } + + [Test] + public async Task FindLastHistoricalConfigChangesByUser_returns_3() + { + await Cleanup(); + var result = await _repository.FindLastHistoricalConfigChangesByUser("adas"); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Count.EqualTo(3)); + } + + [Test] + public async Task Delete_HistoricalConfigChanges_returns_count_2() + { + await Cleanup(); + var resultList = await _repository.FindAll(); + Assert.That(resultList, Has.Count.EqualTo(3)); + + var result = resultList.FirstOrDefault(); + Assert.That(result, Is.Not.Null); + Debug.Assert(result != null, nameof(result) + " != null"); + var deleted = await _repository.DeleteAsync(result.Id); + + Assert.That(deleted, Is.Not.Null); + + var resultAfterDelete = await _repository.FindAll(); + Assert.That(resultAfterDelete, Has.Count.EqualTo(2)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/IntegrationDb.cs b/adas-core.Test/Repositories/IntegrationDb.cs new file mode 100644 index 00000000..22c8585f --- /dev/null +++ b/adas-core.Test/Repositories/IntegrationDb.cs @@ -0,0 +1,64 @@ +using adas_core.Infrastructure.Utils; +using Mongo2Go; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Test.Repositories; + +[SetUpFixture] +[Category("Integration")] +public class IntegrationDb +{ + private const int TimeoutInSeconds = 60; // Set the desired timeout in seconds. + public static MongoDbRunner? Runner { get; private set; } + + private static MongoClient? Client { get; set; } + + public static IMongoDatabase Database { get; private set; } = null!; + public static string DatabaseName { get; } = "IntegrationTestDb"; + + [OneTimeSetUp] + public void InitIntegrationTests() + { + StartMongoDbRunner().Wait(); + MongoDbHostBuilderExtension.ConfigureMongoDbConventions(); + MongoDbHostBuilderExtension.ConfigureRegisterMapClass(); + Client = new MongoClient(Runner?.ConnectionString); + Database = Client.GetDatabase(DatabaseName); + } + + private static async Task StartMongoDbRunner() + { + Runner = MongoDbRunner.Start(); + + // Wait for the MongoDB server to become available or timeout. + var startTime = DateTime.UtcNow; + while (DateTime.UtcNow - startTime < TimeSpan.FromSeconds(TimeoutInSeconds)) + try + { + var testClient = new MongoClient(Runner.ConnectionString); + var adminDb = testClient.GetDatabase("admin"); + await adminDb.RunCommandAsync((Command)"{ping:1}"); + return; // MongoDB server is available, continue. + } + catch + { + // Retry after a short delay. + await Task.Delay(500); + } + + // Timeout reached, dispose the runner and throw an exception. + Runner.Dispose(); + throw new TimeoutException("Timeout while starting MongoDB server."); + } + + [OneTimeTearDown] + public void TeardownIntegrationTests() + { + Client?.Dispose(); + Runner?.Dispose(); + //_runner = null; + //_client = null; + //_fakeDb = null; + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/MasterListRepositoryTest.cs b/adas-core.Test/Repositories/MasterListRepositoryTest.cs new file mode 100644 index 00000000..6b5bc0e0 --- /dev/null +++ b/adas-core.Test/Repositories/MasterListRepositoryTest.cs @@ -0,0 +1,374 @@ +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.Masters; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; + +namespace adas_core.Test.Repositories; + +[TestFixture] +public class MasterListRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + // Set up MongoDB connection + // var client = new MongoClient("mongodb://localhost:27017"); + // _database = client.GetDatabase("TestDatabase"); + + // Initialize ApiSettings + _apiSettings = new ApiSettings + { + AltableOptionList = "altableOptionList", + AllergyList = "allergyList", + DestinationList = "destinationList", + DiagnosisList = "diagnosisList", + DischargeStatusList = "dischargeStatusList", + DoctorList = "doctorList", + DoctorTypeList = "doctorTypeList", + InternalDestinationList = "internalDestinationList", + InsulationList = "insulationList", + LanguageBarrierList = "languageBarrierList", + MobilityOptionList = "mobilityOptionList", + OriginList = "originList", + PatientStatusList = "patientStatusList", + ProcedureList = "procedureList", + TestList = "testList", + ServiceList = "serviceList", + TherapeuticCeilingList = "therapeuticCeilingList", + TreatmentList = "treatmentList", + VisitOptionList = "visitOptionList", + AccessControlList = "accessControlList" + }; + _optionsApiSettings = Options.Create(_apiSettings); + + + // Insert sample data + await IntegrationDb.Database.DropCollectionAsync("MasterLists"); + await IntegrationDb.Database.CreateCollectionAsync("MasterLists"); + + // Create the repository instance + _repository = new MasterListRepository(_optionsApiSettings, IntegrationDb.Database); + + // var masterListCollection = _database.GetCollection("MasterLists"); + // var sampleData = new List + // { + // new MasterList { Id = ObjectId.GenerateNewId(), Name = "Sample MasterList 1" }, + // new MasterList { Id = ObjectId.GenerateNewId(), Name = "Sample MasterList 2" } + // }; + // await masterListCollection.InsertManyAsync(sampleData); + } + + [OneTimeTearDown] + public async Task Cleanup() + { + // Clean up database after tests + await IntegrationDb.Database.DropCollectionAsync("MasterLists"); + } + + private MasterListRepository _repository; + private ApiSettings _apiSettings; + private IOptions _optionsApiSettings; + + [Test] + public async Task InsertOneAsync_ShouldInsertEntity() + { + // Arrange + var masterList = new MasterList { Id = ObjectId.GenerateNewId(), Name = "Test MasterList" }; + + // Act + await _repository.InsertOneAsync(masterList); + await _repository.GetAll(); + var result = await _repository.FindById(masterList.Id, LocaleEnum.Default); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result?.Name, Is.EqualTo("Test MasterList")); + } + + [Test] + public async Task Delete_ShouldDeleteEntity() + { + // Arrange + var masterList = new MasterList { Id = ObjectId.GenerateNewId(), Name = "ToDelete MasterList" }; + await _repository.InsertOneAsync(masterList); + + // Act + await _repository.Delete(masterList.Id); + var result = await _repository.FindById(masterList.Id, LocaleEnum.Default); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public async Task Update_ShouldUpdateEntity() + { + // Arrange + var masterList = new MasterList { Id = ObjectId.GenerateNewId(), Name = "Original MasterList" }; + await _repository.InsertOneAsync(masterList); + masterList.Name = "Updated MasterList"; + + // Act + await _repository.Update(masterList); + var result = await _repository.FindById(masterList.Id, LocaleEnum.Default); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result?.Name, Is.EqualTo("Updated MasterList")); + } + + [Test] + public async Task FindById_ShouldReturnEntity_WhenFound() + { + // Arrange + var masterList = new MasterList { Id = ObjectId.GenerateNewId(), Name = "FindById MasterList" }; + await _repository.InsertOneAsync(masterList); + + // Act + var result = await _repository.FindById(masterList.Id, LocaleEnum.Default); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result?.Name, Is.EqualTo("FindById MasterList")); + } + + [Test] + public async Task FindById_WithDifferentLocale_ShouldReturnEntity_WhenFound() + { + // Arrange + var masterList = new MasterList + { + Id = ObjectId.GenerateNewId(), Name = "FindById MasterList", + DefaultLocale = LocaleEnum.Es, + Options = new List + { + new() + { + Id = ObjectId.GenerateNewId(), + Name = "Opción 1", + Description = "Descripción 1", + LocaleItems = new Locale + { + Eng = new LocaleItem { Name = "Option eng" }, + Ca = new LocaleItem { Name = "Opciò cat" }, + Pt = new LocaleItem { Name = "Opçao por" } + } + } + } + }; + await _repository.InsertOneAsync(masterList); + + // Act + var resultZh = await _repository.FindById(masterList.Id, LocaleEnum.Zh); + var resultPt = await _repository.FindById(masterList.Id, LocaleEnum.Pt); + var resultEnUs = await _repository.FindById(masterList.Id, LocaleEnum.Eng); + var resultCa = await _repository.FindById(masterList.Id, LocaleEnum.Ca); + var resultEs = await _repository.FindById(masterList.Id, LocaleEnum.Es); + + // Assert ZH return default + Assert.That(resultZh, Is.Not.Null); + Assert.That(resultZh?.Options[0].Name, Is.EqualTo("Opción 1")); + Assert.That(resultZh?.Options[0].LocaleItems, Is.Null); + // Assert Pt + Assert.That(resultPt, Is.Not.Null); + Assert.That(resultPt?.Options[0].Name, Is.EqualTo("Opçao por")); + Assert.That(resultPt?.Options[0].LocaleItems, Is.Null); + // Assert Eng + Assert.That(resultEnUs, Is.Not.Null); + Assert.That(resultEnUs?.Options[0].Name, Is.EqualTo("Option eng")); + Assert.That(resultEnUs?.Options[0].LocaleItems, Is.Null); + // Assert resultCa + Assert.That(resultCa, Is.Not.Null); + Assert.That(resultCa?.Options[0].Name, Is.EqualTo("Opciò cat")); + Assert.That(resultCa?.Options[0].LocaleItems, Is.Null); + // Assert resultEs + Assert.That(resultEs, Is.Not.Null); + Assert.That(resultEs?.Options[0].Name, Is.EqualTo("Opción 1")); + Assert.That(resultEs?.Options[0].LocaleItems, Is.Null); + } + + [Test] + public async Task FindById_WithDifferentLocale_ShouldReturnEntity_WhenFound_AndDefaultLocale() + { + // Arrange + var masterList = new MasterList + { + Id = ObjectId.GenerateNewId(), Name = "FindById MasterList", + DefaultLocale = LocaleEnum.Es, + Options = new List + { + new() + { + Id = ObjectId.GenerateNewId(), + Name = "Opción 1", + Description = "Descripción 1" + } + } + }; + await _repository.InsertOneAsync(masterList); + + // Act + var resultZh = await _repository.FindById(masterList.Id, LocaleEnum.Zh); + var resultPt = await _repository.FindById(masterList.Id, LocaleEnum.Pt); + var resultEnUs = await _repository.FindById(masterList.Id, LocaleEnum.Eng); + var resultCa = await _repository.FindById(masterList.Id, LocaleEnum.Ca); + var resultEs = await _repository.FindById(masterList.Id, LocaleEnum.Es); + + // Assert ZH return default + Assert.That(resultZh, Is.Not.Null); + Assert.That(resultZh?.Options[0].Name, Is.EqualTo("Opción 1")); + // Assert Pt + Assert.That(resultPt, Is.Not.Null); + Assert.That(resultPt?.Options[0].Name, Is.EqualTo("Opción 1")); + // Assert Eng + Assert.That(resultEnUs, Is.Not.Null); + Assert.That(resultEnUs?.Options[0].Name, Is.EqualTo("Opción 1")); + // Assert resultCa + Assert.That(resultCa, Is.Not.Null); + Assert.That(resultCa?.Options[0].Name, Is.EqualTo("Opción 1")); + // Assert resultEs + Assert.That(resultEs, Is.Not.Null); + Assert.That(resultEs?.Options[0].Name, Is.EqualTo("Opción 1")); + } + + [Test] + public async Task FindByName_ShouldReturnEntity_WhenFound() + { + // Arrange + var masterList = new MasterList { Id = ObjectId.GenerateNewId(), Name = "FindByName MasterList" }; + await _repository.InsertOneAsync(masterList); + + // Act + var result = await _repository.FindByName(masterList.Name); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result?.Name, Is.EqualTo("FindByName MasterList")); + } + + [Test] + public async Task GetAll_ShouldReturnAllEntities() + { + // Act + var result = await _repository.GetAll(); + + // Assert + Assert.That(result.Count(), Is.GreaterThanOrEqualTo(2)); + } + + [Test] + public async Task GetMasterListByIdAndSearchOptiionsByLocale() + { + // Arrange + var masterList = new MasterList + { + Id = ObjectId.GenerateNewId(), Name = "FindById MasterList", + DefaultLocale = LocaleEnum.Es, + Options = new List + { + new() + { + Id = ObjectId.GenerateNewId(), + Name = "Opción 1", + Description = "AC0.01", + LocaleItems = new Locale + { + Eng = new LocaleItem { Name = "Option eng" }, + Ca = new LocaleItem { Name = "Opciò cat" }, + Pt = new LocaleItem { Name = "Opçao por" } + } + } + } + }; + await _repository.InsertOneAsync(masterList); + var resultPt = await _repository.GetMasterListByIdAndSearchOptions(masterList.Id, + new FilterOptionListElement { Locale = LocaleEnum.Pt, Text = "Op" }); + Assert.That(resultPt, Is.Not.Null); + Assert.That(resultPt[0].Name, Is.EqualTo("Opçao por")); + var resultEng = await _repository.GetMasterListByIdAndSearchOptions(masterList.Id, + new FilterOptionListElement { Locale = LocaleEnum.Eng, Description = "AC0.01" }); + Assert.That(resultEng, Is.Not.Null); + Assert.That(resultEng[0].Name, Is.EqualTo("Option eng")); + var resultEs = await _repository.GetMasterListByIdAndSearchOptions(masterList.Id, + new FilterOptionListElement { Locale = LocaleEnum.Es, Text = "Op" }); + Assert.That(resultEs, Is.Not.Null); + Assert.That(resultEs[0].Name, Is.EqualTo("Opción 1")); + } + + [Test] + public async Task UpdateMasterListOptionByLocale() + { + // Arrange + var listId = ObjectId.GenerateNewId(); + var optionId = ObjectId.GenerateNewId(); + var masterList = new MasterList + { + Id = listId, Name = "FindById MasterList", + DefaultLocale = LocaleEnum.Es, + OptionListDetails = new OptionListDetails { Name = new Element { IsRequired = true } }, + Options = new List + { + new() + { + Id = optionId, + Name = "Opción 1", + Description = "AC0.01", + LocaleItems = new Locale + { + Eng = new LocaleItem { Name = "Option eng" }, + Ca = new LocaleItem { Name = "Opciò cat" }, + Pt = new LocaleItem { Name = "Opçao por" } + } + } + } + }; + await _repository.InsertOneAsync(masterList); + var optionPt = new OptionList + { + Id = optionId, + Name = "Opçao çambiada" + }; + var resultPt = await _repository.UpdateMasterListOption(masterList.Id, optionPt, LocaleEnum.Pt); + Assert.That(resultPt, Is.Not.Null); + Assert.That(resultPt.Name, Is.EqualTo("Opçao çambiada")); + var realResult = await _repository.FindById(listId); + Assert.That(realResult?.Options[0].Name, Is.EqualTo("Opción 1")); + await _repository.UpdateMasterListOption(masterList.Id, optionPt, LocaleEnum.Pt); + // _pointOfCareRepositoryMock.Verify(m => m.FindById(patientLocationId), Times.Once); + } + + [Test] + public async Task FindOptionItemByLocaleAndId() + { + var masterId = ObjectId.GenerateNewId(); + var optionId = ObjectId.GenerateNewId(); + var masterList = new MasterList + { + Id = masterId, Name = "FindById MasterList", + DefaultLocale = LocaleEnum.Es, + Options = new List + { + new() + { + Id = optionId, + Name = "Opción 1", + Description = "AC0.01", + LocaleItems = new Locale + { + Eng = new LocaleItem { Name = "Option eng" }, + Ca = new LocaleItem { Name = "Opciò cat" }, + Pt = new LocaleItem { Name = "Opçao por" } + } + } + } + }; + await _repository.InsertOneAsync(masterList); + var result = await _repository.FindOptionItemById(masterId, optionId, LocaleEnum.Pt); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Id!, Is.EqualTo(optionId)); + Assert.That(result.Name, Is.EqualTo("Opçao por")); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/MedicineRepositoryTest.cs b/adas-core.Test/Repositories/MedicineRepositoryTest.cs new file mode 100644 index 00000000..240676c1 --- /dev/null +++ b/adas-core.Test/Repositories/MedicineRepositoryTest.cs @@ -0,0 +1,132 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class MedicineRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + _testMedicine = new Medicine + { + Id = ObjectId.GenerateNewId(), + Codes = ["12345"], + Notes = ["note1"], + Name = "test", + Group = [MedicineEnum.Group.Metabolic.ToString()] + }; + + await IntegrationDb.Database.DropCollectionAsync("medicines"); + await IntegrationDb.Database.CreateCollectionAsync("medicines"); + + _repository = new MedicineRepository(_optionsApiSettings, IntegrationDb.Database); + + await _repository.InsertOneAsync(_testMedicine); + + var result = _repository.GetMedicineByCodeOrNote(["12345"]); + + Assert.That(result, Is.Not.Null); + } + + private MedicineRepository _repository = null!; + + private readonly ApiSettings _apiSettings = new() + { + Medicines = "medicines" + }; + + private Medicine _testMedicine = null!; + + private IOptions _optionsApiSettings = null!; + + [Test] + public async Task Get_Medicines_Of_Treatments_Return_Empty_List() + { + var result = await _repository.GetMedicineByCodeOrNote([""]); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.Empty); + } + + [Test] + public async Task Get_Medicines_Of_Treatments_By_Code_When_Have_Code_And_Notes_Return_Paracetamol() + { + var id = ObjectId.GenerateNewId(); + var paracetamolMedicine = new Medicine + { + Id = id, + Codes = ["322236009", "322246006", "370151002", "605677", "605779"], + Type = ["ParacetamolOpiates"], + Name = "Paracetamol" + }; + + await _repository.InsertOneAsync(paracetamolMedicine); + + var result = await _repository.GetMedicineByCodeOrNote([ + "Medicación", "Formulary Drug Intake", "1.12 kg", "605677" + ]); + using (Assert.EnterMultipleScope()) + { + Assert.That(result, Is.Not.Null); + Assert.That(id, Is.EqualTo(result.First().Id)); + }; + Assert.That(result, Has.Count.EqualTo(1)); + } + + [Test] + public async Task GetMedicine_Find_Code_Return_Medicine() + { + var result = await _repository.GetMedicine("12345"); + + Assert.That(result, Is.Not.Null); + } + + [Test] + public async Task GetMedicine_Not_Find_Code_Return_Empty() + { + var result = await _repository.GetMedicine("123456"); + + Assert.That(result, Is.Null); + } + + [Test] + public async Task GetMedicine_Find_Note_Return_Medicine() + { + var result = await _repository.GetMedicine("note1"); + + Assert.That(result, Is.Not.Null); + } + + [Test] + public async Task UpdateMedicine_Return_Medicine() + { + var updateMedicine = new Medicine + { + Id = _testMedicine.Id, + Codes = ["12345"], + Notes = ["note1", "note2", "note3"], + Name = "test", + Group = [MedicineEnum.Group.Metabolic.ToString()] + }; + + var result = await _repository.UpdateMedicine(updateMedicine); + var resultUpdate = await _repository.GetMedicineById(_testMedicine.Id); + using (Assert.EnterMultipleScope()) + { + + Assert.That(result, Is.Not.Null); + Assert.That(resultUpdate, Is.Not.Null); + Assert.That(resultUpdate?.Notes != null && resultUpdate.Notes.Contains("note3"), Is.True); + }; + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/MongodbMigrationTest.cs b/adas-core.Test/Repositories/MongodbMigrationTest.cs new file mode 100644 index 00000000..66a95f0e --- /dev/null +++ b/adas-core.Test/Repositories/MongodbMigrationTest.cs @@ -0,0 +1,156 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Migrations.MongoMigrations; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using MongoMigrations.Core; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class MongodbMigrationTest +{ + private PatientRepository _repository; + private IOptions _optionsApiSettings; + + private readonly ApiSettings _apiSettings = new() + { + ArchivePatientsDiagnosis = "patients" + }; + + private static readonly DateTime Now = DateTime.Now; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + await IntegrationDb.Database.DropCollectionAsync("patients"); + await IntegrationDb.Database.CreateCollectionAsync("patients"); + + _repository = new PatientRepository(_optionsApiSettings, IntegrationDb.Database); + + var patients = new List + { + new () + { + Id = PatientId, + PatientId = "patientId1", + PointOfCareId = ObjectId.GenerateNewId(), + Bed = "bed1", + PatientNumber = "patientNumber1", + Person = new Person + { + FirstName = "firstName1", + LastName = "lastName1", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + }, + DisTime = Now + }, + new () + { + PatientId = "patientId2", + PointOfCareId = ObjectId.GenerateNewId(), + Bed = "bed2", + PatientNumber = "patientNumber2", + Person = new Person + { + FirstName = "firstName2", + LastName = "lastName2", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }, + new () + { + PatientId = "patientId3", + PointOfCareId = ObjectId.GenerateNewId(), + Bed = "bed3", + PatientNumber = "patientNumber3", + Person = new Person + { + FirstName = "firstName3", + LastName = "lastName3", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + } + }; + + await _repository.InsertManyAsync(patients); + Assert.That(await _repository.Collection.CountDocumentsAsync(_ => true), Is.EqualTo(3)); + } + + [OneTimeTearDown] + public void Teardown() + { + IntegrationDb.Database.DropCollection("patients"); + IntegrationDb.Database.DropCollection("__migrations"); + } + + [Test] + [Order(1)] + public void IsDatabaseOutdated_ShouldReturnTrue() + { + var runner = CreateRunner(); + + var isUpToDate = runner.IsDatabaseUpToDate(ReadPreference.Primary); + + Assert.That(isUpToDate, Is.False); + } + + + [Test] + [Order(2)] + public void AfterUpdateToLatest_MigrationShouldBeApplied() + { + var runner = CreateRunner(); + + runner.UpdateToLatest(); + var ids = GetAppliedMigrationIds(); + Assert.That(ids, Does.Contain(10)); // 0.1.0 + } + + [Test] + public void RunningMigrationsTwice_ShouldNotFail() + { + var runner = CreateRunner(); + + runner.UpdateToLatest(); + runner.UpdateToLatest(); // segunda ejecución + + var ids = GetAppliedMigrationIds(); + Assert.That(ids, Does.Contain(10)); + } + + // Helpers + private MigrationRunner CreateRunner() + { + var locator = new MigrationLocator(); + locator.LookForMigrationsInAssembly(typeof(U_0_1_0_UpdateDataPatien).Assembly); + return new MigrationRunner( + IntegrationDb.Database, + collectionName: "__migrations", + migrationLocator: locator + ); + } + + private List GetAppliedMigrationIds() + { + var collection = IntegrationDb.Database.GetCollection("__migrations"); + + return collection + .Find(FilterDefinition.Empty) + .ToList() + .Select(d => d["_id"].AsInt32) + .ToList(); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/ObservationArchiveRepositoryTest.cs b/adas-core.Test/Repositories/ObservationArchiveRepositoryTest.cs new file mode 100644 index 00000000..056b2659 --- /dev/null +++ b/adas-core.Test/Repositories/ObservationArchiveRepositoryTest.cs @@ -0,0 +1,82 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class ObservationArchiveRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + var patientObservation1 = new PatientObservation + { + PatientId = PatientId, + Code = "263490005", + CodingSystem = "SNM", + Name = "Estado", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "Sin alergias conocidas" + }; + + var patientObservation2 = new PatientObservation + { + PatientId = PatientId, + Code = "300916003", + CodingSystem = "SNM", + Name = "¿Alergia al látex?", + Status = StatusEnum.Type.Ok, + Time = Now.AddDays(-1), + Value = "No" + }; + + await IntegrationDb.Database.DropCollectionAsync("archive_patients_observations"); + await IntegrationDb.Database.CreateCollectionAsync("archive_patients_observations"); + + _repository = new ObservationArchiveRepository(_optionsApiSettings, IntegrationDb.Database); + + await _repository.InsertOneAsync(patientObservation1); + await _repository.InsertOneAsync(patientObservation2); + } + + private ObservationArchiveRepository _repository; + + private readonly ApiSettings _apiSettings = new() + { + ArchivePatientsDiagnosis = "archive_patients_observations" + }; + + private IOptions _optionsApiSettings; + + private static readonly DateTime Now = DateTime.Now; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + + + [Test] + public async Task DeleteBeforeDate() + { + var filter = Builders.Filter.Eq(p => p.PatientId, PatientId); + + var result = await _repository.Collection.FindAsync(filter); + + Assert.That(result, Is.Not.Null); + Assert.That(result.ToList(), Has.Count.GreaterThan(1)); + + await _repository.DeleteBeforeDate(Now); + + var resultDelete = await _repository.Collection.FindAsync(filter); + + Assert.That(resultDelete, Is.Not.Null); + Assert.That(resultDelete.ToList(), Has.Count.EqualTo(1)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/ObservationRepositoryTest.cs b/adas-core.Test/Repositories/ObservationRepositoryTest.cs new file mode 100644 index 00000000..f634b708 --- /dev/null +++ b/adas-core.Test/Repositories/ObservationRepositoryTest.cs @@ -0,0 +1,1804 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Moq; +using static NUnit.Framework.Is; +using static adas_core.Domain.Enums.GroupedObservationEnum; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Order(1)] +[Category("Integration")] +public class ObservationRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + var patientObservation1 = new PatientObservation + { + Id = Id, + PatientId = PatientId, + Code = "code1", + CodingSystem = "SNM", + Name = "name1", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "value1", + SystemId = "systemId1" + }; + + var patientObservation2 = new PatientObservation + { + PatientId = PatientId, + Code = "code2", + CodingSystem = "MDC", + Name = "name2", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "value2" + }; + + var patientObservation3 = new PatientObservation + { + PatientId = PatientId, + Code = "code1", + CodingSystem = "SNM", + Name = "name1", + Status = StatusEnum.Type.Ok, + Time = Now.AddDays(-1), + Value = "value3", + SystemId = "systemId1" + }; + + var patientObservation4 = new PatientObservation + { + PatientId = PatientId, + Code = "code2", + CodingSystem = "MDC", + Name = "name2", + Status = StatusEnum.Type.Ok, + Time = Now.AddDays(-1), + Value = "value4" + }; + + var patientObservation5 = new PatientObservation + { + PatientId = PatientId, + Code = "code1", + CodingSystem = "SNM", + Name = "name1", + Status = StatusEnum.Type.Ok, + Time = Now.AddDays(-2), + Value = "value5", + SystemId = "systemId1" + }; + + var patientObservation6 = new PatientObservation + { + PatientId = PatientId, + Code = "code2", + CodingSystem = "MDC", + Name = "name2", + Status = StatusEnum.Type.Ok, + Time = Now.AddDays(-2), + Value = "value6" + }; + + await IntegrationDb.Database.DropCollectionAsync("patients_observations"); + await IntegrationDb.Database.CreateCollectionAsync("patients_observations"); + + _logger = new Mock>(); + + _repository = new ObservationRepository(_optionsApiSettings, IntegrationDb.Database, _logger.Object); + + await _repository.InsertOneAsync(patientObservation1); + await _repository.InsertOneAsync(patientObservation2); + await _repository.InsertOneAsync(patientObservation3); + await _repository.InsertOneAsync(patientObservation4); + await _repository.InsertOneAsync(patientObservation5); + await _repository.InsertOneAsync(patientObservation6); + } + + private ObservationRepository _repository; + + private readonly ApiSettings _apiSettings = new() + { + PatientsObservations = "patients_observations" + }; + + private IOptions _optionsApiSettings; + private Mock> _logger; + + private static readonly DateTime Now = DateTime.UtcNow; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + private static readonly ObjectId Id = ObjectId.GenerateNewId(); + + + [Test] + public async Task FindLastObservations_Return_List() + { + var result = await _repository.FindLastObservations(PatientId, "SNM", "code1"); + + var patientObservations = result.ToList(); + Assert.That(patientObservations, Not.Null); + Assert.That(patientObservations.Count(), EqualTo(2)); + } + + [Test] + public async Task FindLastObservationsByCodingSystem_Return_List() + { + var result = await _repository.FindLastObservationsByCodingSystem(PatientId, "SNM"); + + Assert.That(result, Not.Null); + Assert.That(result, Has.Count.EqualTo(3)); + } + + + [Test] + public async Task AggregatedPatientLastObservations_Null_Filter_Return_List() + { + var result = await _repository.AggregatedPatientLastObservations(PatientId, 2); + + Assert.That(result, Not.Null); + Assert.That(result, Has.Count.EqualTo(4)); + } + + [Test] + public async Task AggregatedPatientLastObservations_Filter_Return_List() + { + var filter = new List { "name1" }; + + var result = await _repository.AggregatedPatientLastObservations(PatientId, 2, filter); + + Assert.That(result, Not.Null); + Assert.That(result, Has.Count.EqualTo(2)); + } + + [Test] + public async Task AggregatedPatientLastObservations_Filter_Not_Find_Return_Empty_List() + { + var filter = new List { "name3" }; + + var result = await _repository.AggregatedPatientLastObservations(PatientId, 2, filter); + + Assert.That(result, Not.Null); + Assert.That(result, Empty); + } + + [Test] + public async Task AggregatedPatientLastObservationsByLastDate_Null_Filter_Return_List() + { + var result = await _repository.AggregatedPatientLastObservationsByLastDate(PatientId, 2, Now); + + Assert.That(result, Not.Null); + Assert.That(result, Has.Count.EqualTo(4)); + } + + [Test] + public async Task AggregatedPatientLastObservationsByLastDate_Filter_Return_List() + { + var filter = new List { "name1" }; + + var result = await _repository.AggregatedPatientLastObservationsByLastDate(PatientId, 2, Now, filter); + + Assert.That(result, Not.Null); + Assert.That(result, Has.Count.EqualTo(2)); + } + + [Test] + public async Task AggregatedPatientLastObservationsByLastDate_Filter_Not_Find_Return_Empty_List() + { + var filter = new List { "name3" }; + + var result = await _repository.AggregatedPatientLastObservationsByLastDate(PatientId, 2, Now, filter); + + Assert.That(result, Not.Null); + Assert.That(result, Empty); + } + + [Test] + public async Task AggregatedPatientLastObservationsByField_Null_Filter_Return_List() + { + var result = await _repository.AggregatedPatientLastObservationsByField(PatientId, null); + + Assert.That(result, Not.Null); + Assert.That(result, Has.Count.EqualTo(6)); + } + + [Test] + public async Task AggregatedPatientLastObservationsByField_Filter_Return_List() + { + var field = new List { new() { Name = "name1" } }; + + var result = await _repository.AggregatedPatientLastObservationsByField(PatientId, field); + + Assert.That(result, Not.Null); + Assert.That(result, Has.Count.EqualTo(1)); + } + + + [Test] + public async Task AggregatedPatientLastObservationsByField_Filter_OnlyExpired_Return_Empty_List() + { + var field = new List { new() { Name = "name1", OnlyExpired = true } }; + + var result = await _repository.AggregatedPatientLastObservationsByField(PatientId, field); + + Assert.That(result, Not.Null); + Assert.That(result, Empty); + } + + + [Test] + public async Task DeleteByPatientId() + { + var patientObservation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = ObjectId.GenerateNewId(), + Code = "code", + CodingSystem = "MDC", + Name = "name", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "value" + }; + + await _repository.InsertOneAsync(patientObservation); + + var result = await _repository.Collection.FindAsync(t => t.PatientId == patientObservation.PatientId); + + Assert.That(result, Not.Null); + Assert.That(result.ToList(), Has.Count.EqualTo(1)); + + await _repository.DeleteByPatientId(patientObservation.PatientId); + + var resultDelete = await _repository.Collection.FindAsync(t => t.PatientId == patientObservation.PatientId); + + Assert.That(resultDelete, Not.Null); + Assert.That(resultDelete.ToList(), Empty); + } + + [Test] + public async Task FindByPatientIdAsync_Return_Cursor() + { + var result = await _repository.FindByPatientIdAsync(PatientId); + + Assert.That(result, Not.Null); + Assert.That(result.ToList(), Has.Count.EqualTo(6)); + } + + [Test] + public async Task DeleteAsync() + { + var patientObservation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = ObjectId.GenerateNewId(), + Code = "code", + CodingSystem = "MDC", + Name = "name", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "value" + }; + + await _repository.InsertOneAsync(patientObservation); + + var result = await _repository.Collection.FindAsync(t => t.Id == patientObservation.Id); + + Assert.That(result, Not.Null); + Assert.That(result.ToList(), Has.Count.EqualTo(1)); + + await _repository.DeleteAsync(patientObservation.Id); + + var resultDelete = await _repository.Collection.FindAsync(t => t.Id == patientObservation.Id); + + Assert.That(resultDelete, Not.Null); + Assert.That(resultDelete.ToList(), Empty); + } + + [Test] + public async Task DeleteOlderDaysAsync() + { + var patientObservation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = ObjectId.GenerateNewId(), + Code = "code", + CodingSystem = "MDC", + Name = "name", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "value" + }; + + var patientObservationOld = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientObservation.PatientId, + Code = "code", + CodingSystem = "MDC", + Name = "name", + Status = StatusEnum.Type.Ok, + Time = Now.AddDays(-15), + Value = "value" + }; + + await _repository.InsertOneAsync(patientObservation); + await _repository.InsertOneAsync(patientObservationOld); + + var result = await _repository.Collection.FindAsync(t => t.Name == patientObservation.Name); + + Assert.That(result, Not.Null); + Assert.That(result.ToList(), Has.Count.EqualTo(2)); + + await _repository.DeleteOlderDaysAsync(patientObservation.Name, 10); + + var resultDelete = await _repository.Collection.FindAsync(t => t.Name == patientObservation.Name); + + Assert.That(resultDelete, Not.Null); + Assert.That(resultDelete.ToList(), Has.Count.EqualTo(1)); + + await _repository.DeleteByPatientId(patientObservation.PatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == patientObservation.PatientId); + + Assert.That(resultClear, Not.Null); + Assert.That(resultClear.ToList(), Empty); + } + + [Test] + public async Task DeleteOlderSecondsAsync() + { + var patientObservation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = ObjectId.GenerateNewId(), + Code = "code", + CodingSystem = "MDC", + Name = "name", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "value" + }; + + var patientObservationOld = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientObservation.PatientId, + Code = "code", + CodingSystem = "MDC", + Name = "name", + Status = StatusEnum.Type.Ok, + Time = Now.AddSeconds(-15), + Value = "value" + }; + + + await _repository.InsertOneAsync(patientObservation); + await _repository.InsertOneAsync(patientObservationOld); + + var result = await _repository.Collection.FindAsync(t => t.Name == patientObservation.Name); + + Assert.That(result, Not.Null); + Assert.That(result.ToList(), Has.Count.EqualTo(2)); + + await _repository.DeleteOlderSecondsAsync(patientObservation.Name, 10); + + var resultDelete = await _repository.Collection.FindAsync(t => t.Name == patientObservation.Name); + + Assert.That(resultDelete, Not.Null); + Assert.That(resultDelete.ToList(), Has.Count.EqualTo(1)); + + await _repository.DeleteByPatientId(patientObservation.PatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == patientObservation.PatientId); + + Assert.That(resultClear, Not.Null); + Assert.That(resultClear.ToList(), Empty); + } + + [Test] + public async Task DeleteOlderNumberAsync() + { + var patientObservation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = ObjectId.GenerateNewId(), + Code = "code", + CodingSystem = "MDC", + Name = "name", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "value" + }; + + var patientObservationOld1 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientObservation.PatientId, + Code = "code", + CodingSystem = "MDC", + Name = "name", + Status = StatusEnum.Type.Ok, + Time = Now.AddSeconds(-10), + Value = "value" + }; + + var patientObservationOld2 = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patientObservation.PatientId, + Code = "code", + CodingSystem = "MDC", + Name = "name", + Status = StatusEnum.Type.Ok, + Time = Now.AddSeconds(-15), + Value = "value" + }; + + + await _repository.InsertOneAsync(patientObservation); + await _repository.InsertOneAsync(patientObservationOld1); + await _repository.InsertOneAsync(patientObservationOld2); + + var result = await _repository.Collection.FindAsync(t => t.Name == patientObservation.Name); + + Assert.That(result, Not.Null); + Assert.That(result.ToList(), Has.Count.EqualTo(3)); + + await _repository.DeleteOlderNumberAsync(patientObservation.Name, 2); + + var resultDelete = await _repository.Collection.FindAsync(t => t.Name == patientObservation.Name); + + Assert.That(resultDelete, Not.Null); + Assert.That(resultDelete.ToList(), Has.Count.EqualTo(2)); + + await _repository.DeleteByPatientId(patientObservation.PatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == patientObservation.PatientId); + + Assert.That(resultClear, Not.Null); + Assert.That(resultClear.ToList(), Empty); + } + + [Test] + public async Task ExistBySystemId_Return_List() + { + var result = await _repository.ExistBySystemId(PatientId, "systemId1"); + + //Assert.That(result, Is.Not.Null); + Assert.That(result, True); + } + + [Test] + public async Task FindLastObservationBeforeDate_Return_Obs() + { + var result = await _repository.FindLastObservationBeforeDate(PatientId, "name1", Now); + + Assert.That(result, Not.Null); + Assert.That(result!.Time, LessThan(Now)); + } + + [Test] + public async Task FindAnyWithSameDate_Return_List_Obs() + { + var result = await _repository.FindAnyWithSameDate(PatientId, "name1", Now); + + Assert.That(result, Not.Null); + Assert.That(result, Has.Count.EqualTo(1)); + } + + [Test] + public async Task FindAnyBeforeDate_Return_List_Obs() + { + var result = await _repository.FindAnyBeforeDate(PatientId, Now.AddDays(-1)); + + Assert.That(result, Not.Null); + Assert.That(result, Has.Count.EqualTo(2)); + } + + [Test] + public async Task AggregatedPatientActiveIntravenousLinesObservations_Null_Filter_Return_List_Obs() + { + var result = await _repository.AggregatedPatientActiveIntravenousLinesObservations(PatientId); + + Assert.That(result, Not.Null); + Assert.That(result, Has.Count.EqualTo(0)); + } + + [Test] + public async Task + AggregatedPatientActiveIntravenousLinesObservations_Filter_Not_PatientIntravenousLineasValue_Return_List_Obs() + { + var result = await _repository.AggregatedPatientActiveIntravenousLinesObservations(PatientId); + + Assert.That(result, Not.Null); + Assert.That(result, Has.Count.EqualTo(0)); + } + + [Test] + public async Task + AggregatedPatientActiveIntravenousLinesObservations_Filter_PatientIntravenousLineasValue_Return_List_Obs() + { + var intravenousLines = new PatientIntravenousLinesValue + { + Action = "action1", Duration = "duration1", InsertTime = Now, RemoveTime = Now, Location = "location1", + Type = "type1" + }; + + var patientObservation = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = ObjectId.GenerateNewId(), + Code = "codeIntra", + CodingSystem = "MDC", + Name = "IntravenousLinesObs", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = intravenousLines + }; + + await _repository.InsertOneAsync(patientObservation); + + var result = + await _repository.AggregatedPatientActiveIntravenousLinesObservations(patientObservation.PatientId); + + Assert.That(result, Not.Null); + Assert.That(result, Has.Count.EqualTo(1)); + + await _repository.DeleteByPatientId(patientObservation.PatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == patientObservation.PatientId); + + Assert.That(resultClear, Not.Null); + Assert.That(resultClear.ToList(), Has.Count.EqualTo(0)); + } + + [Test] + public async Task FindAllLastPatientObservationTime_Return_List() + { + var result = await _repository.FindAllLastPatientObservationTime(); + + Assert.That(result, Not.Null); + Assert.That(result.ToList(), Not.Empty); + } + + [Test] + public async Task FindById_Return_null() + { + var result = await _repository.FindById(ObjectId.GenerateNewId()); + + Assert.That(result, Null); + } + + [Test] + public async Task FindById_Return_Obs() + { + var result = await _repository.FindById(Id); + + Assert.That(result, Not.Null); + Assert.That(result!.Id, EqualTo(Id)); + } + + [Test] + public async Task UpdateExpiredObservations() + { + var newPatientId = ObjectId.GenerateNewId(); + + var lastNumber = 5; + + for (var i = 0; i < lastNumber; i++) + { + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameUpdate", + Status = StatusEnum.Type.Ok, + Time = Now.AddHours(-i), + Value = $"value-{i}", + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + var list = _repository.Collection.Find(p => p.PatientId == newPatientId).ToList(); + + Assert.That(list, Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(list.Count(l => l.Expired), EqualTo(0)); + Assert.That(list.Count(l => !l.Expired), EqualTo(5)); + }; + await _repository.UpdateExpiredObservations(list); + + var result = await _repository.Collection.FindAsync(p => p.PatientId == newPatientId); + var listResult = result.ToList(); + Assert.That(listResult, Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(listResult.Count(l => l.Expired), EqualTo(5)); + Assert.That(listResult.Count(l => !l.Expired), EqualTo(0)); + }; + await _repository.DeleteByPatientId(newPatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == newPatientId); + var resultClearList = resultClear.ToList(); + Assert.That(resultClearList, Not.Null); + Assert.That(resultClearList, Has.Count.EqualTo(0)); + } + + [Test] + public async Task FindAll_Return_List_Obs() + { + var result = await _repository.FindAll(); + + var patientObservations = result as PatientObservation[] ?? result.ToArray(); + Assert.That(patientObservations, Not.Null); + Assert.That(patientObservations, Not.Empty); + } + + [Test] + public async Task FindNotExpired_Return_List_Obs() + { + var result = await _repository.FindNotExpired(["name1", "name2", "name2"]); + + var patientObservations = result as PatientObservation[] ?? result.ToArray(); + Assert.That(patientObservations, Not.Null); + Assert.That(patientObservations.Count(), GreaterThan(0)); + } + + [Test] + public async Task ExpireExpiredObservations() + { + var configObservationItems = new List + { + new() + { + Id = ObjectId.GenerateNewId(), + Name = "name1", + Expires = 480 + }, + new() + { + Id = ObjectId.GenerateNewId(), + Name = "name2", + Expires = 480 + }, + new() + { + Id = ObjectId.GenerateNewId(), + Name = "name3", + Expires = 480 + }, + new() + { + Id = ObjectId.GenerateNewId(), + Name = "name4", + Expires = 480 + } + }; + + await _repository.ExpireExpiredObservations(configObservationItems); + + var result = await _repository.FindNotExpired(["name1", "name2", "name2"]); + + var patientObservations = result as PatientObservation[] ?? result.ToArray(); + Assert.That(patientObservations, Not.Null); + Assert.That(patientObservations.ToList(), Has.Count.EqualTo(2)); + } + + [Test] + public async Task FindByName_Return_List_Obs() + { + var result = await _repository.FindByName("name1"); + + var patientObservations = result as PatientObservation[] ?? result.ToArray(); + Assert.That(patientObservations, Not.Null); + Assert.That(patientObservations.Count(), GreaterThan(0)); + } + + [Test] + public async Task AggregatedPatientGroupedObservations_Null_groupedField_Return_List_Emty() + { + var groupedField = new GroupedField(); + + var result = await _repository.AggregatedPatientGroupedObservations(PatientId, groupedField); + using (Assert.EnterMultipleScope()) + { + Assert.That(result, Not.Null); + Assert.That(!result.Any(), True); + }; + } + + [Test] + public async Task AggregatedPatientGroupedObservations_Hour_Firs_Return_List_9() + { + var groupedField = new GroupedField + { + Name = "nameG", + Group = "nameGroup", + Regularity = Regularity.Hour, + Result = [Result.First], + Max = 8 + }; + + const int lastNumber = 30; + + var newPatientId = ObjectId.GenerateNewId(); + + for (var i = 0; i < lastNumber; i++) + { + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = Now.AddHours(-i), + Value = $"value-{i}", + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + var result = await _repository.AggregatedPatientGroupedObservations(newPatientId, groupedField); + + Assert.That(result, Not.Null); + Assert.That(result, Has.Count.EqualTo(9)); + + await _repository.DeleteByPatientId(newPatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == newPatientId); + + Assert.That(resultClear, Not.Null); + Assert.That(resultClear.ToList(), Has.Count.EqualTo(0)); + } + + [Test] + public async Task AggregatedPatientGroupedObservations_Hour_Firs_Since_Last_Return_List_8() + { + var groupedField = new GroupedField + { + Name = "nameG", + Group = "nameGroup", + Regularity = Regularity.Hour, + Result = [Result.First], + Max = 8, + Since = Since.Last + }; + + var newPatientId = ObjectId.GenerateNewId(); + + const int lastNumber = 30; + + for (var i = 0; i < lastNumber; i++) + { + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = Now.AddHours(-i), + Value = $"value-{i}", + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + var result = await _repository.AggregatedPatientGroupedObservations(newPatientId, groupedField); + + Assert.That(result, Not.Null); + Assert.That(result, Has.Count.EqualTo(8)); + + await _repository.DeleteByPatientId(newPatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == newPatientId); + + Assert.That(resultClear, Not.Null); + Assert.That(resultClear.ToList(), Has.Count.EqualTo(0)); + } + + [Test] + public async Task AggregatedPatientGroupedObservations_Insert_5_Hour_Firs_Return_List_5() + { + var groupedField = new GroupedField + { + Name = "nameG", + Group = "nameGroup", + Regularity = Regularity.Hour, + Result = [Result.First], + Max = 8 + }; + + var newPatientId = ObjectId.GenerateNewId(); + + const int lastNumber = 5; + + for (var i = 0; i < lastNumber; i++) + { + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = Now.AddHours(-i), + Value = $"value-{i}", + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + var result = await _repository.AggregatedPatientGroupedObservations(newPatientId, groupedField); + + Assert.That(result, Not.Null); + //Assert.That(result, Has.Count.EqualTo(5)); + Assert.That(result, Has.Count.EqualTo(5).Or.EqualTo(4)); + await _repository.DeleteByPatientId(newPatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == newPatientId); + + Assert.That(resultClear, Not.Null); + Assert.That(resultClear.ToList(), Has.Count.EqualTo(0)); + } + + + [Test] + public async Task AggregatedPatientGroupedObservations_second_Firs_Return_List() + { + var groupedField = new GroupedField + { + Name = "nameG", + Group = "nameGroup", + Regularity = Regularity.Second, + Result = [Result.First], + Max = 60 + }; + + var newPatientId = ObjectId.GenerateNewId(); + + const int lastNumber = 120; + + for (var i = 0; i < lastNumber; i++) + { + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = Now.AddSeconds(-i), + Value = $"value-{i}", + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + var result = await _repository.AggregatedPatientGroupedObservations(newPatientId, groupedField); + + Assert.That(result, Not.Null); + Assert.That(result, Not.Empty); + + await _repository.DeleteByPatientId(newPatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == newPatientId); + + Assert.That(resultClear, Not.Null); + Assert.That(resultClear.ToList(), Has.Count.EqualTo(0)); + } + + [Test] + public async Task AggregatedPatientGroupedObservations_Minute_Firs_Return_List_8() + { + var groupedField = new GroupedField + { + Name = "nameG", + Group = "nameGroup", + Regularity = Regularity.Minute, + Result = [Result.First], + Max = 8 + }; + + var newPatientId = ObjectId.GenerateNewId(); + + const int lastNumber = 30; + + for (var i = 0; i < lastNumber; i++) + { + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = Now.AddMinutes(-i), + Value = $"value-{i}", + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + var result = await _repository.AggregatedPatientGroupedObservations(newPatientId, groupedField); + + Assert.That(result, Not.Null); + Assert.That(result, Has.Count.EqualTo(8)); + + await _repository.DeleteByPatientId(newPatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == newPatientId); + + Assert.That(resultClear, Not.Null); + Assert.That(resultClear.ToList(), Has.Count.EqualTo(0)); + } + + [Test] + public async Task AggregatedPatientGroupedObservations_Day_Firs_Return_List_8() + { + var groupedField = new GroupedField + { + Name = "nameG", + Group = "nameGroup", + Regularity = Regularity.Day, + Result = [Result.First], + Max = 8 + }; + + var newPatientId = ObjectId.GenerateNewId(); + + const int lastNumber = 30; + + for (var i = 0; i < lastNumber; i++) + { + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = Now.AddDays(-i), + Value = $"value-{i}", + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + var result = await _repository.AggregatedPatientGroupedObservations(newPatientId, groupedField); + + Assert.That(result, Not.Null); + Assert.That(result, Has.Count.EqualTo(8)); + + await _repository.DeleteByPatientId(newPatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == newPatientId); + + Assert.That(resultClear, Not.Null); + Assert.That(resultClear.ToList(), Has.Count.EqualTo(0)); + } + + [Test] + public async Task AggregatedPatientGroupedObservations_Shift_Firs_Return_List_9() + { + var groupedField = new GroupedField + { + Name = "nameG", + Group = "nameGroup", + Regularity = Regularity.Shift, + Result = [Result.First], + Max = 8 + }; + + var newPatientId = ObjectId.GenerateNewId(); + + const int lastNumber = 30; + + for (var i = 0; i < lastNumber; i++) + { + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = Now.AddHours(-i), + Value = $"value-{i}", + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + var result = await _repository.AggregatedPatientGroupedObservations(newPatientId, groupedField); + + Assert.That(result, Not.Null); + Assert.That(result, Has.Count.EqualTo(9)); + Assert.That(result.FindAll(r => r.Contains("first")), Has.Count.EqualTo(9)); + + await _repository.DeleteByPatientId(newPatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == newPatientId); + + Assert.That(resultClear, Not.Null); + Assert.That(resultClear.ToList(), Has.Count.EqualTo(0)); + } + + [Test] + public async Task AggregatedPatientGroupedObservations_Insert_5_Hour_last_Return_List_5() + { + var groupedField = new GroupedField + { + Name = "nameG", + Group = "nameGroup", + Regularity = Regularity.Hour, + Result = [Result.Last], + Max = 8 + }; + + var newPatientId = ObjectId.GenerateNewId(); + + const int lastNumber = 5; + + for (var i = 0; i < lastNumber; i++) + { + var time = new DateTime(Now.Year, Now.Month, Now.Day, Now.AddHours(-i).Hour, 30, 00, 00); + + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = time, + Value = $"value-{i}", + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + for (var i = 0; i < lastNumber; i++) + { + var time = new DateTime(Now.Year, Now.Month, Now.Day, Now.AddHours(-i).Hour, 15, 00, 00); + + + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = time, + Value = $"value-{i}", + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + var result = await _repository.AggregatedPatientGroupedObservations(newPatientId, groupedField); + + Assert.That(result, Not.Null); + Assert.That(result, Has.Count.EqualTo(5)); + //Assert.That(result, Has.Count.EqualTo(5).Or.EqualTo(4)); + Assert.That(result.FindAll(r => r.Contains("last")), Has.Count.EqualTo(5)); + + await _repository.DeleteByPatientId(newPatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == newPatientId); + + Assert.That(resultClear, Not.Null); + Assert.That(resultClear.ToList(), Has.Count.EqualTo(0)); + } + + [Test] + public async Task AggregatedPatientGroupedObservations_Insert_5_Hour_sum_Return_List_5() + { + var groupedField = new GroupedField + { + Name = "nameG", + Group = "nameGroup", + Regularity = Regularity.Hour, + Result = [Result.Sum], + Max = 8 + }; + + var newPatientId = ObjectId.GenerateNewId(); + + const int lastNumber = 5; + + for (var i = 0; i < lastNumber; i++) + { + var time = new DateTime(Now.Year, Now.Month, Now.Day, Now.AddHours(-i).Hour, 30, 00, 00); + + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = time, + Value = 5, + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + for (var i = 0; i < lastNumber; i++) + { + var time = new DateTime(Now.Year, Now.Month, Now.Day, Now.AddHours(-i).Hour, 15, 00, 00); + + + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = time, + Value = 5, + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + var result = await _repository.AggregatedPatientGroupedObservations(newPatientId, groupedField); + + await _repository.DeleteByPatientId(newPatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == newPatientId); + + Assert.That(resultClear, Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(resultClear.ToList(), Has.Count.EqualTo(0)); + + Assert.That(result, Not.Null); + }; + + Assert.That(result, Has.Count.EqualTo(5)); + //Assert.That(result, Has.Count.EqualTo(5).Or.EqualTo(4)); + Assert.That(result.FindAll(r => r.Contains("sum")), Has.Count.EqualTo(5)); + + + for (var i = 0; i < lastNumber; i++) Assert.That(result[i]["sum"].ToInt32(), EqualTo(10).Or.EqualTo(5)); + } + + [Test] + public async Task AggregatedPatientGroupedObservations_Insert_5_Hour_Min_Return_List_5() + { + var groupedField = new GroupedField + { + Name = "nameG", + Group = "nameGroup", + Regularity = Regularity.Hour, + Result = [Result.Min], + Max = 8 + }; + + var newPatientId = ObjectId.GenerateNewId(); + + const int lastNumber = 5; + + for (var i = 0; i < lastNumber; i++) + { + var time = new DateTime(Now.Year, Now.Month, Now.Day, Now.AddHours(-i).Hour, 30, 00, 00); + + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = time, + Value = 5, + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + for (var i = 0; i < lastNumber; i++) + { + var time = new DateTime(Now.Year, Now.Month, Now.Day, Now.AddHours(-i).Hour, 15, 00, 00); + + + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = time, + Value = 3, + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + var result = await _repository.AggregatedPatientGroupedObservations(newPatientId, groupedField); + + Assert.That(result, Not.Null); + Assert.That(result, Has.Count.EqualTo(5)); + //Assert.That(result, Has.Count.EqualTo(5).Or.EqualTo(4)); + Assert.That(result.FindAll(r => r.Contains("min")), Has.Count.EqualTo(5)); + for (var i = 0; i < lastNumber; i++) Assert.That(result[i]["min"]["value"].ToInt32(), EqualTo(3)); + + await _repository.DeleteByPatientId(newPatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == newPatientId); + + Assert.That(resultClear, Not.Null); + Assert.That(resultClear.ToList(), Has.Count.EqualTo(0)); + } + + [Test] + public async Task AggregatedPatientGroupedObservations_Insert_5_Hour_Max_Return_List_5() + { + var groupedField = new GroupedField + { + Name = "nameG", + Group = "nameGroup", + Regularity = Regularity.Hour, + Result = [Result.Max], + Max = 8 + }; + + var newPatientId = ObjectId.GenerateNewId(); + + const int lastNumber = 5; + + for (var i = 0; i < lastNumber; i++) + { + var time = new DateTime(Now.Year, Now.Month, Now.Day, Now.AddHours(-i).Hour, 30, 00, 00); + + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = time, + Value = 5, + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + for (var i = 0; i < lastNumber; i++) + { + var time = new DateTime(Now.Year, Now.Month, Now.Day, Now.AddHours(-i).Hour, 15, 00, 00); + + + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = time, + Value = 3, + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + var result = await _repository.AggregatedPatientGroupedObservations(newPatientId, groupedField); + + await _repository.DeleteByPatientId(newPatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == newPatientId); + + Assert.That(resultClear, Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(resultClear.ToList(), Has.Count.EqualTo(0)); + + Assert.That(result, Not.Null); + }; + + Assert.That(result, Has.Count.EqualTo(5)); + //Assert.That(result, Has.Count.EqualTo(5).Or.EqualTo(4)); + Assert.That(result.FindAll(r => r.Contains("max")), Has.Count.EqualTo(5)); + for (var i = 0; i < lastNumber; i++) Assert.That(result[i]["max"]["value"].ToInt32(), EqualTo(5).Or.EqualTo(3)); + } + + [Test] + public async Task AggregatedPatientGroupedObservations_Insert_5_Hour_Average_Return_List_5() + { + var groupedField = new GroupedField + { + Name = "nameG", + Group = "nameGroup", + Regularity = Regularity.Hour, + Result = [Result.Average], + Max = 8 + }; + + var newPatientId = ObjectId.GenerateNewId(); + + const int lastNumber = 5; + + for (var i = 0; i < lastNumber; i++) + { + var time = new DateTime(Now.Year, Now.Month, Now.Day, Now.AddHours(-i).Hour, 30, 00, 00); + + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = time, + Value = 5, + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + for (var i = 0; i < lastNumber; i++) + { + var time = new DateTime(Now.Year, Now.Month, Now.Day, Now.AddHours(-i).Hour, 15, 00, 00); + + + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = time, + Value = 3, + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + var result = await _repository.AggregatedPatientGroupedObservations(newPatientId, groupedField); + + await _repository.DeleteByPatientId(newPatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == newPatientId); + + Assert.That(resultClear, Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(resultClear.ToList(), Has.Count.EqualTo(0)); + + Assert.That(result, Not.Null); + }; + Assert.That(result, Has.Count.EqualTo(5)); + //Assert.That(result, Has.Count.EqualTo(5).Or.EqualTo(4)); + Assert.That(result.FindAll(r => r.Contains("average")), Has.Count.EqualTo(5)); + + for (var i = 0; i < lastNumber; i++) Assert.That(result[i]["average"].ToInt32(), EqualTo(4).Or.EqualTo(3)); + } + + [Test] + public async Task AggregatedPatientGroupedObservations_Insert_5_Hour_Count_Return_List_5() + { + var groupedField = new GroupedField + { + Name = "nameG", + Group = "nameGroup", + Regularity = Regularity.Hour, + Result = [Result.Count], + Max = 8 + }; + + var newPatientId = ObjectId.GenerateNewId(); + + const int lastNumber = 5; + + for (var i = 0; i < lastNumber; i++) + { + var time = new DateTime(Now.Year, Now.Month, Now.Day, Now.AddHours(-i).Hour, 30, 00, 00); + + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = time, + Value = 5, + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + for (var i = 0; i < lastNumber; i++) + { + var time = new DateTime(Now.Year, Now.Month, Now.Day, Now.AddHours(-i).Hour, 15, 00, 00); + + + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = time, + Value = 3, + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + var result = await _repository.AggregatedPatientGroupedObservations(newPatientId, groupedField); + + await _repository.DeleteByPatientId(newPatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == newPatientId); + + Assert.That(resultClear, Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(resultClear.ToList(), Has.Count.EqualTo(0)); + + Assert.That(result, Not.Null); + }; + + Assert.That(result, Has.Count.EqualTo(5)); + //Assert.That(result.Count, Is.EqualTo(5).Or.EqualTo(4)); + Assert.That(result.FindAll(r => r.Contains("count")), Has.Count.EqualTo(5)); + for (var i = 0; i < lastNumber; i++) Assert.That(result[i]["count"].ToInt32(), EqualTo(2).Or.EqualTo(1)); + } + + [Test] + public async Task AggregatedPatientGroupedObservations_Insert_5_Hour_HalfHour_Return_List_5() + { + var groupedField = new GroupedField + { + Name = "nameG", + Group = "nameGroup", + Regularity = Regularity.Hour, + Result = [Result.HalfHour], + Max = 8 + }; + + var newPatientId = ObjectId.GenerateNewId(); + + const int lastNumber = 5; + + for (var i = 0; i < lastNumber; i++) + { + var time = new DateTime(Now.Year, Now.Month, Now.Day, Now.AddHours(-i).Hour, 31, 00, 00); + + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = time, + Value = 5, + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + for (var i = 0; i < lastNumber; i++) + { + var time = new DateTime(Now.Year, Now.Month, Now.Day, Now.AddHours(-i).Hour, 15, 00, 00); + + + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = time, + Value = 3, + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + var result = await _repository.AggregatedPatientGroupedObservations(newPatientId, groupedField); + + await _repository.DeleteByPatientId(newPatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == newPatientId); + + Assert.That(resultClear, Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(resultClear.ToList(), Has.Count.EqualTo(0)); + + Assert.That(result, Not.Null); + }; + + Assert.That(result, Has.Count.EqualTo(5)); + //Assert.That(result, Has.Count.EqualTo(5).Or.EqualTo(3)); + + Assert.That(result.FindAll(r => r.Contains("all")), Has.Count.EqualTo(5).Or.EqualTo(3)); + //lastNumber = result.Count; + + //for (var i = 0; i < lastNumber; i++) + //{ + // Assert.Multiple(() => + // { + // Assert.That(result[i]["all"][0]["value"].ToInt32(), Is.EqualTo(3)); + // Assert.That(result[i]["all"][1]["value"].ToInt32(), Is.EqualTo(5).Or.EqualTo(3)); + // }); + //} + } + + [Test] + public async Task AggregatedPatientGroupedObservations_Insert_5_Hour_LastFilled_Return_List_5() + { + var groupedField = new GroupedField + { + Name = "nameG", + Group = "nameGroup", + Regularity = Regularity.Hour, + Result = [Result.LastFilled], + Max = 8 + }; + + var newPatientId = ObjectId.GenerateNewId(); + + var lastNumber = 5; + + for (var i = 0; i < lastNumber; i++) + { + var time = new DateTime(Now.Year, Now.Month, Now.Day, Now.AddHours(-i).Hour, 31, 00, 00); + + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = time, + Value = 5, + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + for (var i = 0; i < lastNumber; i++) + { + var time = new DateTime(Now.Year, Now.Month, Now.Day, Now.AddHours(-i).Hour, 15, 00, 00); + + + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = time, + Value = 3, + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + var result = await _repository.AggregatedPatientGroupedObservations(newPatientId, groupedField); + + await _repository.DeleteByPatientId(newPatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == newPatientId); + + Assert.That(resultClear, Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(resultClear.ToList(), Has.Count.EqualTo(0)); + + Assert.That(result, Not.Null); + }; + + Assert.That(result, Has.Count.EqualTo(5)); + //Assert.That(result, Has.Count.EqualTo(5).Or.EqualTo(4)); + Assert.That(result.FindAll(r => r.Contains("lastfilled")), Has.Count.EqualTo(5).Or.Count.EqualTo(3)); + lastNumber = result.Count; + for (var i = 0; i < lastNumber; i++) + Assert.That(result[i]["lastfilled"]["value"].ToInt32(), EqualTo(5).Or.EqualTo(3)); + } + + [Test] + public async Task AggregatedPatientGroupedObservations_Insert_5_Hour_LastAll_Return_List_5() + { + var groupedField = new GroupedField + { + Name = "nameG", + Group = "nameGroup", + Regularity = Regularity.Hour, + Result = [Result.LastAll], + Max = 8 + }; + + var newPatientId = ObjectId.GenerateNewId(); + + const int lastNumber = 5; + + for (var i = 0; i < lastNumber; i++) + { + var time = new DateTime(Now.Year, Now.Month, Now.Day, Now.AddHours(-i).Hour, 31, 00, 00); + + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = time, + Value = 5, + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + for (var i = 0; i < lastNumber; i++) + { + var time = new DateTime(Now.Year, Now.Month, Now.Day, Now.AddHours(-i).Hour, 15, 00, 00); + + + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = time, + Value = 3, + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + var result = await _repository.AggregatedPatientGroupedObservations(newPatientId, groupedField); + + Assert.That(result, Not.Null); + Assert.That(result, Has.Count.EqualTo(5)); + //Assert.That(result, Has.Count.EqualTo(5).Or.EqualTo(4)); + + await _repository.DeleteByPatientId(newPatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == newPatientId); + + Assert.That(resultClear, Not.Null); + Assert.That(resultClear.ToList(), Has.Count.EqualTo(0)); + } + + [Test] + public async Task AggregatedPatientGroupedObservations_Times_Firs_Return_List_8() + { + var groupedField = new GroupedField + { + Name = "nameG", + Group = "nameGroup", + Regularity = Regularity.Times, + Result = [Result.First], + Max = 8 + }; + + var newPatientId = ObjectId.GenerateNewId(); + + const int lastNumber = 30; + + for (var i = 0; i < lastNumber; i++) + { + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = newPatientId, + Code = "code", + CodingSystem = "SNM", + Name = "nameG", + Status = StatusEnum.Type.Ok, + Time = DateTime.Now, + Value = $"value-{i}", + SystemId = "systemId1" + }; + + await _repository.InsertOneAsync(obs); + } + + //Thread.Sleep(TimeSpan.FromSeconds(1)); + + await _repository.FindAll(); + + var result = await _repository.AggregatedPatientGroupedObservations(newPatientId, groupedField); + + Assert.That(result, + Has.Count.AnyOf(8, + 7)); //TODO: RevisarHay algún test que interfiere en el resultado. La primera vez que se lanza devuelve 7 y la segunda 8 + + await _repository.DeleteByPatientId(newPatientId); + + var resultClear = await _repository.Collection.FindAsync(t => t.PatientId == newPatientId); + + Assert.That(resultClear.ToList(), Empty); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/PatientArchiveRepositoryTest.cs b/adas-core.Test/Repositories/PatientArchiveRepositoryTest.cs new file mode 100644 index 00000000..bb682a45 --- /dev/null +++ b/adas-core.Test/Repositories/PatientArchiveRepositoryTest.cs @@ -0,0 +1,106 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class PatientArchiveRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + var patient1 = new Patient + { + UnitString = "PointOfCare", + Bed = "bed1", + PatientNumber = "patientNumber1", + Person = new Person + { + FirstName = "firstName1", + LastName = "lastName1", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }; + + var patient2 = new Patient + { + UnitString = "PointOfCare", + Bed = "bed2", + PatientNumber = "patientNumber2", + Person = new Person + { + FirstName = "firstName2", + LastName = "lastName2", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }; + + var patient3 = new Patient + { + UnitString = "PointOfCare", + Bed = "bed3", + PatientNumber = "patientNumber3", + Person = new Person + { + FirstName = "firstName3", + LastName = "lastName3", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }; + + await IntegrationDb.Database.DropCollectionAsync("archive_patient"); + await IntegrationDb.Database.CreateCollectionAsync("archive_patient"); + + _repository = new PatientArchiveRepository(_optionsApiSettings, IntegrationDb.Database); + + await _repository.InsertOneAsync(patient1); + await _repository.InsertOneAsync(patient2); + await _repository.InsertOneAsync(patient3); + } + + private PatientArchiveRepository _repository; + + private readonly ApiSettings _apiSettings = new() + { + ArchivePatientsDiagnosis = "archive_patients" + }; + + private IOptions _optionsApiSettings; + + [Test] + public async Task FindByPatientNumber_Not_Find_Return_null() + { + var result = await _repository.FindByPatientNumber(""); + + Assert.That(result, Is.Null); + } + + [Test] + public async Task FindByPatientNumber_Find_Return_Patient() + { + var result = await _repository.FindByPatientNumber("patientNumber1"); + + Assert.That(result, Is.Not.Null); + Assert.That(result.PatientNumber, Is.EqualTo("patientNumber1")); + } + + [Test] + public async Task FindAll_Return_List_Patients() + { + var result = await _repository.FindAll(); + + Assert.That(result, Is.Not.Null); + Assert.That(result.Count, Is.EqualTo(3)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/PatientRepositoryTest.cs b/adas-core.Test/Repositories/PatientRepositoryTest.cs new file mode 100644 index 00000000..63d297f4 --- /dev/null +++ b/adas-core.Test/Repositories/PatientRepositoryTest.cs @@ -0,0 +1,624 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class PatientRepositoryTest +{ + //private static readonly ObjectId id = ObjectId.GenerateNewId(); + + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + var patient1 = new Patient + { + Id = PatientId, + PatientId = "patientId1", + UnitId = _unit1, + PointOfCareId = _poc1.Id, + PatientNumber = "patientNumber1", + Person = new Person + { + FirstName = "firstName1", + LastName = "lastName1", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + }, + DisTime = Now, + Origin = new OptionList + { + Id = OriginListOptionId, + Name = "Urlogy" + } + }; + + var patient2 = new Patient + { + UnitId = _unit1, + PatientId = "patientId2", + PointOfCareId = _poc2.Id, + PatientNumber = "patientNumber2", + Person = new Person + { + FirstName = "firstName2", + LastName = "lastName2", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }; + + var patient3 = new Patient + { + UnitId = _unit1, + PatientId = "patientId3", + PointOfCareId = _poc3.Id, + PatientNumber = "patientNumber3", + Person = new Person + { + FirstName = "firstName3", + LastName = "lastName3", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }; + var patient4 = new Patient + { + UnitId = _pocMoved.UnitId, + PatientId = "patientId4", + PointOfCareId = _pocMoved.Id, + PatientNumber = "patientNumber4", + Person = new Person + { + FirstName = "firstName4", + LastName = "lastName4", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }; + var patient5 = new Patient + { + UnitId = _pocPushed.UnitId, + PatientId = "patientId5", + PointOfCareId = _pocPushed.Id, + PatientNumber = "patientNumber5", + Person = new Person + { + FirstName = "firstName5", + LastName = "lastName5", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }; + + await IntegrationDb.Database.DropCollectionAsync("patient"); + await IntegrationDb.Database.CreateCollectionAsync("patient"); + await IntegrationDb.Database.DropCollectionAsync("pointOfCares"); + await IntegrationDb.Database.CreateCollectionAsync("pointOfCares"); + await IntegrationDb.Database.DropCollectionAsync("units"); + await IntegrationDb.Database.CreateCollectionAsync("units"); + await IntegrationDb.Database.DropCollectionAsync("list_origin"); + await IntegrationDb.Database.CreateCollectionAsync("list_origin"); + + _repository = new PatientRepository(_optionsApiSettings, IntegrationDb.Database); + _repositoryPoc = new PointOfCareRepository(_optionsApiSettings, IntegrationDb.Database); + _repositoryUnit = new UnitRepository(_optionsApiSettings, IntegrationDb.Database); + _repositoryList = new MasterListRepository(_optionsApiSettings, IntegrationDb.Database); + + await _repositoryPoc.InsertOneAsync(_poc1); + await _repositoryPoc.InsertOneAsync(_poc2); + await _repositoryPoc.InsertOneAsync(_poc3); + await _repositoryPoc.InsertOneAsync(_pocMoved); + await _repositoryPoc.InsertOneAsync(_pocPushed); + await _repository.InsertOneAsync(patient1); + await _repository.InsertOneAsync(patient2); + await _repository.InsertOneAsync(patient3); + await _repository.InsertOneAsync(patient4); + await _repository.InsertOneAsync(patient5); + await _repositoryUnit.InsertOneAsync(_unit); + await _repositoryList.InsertOneAsync(_originList); + + await _repository.FindByPatientNumber("patientNumber1"); + + await _repository.InsertManyAsync([patient1, patient2, patient3, patient4, patient5]); + Assert.That(await _repository.Collection.CountDocumentsAsync(_ => true), Is.EqualTo(5)); + } + + private PatientRepository _repository; + private PointOfCareRepository _repositoryPoc; + private UnitRepository _repositoryUnit; + private MasterListRepository _repositoryList; + + private readonly ApiSettings _apiSettings = new() + { + ArchivePatientsDiagnosis = "patients" + }; + + private IOptions _optionsApiSettings; + + private static readonly DateTime Now = DateTime.Now; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + private static readonly ObjectId OriginListId = ObjectId.GenerateNewId(); + private static readonly ObjectId OriginListOptionId = ObjectId.GenerateNewId(); + + private readonly ObjectId _unit1 = new("662760a96bdc7150b49fe29c"); + + private readonly Unit _unit = new() + { + Id = ObjectId.Parse("662760a96bdc7150b49fe29c"), + Name = "unit1", + OriginListId = OriginListId + }; + + private readonly OriginList _originList = new() + { + Id = OriginListId, + Name = "OriginList", + Options = new List + { + new() + { + Id = OriginListOptionId, + Name = "Urlogy", + LocaleItems = new Locale + { + Es = new LocaleItem { Name = "Urologia" } + } + } + }, + CanAddElement = true, + ListType = MasterListType.OriginList, + DefaultLocale = LocaleEnum.Eng + }; + + private readonly PointOfCare _poc1 = new() + { + Bed = "Bed1", + Id = ObjectId.GenerateNewId(), + Room = "Room1", + UnitId = ObjectId.GenerateNewId() + }; + + private readonly PointOfCare _poc2 = new() + { + Bed = "Bed1", + Id = ObjectId.GenerateNewId(), + Room = "Room1", + UnitId = ObjectId.GenerateNewId() + }; + + private readonly PointOfCare _poc3 = new() + { + Bed = "Bed1", + Id = ObjectId.GenerateNewId(), + Room = "Room1", + UnitId = ObjectId.GenerateNewId() + }; + + private readonly PointOfCare _pocMoved = new() + { + Bed = VirtualPointOfCare.Moved.ToString(), + Id = ObjectId.GenerateNewId(), + Room = VirtualPointOfCare.Moved.ToString(), + UnitId = ObjectId.GenerateNewId() + }; + + private readonly PointOfCare _pocPushed = new() + { + Bed = VirtualPointOfCare.Pushed.ToString(), + Id = ObjectId.GenerateNewId(), + Room = VirtualPointOfCare.Pushed.ToString(), + UnitId = ObjectId.GenerateNewId() + }; + + [Test] + public async Task FindInActivePoC() + { + var result = await _repository.FindInActivePoC(); + Assert.That(result, Is.Not.Null); + Assert.That(result.Count, Is.EqualTo(3)); + } + + [Test] + public async Task FindInInactivePoC() + { + var result = await _repository.FindInInactivePoC(); + Assert.That(result, Is.Not.Null); + Assert.That(result.Count, Is.EqualTo(2)); + } + + [Test] + public async Task FindById_Find_Return_Patient() + { + var result = await _repository.FindById(PatientId); + + Assert.That(result, Is.Not.Null); + Assert.That(result.PatientNumber, Is.EqualTo("patientNumber1")); + } + + [Test] + public async Task FindByPatientNumber_Not_Find_Return_Patient() + { + var result = await _repository.FindByPatientNumber(""); + + Assert.That(result, Is.Null); + } + + [Test] + public async Task FindByPatientNumber_Find_Return_Patient() + { + var result = await _repository.FindByPatientNumber("patientNumber1"); + + Assert.That(result, Is.Not.Null); + Assert.That(result.PatientNumber, Is.EqualTo("patientNumber1")); + } + + [Test] + public async Task FindAll_Return_List_Patients() + { + var result = await _repository.FindAll(); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Count.EqualTo(5)); + } + + [Test] + public async Task Update_with_base_UpdateOneAsync() + { + var patient = new Patient + { + Id = ObjectId.GenerateNewId(), + UnitString = "PointOfCare", + Bed = "bed", + PatientNumber = "patientNumber", + Person = new Person + { + FirstName = "firstName", + LastName = "lastName", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }; + + var patientUpdate = new Patient + { + Id = patient.Id, + UnitString = "PointOfCare", + Bed = "bed", + PatientNumber = "patientNumberUpdate", + Person = new Person + { + FirstName = "firstName", + LastName = "lastName", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }; + + await _repository.InsertOneAsync(patient); + + var result = await _repository.FindByPatientNumber("patientNumberUpdate"); + + Assert.That(result, Is.Null); + + await _repository.Update(patientUpdate); + + var resultUpdate = await _repository.FindByPatientNumber("patientNumberUpdate"); + + Assert.That(resultUpdate, Is.Not.Null); + Assert.That(resultUpdate.PatientNumber, Is.EqualTo("patientNumberUpdate")); + + await _repository.Delete(patient.Id); + } + + [Test] + public async Task Delete() + { + var patientDelete = new Patient + { + Id = ObjectId.GenerateNewId(), + UnitString = "PointOfCare", + Bed = "bedDelete", + PatientNumber = "patientNumberDelete", + Person = new Person + { + FirstName = "firstName", + LastName = "lastName", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }; + + await _repository.InsertOneAsync(patientDelete); + + var result = await _repository.FindByPatientNumber("patientNumberDelete"); + + Assert.That(result, Is.Not.Null); + + await _repository.Delete(patientDelete.Id); + + var resultDelete = await _repository.FindByPatientNumber("patientNumberDelete"); + + Assert.That(resultDelete, Is.Null); + } + + + // [Test] + // public async Task UpdateLocation() + // { + // var patient = new Patient() + // { + // Id = ObjectId.GenerateNewId(), + // UnitId = _unit1, + // PointOfCareId = ObjectId.GenerateNewId(), + // PatientNumber = "patientNumberLocation", + // Person = new Person + // { + // FirstName = "firstName", + // LastName = "lastName", + // BirthDate = new DateTime(), + // Gender = Gender.Male, + // } + // }; + // + // var location = new PatientLocation("PointOfCare", "BedLocationUpdate"); + // + // await _repository.InsertOneAsync(patient); + // + // var result = await _repository.FindByPointOfCare(_poc1.Id); + // + // // Assert.That(result, Is.Null); + // + // await _repository.UpdateLocation(result.FirstOrDefault().Id,location); + // + // var resultUpdate = await _repository.FindByPointOfCare(_poc1.Id); + // + // Assert.That(resultUpdate, Is.Not.Null); + // Assert.That(resultUpdate.FirstOrDefault().Bed, Is.EqualTo("BedLocationUpdate")); + // + // await _repository.Delete(patient.Id); + // } + + [Test] + public async Task UpdateAttendingDoctor() + { + var doctor = new Person + { + FirstName = "firstNameDoctor2", + LastName = "lastNameDoctor2", + Gender = PatientEnum.Gender.Female + }; + + var result = await _repository.FindByPointOfCare(_poc1.Id); + + await _repository.UpdateAttendingDoctor(result.First().Id, doctor); + + var resultUpdate = await _repository.FindByPointOfCare(_poc1.Id); + + Assert.That(resultUpdate, Is.Not.Null); + Assert.That(resultUpdate.First().AttendingDoctor?.FirstName, Is.EqualTo("firstNameDoctor2")); + } + + // [Test] + // public async Task UpdatePatientData_UpdatePatientNumber_true() + // { + // var patient1 = new Person + // { + // FirstName = "firstNamePatient1", + // LastName = "lastNamePatient1", + // BirthDate = new DateTime(), + // Gender = Gender.Male + // }; + // + // var patient2 = new Person + // { + // FirstName = "firstNamePatient2", + // LastName = "lastNamePatient2", + // BirthDate = new DateTime(), + // Gender = Gender.Female + // }; + // + // var patient = new Patient() + // { + // Id = ObjectId.GenerateNewId(), + // UnitId = _unit1, + // PointOfCareId = ObjectId.GenerateNewId(), + // PatientNumber = "patientNumber", + // Person = patient1 + // }; + // + // var location = new PatientLocation("PointOfCare", "bed"); + // + // await _repository.InsertOneAsync(patient); + // + // var result = await _repository.FindByLocation(location); + // + // Assert.That(result, Is.Not.Null); + // Assert.That(result.Person?.FirstName, Is.EqualTo("firstNamePatient1")); + // + // await _repository.UpdatePatientData(patient.Id, "patientNumberUpdate", patient2, true); + // + // var resultUpdate = await _repository.FindByLocation(location); + // + // Assert.That(resultUpdate, Is.Not.Null); + // Assert.That(resultUpdate.Person?.FirstName, Is.EqualTo("firstNamePatient2")); + // + // await _repository.Delete(patient.Id); + // } + + // [Test] + // public async Task UpdatePatientData_UpdatePatientNumber_false() + // { + // var patient1 = new Person + // { + // FirstName = "firstNamePatient1", + // LastName = "lastNamePatient1", + // BirthDate = new DateTime(), + // Gender = Gender.Male + // }; + // + // var patient2 = new Person + // { + // FirstName = "firstNamePatient2", + // LastName = "lastNamePatient2", + // BirthDate = new DateTime(), + // Gender = Gender.Female + // }; + // + // var patient = new Patient() + // { + // Id = ObjectId.GenerateNewId(), + // UnitId = _unit1, + // PointOfCareId = ObjectId.GenerateNewId(), + // PatientNumber = "patientNumber", + // Person = patient1 + // }; + // + // var location = new PatientLocation("PointOfCare", "bed"); + // + // await _repository.InsertOneAsync(patient); + // + // var result = await _repository.FindByLocation(location); + // + // Assert.That(result, Is.Not.Null); + // Assert.Multiple(() => + // { + // Assert.That(result.Person?.FirstName, Is.EqualTo("firstNamePatient1")); + // Assert.That(result.PatientNumber, Is.EqualTo("patientNumber")); + // }); + // await _repository.UpdatePatientData(patient.Id, "patientNumberUpdate", patient2, false); + // + // var resultUpdate = await _repository.FindByLocation(location); + // + // Assert.That(resultUpdate, Is.Not.Null); + // Assert.Multiple(() => + // { + // Assert.That(resultUpdate.Person?.FirstName, Is.EqualTo("firstNamePatient2")); + // Assert.That(result.PatientNumber, Is.EqualTo("patientNumber")); + // }); + // await _repository.Delete(patient.Id); + // } + + // [Test] + // public async Task UpdateOne() + // { + // var person1 = new Person + // { + // FirstName = "firstNamePatient1", + // LastName = "lastNamePatient1", + // BirthDate = new DateTime(), + // Gender = Gender.Male + // }; + // + // var person2 = new Person + // { + // FirstName = "firstNamePatient2", + // LastName = "lastNamePatient2", + // BirthDate = new DateTime(), + // Gender = Gender.Female + // }; + // + // var patient1 = new Patient() + // { + // Id = ObjectId.GenerateNewId(), + // UnitId = _unit1, + // PointOfCareId = ObjectId.GenerateNewId(), + // PatientNumber = "patientNumber", + // Person = person1 + // }; + // + // var patient2 = new Patient() + // { + // Id = patient1.Id, + // UnitId = _unit1, + // PointOfCareId = ObjectId.GenerateNewId(), + // PatientNumber = "patientNumberUpdate", + // Person = person2 + // }; + // + // var location1 = new PatientLocation("PointOfCare", "bed"); + // var location2 = new PatientLocation("PointOfCare2", "bed2"); + // + // await _repository.InsertOneAsync(patient1); + // + // var result = await _repository.FindByLocation(location1); + // + // Assert.That(result, Is.Not.Null); + // Assert.Multiple(() => + // { + // Assert.That(result.Person?.FirstName, Is.EqualTo("firstNamePatient1")); + // Assert.That(result.PatientNumber, Is.EqualTo("patientNumber")); + // }); + // await _repository.UpdateOne(patient2); + // + // var resultLocation1 = await _repository.FindByLocation(location1); + // + // Assert.That(resultLocation1, Is.Null); + // + // var resultLocation2 = await _repository.FindByLocation(location2); + // + // Assert.That(resultLocation2, Is.Not.Null); + // Assert.Multiple(() => + // { + // Assert.That(resultLocation2.Person?.FirstName, Is.EqualTo("firstNamePatient2")); + // Assert.That(resultLocation2.PatientNumber, Is.EqualTo("patientNumberUpdate")); + // }); + // await _repository.Delete(patient1.Id); + // } + + [Test] + public async Task FindByPatientId_Not_Find_Return_Patient() + { + var result = await _repository.FindByPatientId(""); + + Assert.That(result, Is.Null); + } + + // [Test] + // public async Task FindByPatientId_Find_Return_Patient() + // { + // var result = await _repository.FindByPatientId(_poc1.Patient.Id); + // + // Assert.That(result, Is.Not.Null); + // Assert.That(result.PatientNumber, Is.EqualTo("patientNumber1")); + // } + + [Test] + public async Task FindByPointOfCare_Find_Return_Patients() + { + var result = await _repository.FindByPointOfCare(_poc1.Id); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.Not.Empty); + // Assert.That(result.Count.Equals(1)); + } + + [Test] + public async Task FindByPointOfCare_Not_Find_Return_Patients() + { + var result = await _repository.FindByPointOfCare(ObjectId.GenerateNewId()); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.Empty); + } + + [Test] + public async Task FindDischargedPatients_Find_Return_Patients() + { + var result = await _repository.FindDischargedPatients(); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.Not.Empty); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/PoCMappingRepositoryTest.cs b/adas-core.Test/Repositories/PoCMappingRepositoryTest.cs new file mode 100644 index 00000000..3500111b --- /dev/null +++ b/adas-core.Test/Repositories/PoCMappingRepositoryTest.cs @@ -0,0 +1,78 @@ +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class PoCMappingRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + var mappingItem1 = new PoCMappingItem + { + OriginalPoC = "original1", + NewPoC = "new1", + Beds = + [ + new List { "bed1", "bed 1" }, + new List { "bed2", "bed 2" }, + new List { "bed3", "bed 3" }, + new List { "bed4", "bed 4" }, + new List { "bed5", "bed 5" }, + new List { "bed6", "bed 6" }, + new List { "bed7", "bed 7" }, + new List { "bed8", "bed 8" }, + new List { "bed9", "bed 9" }, + new List { "bed10", "bed 10" }, + new List { "bed11", "bed 11" } + ] + }; + + var pointOfCares = new List { mappingItem1 }; + + var mapping = new PoCMapping + { + Id = "PV1", + PointOfCares = pointOfCares + }; + + await IntegrationDb.Database.DropCollectionAsync("mappings"); + await IntegrationDb.Database.CreateCollectionAsync("mappings"); + + _repository = new PoCMappingRepository(_optionsApiSettings, IntegrationDb.Database); + + await _repository.InsertOneAsync(mapping); + } + + private PoCMappingRepository _repository; + + private readonly ApiSettings _apiSettings = new(); + + private IOptions _optionsApiSettings; + + + [Test] + public async Task FindByKey_Find_Return_Mapping() + { + var result = await _repository.FindByKey("PV1"); + + Assert.That(result, Is.Not.Null); + Assert.That(result!.PointOfCares, Has.Count.GreaterThan(0)); + Assert.That(result.PointOfCares[0].Beds, Has.Count.GreaterThan(0)); + } + + [Test] + public async Task FindByKey_Not_Find_Return_Mapping() + { + var result = await _repository.FindByKey("PV2"); + + Assert.That(result, Is.Null); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/PoCSettingsRepositoryTest.cs b/adas-core.Test/Repositories/PoCSettingsRepositoryTest.cs new file mode 100644 index 00000000..d286bf53 --- /dev/null +++ b/adas-core.Test/Repositories/PoCSettingsRepositoryTest.cs @@ -0,0 +1,117 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +public class PoCSettingsRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + await IntegrationDb.Database.DropCollectionAsync(_apiSettings.PoCSettings); + await IntegrationDb.Database.CreateCollectionAsync(_apiSettings.PoCSettings); + + _repository = new PoCSettingsRepository(_optionsApiSettings, IntegrationDb.Database); + + await _repository.InsertOneAsync(TestPoCSettings); + } + + private PoCSettingsRepository _repository; + + private readonly ApiSettings _apiSettings = new() + { + PoCSettings = "poc_settings" + }; + + private IOptions _optionsApiSettings; + + private static readonly ObjectId TestObjectId = ObjectId.GenerateNewId(); + private static readonly PatientLocation TestLocation = new("POC1", "Bed1"); + + private static readonly PoCSettings TestPoCSettings = new() + { + Id = TestObjectId, + PatientLocation = TestLocation + }; + + [Test] + public async Task Delete_WhenCalled_ShouldRemovePoCSettings() + { + // Arrange + var newObjectId = ObjectId.GenerateNewId(); + var newPoCSettings = new PoCSettings + { + Id = newObjectId, + PatientLocation = new PatientLocation("poc2", "bed2") + }; + await _repository.InsertOneAsync(newPoCSettings); + + // Act + await _repository.Delete(newObjectId); + + // Assert + var result = await _repository.FindById(newObjectId); + Assert.That(result, Is.Null); + } + + [Test] + public async Task FindAll_WhenCalled_ShouldReturnAllPoCSettings() + { + // Act + var result = await _repository.FindAll(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.Not.Empty); + } + + [Test] + public async Task FindById_WhenCalled_ShouldReturnPoCSettings() + { + // Act + var result = await _repository.FindById(TestObjectId); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result?.Id, Is.EqualTo(TestObjectId)); + } + + [Test] + public async Task FindByLocation_WhenCalled_ShouldReturnPoCSettings() + { + // Act + var result = await _repository.FindByLocation(TestLocation); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result?.PatientLocation, Is.EqualTo(TestLocation)); + } + + [Test] + public async Task Update_WhenCalled_ShouldUpdatePoCSettings() + { + // Arrange + var updatedPoCSettings = new PoCSettings + { + Id = TestObjectId, + PatientLocation = new PatientLocation("POC3", "Bed3") + }; + + // Act + await _repository.Update(updatedPoCSettings); + + // Assert + var result = await _repository.FindById(TestObjectId); + Assert.That(result, Is.Not.Null); + //Assert.That(result?.PatientLocation?.PointOfCare, Is.EqualTo("POC3")); + Assert.That(result?.PatientLocation?.Bed, Is.EqualTo("Bed3")); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/PointOfCareRepositoryTests.cs b/adas-core.Test/Repositories/PointOfCareRepositoryTests.cs new file mode 100644 index 00000000..48991524 --- /dev/null +++ b/adas-core.Test/Repositories/PointOfCareRepositoryTests.cs @@ -0,0 +1,181 @@ +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Repositories; +using adas_core.Test.Utilities; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Moq; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class PointOfCareRepositoryTests +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(new ApiSettings()); + _mockCollection = new Mock>(); + _mockDatabase = new Mock(); + _mockDatabase.Setup(db => db.GetCollection(It.IsAny(), null)) + .Returns(_mockCollection.Object); + + await IntegrationDb.Database.DropCollectionAsync("pointOfCares"); + await IntegrationDb.Database.CreateCollectionAsync("pointOfCares"); + _repository = new PointOfCareRepository(_optionsApiSettings, IntegrationDb.Database); + } + + private PointOfCareRepository _repository; + private Mock> _mockCollection; + private Mock _mockDatabase; + private IOptions _optionsApiSettings; + + [Test] + public async Task InsertOneAsync_ValidPointOfCare_InsertsSuccessfully() + { + // Arrange + var pointOfCare = TestUtilities.CreateValidPointOfCare(); + + // Act + await _repository.InsertOneAsync(pointOfCare); + + // Assert + var insertedPointOfCare = await _repository.FindById(pointOfCare.Id); + Assert.That(insertedPointOfCare, Is.Not.Null, "Inserted point of care should not be null"); + } + + [Test] + public async Task Delete_ValidId_DeletesSuccessfully() + { + var pointOfCare = TestUtilities.CreateValidPointOfCare(); + await _repository.InsertOneAsync(pointOfCare); + + await _repository.Delete(pointOfCare.Id); + + // Assert + var deletedPointOfCare = await _repository.FindById(pointOfCare.Id); + Assert.That(deletedPointOfCare, Is.Null, "Deleted point of care should be null"); + } + + [Test] + public async Task Update_ValidPointOfCare_UpdatesSuccessfully() + { + // Arrange + var originalPointOfCare = TestUtilities.CreateValidPointOfCare(); + await _repository.InsertOneAsync(originalPointOfCare); // Insert the original point of care + + // Modify some properties to update + originalPointOfCare.Room = "Updated Room"; + originalPointOfCare.Bed = "Updated Bed"; + + // Act + await _repository.Update(originalPointOfCare); // Update the point of care + + // Retrieve the updated point of care + var updatedPointOfCare = await _repository.FindById(originalPointOfCare.Id); + + // Assert + Assert.That(updatedPointOfCare, Is.Not.Null, "Updated point of care should not be null"); + Assert.That(updatedPointOfCare?.Room, Is.EqualTo(originalPointOfCare.Room), "Room should be updated"); + Assert.That(updatedPointOfCare?.Bed, Is.EqualTo(originalPointOfCare.Bed), "Bed should be updated"); + } + + + [Test] + public async Task FindById_ExistingId_ReturnsPointOfCare() + { + // Arrange + var pointOfCareId = ObjectId.GenerateNewId(); + var expectedPointOfCare = TestUtilities.CreateValidPointOfCare(); + expectedPointOfCare.Id = pointOfCareId; + + // Insert a PointOfCare document into the database + await _repository.InsertOneAsync(expectedPointOfCare); + + // Act + var result = await _repository.FindById(pointOfCareId); + + // Assert + Assert.That(result, Is.Not.Null, "Returned PointOfCare should not be null"); + Assert.That(result!.Id, Is.EqualTo(expectedPointOfCare.Id), "Returned PointOfCare should have the expected ID"); + // Add additional assertions to compare other properties if needed + } + + [Test] + public async Task FindByUnitAndStatus_ExistingUnitAndStatus_ReturnsMatchingPointOfCares() + { + // Arrange + var expectedPointOfCare = TestUtilities.CreateValidPointOfCare(); + + // Insert PointOfCare documents into the database with the specified unit ID and status + + await _repository.InsertOneAsync(expectedPointOfCare); + + // Act + var result = await _repository.FindByUnitAndStatus(expectedPointOfCare.UnitId, expectedPointOfCare.Status); + + var resultList = result.ToList(); + // Assert + Assert.That(resultList, Is.Not.Null, "Returned collection should not be null"); + Assert.That(resultList.First().Id, Is.EqualTo(expectedPointOfCare.Id), + "Returned collection should contain expected PointOfCare object"); + } + + [Test] + public async Task FindByRoom_ValidRoom_ReturnsMatchingPointOfCares() + { + var expectedPointOfCare = TestUtilities.CreateValidPointOfCare(); + await _repository.InsertOneAsync(expectedPointOfCare); + + // Act + if (expectedPointOfCare.Unit != null) + { + var result = await _repository.FindByRoom(expectedPointOfCare.Room); + + var resultList = result?.ToList(); + + // Assert + Assert.That(resultList, Is.Not.Null, "Returned collection should not be null"); + + Assert.That(resultList?.Any(p => p.Id == expectedPointOfCare.Id), Is.True, + "Returned collection should contain expected PointOfCare object"); + } + } + + [Test] + public async Task FindByBed_ValidBed_ReturnsMatchingPointOfCares() + { + var expectedPointOfCare = TestUtilities.CreateValidPointOfCare(); + await _repository.InsertOneAsync(expectedPointOfCare); + + // Act + var result = await _repository.FindByBed(expectedPointOfCare.Bed); + + // Convert the result to a list or an array + var resultList = result?.ToList(); + + // Assert + Assert.That(resultList, Is.Not.Null, "Returned collection should not be null"); + + // Check if the expected point of care is contained within the result + Assert.That(resultList?.First().Bed, Is.EqualTo(expectedPointOfCare.Bed), + "Returned collection should contain expected PointOfCare object"); + } + + [Test] + public async Task GetAll_ReturnsAllPointOfCares() + { + var poc = TestUtilities.CreateValidPointOfCare(); + await _repository.InsertOneAsync(poc); + + var result = await _repository.GetAll(); + + var resultList = result?.ToList(); + // Assert + Assert.That(resultList?.Any(p => p.Id == poc.Id), Is.True, + "Returned collection should contain the expected PointOfCare object with the specified Id"); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/PumpAlarmEventRepositoryTest.cs b/adas-core.Test/Repositories/PumpAlarmEventRepositoryTest.cs new file mode 100644 index 00000000..a21ec64b --- /dev/null +++ b/adas-core.Test/Repositories/PumpAlarmEventRepositoryTest.cs @@ -0,0 +1,395 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Pumps; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class PumpAlarmEventRepositoryTest +{ + private PumpAlarmEventRepository _repo; + private IOptions _apiSettings; + + private static readonly DateTime Now = DateTime.UtcNow; + private const string DeviceA = "Device-A"; + private const string DeviceB = "Device-B"; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + + // ------------------------------------------------------------------- + // INIT – Setup inicial de colección con índices y datos de prueba + // ------------------------------------------------------------------- + [OneTimeSetUp] + public async Task Init() + { + _apiSettings = Options.Create(new ApiSettings + { + PumpAlarmEvent = "pump_alarm_event" + }); + + await IntegrationDb.Database.DropCollectionAsync("pump_alarm_event"); + await IntegrationDb.Database.CreateCollectionAsync("pump_alarm_event"); + + _repo = new PumpAlarmEventRepository( + _apiSettings, + IntegrationDb.Database + ); + + await _repo.CreateIndexes(); + + var initialEvents = new List + { + new() + { + Id = ObjectId.GenerateNewId(), + DeviceId = DeviceA, + PatientId = PatientId, + AlarmType = PumpEnum.AlarmType.Attention, + Time = Now.AddSeconds(-1) + }, + new() + { + Id = ObjectId.GenerateNewId(), + DeviceId = DeviceA, + PatientId = PatientId, + AlarmType = PumpEnum.AlarmType.Occlusion, + Time = Now.AddSeconds(-3) + }, + new() + { + Id = ObjectId.GenerateNewId(), + DeviceId = DeviceB, + PatientId = PatientId, + AlarmType = PumpEnum.AlarmType.Occlusion, + Time = Now.AddSeconds(-2) + } + }; + + await IntegrationDb.Database + .GetCollection("pump_alarm_event") + .InsertManyAsync(initialEvents); + + var count = await _repo.Collection.CountDocumentsAsync(_ => true); + Assert.That(count, Is.EqualTo(3)); + } + + // ------------------------------------------------------------------- + // INSERT + // ------------------------------------------------------------------- + [Test] + public async Task InsertAsync_Works() + { + var evt = new PumpAlarmEvent + { + Id = ObjectId.GenerateNewId(), + DeviceId = "Device-C", + PatientId = PatientId, + AlarmType = PumpEnum.AlarmType.Occlusion, + Time = Now + }; + + await _repo.InsertAsync(evt); + + var found = await _repo.Collection.FindAsync(x => x.Id == evt.Id); + Assert.That(found.FirstOrDefault(), Is.Not.Null); + } + + // ------------------------------------------------------------------- + // FIND BY DEVICE ID + // ------------------------------------------------------------------- + [Test] + public async Task FindByDeviceIdAsync_ReturnsOrdered() + { + var result = await _repo.FindByDeviceIdAsync(DeviceA); + + var list = result.ToList(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(list, Is.Not.Empty); + Assert.That(list.First().Time, Is.GreaterThanOrEqualTo(list.Last().Time)); + } + } + + [Test] + public async Task FindByDeviceIdAsync_WithDateRange_Works() + { + var from = Now.AddSeconds(-2); + var to = Now; + + var result = await _repo.FindByDeviceIdAsync(DeviceA, from, to); + + using (Assert.EnterMultipleScope()) + { + var pumpAlarmEvents = result.ToList(); + Assert.That(pumpAlarmEvents, Is.Not.Empty); + Assert.That(pumpAlarmEvents.All(x => x.Time >= from && x.Time <= to), Is.True); + } + } + + [Test] + public async Task FindByDeviceIdAsync_WithLimit_Works() + { + var result = await _repo.FindByDeviceIdAsync(DeviceA, limit: 1); + + var list = result.ToList(); + Assert.That(list, Has.Count.EqualTo(1)); + } + + // ------------------------------------------------------------------- + // FIND LAST BY DEVICE + // ------------------------------------------------------------------- + [Test] + public async Task FindLastByDeviceIdAsync_Works() + { + // Arrange: dataset ya insertado en Init() + + var events = await _repo.FindByDeviceIdAsync("Device-A"); + var expected = events.OrderByDescending(e => e.Time).First(); + + // Act + var evt = await _repo.FindLastByDeviceIdAsync("Device-A"); + + // Assert + Assert.That(evt, Is.Not.Null); + Assert.That(evt!.Id, Is.EqualTo(expected.Id)); // comparación robusta + } + + // ------------------------------------------------------------------- + // DELETE BY PATIENT ID + // ------------------------------------------------------------------- + [Test] + public async Task DeleteByPatientId_Works() + { + var patient2 = ObjectId.GenerateNewId(); + + var evt = new PumpAlarmEvent + { + Id = ObjectId.GenerateNewId(), + DeviceId = "Device-X", + PatientId = patient2, + AlarmType = PumpEnum.AlarmType.Occlusion, + Time = Now + }; + + await _repo.InsertAsync(evt); + + await _repo.DeleteByPatientId(patient2); + + var found = await _repo.Collection.FindAsync(x => x.PatientId == patient2); + Assert.That(await found.AnyAsync(), Is.False); + } + + // ------------------------------------------------------------------- + // UPDATE MANY (CAMBIO DE PATIENTID O SIMILAR) + // ------------------------------------------------------------------- + + [Test] + public async Task UpdateManyObjectIdByFiledNameAsync_Works() + { + var oldId = ObjectId.GenerateNewId(); + var newId = ObjectId.GenerateNewId(); + + var evt = new PumpAlarmEvent + { + Id = ObjectId.GenerateNewId(), + DeviceId = "Device-U", + PatientId = oldId, + AlarmType = PumpEnum.AlarmType.Attention, + Time = Now + }; + + await _repo.InsertAsync(evt); + + var updated = await _repo.UpdateManyObjectIdByFiledNameAsync("PatientId", newId, oldId); + + Assert.That(updated, Is.EqualTo(1)); + + var found = await _repo.Collection.FindAsync(x => x.PatientId == newId); + Assert.That(await found.AnyAsync(), Is.True); + } + + [Test] + public async Task FindByDeviceIdAsync_ReturnsEmpty_WhenOutOfRange() + { + var from = Now.AddYears(-2); + var to = Now.AddYears(-2).AddDays(1); + + var result = await _repo.FindByDeviceIdAsync("Device-A", from, to); + + Assert.That(result, Is.Empty); + } + + [Test] + public async Task FindByDeviceIdAsync_RespectsLimitAndOrder() + { + // Semilla adicional para asegurar > 1 + var events = new[] + { + new PumpAlarmEvent { Id = ObjectId.GenerateNewId(), DeviceId = DeviceA, PatientId = PatientId, AlarmType = PumpEnum.AlarmType.AirInLine, Time = Now.AddMilliseconds(-10) }, + new PumpAlarmEvent { Id = ObjectId.GenerateNewId(), DeviceId = DeviceA, PatientId = PatientId, AlarmType = PumpEnum.AlarmType.Occlusion, Time = Now.AddMilliseconds(-5) } + }; + await _repo.Collection.InsertManyAsync(events); + + var result = await _repo.FindByDeviceIdAsync(DeviceA, limit: 1); + var list = result.ToList(); + + using (Assert.EnterMultipleScope()) + { + Assert.That(list, Has.Count.EqualTo(1)); + var last = await _repo.FindLastByDeviceIdAsync(DeviceA); + Assert.That(list[0].Id, Is.EqualTo(last!.Id)); + } + } + + [Test] + public async Task InsertAsync_AllowsNullOptionalFields() + { + var evt = new PumpAlarmEvent + { + Id = ObjectId.GenerateNewId(), + DeviceId = "Device-Nulls", + PatientId = null, // opcional + AlarmType = null, // opcional + Time = Now + }; + + await _repo.InsertAsync(evt); + + var found = await _repo.Collection.FindAsync(x => x.Id == evt.Id); + Assert.That(found.FirstOrDefault(), Is.Not.Null); + } + + [Test] + public async Task DeleteByPatientId_DeletesMany_AndIsIdempotent() + { + var pid = ObjectId.GenerateNewId(); + + var docs = Enumerable.Range(0, 3).Select(i => new PumpAlarmEvent + { + Id = ObjectId.GenerateNewId(), + DeviceId = "Device-Del", + PatientId = pid, + AlarmType = PumpEnum.AlarmType.Attention, + Time = Now.AddSeconds(-i) + }); + + await _repo.Collection.InsertManyAsync(docs); + + // Primera vez: borra 3 + await _repo.DeleteByPatientId(pid); + var left = await _repo.Collection.FindAsync(x => x.PatientId == pid); + Assert.That(left.ToList(), Is.Empty); + + Func act = async () => await _repo.DeleteByPatientId(pid); + Assert.That(act, Throws.Nothing); + } + + + [Test] + public async Task UpdateManyObjectIdByFiledNameAsync_UpdatesMultiple() + { + var oldId = ObjectId.GenerateNewId(); + var newId = ObjectId.GenerateNewId(); + + var events = Enumerable.Range(0, 4).Select(i => new PumpAlarmEvent + { + Id = ObjectId.GenerateNewId(), + DeviceId = "Device-U", + PatientId = oldId, + AlarmType = PumpEnum.AlarmType.AirInLine, + Time = Now.AddMilliseconds(-i) + }); + + await _repo.Collection.InsertManyAsync(events); + + var modified = await _repo.UpdateManyObjectIdByFiledNameAsync("PatientId", newId, oldId); + + using (Assert.EnterMultipleScope()) + { + Assert.That(modified, Is.EqualTo(4)); + var foundNew = await _repo.Collection.FindAsync(x => x.PatientId == newId); + Assert.That(foundNew.ToList(), Has.Count.EqualTo(4)); + } + } + [Test] + public async Task FindByDeviceIdAsync_InclusiveRange_BoundsRespected() + { + var start = Now.AddMinutes(-30); + var end = Now.AddMinutes(-29); + + var exactEvt = new PumpAlarmEvent + { + Id = ObjectId.GenerateNewId(), + DeviceId = "Device-Range", + PatientId = PatientId, + AlarmType = PumpEnum.AlarmType.Attention, + Time = end // exactamente igual al to + }; + + await _repo.InsertAsync(exactEvt); + + var result = await _repo.FindByDeviceIdAsync("Device-Range", from: start, to: end); + + using (Assert.EnterMultipleScope()) + { + var pumpAlarmEvents = result as PumpAlarmEvent[] ?? result.ToArray(); + Assert.That(pumpAlarmEvents, Is.Not.Empty); + Assert.That(pumpAlarmEvents.All(r => r.Time >= start && r.Time <= end), Is.True); + } + } + + [Test] + public async Task FindLastByDeviceIdAsync_TracksNewest() + { + const string dev = "Device-LastCheck"; + + var e1 = new PumpAlarmEvent { Id = ObjectId.GenerateNewId(), DeviceId = dev, PatientId = PatientId, AlarmType = PumpEnum.AlarmType.AirInLine, Time = Now.AddSeconds(-10) }; + var e2 = new PumpAlarmEvent { Id = ObjectId.GenerateNewId(), DeviceId = dev, PatientId = PatientId, AlarmType = PumpEnum.AlarmType.Occlusion, Time = Now.AddSeconds(-5) }; + + await _repo.Collection.InsertManyAsync([e1, e2]); + + var last1 = await _repo.FindLastByDeviceIdAsync(dev); + Assert.That(last1!.Id, Is.EqualTo(e2.Id)); + + var e3 = new PumpAlarmEvent { Id = ObjectId.GenerateNewId(), DeviceId = dev, PatientId = PatientId, AlarmType = PumpEnum.AlarmType.Attention, Time = Now }; + await _repo.InsertAsync(e3); + + var last2 = await _repo.FindLastByDeviceIdAsync(dev); + Assert.That(last2!.Id, Is.EqualTo(e3.Id)); + } + + [Test] + public async Task FindByDeviceIdAsync_MultipleOverlappingRanges() + { + const string dev = "Device-Timeline"; + + var batch = new[] + { + new PumpAlarmEvent { Id = ObjectId.GenerateNewId(), DeviceId = dev, PatientId = PatientId, AlarmType = PumpEnum.AlarmType.AirInLine, Time = Now.AddMinutes(-20) }, + new PumpAlarmEvent { Id = ObjectId.GenerateNewId(), DeviceId = dev, PatientId = PatientId, AlarmType = PumpEnum.AlarmType.Occlusion, Time = Now.AddMinutes(-10) }, + new PumpAlarmEvent { Id = ObjectId.GenerateNewId(), DeviceId = dev, PatientId = PatientId, AlarmType = PumpEnum.AlarmType.Attention, Time = Now.AddMinutes(-5) } + }; + + await _repo.Collection.InsertManyAsync(batch); + + var r1 = await _repo.FindByDeviceIdAsync(dev, from: Now.AddMinutes(-30), to: Now.AddMinutes(-15)); + var r2 = await _repo.FindByDeviceIdAsync(dev, from: Now.AddMinutes(-12), to: Now.AddMinutes(-8)); + var r3 = await _repo.FindByDeviceIdAsync(dev, from: Now.AddMinutes(-6), to: Now); + + using (Assert.EnterMultipleScope()) + { + var r1List = r1.ToList(); + Assert.That(r1List, Has.Count.EqualTo(1)); // -20 + var r2List = r2.ToList(); + Assert.That(r2List, Has.Count.EqualTo(1)); // -10 + var r3List = r3.ToList(); + Assert.That(r3List, Has.Count.EqualTo(1)); // -5 + } + } + +} diff --git a/adas-core.Test/Repositories/PumpAlarmStateRepositoryTest.cs b/adas-core.Test/Repositories/PumpAlarmStateRepositoryTest.cs new file mode 100644 index 00000000..f47e4010 --- /dev/null +++ b/adas-core.Test/Repositories/PumpAlarmStateRepositoryTest.cs @@ -0,0 +1,323 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Pumps; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class PumpAlarmStateRepositoryTest +{ + private PumpAlarmStateRepository _repo; + private IOptions _apiSettings; + + private static readonly DateTime Now = DateTime.UtcNow; + private const string DeviceA = "Device-A"; + private const string DeviceB = "Device-B"; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + + // ------------------------------------------------------------------- + // INIT: preparar colección, índices y datos iniciales + // ------------------------------------------------------------------- + [OneTimeSetUp] + public async Task Init() + { + _apiSettings = Options.Create(new ApiSettings + { + PumpAlarmState = "pump_alarm_state" + }); + + await IntegrationDb.Database.DropCollectionAsync("pump_alarm_state"); + await IntegrationDb.Database.CreateCollectionAsync("pump_alarm_state"); + + _repo = new PumpAlarmStateRepository( + _apiSettings, + IntegrationDb.Database + ); + + await _repo.CreateIndexes(); + + // datos iniciales + var initial = new List + { + new() + { + Id = ObjectId.GenerateNewId(), + DeviceId = DeviceA, + PatientId = PatientId, + AlarmType = PumpEnum.AlarmType.Attention, + AlarmCodeMdc = "AC001", + LastUpdated = Now.AddSeconds(-3) + }, + new() + { + Id = ObjectId.GenerateNewId(), + DeviceId = DeviceA, + PatientId = PatientId, + AlarmType = PumpEnum.AlarmType.Occlusion, + AlarmCodeMdc = "AC002", + LastUpdated = Now.AddSeconds(-1) + }, + new() + { + Id = ObjectId.GenerateNewId(), + DeviceId = DeviceB, + PatientId = PatientId, + AlarmType = PumpEnum.AlarmType.Occlusion, + AlarmCodeMdc = "AC003", + LastUpdated = Now.AddSeconds(-2) + } + }; + + await _repo.Collection.InsertManyAsync(initial); + + var count = await _repo.Collection.CountDocumentsAsync(_ => true); + Assert.That(count, Is.EqualTo(3)); + } + + // ------------------------------------------------------------------- + // FIND ACTIVE + // ------------------------------------------------------------------- + [Test] + public async Task FindActiveAsync_ReturnsCorrectAlarm() + { + var alarm = await _repo.FindActiveAsync(DeviceA, PumpEnum.AlarmType.Occlusion, "AC002"); + + using (Assert.EnterMultipleScope()) + { + Assert.That(alarm, Is.Not.Null); + Assert.That(alarm!.DeviceId, Is.EqualTo(DeviceA)); + Assert.That(alarm.AlarmType, Is.EqualTo(PumpEnum.AlarmType.Occlusion)); + Assert.That(alarm.AlarmCodeMdc, Is.EqualTo("AC002")); + } + } + + [Test] + public async Task FindActiveAsync_ReturnsNull_WhenNotFound() + { + var alarm = await _repo.FindActiveAsync(DeviceA, PumpEnum.AlarmType.Occlusion, "XXX"); + Assert.That(alarm, Is.Null); + } + + // ------------------------------------------------------------------- + // UPSERT ACTIVE + // ------------------------------------------------------------------- + [Test] + public async Task UpsertActiveAsync_InsertsNew() + { + var alarm = new PumpAlarmState + { + Id = ObjectId.GenerateNewId(), + DeviceId = "Device-C", + AlarmType = PumpEnum.AlarmType.Attention, + AlarmCodeMdc = "NEW", + PatientId = PatientId, + LastUpdated = Now + }; + + await _repo.UpsertActiveAsync(alarm); + + var found = await _repo.FindActiveAsync("Device-C", PumpEnum.AlarmType.Attention, "NEW"); + + Assert.That(found, Is.Not.Null); + } + + [Test] + public async Task UpsertActiveAsync_UpdatesExisting() + { + var updated = new PumpAlarmState + { + Id = ObjectId.GenerateNewId(), // será ignorado por replace + DeviceId = DeviceA, + AlarmType = PumpEnum.AlarmType.Occlusion, + AlarmCodeMdc = "AC002", + PatientId = PatientId, + LastUpdated = Now.AddMinutes(1) + }; + + await _repo.UpsertActiveAsync(updated); + + var found = await _repo.FindActiveAsync(DeviceA, PumpEnum.AlarmType.Occlusion, "AC002"); + + Assert.That(found, Is.Not.Null); + Assert.That(found!.LastUpdated, Is.EqualTo(updated.LastUpdated).Within(TimeSpan.FromMilliseconds(2))); + } + + // ------------------------------------------------------------------- + // REMOVE ACTIVE ALARM + // ------------------------------------------------------------------- + [Test] + public async Task RemoveAsync_Works() + { + var alarm = new PumpAlarmState + { + Id = ObjectId.GenerateNewId(), + DeviceId = "Device-D", + AlarmType = PumpEnum.AlarmType.Attention, + AlarmCodeMdc = "D1", + PatientId = PatientId, + LastUpdated = Now + }; + + await _repo.UpsertActiveAsync(alarm); + + await _repo.RemoveAsync("Device-D", PumpEnum.AlarmType.Attention, "D1"); + + var found = await _repo.FindActiveAsync("Device-D", PumpEnum.AlarmType.Attention, "D1"); + Assert.That(found, Is.Null); + } + + // ------------------------------------------------------------------- + // DELETE BY PATIENT ID + // ------------------------------------------------------------------- + [Test] + public async Task DeleteByPatientId_Works() + { + var pid = ObjectId.GenerateNewId(); + + var alarm = new PumpAlarmState + { + Id = ObjectId.GenerateNewId(), + DeviceId = "Device-E", + AlarmType = PumpEnum.AlarmType.Occlusion, + AlarmCodeMdc = "DDD", + PatientId = pid, + LastUpdated = Now + }; + + await _repo.UpsertActiveAsync(alarm); + + await _repo.DeleteByPatientId(pid); + + var list = await _repo.Collection.FindAsync(x => x.PatientId == pid); + Assert.That(await list.AnyAsync(), Is.False); + } + + // ------------------------------------------------------------------- + // FIND ALL ACTIVE BY DEVICE + // ------------------------------------------------------------------- + [Test] + public async Task FindAllActiveByDeviceAsync_ReturnsCorrect() + { + var result = await _repo.FindAllActiveByDeviceAsync(DeviceA); + + var pumpAlarmStates = result.ToList(); + Assert.That(pumpAlarmStates, Is.Not.Empty); + Assert.That(pumpAlarmStates.All(x => x.DeviceId == DeviceA), Is.True); + } + + // ------------------------------------------------------------------- + // UPDATE MANY BY FIELD + // ------------------------------------------------------------------- + [Test] + public async Task UpdateManyObjectIdByFieldNameAsync_Works() + { + var oldId = ObjectId.GenerateNewId(); + var newId = ObjectId.GenerateNewId(); + + // Insert one alarm to update + var alarm = new PumpAlarmState + { + Id = ObjectId.GenerateNewId(), + DeviceId = "Device-F", + AlarmType = PumpEnum.AlarmType.Occlusion, + AlarmCodeMdc = "FFF", + PatientId = oldId, + LastUpdated = Now + }; + + await _repo.UpsertActiveAsync(alarm); + + var updated = await _repo.UpdateManyObjectIdByFieldNameAsync("PatientId", newId, oldId); + + using (Assert.EnterMultipleScope()) + { + Assert.That(updated, Is.EqualTo(1)); + + var found = await _repo.Collection.FindAsync(x => x.PatientId == newId); + Assert.That(await found.AnyAsync(), Is.True); + } + } + + [Test] + public async Task UpsertActiveAsync_NoDuplicateOnRepeatedCalls() + { + var alarm = new PumpAlarmState + { + Id = ObjectId.GenerateNewId(), + DeviceId = "Device-Repeat", + AlarmType = PumpEnum.AlarmType.Attention, + AlarmCodeMdc = "R1", + PatientId = PatientId, + LastUpdated = Now + }; + + await _repo.UpsertActiveAsync(alarm); + await _repo.UpsertActiveAsync(alarm); // segunda llamada idéntica + + var alarmsEnum = await _repo.FindAllActiveByDeviceAsync("Device-Repeat"); + var list = alarmsEnum.ToList(); + Assert.That(list, Has.Count.EqualTo(1)); + } + + [Test] + public async Task UpsertActiveAsync_InsertsWhenAlarmCodeIsNull() + { + var alarm = new PumpAlarmState + { + Id = ObjectId.GenerateNewId(), + DeviceId = "Device-NullCode", + AlarmType = PumpEnum.AlarmType.Occlusion, + AlarmCodeMdc = null, + PatientId = PatientId, + LastUpdated = Now + }; + + await _repo.UpsertActiveAsync(alarm); + + var found = await _repo.FindActiveAsync("Device-NullCode", PumpEnum.AlarmType.Occlusion, null); + + Assert.That(found, Is.Not.Null); + Assert.That(found!.AlarmCodeMdc, Is.Null); + } + + [Test] + public async Task RemoveAsync_RemovesAllMatching() + { + var alarms = new[] + { + new PumpAlarmState + { + Id = ObjectId.GenerateNewId(), + DeviceId = "Device-Multi", + AlarmType = PumpEnum.AlarmType.AirInLine, + AlarmCodeMdc = "XX1", + PatientId = PatientId, + LastUpdated = Now + }, + new PumpAlarmState + { + Id = ObjectId.GenerateNewId(), + DeviceId = "Device-Multi", + AlarmType = PumpEnum.AlarmType.AirInLine, + AlarmCodeMdc = "XX2", + PatientId = PatientId, + LastUpdated = Now + } + }; + + foreach (var a in alarms) + await _repo.UpsertActiveAsync(a); + + await _repo.RemoveAsync("Device-Multi", PumpEnum.AlarmType.AirInLine); + + var remaining = await _repo.FindAllActiveByDeviceAsync("Device-Multi"); + + Assert.That(remaining, Is.Empty); + } + +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/PumpArchiveRepositoryTest.cs b/adas-core.Test/Repositories/PumpArchiveRepositoryTest.cs new file mode 100644 index 00000000..f0ac475b --- /dev/null +++ b/adas-core.Test/Repositories/PumpArchiveRepositoryTest.cs @@ -0,0 +1,193 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Pumps; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Driver; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class PumpArchiveRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + var pump1 = new PumpObservation + { + Code = "Tile-6", + Time = Now, + Name = "g-1-pump-Tile-6", + Number = 2, + Total = 7, + TotalAux = 0, + Status = PumpEnum.Status.Infusing, + PumpMode = PumpEnum.Mode.Infusing, + InfusingStatus = PumpEnum.InfusingStatus.Infusing, + Pressure = new CommonPumpTypes.PumpValue + { + Value = 6, + Units = "mmHg" + }, + DrugName = "Nutrición Parenteral", + Concentration = new CommonPumpTypes.PumpValue + { + Value = 1, + Units = "ml/ml" + }, + DrugAmount = new CommonPumpTypes.PumpValue + { + Value = 1, + Units = "ml" + }, + DiluentVolume = new CommonPumpTypes.PumpValue + { + Value = 1, + Units = "ml" + }, + DoseRate = new CommonPumpTypes.PumpValue + { + Value = 27, + Units = "ml/h" + }, + Rate = new CommonPumpTypes.PumpValue + { + Value = 27, + Units = "ml/h" + }, + VolumeInfused = new CommonPumpTypes.PumpValue + { + Value = 127.52, + Units = "ml" + }, + VolumeRemaining = new CommonPumpTypes.PumpValue + { + Value = 0.00001 + }, + TimeRemaining = new CommonPumpTypes.PumpValue + { + Value = 0, + Units = "min" + }, + PatientWeight = new CommonPumpTypes.PumpValue + { + Value = 1, + Units = "kg" + }, + AlarmType = PumpEnum.AlarmType.Attention, + GatewayNumber = 1, + IsAux = false, + IsInfusing = true + }; + + var pump2 = new PumpObservation + { + Code = "Tile-6", + Time = Now.AddDays(-10), + Name = "g-1-pump-Tile-6", + Number = 2, + Total = 7, + TotalAux = 0, + Status = PumpEnum.Status.Infusing, + PumpMode = PumpEnum.Mode.Infusing, + InfusingStatus = PumpEnum.InfusingStatus.Infusing, + Pressure = new CommonPumpTypes.PumpValue + { + Value = 6, + Units = "mmHg" + }, + DrugName = "Nutrición Parenteral", + Concentration = new CommonPumpTypes.PumpValue + { + Value = 1, + Units = "ml/ml" + }, + DrugAmount = new CommonPumpTypes.PumpValue + { + Value = 1, + Units = "ml" + }, + DiluentVolume = new CommonPumpTypes.PumpValue + { + Value = 1, + Units = "ml" + }, + DoseRate = new CommonPumpTypes.PumpValue + { + Value = 27, + Units = "ml/h" + }, + Rate = new CommonPumpTypes.PumpValue + { + Value = 27, + Units = "ml/h" + }, + VolumeInfused = new CommonPumpTypes.PumpValue + { + Value = 127.52, + Units = "ml" + }, + VolumeRemaining = new CommonPumpTypes.PumpValue + { + Value = 0.00001 + }, + TimeRemaining = new CommonPumpTypes.PumpValue + { + Value = 0, + Units = "min" + }, + PatientWeight = new CommonPumpTypes.PumpValue + { + Value = 1, + Units = "kg" + }, + AlarmType = PumpEnum.AlarmType.Attention, + GatewayNumber = 1, + IsAux = false, + IsInfusing = true + }; + + await IntegrationDb.Database.DropCollectionAsync("archive_patients_pumpobservations"); + await IntegrationDb.Database.CreateCollectionAsync("archive_patients_pumpobservations"); + + _repository = new PumpArchiveRepository(_optionsApiSettings, IntegrationDb.Database); + + await _repository.InsertOneAsync(pump1); + await _repository.InsertOneAsync(pump2); + + Assert.That(await _repository.Collection.CountDocumentsAsync(_ => true), Is.EqualTo(2)); + } + + private PumpArchiveRepository _repository; + + private readonly ApiSettings _apiSettings = new() + { + ArchivePatientsPumpobservations = "archive_patients_pumpobservations" + }; + + private IOptions _optionsApiSettings; + + private static readonly DateTime Now = DateTime.Now; + + + [Test] + public async Task DeleteBeforeDate() + { + var result = await _repository.Collection.FindAsync(_ => true); + + Assert.That(result, Is.Not.Null); + Assert.That(result.ToList(), Has.Count.GreaterThan(1)); + + await _repository.DeleteBeforeDate(Now.AddDays(-1)); + + var resultDelete = await _repository.Collection.FindAsync(_ => true); + + Assert.That(resultDelete, Is.Not.Null); + Assert.That(resultDelete.ToList(), Has.Count.EqualTo(1)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/PumpObservationRepositoryTest.cs b/adas-core.Test/Repositories/PumpObservationRepositoryTest.cs new file mode 100644 index 00000000..6a897796 --- /dev/null +++ b/adas-core.Test/Repositories/PumpObservationRepositoryTest.cs @@ -0,0 +1,416 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Pumps; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class PumpObservationRepositoryTest +{ + private PumpObservationRepository _repo; + private IOptions _apiSettings; + + private static readonly DateTime Now = DateTime.UtcNow; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + + // ------------------------------------------------------------------- + // INIT – SETUP DE LA COLECCIÓN CON DATOS DE PRUEBA + // ------------------------------------------------------------------- + [OneTimeSetUp] + public async Task Init() + { + _apiSettings = Options.Create(new ApiSettings + { + PumpObservations = "pump_observations" + }); + + await IntegrationDb.Database.DropCollectionAsync("pump_observations"); + await IntegrationDb.Database.CreateCollectionAsync("pump_observations"); + + _repo = new PumpObservationRepository( + _apiSettings, + IntegrationDb.Database + ); + + await _repo.CreateIndexes(); + + var samples = new List + { + new() + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + DeviceId = "Device-A", + Name = "PumpX", + Time = Now.AddSeconds(-1), + MessageType = PumpEnum.PumpMessageType.Observation + }, + new() + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + DeviceId = "Device-A", + Name = "PumpY", + Time = Now.AddSeconds(-2), + MessageType = PumpEnum.PumpMessageType.Observation + }, + new() + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + DeviceId = "Device-A", + Name = "PumpX", + Time = Now.AddSeconds(-3), + MessageType = PumpEnum.PumpMessageType.Observation + } + }; + + await _repo.InsertManyAsync(samples); + + var count = await _repo.Collection.CountDocumentsAsync(_ => true); + Assert.That(count, Is.EqualTo(3)); + } + + // ------------------------------------------------------------------- + // FIND LAST OBSERVATIONS (LÍMITE = 2) + // ------------------------------------------------------------------- + [Test] + public async Task FindLastObservations_ReturnsTwo() + { + var list = await _repo.FindLastObservations(PatientId, "PumpX"); + + Assert.That(list, Is.Not.Null); + Assert.That(list, Has.Count.EqualTo(2)); + Assert.That(list[0].Time, Is.GreaterThanOrEqualTo(list[1].Time)); + } + + // ------------------------------------------------------------------- + // INSERT + // ------------------------------------------------------------------- + [Test] + public async Task InsertAsync_Works() + { + var pump = new PumpObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = ObjectId.GenerateNewId(), + DeviceId = "Device-B", + Time = Now + }; + + await _repo.InsertAsync(pump); + + var found = await _repo.Collection.FindAsync(x => x.Id == pump.Id); + Assert.That(found.FirstOrDefault(), Is.Not.Null); + } + + [Test] + public async Task InsertManyAsync_Works() + { + var list = new List + { + new() { Id = ObjectId.GenerateNewId(), DeviceId="D", Time=Now }, + new() { Id = ObjectId.GenerateNewId(), DeviceId="D", Time=Now } + }; + + await _repo.InsertManyAsync(list); + + var count = await _repo.Collection.CountDocumentsAsync(x => list.Select(y => y.Id).Contains(x.Id)); + Assert.That(count, Is.EqualTo(2)); + } + + // ------------------------------------------------------------------- + // FIND METHODS + // ------------------------------------------------------------------- + [Test] + public async Task FindByDeviceIdAsync_ReturnsOrdered() + { + var result = await _repo.FindByDeviceIdAsync("Device-A"); + + var pumpObservations = result.ToList(); + using (Assert.EnterMultipleScope()) + { + Assert.That(pumpObservations, Is.Not.Empty); + Assert.That(pumpObservations.First().Time, Is.GreaterThanOrEqualTo(pumpObservations.Last().Time)); + } + } + + [Test] + public async Task FindByPatientAsync_ReturnsOrdered() + { + var result = await _repo.FindByPatientAsync(PatientId); + + var pumpObservations = result.ToList(); + using (Assert.EnterMultipleScope()) + { + Assert.That(pumpObservations, Is.Not.Empty); + Assert.That(pumpObservations.First().Time, Is.GreaterThanOrEqualTo(pumpObservations.Last().Time)); + } + } + + [Test] + public async Task FindLastByDeviceIdAsync_Works() + { + var obs = await _repo.FindLastByDeviceIdAsync("Device-A"); + + Assert.That(obs, Is.Not.Null); + Assert.That(obs.DeviceId, Is.EqualTo("Device-A")); + } + + [Test] + public async Task FindByPatientId_Works() + { + var list = await _repo.FindByPatientId(PatientId); + + Assert.That(list.Any(), Is.True); + } + + // ------------------------------------------------------------------- + // AGGREGATED LAST OBSERVATIONS + // ------------------------------------------------------------------- + [Test] + public async Task AggregatedPatientLastObservations_DistinctByName() + { + var result = await _repo.AggregatedPatientLastObservations(PatientId); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result, Is.Not.Empty); + Assert.That(result, Has.Count.LessThanOrEqualTo(2)); + } + } + + // ------------------------------------------------------------------- + // DELETE METHODS + // ------------------------------------------------------------------- + [Test] + public async Task DeleteByPatientId_Works() + { + var pid = ObjectId.GenerateNewId(); + + await _repo.InsertAsync(new PumpObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = pid, + DeviceId = "Z", + Time = Now + }); + + await _repo.DeleteByPatientId(pid); + + var list = await _repo.FindByPatientId(pid); + Assert.That(list.Any(), Is.False); + } + + [Test] + public async Task DeleteOlderThanDaysAsync_Works() + { + var pid = ObjectId.GenerateNewId(); + + await _repo.InsertAsync(new PumpObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = pid, + Name = "TestOld", + Time = Now.AddDays(-10) + }); + + var deleted = await _repo.DeleteOlderThanDaysAsync(5); + + Assert.That(deleted, Is.GreaterThanOrEqualTo(1)); + } + + [Test] + public async Task DeleteOlderNumberAsync_Works() + { + const string name = "HistoryPump"; + + var obs = Enumerable.Range(0, 5).Select(i => + new PumpObservation + { + Id = ObjectId.GenerateNewId(), + DeviceId = "H", + Name = name, + Time = Now.AddSeconds(-i) + }); + + await _repo.InsertManyAsync(obs); + + var deleted = await _repo.DeleteOlderNumberAsync(name, 2); + + Assert.That(deleted, Is.EqualTo(3)); + } + + [Test] + public async Task DeleteKeepLastNAsync_Works() + { + var items = Enumerable.Range(0, 8).Select(i => + new PumpObservation + { + Id = ObjectId.GenerateNewId(), + DeviceId = "Device-Clean", + Time = Now.AddSeconds(-i) + }); + + await _repo.InsertManyAsync(items); + + var deleted = await _repo.DeleteKeepLastNAsync(3); + + Assert.That(deleted, Is.EqualTo(5)); + } + + // ------------------------------------------------------------------- + // UPDATE METHOD + // ------------------------------------------------------------------- + [Test] + public async Task UpdateManyObjectIdByFieldAsync_Works() + { + var oldId = ObjectId.GenerateNewId(); + var newId = ObjectId.GenerateNewId(); + + await _repo.InsertAsync(new PumpObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = oldId, + DeviceId = "UpdateTest", + Time = Now + }); + + var updated = await _repo.UpdateManyObjectIdByFieldAsync("PatientId", newId, oldId); + + Assert.That(updated, Is.EqualTo(1)); + + var found = await _repo.FindByPatientId(newId); + Assert.That(found.Any(), Is.True); + } + + // ------------------------------------------------------------------- + // AGGREGATION: LAST OBSERVATION TIME PER PATIENT + // ------------------------------------------------------------------- + [Test] + public async Task FindAllLastPatientObservationTimeAsync_Works() + { + var dict = await _repo.FindAllLastPatientObservationTimeAsync(); + + Assert.That(dict, Is.Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(dict.ContainsKey(PatientId), Is.True); + Assert.That(dict[PatientId], Is.LessThanOrEqualTo(DateTime.UtcNow)); + } + } + + [Test] + public async Task FindByDeviceIdAsync_EmptyWhenOutOfDateRange() + { + var from = Now.AddYears(-1); + var to = Now.AddYears(-1).AddHours(1); + + var result = await _repo.FindByDeviceIdAsync("Device-A", from, to); + + Assert.That(result, Is.Empty); + } + + [Test] + public async Task FindByPatientAsync_RespectsLimit() + { + var result = await _repo.FindByPatientAsync(PatientId, limit: 1); + var list = result.ToList(); + Assert.That(list, Has.Count.EqualTo(1)); + } + + [Test] + public async Task FindLastByDeviceIdAsync_ReturnsNullWhenNotExists() + { + var obs = await _repo.FindLastByDeviceIdAsync("Device-DoesNotExist"); + + Assert.That(obs, Is.Null); + } + + [Test] + public async Task InsertManyAsync_IgnoresEmptyList() + { + var empty = new List(); + + Func act = () => _repo.InsertManyAsync(empty); + + Assert.That(act, Throws.Nothing); + } + + + [Test] + public async Task DeleteOlderNumberAsync_DoesNothingWhenCountBelowLimit() + { + const string name = "LimitTestPump"; + + var obs = new PumpObservation + { + Id = ObjectId.GenerateNewId(), + DeviceId = "Device-X", + Name = name, + Time = Now + }; + + await _repo.InsertAsync(obs); + + var deleted = await _repo.DeleteOlderNumberAsync(name, maxCount: 5); + + Assert.That(deleted, Is.Zero); + } + + [Test] + public async Task DeleteOlderThanDaysAsync_DoesNotDeleteRecentObservations() + { + var obs = new PumpObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + DeviceId = "Device-Recent", + Name = "RecentPump", + Time = Now.AddMinutes(-1) // reciente + }; + + await _repo.InsertAsync(obs); + + var deleted = await _repo.DeleteOlderThanDaysAsync(10); + + Assert.That(deleted, Is.Zero); + } + + [Test] + public async Task AggregatedPatientLastObservations_RemovesDuplicatesByCodeAndName() + { + var patient = ObjectId.GenerateNewId(); + + var p1 = new PumpObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patient, + DeviceId = "DevA", + Code = "C1", + Name = "PumpA", + Time = Now + }; + + var p2 = new PumpObservation + { + Id = ObjectId.GenerateNewId(), + PatientId = patient, + DeviceId = "DevA", + Code = "C1", + Name = "PumpA", + Time = Now.AddSeconds(-1) + }; + + await _repo.InsertManyAsync([p1, p2]); + + var result = await _repo.AggregatedPatientLastObservations(patient); + + Assert.That(result, Has.Count.EqualTo(1)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/PumpStateRepositoryTest.cs b/adas-core.Test/Repositories/PumpStateRepositoryTest.cs new file mode 100644 index 00000000..763c2de0 --- /dev/null +++ b/adas-core.Test/Repositories/PumpStateRepositoryTest.cs @@ -0,0 +1,185 @@ +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Pumps; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using adas_core.Domain.Enums; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class PumpStateRepositoryTest +{ + private PumpStateRepository _repo; + private IOptions _apiSettings; + + private static readonly DateTime Now = DateTime.UtcNow; + private const string DeviceA = "Device-A"; + private const string DeviceB = "Device-B"; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + + // ============================================================ + // INIT + // ============================================================ + [OneTimeSetUp] + public async Task Init() + { + _apiSettings = Options.Create(new ApiSettings + { + PumpStates = "pump_states" + }); + + await IntegrationDb.Database.DropCollectionAsync("pump_states"); + await IntegrationDb.Database.CreateCollectionAsync("pump_states"); + + _repo = new PumpStateRepository( + _apiSettings, + IntegrationDb.Database + ); + + await _repo.CreateIndexes(); + + // Insertar estados iniciales + var initialStates = new List + { + new() + { + Id = ObjectId.GenerateNewId(), + DeviceId = DeviceA, + PatientId = PatientId, + LastUpdated = Now, + IsInfusing = true, + Status = PumpEnum.Status.Infusing, + PumpMode = PumpEnum.Mode.Infusing + }, + new() + { + Id = ObjectId.GenerateNewId(), + DeviceId = DeviceB, + LastUpdated = Now.AddSeconds(-10), + IsInfusing = false, + Status = PumpEnum.Status.NotInfusing + } + }; + + await IntegrationDb.Database.GetCollection("pump_states") + .InsertManyAsync(initialStates); + + var count = await _repo.Collection.CountDocumentsAsync(_ => true); + Assert.That(count, Is.EqualTo(2)); + } + + // ============================================================ + // FIND BY DEVICE ID + // ============================================================ + [Test] + public async Task FindByDeviceIdAsync_ReturnsCorrectState() + { + var state = await _repo.FindByDeviceIdAsync(DeviceA); + + Assert.That(state, Is.Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(state!.DeviceId, Is.EqualTo(DeviceA)); + Assert.That(state.IsInfusing, Is.True); + } + } + + // ============================================================ + // UPSERT (INSERT + UPDATE) + // ============================================================ + [Test] + public async Task UpsertAsync_InsertsNewWhenNotExists() + { + var newState = new PumpState + { + DeviceId = "Device-C", + PatientId = ObjectId.GenerateNewId(), + LastUpdated = Now, + IsInfusing = false + }; + + await _repo.UpsertAsync(newState); + + var found = await _repo.FindByDeviceIdAsync("Device-C"); + Assert.That(found, Is.Not.Null); + Assert.That(found!.IsInfusing, Is.False); + } + + [Test] + public async Task UpsertAsync_UpdatesExistingState() + { + var updated = new PumpState + { + DeviceId = DeviceA, + PatientId = PatientId, + LastUpdated = Now.AddMinutes(1), + IsInfusing = false, + Status = PumpEnum.Status.NotInfusing + }; + + await _repo.UpsertAsync(updated); + + var found = await _repo.FindByDeviceIdAsync(DeviceA); + + Assert.That(found, Is.Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(found!.IsInfusing, Is.False); + Assert.That(found.Status, Is.EqualTo(PumpEnum.Status.NotInfusing)); + Assert.That( + found.LastUpdated, + Is.EqualTo(updated.LastUpdated).Within(TimeSpan.FromMilliseconds(1)) + ); + } + } + + // ============================================================ + // GET ALL + // ============================================================ + [Test] + public async Task GetAllAsync_ReturnsAllStates() + { + var list = await _repo.GetAllAsync(); + + var pumpStates = list.ToList(); + Assert.That(pumpStates, Is.Not.Null); + Assert.That(pumpStates, Has.Count.GreaterThanOrEqualTo(2)); + + var containsA = pumpStates.Any(x => x.DeviceId == DeviceA); + var containsB = pumpStates.Any(x => x.DeviceId == DeviceB); + + using (Assert.EnterMultipleScope()) + { + Assert.That(containsA, Is.True); + Assert.That(containsB, Is.True); + } + } + + // ============================================================ + // UNIQUE INDEX: UPSERT OVERWRITES (NO DUPLICADOS) + // ============================================================ + [Test] + public async Task UpsertAsync_DoesNotCreateDuplicates() + { + var before = await _repo.GetAllAsync(); + var countBefore = before.Count(); + + // Insert (upsert) for existing DeviceA + var state = new PumpState + { + DeviceId = DeviceA, + LastUpdated = Now.AddMinutes(5) + }; + + await _repo.UpsertAsync(state); + + var after = await _repo.GetAllAsync(); + var countAfter = after.Count(); + + // No debe aumentar, porque es un update, no un insert + Assert.That(countAfter, Is.EqualTo(countBefore)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/RecordingAlertArchiveRepositoryTest.cs b/adas-core.Test/Repositories/RecordingAlertArchiveRepositoryTest.cs new file mode 100644 index 00000000..ba96550c --- /dev/null +++ b/adas-core.Test/Repositories/RecordingAlertArchiveRepositoryTest.cs @@ -0,0 +1,73 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class RecordingAlertArchiveRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + var recordingAlert1 = new PatientRecordingAlert + { + PatientId = PatientId, + IsRecording = true, + Time = Now + }; + + var recordingAlert2 = new PatientRecordingAlert + { + PatientId = ObjectId.GenerateNewId(), + IsRecording = true, + Time = Now.AddDays(-10) + }; + + await IntegrationDb.Database.DropCollectionAsync("archive_patients_recordingalerts"); + await IntegrationDb.Database.CreateCollectionAsync("archive_patients_recordingalerts"); + + _repository = new RecordingAlertArchiveRepository(_optionsApiSettings, IntegrationDb.Database); + + await _repository.InsertOneAsync(recordingAlert1); + await _repository.InsertOneAsync(recordingAlert2); + + Assert.That(await _repository.Collection.CountDocumentsAsync(_ => true), Is.EqualTo(2)); + } + + private RecordingAlertArchiveRepository _repository; + + private readonly ApiSettings _apiSettings = new() + { + ArchivePatientsRecordingalerts = "archive_patients_recordingalerts" + }; + + private IOptions _optionsApiSettings; + + private static readonly DateTime Now = DateTime.Now; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + + + [Test] + public async Task DeleteBeforeDate() + { + var result = await _repository.Collection.FindAsync(_ => true); + + Assert.That(result, Is.Not.Null); + Assert.That(result.ToList(), Has.Count.GreaterThan(1)); + + await _repository.DeleteBeforeDate(Now.AddDays(-1)); + + var resultDelete = await _repository.Collection.FindAsync(_ => true); + + Assert.That(resultDelete, Is.Not.Null); + Assert.That(resultDelete.ToList(), Has.Count.EqualTo(1)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/RecordingAlertRepositoryTest.cs b/adas-core.Test/Repositories/RecordingAlertRepositoryTest.cs new file mode 100644 index 00000000..a8c441a8 --- /dev/null +++ b/adas-core.Test/Repositories/RecordingAlertRepositoryTest.cs @@ -0,0 +1,281 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class RecordingAlertRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + var recordingAlert1 = new PatientRecordingAlert + { + PatientId = PatientId, + IsRecording = true, + Time = Now + }; + + var recordingAlert2 = new PatientRecordingAlert + { + PatientId = ObjectId.GenerateNewId(), + IsRecording = true, + Time = Now.AddDays(-10) + }; + + await IntegrationDb.Database.DropCollectionAsync("archive_patients_recordingalerts"); + await IntegrationDb.Database.CreateCollectionAsync("archive_patients_recordingalerts"); + + _repository = new RecordingAlertRepository(_optionsApiSettings, IntegrationDb.Database); + + await _repository.InsertOneAsync(recordingAlert1); + await _repository.InsertOneAsync(recordingAlert2); + + Assert.That(await _repository.Collection.CountDocumentsAsync(_ => true), Is.EqualTo(2)); + } + + private RecordingAlertRepository _repository; + + private readonly ApiSettings _apiSettings = new() + { + ArchivePatientsRecordingalerts = "archive_patients_recordingalerts" + }; + + private IOptions _optionsApiSettings; + + private static readonly DateTime Now = DateTime.Now; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + + + [Test] + public async Task DeleteAsync() + { + var recordingAlert = new PatientRecordingAlert + { + Id = ObjectId.GenerateNewId(), + PatientId = ObjectId.GenerateNewId(), + IsRecording = true, + Time = Now + }; + + await _repository.InsertOneAsync(recordingAlert); + + var result = await _repository.Collection.FindAsync(p => p.Id == recordingAlert.Id); + + Assert.That(result.FirstOrDefault(), Is.Not.Null); + + await _repository.DeleteAsync(recordingAlert.Id); + + var resultDelete = await _repository.Collection.FindAsync(p => p.Id == recordingAlert.Id); + + Assert.That(resultDelete.FirstOrDefault(), Is.Null); + } + + + [Test] + public async Task DeleteOlderDaysAsync() + { + var recordingAlert = new PatientRecordingAlert + { + Id = ObjectId.GenerateNewId(), + PatientId = ObjectId.GenerateNewId(), + IsRecording = true, + Time = Now, + Name = "recordingName1" + }; + + var recordingAlertOld = new PatientRecordingAlert + { + Id = ObjectId.GenerateNewId(), + PatientId = recordingAlert.PatientId, + IsRecording = true, + Time = Now.AddDays(-5), + Name = "recordingName1" + }; + + await _repository.InsertOneAsync(recordingAlert); + await _repository.InsertOneAsync(recordingAlertOld); + + var result = await _repository.Collection.FindAsync(p => p.PatientId == recordingAlert.PatientId); + + Assert.That(result.ToList(), Has.Count.EqualTo(2)); + + await _repository.DeleteOlderDaysAsync("recordingName1", 2); + + var resultDelete = await _repository.Collection.FindAsync(p => p.PatientId == recordingAlert.PatientId); + + Assert.That(resultDelete.ToList(), Has.Count.EqualTo(1)); + + await _repository.Collection.DeleteManyAsync(d => d.PatientId == recordingAlert.PatientId); + + var resultClear = await _repository.Collection.FindAsync(p => p.PatientId == recordingAlert.PatientId); + + Assert.That(resultClear.ToList(), Has.Count.EqualTo(0)); + } + + [Test] + public async Task DeleteOlderNumberAsync() + { + var recordingAlert = new PatientRecordingAlert + { + Id = ObjectId.GenerateNewId(), + PatientId = ObjectId.GenerateNewId(), + IsRecording = true, + Time = Now, + Name = "recordingName1" + }; + + var recordingAlertOld = new PatientRecordingAlert + { + Id = ObjectId.GenerateNewId(), + PatientId = recordingAlert.PatientId, + IsRecording = true, + Time = Now.AddDays(-5), + Name = "recordingName1" + }; + + await _repository.InsertOneAsync(recordingAlert); + await _repository.InsertOneAsync(recordingAlertOld); + + var result = await _repository.Collection.FindAsync(p => p.Name == "recordingName1"); + + Assert.That(result.ToList(), Has.Count.EqualTo(2)); + + await _repository.DeleteOlderNumberAsync("recordingName1", 1); + + var resultDelete = await _repository.Collection.FindAsync(p => p.Name == "recordingName1"); + + Assert.That(resultDelete.ToList(), Has.Count.EqualTo(1)); + } + + [Test] + public async Task DeleteByPatientId() + { + var recordingAlert = new PatientRecordingAlert + { + Id = ObjectId.GenerateNewId(), + PatientId = ObjectId.GenerateNewId(), + IsRecording = true, + Time = Now + }; + + var recordingAlertOld = new PatientRecordingAlert + { + Id = ObjectId.GenerateNewId(), + PatientId = recordingAlert.PatientId, + IsRecording = true, + Time = Now.AddDays(-5) + }; + + await _repository.InsertOneAsync(recordingAlert); + await _repository.InsertOneAsync(recordingAlertOld); + + var result = await _repository.Collection.FindAsync(p => p.PatientId == recordingAlert.PatientId); + + Assert.That(result.FirstOrDefault(), Is.Not.Null); + + await _repository.DeleteByPatientId(recordingAlert.PatientId); + + var resultDelete = await _repository.Collection.FindAsync(p => p.PatientId == recordingAlert.PatientId); + + Assert.That(resultDelete.FirstOrDefault(), Is.Null); + } + + [Test] + public async Task FindByPatientIdAsync() + { + var result = await _repository.FindByPatientIdAsync(PatientId); + + Assert.That(result.FirstOrDefault(), Is.Not.Null); + } + + [Test] + public async Task FindLastObservations() + { + var recordingAlert = new PatientRecordingAlert + { + Id = ObjectId.GenerateNewId(), + PatientId = ObjectId.GenerateNewId(), + IsRecording = true, + Time = Now, + Name = "recordingName1" + }; + + var recordingAlertOld = new PatientRecordingAlert + { + Id = ObjectId.GenerateNewId(), + PatientId = recordingAlert.PatientId, + IsRecording = true, + Time = Now.AddDays(-5), + Name = "recordingName1" + }; + + await _repository.InsertOneAsync(recordingAlert); + await _repository.InsertOneAsync(recordingAlertOld); + + var result = await _repository.Collection.FindAsync(_ => true); + + Assert.That(result, Is.Not.Null); + Assert.That(result.ToList(), Has.Count.GreaterThan(2)); + + var resultFind = await _repository.FindLastObservations(recordingAlert.PatientId, "recordingName1", 1); + + Assert.That(resultFind, Is.Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(resultFind.ToList(), Has.Count.EqualTo(1)); + Assert.That(resultFind.FirstOrDefault()?.Time.Day, Is.EqualTo(Now.Day)); + Assert.That(resultFind.FirstOrDefault()?.Time.Month, Is.EqualTo(Now.Month)); + Assert.That(resultFind.FirstOrDefault()?.Time.Year, Is.EqualTo(Now.Year)); + }; + await _repository.DeleteByPatientId(recordingAlert.PatientId); + + var resultDelete = await _repository.Collection.FindAsync(p => p.PatientId == recordingAlert.PatientId); + + Assert.That(resultDelete.FirstOrDefault(), Is.Null); + } + + [Test] + public async Task UpdateManyObjectId() + { + var recordingAlert = new PatientRecordingAlert + { + Id = ObjectId.GenerateNewId(), + PatientId = ObjectId.GenerateNewId(), + IsRecording = true, + Time = Now + }; + + var newPatientId = ObjectId.GenerateNewId(); + + await _repository.InsertOneAsync(recordingAlert); + + var result = await _repository.FindByPatientIdAsync(recordingAlert.PatientId); + + Assert.That(result.FirstOrDefault(), Is.Not.Null); + + await _repository.UpdateManyObjectId("patientid", newPatientId, recordingAlert.PatientId); + + var resultOldId = await _repository.FindByPatientIdAsync(recordingAlert.PatientId); + + Assert.That(resultOldId.FirstOrDefault(), Is.Null); + + var resultNewId = await _repository.FindByPatientIdAsync(newPatientId); + + Assert.That(resultNewId.FirstOrDefault(), Is.Not.Null); + + await _repository.DeleteByPatientId(newPatientId); + + var resultDelete = await _repository.FindByPatientIdAsync(newPatientId); + + Assert.That(resultDelete.FirstOrDefault(), Is.Null); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/SectionRepositoryTest.cs b/adas-core.Test/Repositories/SectionRepositoryTest.cs new file mode 100644 index 00000000..6c180a7b --- /dev/null +++ b/adas-core.Test/Repositories/SectionRepositoryTest.cs @@ -0,0 +1,261 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Serilog; +using static adas_core.Domain.Models.MongoModels.Section; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class SectionRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + var boxes = new List + { + new() { Bed = "bed1", IsActive = true }, + new() { Bed = "bed2", IsActive = true }, + new() { Bed = "bed3", IsActive = true }, + new() { Bed = "bed4", IsActive = true }, + new() { Bed = "bed5", IsActive = true }, + new() { Bed = "bed6", IsActive = true } + }; + + var section1 = new Section + { + _id = SectionId1, + Id = "UCIP1", + SectionTitle = "UCI PEDIÁTRICA 1", + PointOfCare = "UCIP1", + Items = + [ + new SectionItem + { + Group = "Pasillo 1", + Boxes = boxes + } + ] + }; + + var section2 = new Section + { + _id = SectionId2, + Id = "UCIP2", + SectionTitle = "UCI PEDIÁTRICA 2", + PointOfCare = "UCIP2", + Items = + [ + new SectionItem + { + Group = "Pasillo 1", + Boxes = boxes + } + ] + }; + + //await IntegrationDb.Database.DropCollectionAsync("config_sections"); + await IntegrationDb.Database.CreateCollectionAsync("config_sections"); + + _repository = new SectionRepository(_optionsApiSettings, IntegrationDb.Database); + + await _repository.InsertOneAsync(section1); + await _repository.InsertOneAsync(section2); + } + + private SectionRepository _repository; + + private readonly ApiSettings _apiSettings = new() + { + ConfigSections = "config_sections" + }; + + private IOptions _optionsApiSettings; + + private static readonly ObjectId SectionId1 = ObjectId.GenerateNewId(); + private static readonly ObjectId SectionId2 = ObjectId.GenerateNewId(); + + [Test] + public async Task GetAll_Return_List_Sections() + { + var result = await _repository.GetAll(); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Count.EqualTo(2)); + } + + [Test] + public async Task FindBySection_Not_Find_Return_null() + { + var result = await _repository.FindBySection(""); + + Assert.That(result, Is.Null); + } + + [Test] + public async Task FindBySection_Find_Return_Section() + { + var result = await _repository.FindBySection("UCI PEDIÁTRICA 2"); + + Assert.That(result, Is.Not.Null); + Assert.That(result!.SectionTitle, Is.EqualTo("UCI PEDIÁTRICA 2")); + } + + [Test] + public async Task FindByPointOfCare_Not_Find_Return_null() + { + var result = await _repository.FindByPointOfCare(""); + + Assert.That(result, Is.Null); + } + + [Test] + public async Task FindByPointOfCare_Find_Return_Section() + { + var result = await _repository.FindByPointOfCare("UCIP1"); + + Assert.That(result, Is.Not.Null); + Assert.That(result!.SectionTitle, Is.EqualTo("UCI PEDIÁTRICA 1")); + } + + [Test] + public async Task FindById_Not_Find_Return_null() + { + var result = await _repository.FindById(""); + + Assert.That(result, Is.Null); + } + + [Test] + public async Task FindById_Find_Return_Section() + { + var result = await _repository.FindById("UCIP1"); + + Assert.That(result, Is.Not.Null); + Assert.That(result!.SectionTitle, Is.EqualTo("UCI PEDIÁTRICA 1")); + } + + [Test] + public async Task FindById_Not_Find_Id_Return_null() + { + var result = await _repository.FindById(ObjectId.GenerateNewId()); + + Assert.That(result, Is.Null); + } + + [Test] + public async Task FindById_Find_Id_Return_Section() + { + var result = await _repository.FindById(SectionId2); + + Assert.That(result, Is.Not.Null); + Assert.That(result!.SectionTitle, Is.EqualTo("UCI PEDIÁTRICA 2")); + } + + [Test] + public async Task FindByLocation_Not_Find_Return_Emty_List() + { + PatientLocation location = new("UCIP3", "bed1"); + var result = await _repository.FindByLocation(location); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.Empty); + } + + [Test] + public async Task FindByLocation_Find_Return_List() + { + PatientLocation location = new("UCIP1", "bed1"); + var result = await _repository.FindByLocation(location); + + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Count.EqualTo(1)); + Assert.That(result[0].SectionTitle, Is.EqualTo("UCI PEDIÁTRICA 1")); + } + + + [Test] + public async Task InsertUpdateAndDeleteSection() + { + var updateId = ObjectId.GenerateNewId(); + + var boxes = new List + { + new() { Bed = "bed1", IsActive = true }, + new() { Bed = "bed2", IsActive = true }, + new() { Bed = "bed3", IsActive = true }, + new() { Bed = "bed4", IsActive = true }, + new() { Bed = "bed5", IsActive = true }, + new() { Bed = "bed6", IsActive = true } + }; + + var section1 = new Section + { + _id = ObjectId.GenerateNewId(), + Id = "UCIP_U_Id", + SectionTitle = "UCI PEDIÁTRICA U", + PointOfCare = "UCIP_U", + Items = + [ + new SectionItem + { + Group = "Pasillo 1", + Boxes = boxes + } + ] + }; + + var sectionUpdate = new Section + { + _id = updateId, + Id = "UCIP_U_Id", + SectionTitle = "UCI PEDIÁTRICA UPDATE", + PointOfCare = "UCIP_U", + Items = + [ + new SectionItem + { + Group = "Pasillo 1", + Boxes = boxes + } + ] + }; + + + await _repository.InsertOneAsync(section1); + try + { + await _repository.Collection.CountDocumentsAsync(_ => true); + } + catch (Exception ex) + { + Log.Debug(ex.ToString()); + throw; + } + + var resultInsert = await _repository.FindById("UCIP_U_Id"); + + Assert.That(resultInsert, Is.Not.Null); + Assert.That(resultInsert.SectionTitle, Is.EqualTo("UCI PEDIÁTRICA U")); + + var result = await _repository.UpdateSection(sectionUpdate); + + Assert.That(result, Is.Not.Null); + Assert.That(result.SectionTitle, Is.Not.EqualTo("UCI PEDIÁTRICA U")); + Assert.That(result.SectionTitle, Is.EqualTo("UCI PEDIÁTRICA UPDATE")); + + await _repository.Collection.DeleteOneAsync(s => s.Id == "UCIP_U_Id"); + + var resultDelete = await _repository.FindById("UCIP_U_Id"); + + Assert.That(resultDelete, Is.Null); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/ServiceConfigRepositoryTest.cs b/adas-core.Test/Repositories/ServiceConfigRepositoryTest.cs new file mode 100644 index 00000000..a67e6d77 --- /dev/null +++ b/adas-core.Test/Repositories/ServiceConfigRepositoryTest.cs @@ -0,0 +1,63 @@ +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class ServiceConfigRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + var serviceConfig = new ServiceConfig + { + Id = _id, + StrId = Id.ToString() + }; + + await IntegrationDb.Database.DropCollectionAsync("service_config"); + await IntegrationDb.Database.CreateCollectionAsync("service_config"); + + _repository = new ServiceConfigRepository(_optionsApiSettings, IntegrationDb.Database); + + await _repository.InsertOneAsync(serviceConfig); + } + + private ServiceConfigRepository _repository; + + private readonly ApiSettings _apiSettings = new() + { + ServiceConfig = "service_config" + }; + + private IOptions _optionsApiSettings; + + private readonly ObjectId _id = ObjectId.GenerateNewId(); + private static readonly ObjectId Id = ObjectId.GenerateNewId(); + + + [Test] + public async Task FindById_Find_string() + { + var result = await _repository.FindById(Id.ToString()); + + Assert.That(result, Is.Not.Null); + Assert.That(result!.StrId, Is.EqualTo(Id.ToString())); + } + + [Test] + public async Task FindById_Find_ObjectId() + { + var result = await _repository.FindById(_id); + + Assert.That(result, Is.Not.Null); + Assert.That(result!.Id, Is.EqualTo(_id)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/TreatmentArchiveRepositoryTest.cs b/adas-core.Test/Repositories/TreatmentArchiveRepositoryTest.cs new file mode 100644 index 00000000..4c2c9afb --- /dev/null +++ b/adas-core.Test/Repositories/TreatmentArchiveRepositoryTest.cs @@ -0,0 +1,95 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class TreatmentArchiveRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + var treatment1 = new PatientTreatment + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + OrderControl = OrderControlType.Nw, + PlacerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" }, + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [], + Routes = [] + }; + + var treatment2 = new PatientTreatment + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + OrderControl = OrderControlType.Dc, + PlacerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" }, + OrderStatus = "A", + OrderTime = DateTime.Now.AddDays(-5), + Notes = [], + Routes = [] + }; + + await IntegrationDb.Database.DropCollectionAsync("archive_patients_treatments"); + await IntegrationDb.Database.CreateCollectionAsync("archive_patients_treatments"); + + _repository = new TreatmentArchiveRepository(_optionsApiSettings, IntegrationDb.Database); + + await _repository.InsertOneAsync(treatment1); + await _repository.InsertOneAsync(treatment2); + + Assert.That(_repository.Collection.FindAsync(_ => true).Result.ToList(), Has.Count.EqualTo(2)); + } + + private TreatmentArchiveRepository _repository; + + private readonly ApiSettings _apiSettings = new() + { + ArchivePatientsTreatments = "archive_patients_treatments" + }; + + private IOptions _optionsApiSettings; + + private static readonly DateTime Now = DateTime.Now; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + + + [Test] + public async Task DeleteBeforeDate() + { + var result = await _repository.Collection.FindAsync(p => p.PatientId == PatientId); + + Assert.That(result, Is.Not.Null); + Assert.That(result.ToList(), Has.Count.GreaterThan(1)); + + await _repository.DeleteBeforeDate(Now.AddHours(-2)); + + var resultDelete = await _repository.Collection.FindAsync(p => p.PatientId == PatientId); + + Assert.That(resultDelete, Is.Not.Null); + Assert.That(resultDelete.ToList(), Has.Count.EqualTo(1)); + } + + [Test] + public async Task FindAllFromPatient() + { + var result = await _repository.FindAllFromPatient(PatientId); + + Assert.That(result, Is.Not.Null); + Assert.That(result.ToList(), Has.Count.GreaterThan(0)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/TreatmentRepositoryTest.cs b/adas-core.Test/Repositories/TreatmentRepositoryTest.cs new file mode 100644 index 00000000..2307f7d2 --- /dev/null +++ b/adas-core.Test/Repositories/TreatmentRepositoryTest.cs @@ -0,0 +1,186 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class TreatmentRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + _treatment1 = new PatientTreatment + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + OrderControl = OrderControlType.Nw, + PlacerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" }, + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [], + Routes = [] + }; + + _treatment2 = new PatientTreatment + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + OrderControl = OrderControlType.Dc, + PlacerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" }, + OrderStatus = "A", + OrderTime = DateTime.Now.AddDays(-5), + Notes = [], + Routes = [], + RequestedGiveCodesStatus = + [ + new CodeStatus + { Code = new Code { CodingSystem = "CS", Identifier = "I", Text = "T" }, Status = "status" } + ] + }; + + await IntegrationDb.Database.DropCollectionAsync("archive_patients_treatments"); + await IntegrationDb.Database.CreateCollectionAsync("archive_patients_treatments"); + + _repository = new TreatmentRepository(_optionsApiSettings, IntegrationDb.Database); + + await _repository.InsertOneAsync(_treatment1); + await _repository.InsertOneAsync(_treatment2); + + Assert.That(_repository.Collection.FindAsync(_ => true).Result.ToList(), Has.Count.EqualTo(2)); + } + + private TreatmentRepository _repository = null!; + + private readonly ApiSettings _apiSettings = new() + { + ArchivePatientsTreatments = "archive_patients_treatments" + }; + + private IOptions _optionsApiSettings = null!; + + //private static readonly DateTime now = DateTime.Now; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + private PatientTreatment _treatment1 = null!; + private PatientTreatment _treatment2 = null!; + + + [Test] + public async Task GetByPatientId_Not_Find_Return_Null() + { + var result = await _repository.GetByPatientId(ObjectId.GenerateNewId()); + + var patientTreatments = result.ToList(); + Assert.That(patientTreatments, Is.Not.Null); + Assert.That(patientTreatments.ToList(), Is.Empty); + } + + [Test] + public async Task GetByPatientId_Find_Return_Null() + { + var result = await _repository.GetByPatientId(PatientId); + + var patientTreatments = result.ToList(); + Assert.That(patientTreatments, Is.Not.Null); + Assert.That(patientTreatments.ToList(), Has.Count.EqualTo(2)); + } + + [Test] + public async Task DeleteAsync() + { + var treatment = new PatientTreatment + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + OrderControl = OrderControlType.Nw, + PlacerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" }, + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [], + Routes = [] + }; + + await _repository.InsertOneAsync(treatment); + + var result = await _repository.Collection.FindAsync(t => t.Id == treatment.Id); + + Assert.That(result, Is.Not.Null); + Assert.That(result.ToList(), Has.Count.EqualTo(1)); + + await _repository.DeleteAsync(treatment.Id); + + var resultDelete = await _repository.Collection.FindAsync(t => t.Id == treatment.Id); + + Assert.That(resultDelete, Is.Not.Null); + Assert.That(resultDelete.ToList(), Has.Count.EqualTo(0)); + } + + [Test] + public async Task FindByPatientIdAsync_Find_Return_List_Traetments() + { + var result = await _repository.FindByPatientIdAsync(PatientId); + + Assert.That(result, Is.Not.Null); + Assert.That(result.ToList(), Has.Count.EqualTo(2)); + } + + [Test] + public async Task DeleteByPatientId() + { + var treatment = new PatientTreatment + { + Id = ObjectId.GenerateNewId(), + PatientId = ObjectId.GenerateNewId(), + OrderControl = OrderControlType.Nw, + PlacerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" }, + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [], + Routes = [] + }; + + await _repository.InsertOneAsync(treatment); + + var result = await _repository.Collection.FindAsync(t => t.PatientId == treatment.PatientId); + + Assert.That(result, Is.Not.Null); + Assert.That(result.ToList(), Has.Count.EqualTo(1)); + + await _repository.DeleteByPatientId(treatment.PatientId); + + var resultDelete = await _repository.Collection.FindAsync(t => t.PatientId == treatment.PatientId); + + Assert.That(resultDelete, Is.Not.Null); + Assert.That(resultDelete.ToList(), Has.Count.EqualTo(0)); + } + + [Test] + public async Task FindBolusTreatments_Find_Return_Treatment() + { + var result = await _repository.FindBolusTreatments(PatientId); + + Assert.That(result, Is.Not.Null); + Assert.That(result.ToList(), Has.Count.EqualTo(1)); + } + + [Test] + public async Task FindByPatientId_Find_Return_List_Traetments() + { + var result = await _repository.FindByPatientIdAsync(PatientId); + + Assert.That(result, Is.Not.Null); + Assert.That(result.ToList(), Has.Count.EqualTo(2)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/UnitRepositoryTest.cs b/adas-core.Test/Repositories/UnitRepositoryTest.cs new file mode 100644 index 00000000..ac0f7115 --- /dev/null +++ b/adas-core.Test/Repositories/UnitRepositoryTest.cs @@ -0,0 +1,377 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class UnitRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + // Set up your test environment, including database connection + + // Mocking ApiSettings + _optionsApiSettings = Options.Create(_apiSettings); + + // Creating sample unit data + var unit1 = new Unit + { + Id = ObjectId.GenerateNewId(), + Name = "unit1" + // Add other properties as needed + }; + + var unit2 = new Unit + { + Id = ObjectId.GenerateNewId(), + Name = "unit2" + // Add other properties as needed + }; + + // Insert sample data into the database + await IntegrationDb.Database.DropCollectionAsync("units"); + await IntegrationDb.Database.CreateCollectionAsync("units"); + _repository = new UnitRepository(_optionsApiSettings, IntegrationDb.Database); + await _repository.InsertOneUnit(unit1); + await _repository.InsertOneUnit(unit2); + } + + private UnitRepository _repository; + + private readonly ApiSettings _apiSettings = new() + { + Units = "units" + }; + + private IOptions _optionsApiSettings; + + [Test] + public async Task InsertOneUnit_ValidUnit_ReturnsUnit() + { + // Arrange + var unit = new Unit + { + Id = ObjectId.GenerateNewId(), + Title = "Test Unit", + Name = "Test Unit", + LastUpdate = DateTime.UtcNow, + PointOfCareIds = [ObjectId.GenerateNewId()], + Status = StatusEnum.Type.Ok, + Configuration = new UnitConfiguration + { + AutoAdt = true, + }, + AltableOptionListId = ObjectId.GenerateNewId(), + AllergyListId = ObjectId.GenerateNewId() + }; + + // Act + var insertResult = await _repository.InsertOneUnit(unit); + var findResult = await _repository.FindById(unit.Id); + + // Assert + Assert.That(insertResult, Is.Not.Null); + Assert.That(findResult, Is.Not.Null); + Assert.That(findResult?.Id, Is.EqualTo(unit.Id)); + Assert.That(findResult?.Title, Is.EqualTo(unit.Title)); + Assert.That(findResult?.Name, Is.EqualTo(unit.Name)); + } + + // [Test] + // public async Task FindByLocation_ValidLocation_ReturnsUnit() + // { + // // Arrange + // var location = new PatientLocation + // { + // Bed = "TestBed", + // UnitName = "TestUnit" + // }; + // + // var unit = new Unit + // { + // Id = ObjectId.GenerateNewId(), + // Title = "Test Unit", + // Name = "Test Unit", + // PointOfCares = new List + // { + // new PointOfCare { Bed = "TestBed", UnitName = "TestUnit" } + // } + // }; + // + // // Insert the unit into the MongoDB database + // await _repository.InsertOneUnit(unit); + // + // // Act + // var result = await _repository.FindByLocation(location); + // + // // Assert + // Assert.That(result, Is.Not.Null); + // Assert.That(result?.Id, Is.EqualTo(unit.Id)); + // } + + [Test] + public async Task FindById_ValidId_ReturnsUnit() + { + // Arrange + var unitId = ObjectId.GenerateNewId(); + var unit = new Unit + { + Id = unitId, + Title = "Test Unit", + Name = "Test Unit" + }; + + await _repository.InsertOneUnit(unit); + + // Act + var result = await _repository.FindById(unitId); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result?.Id, Is.EqualTo(unitId)); + } + + [Test] + public async Task FindById_InvalidId_ReturnsNull() + { + // Arrange + var invalidId = ObjectId.GenerateNewId(); + + // Act + var result = await _repository.FindById(invalidId); + + // Assert + Assert.That(result, Is.Null); + } + + // [Test] + // public async Task FindByMasterListId_ValidId_ReturnsMatchingUnits() + // { + // // Arrange + // var objectId = ObjectId.GenerateNewId(); + // var masterListType = MasterListType.DestinationList; + // + // // Insert units associated with the given ObjectId and MasterListType + // var units = new List + // { + // new Unit + // { + // Id = ObjectId.GenerateNewId(), + // // Set other properties + // DestinationList = objectId // Set property dynamically based on masterListType + // }, + // new Unit + // { + // Id = ObjectId.GenerateNewId(), + // // Set other properties + // DestinationList = ObjectId.GenerateNewId() // Different ObjectId + // }, + // // Add more units as needed + // }; + // + // foreach (var unit in units) + // { + // await _repository.InsertOneUnit(unit); + // } + // + // // Act + // var result = await _repository.FindByMasterListId(objectId, masterListType); + // + // // Assert + // Assert.That(result, Is.Not.Null); + // Assert.That(result, + // Has.All.Property("SomeTypeId").EqualTo(objectId)); // Check if all returned units have the expected ObjectId + // // Add more assertions as needed + // } + + [Test] + public async Task FindByName_ValidName_ReturnsUnit() + { + // Arrange + var unitName = "TestUnit"; + var unit = new Unit + { + Id = ObjectId.GenerateNewId(), + Title = "Test Unit", + Name = unitName + }; + + await _repository.InsertOneUnit(unit); + + // Act + var result = await _repository.FindByName(unitName); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result?.Name, Is.EqualTo(unitName)); + } + + [Test] + public async Task FindByName_InvalidName_ReturnsNull() + { + // Arrange + var invalidName = "NonExistentUnitName"; + + // Act + var result = await _repository.FindByName(invalidName); + + // Assert + Assert.That(result, Is.Null); + } + + // [Test] + // public async Task FindByPointOfCare_ValidPointOfCare_ReturnsUnit() + // { + // // Arrange + // var pointOfCare = new PointOfCare + // { + // Bed = "TestBed", + // Room = "TestRoom", + // UnitName = "TestUnit" + // }; + // + // var unit = new Unit + // { + // Id = ObjectId.GenerateNewId(), + // Title = "Test Unit", + // Name = "Test Unit", + // PointOfCares = new System.Collections.Generic.List + // { + // pointOfCare + // } + // }; + // + // await _repository.InsertOneUnit(unit); + // + // // Act + // var result = await _repository.FindByPointOfCare(pointOfCare); + // + // // Assert + // Assert.That(result, Is.Not.Null); + // Assert.That(result?.PointOfCares, Contains.Item(pointOfCare)); + // } + + // [Test] + // public async Task FindByPointOfCare_InvalidPointOfCare_ReturnsNull() + // { + // // Arrange + // var invalidPointOfCare = new PointOfCare + // { + // Bed = "NonExistentBed", + // Room = "NonExistentRoom", + // UnitName = "NonExistentUnitName" + // }; + // + // // Act + // var result = await _repository.FindByPointOfCare(invalidPointOfCare); + // + // // Assert + // Assert.That(result, Is.Null); + // } + + // [Test] + // public async Task FindByUnitName_ValidUnitName_ReturnsMatchingUnits() + // { + // // Arrange + // var unitName = "TestUnit"; + // + // var units = new List + // { + // new Unit + // { + // Id = ObjectId.GenerateNewId(), + // Name = unitName, + // }, + // new Unit + // { + // Id = ObjectId.GenerateNewId(), + // Name = unitName, + // }, + // }; + // + // foreach (var unit in units) + // { + // await _repository.InsertOneUnit(unit); + // } + // + // // Act + // var result = await _repository.FindByUnitName(unitName); + // + // // Assert + // Assert.That(result, Is.Not.Null); + // Assert.That(result, Has.Count.EqualTo(units.Count)); + // } + + // [Test] + // public async Task FindByUnitName_InvalidUnitName_ReturnsEmptyList() + // { + // // Arrange + // var invalidUnitName = "NonExistentUnitName"; + // + // // Act + // var result = await _repository.FindByUnitName(invalidUnitName); + // + // // Assert + // Assert.That(result, Is.Not.Null); + // Assert.That(result, Is.Empty); + // } + + [Test] + public async Task GetAll_ReturnsAllUnits() + { + // Arrange + + var units = new List(); + if (units == null) throw new ArgumentNullException(nameof(units)); + for (var i = 0; i < 5; i++) + { + var unit = new Unit + { + Id = ObjectId.GenerateNewId() + }; + units.Add(unit); + await _repository.InsertOneUnit(unit); + } + + // Act + var result = await _repository.GetAll(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Has.Count.Not.EqualTo(0)); + } + + [Test] + public async Task UpdateUnit_ValidUnit_ReturnsUpdatedUnit() + { + // Arrange + var originalUnit = new Unit + { + Id = ObjectId.GenerateNewId(), + Title = "Original Title" + }; + + var updatedUnit = new Unit + { + Id = originalUnit.Id, + Title = "Updated Title" + }; + + await _repository.InsertOneUnit(originalUnit); + + // Act + var result = await _repository.UpdateUnit(updatedUnit); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result?.Id, Is.EqualTo(originalUnit.Id)); + Assert.That(result?.Title, Is.EqualTo(updatedUnit.Title)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Repositories/UserRepositoryTest.cs b/adas-core.Test/Repositories/UserRepositoryTest.cs new file mode 100644 index 00000000..7413ad6b --- /dev/null +++ b/adas-core.Test/Repositories/UserRepositoryTest.cs @@ -0,0 +1,67 @@ +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Repositories; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Repositories; + +[TestFixture] +[Category("Integration")] +public class UserRepositoryTest +{ + [OneTimeSetUp] + public async Task Init() + { + _optionsApiSettings = Options.Create(_apiSettings); + + var user1 = new User + { + Id = ObjectId.GenerateNewId(), + UserName = "username1", + Password = "password1" + //Rol = "rol1" + }; + + var user2 = new User + { + Id = ObjectId.GenerateNewId(), + UserName = "username2", + Password = "password2" + //Rol = "rol2" + }; + + await IntegrationDb.Database.DropCollectionAsync("users"); + await IntegrationDb.Database.CreateCollectionAsync("users"); + + _repository = new UserRepository(_optionsApiSettings, IntegrationDb.Database); + + await _repository.InsertOneAsync(user1); + await _repository.InsertOneAsync(user2); + } + + private UserRepository _repository = null!; + + private readonly ApiSettings _apiSettings = new() + { + Users = "users" + }; + + private IOptions _optionsApiSettings = null!; + + + [Test] + public async Task GetUser() + { + var result = await _repository.GetUser("username1", "password1"); + + Assert.That(result, Is.Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(result!.UserName, Is.EqualTo("username1")); + Assert.That(result.Password, Is.EqualTo("password1")); + //Assert.That(result.Rol, Is.EqualTo("rol1")); + }; + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/AdmissionServiceTest.cs b/adas-core.Test/Services/AdmissionServiceTest.cs new file mode 100644 index 00000000..c8315d9e --- /dev/null +++ b/adas-core.Test/Services/AdmissionServiceTest.cs @@ -0,0 +1,574 @@ +using System.Security.Claims; +using adas_core.Application.Exceptions; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using adas_core.Test.Utilities; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; +using Moq; + +namespace adas_core.Test.Services; + +public class AdmissionServiceTest +{ + private Mock _admissionRepositoryMock; + private AdmissionService _admissionService; + private Mock _auditServiceMock; + private Mock _clientMessageServiceMock; + private Mock _dischargeServiceMock; + + private Mock _displayServiceMock; + + //Logs de auditoria + private Mock _httpContextAccessorMock; + private Mock> _loggerMock; + private Mock _masterListServiceFactoryMock; + private Mock _patientArchiveRepoMock; + private Mock _patientServiceMock; + private Mock _pointOfCareServiceMock; + private Mock _subscribersServiceMock; + private Mock _unitServiceMock; + + [SetUp] + public void Setup() + { + _loggerMock = new Mock>(); + _subscribersServiceMock = new Mock(); + _admissionRepositoryMock = new Mock(); + _pointOfCareServiceMock = new Mock(); + _clientMessageServiceMock = new Mock(); + _unitServiceMock = new Mock(); + _patientServiceMock = new Mock(); + _displayServiceMock = new Mock(); + _dischargeServiceMock = new Mock(); + _patientArchiveRepoMock = new Mock(); + _auditServiceMock = new Mock(); + _httpContextAccessorMock = new Mock(); + _masterListServiceFactoryMock = new Mock(); + // Simulación de contexto HTTP y usuario autenticado + var userClaims = new ClaimsPrincipal(new ClaimsIdentity([ + new Claim(ClaimTypes.Name, "TestUser") + ], "mock")); + + var httpContextMock = new DefaultHttpContext + { + User = userClaims + }; + + _httpContextAccessorMock.Setup(accessor => accessor.HttpContext) + .Returns(httpContextMock); + _admissionService = new AdmissionService(_loggerMock.Object, + _subscribersServiceMock.Object, + _admissionRepositoryMock.Object, + _clientMessageServiceMock.Object, + _unitServiceMock.Object, + _patientServiceMock.Object, + _pointOfCareServiceMock.Object, + _displayServiceMock.Object, + _dischargeServiceMock.Object, + _patientArchiveRepoMock.Object, + _httpContextAccessorMock.Object, + _auditServiceMock.Object, + _masterListServiceFactoryMock.Object); + } + + [Test] + public async Task DeleteAdmissionAsync_ValidAdmission_DeletesSuccessfully() + { + // Arrange + var admissionId = ObjectId.GenerateNewId(); + var admission = new Admission { Id = admissionId }; + + _admissionRepositoryMock.Setup(repo => repo.FindById(admissionId)) + .ReturnsAsync(admission); + + // Act + await _admissionService.DeleteAdmissionAsync(admission); + + // Assert + _admissionRepositoryMock.Verify(repo => repo.Delete(admissionId), Times.Once); + } + + [Test] + public async Task DeleteAdmissionByIdAsync_AdmissionExists_DeletesSuccessfully() + { + // Arrange + var admissionId = ObjectId.GenerateNewId(); + var admission = new Admission { Id = admissionId, PointOfCareId = ObjectId.GenerateNewId() }; + + _admissionRepositoryMock.Setup(repo => repo.FindById(admissionId)) + .ReturnsAsync(admission); + + // Act + await _admissionService.DeleteAdmissionByIdAsync(admissionId); + + // Assert + _admissionRepositoryMock.Verify(repo => repo.Delete(admissionId), Times.Once); + } + + [Test] + public async Task GetAdmissionByIdAsync_AdmissionExists_ReturnsAdmissionWithPatientLocation() + { + // Arrange + var admissionId = ObjectId.GenerateNewId(); + var pointOfCareId = ObjectId.GenerateNewId(); + var admission = new Admission { Id = admissionId, PointOfCareId = pointOfCareId }; + var poc = new PointOfCare { Id = pointOfCareId, UnitName = "TestUnit", Bed = "TestBed", Room = "TestRoom" }; + + _admissionRepositoryMock.Setup(repo => repo.FindById(admissionId)) + .ReturnsAsync(admission); + _pointOfCareServiceMock.Setup(repo => repo.GetInfo(pointOfCareId, null, false, It.IsAny())) + .ReturnsAsync(poc); + + // Act + var result = await _admissionService.GetAdmissionByIdAsync(admissionId); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result?.PatientLocation, Is.Not.Null); + Assert.That(result?.PatientLocation?.UnitName, Is.EqualTo("TestUnit")); + Assert.That(result?.PatientLocation?.Bed, Is.EqualTo("TestBed")); + Assert.That(result?.PatientLocation?.Room, Is.EqualTo("TestRoom")); + } + + [Test] + public async Task GetAdmissionsAsync_ReturnsAdmissionsWithPatientLocations() + { + // Arrange + var admission1Id = ObjectId.GenerateNewId(); + var admission2Id = ObjectId.GenerateNewId(); + var poc1Id = ObjectId.GenerateNewId(); + var poc2Id = ObjectId.GenerateNewId(); + var admission1 = new Admission { Id = admission1Id, PointOfCareId = poc1Id }; + var admission2 = new Admission { Id = admission2Id, PointOfCareId = poc2Id }; + var admissionsList = new List { admission1, admission2 }; + + _admissionRepositoryMock.Setup(repo => repo.FindAll()) + .ReturnsAsync(admissionsList); + _pointOfCareServiceMock.Setup(repo => repo.GetInfo(poc1Id, null, false, It.IsAny())) + .ReturnsAsync(TestUtilities.CreateValidPointOfCare()); + _pointOfCareServiceMock.Setup(repo => repo.GetInfo(poc2Id, null, false, It.IsAny())) + .ReturnsAsync(TestUtilities.CreateValidPointOfCare()); + + // Act + var result = await _admissionService.GetAdmissionsAsync(); + + // Assert + var admissions = result.ToList(); + Assert.That(admissions, Is.Not.Null); + Assert.That(admissions.Count(), Is.EqualTo(2)); + var admissionArray = admissions.ToArray(); + Assert.That(admissionArray.First().PatientLocation, Is.Not.Null); + Assert.That(admissionArray.First().PatientLocation?.UnitName, Is.EqualTo("Test Unit")); + Assert.That(admissionArray.First().PatientLocation?.Bed, Is.EqualTo("Test Bed")); + Assert.That(admissionArray.First().PatientLocation?.Room, Is.EqualTo("Test Room")); + } + + // [Test] + // public async Task InsertAdmission_WhenPointOfCareExists_InsertsAdmission() + // { + // // Arrange + // var admission = new Admission + // { + // Id = ObjectId.GenerateNewId(), + // AdmissionDate = DateTime.UtcNow, + // Nhc = "TestNhc", + // PointOfCareId = ObjectId.GenerateNewId(), + // Person = new Person(), + // Origin = TestUtilities.CreateValidOptionList(), + // Diagnosis = TestUtilities.CreateValidOptionList(), + // Allergies = new List { TestUtilities.CreateValidOptionList() }, + // Insulation = TestUtilities.CreateValidOptionList(), + // LanguageBarrier = TestUtilities.CreateValidOptionList() + // }; + // + // var pointOfCare = TestUtilities.CreateValidPointOfCare(); + // + // // Setup the pointOfCareServiceMock to return a valid point of care + // pointOfCareServiceMock + // .Setup(repo => repo.GetInfo(It.IsAny())) + // .ReturnsAsync(pointOfCare); + // + // // Act + // var result = await admissionService.InsertAdmission(admission); + // + // // Assert + // Assert.That(result, Is.Not.Null); + // Assert.That(result, Is.EqualTo(admission)); + // admissionRepositoryMock.Verify(repo => repo.InsertOneAsyncAndReturn(It.IsAny()), Times.Once); + // pointOfCareServiceMock.Verify(repo => repo.Update(It.IsAny()), Times.Once); + // } + + [Test] + public async Task UpdateAdmissionAsync_WhenAdmissionExists_UpdatesAdmission() + { + // Arrange + var admissionId = ObjectId.GenerateNewId(); + var admission = new Admission + { + Id = admissionId, + AdmissionDate = DateTime.UtcNow, + Nhc = "TestNhc", + PointOfCareId = ObjectId.GenerateNewId(), + Person = new Person(), + Origin = TestUtilities.CreateValidOptionList(), + Diagnosis = TestUtilities.CreateValidOptionList(), + Allergies = [TestUtilities.CreateValidOptionList()], + Insulation = TestUtilities.CreateValidOptionList(), + LanguageBarrier = [TestUtilities.CreateValidOptionList()] + }; + + var oldAdmission = new Admission + { + Id = admissionId, + AdmissionDate = DateTime.UtcNow.AddDays(-1), // Update admission date + Nhc = "OldNhc", + PointOfCareId = ObjectId.GenerateNewId(), // Change PointOfCareId + Person = new Person(), + Origin = TestUtilities.CreateValidOptionList(), + Diagnosis = TestUtilities.CreateValidOptionList(), + Allergies = [TestUtilities.CreateValidOptionList()], + Insulation = TestUtilities.CreateValidOptionList(), + LanguageBarrier = [TestUtilities.CreateValidOptionList()] + }; + + _admissionRepositoryMock + .Setup(repo => repo.FindById(admissionId)) + .ReturnsAsync(oldAdmission); + + var updatedPointOfCare = TestUtilities.CreateValidPointOfCare(); + _pointOfCareServiceMock + .Setup(repo => repo.GetInfo(admission.PointOfCareId.Value, null, false, It.IsAny())) + .ReturnsAsync(updatedPointOfCare); + + // Act + await _admissionService.UpdateAdmissionAsync(admission); + + // Assert + _admissionRepositoryMock.Verify(repo => repo.Update(admission), Times.Once); + _pointOfCareServiceMock.Verify(repo => repo.GetInfo(admission.PointOfCareId.Value, null, false, It.IsAny()), Times.Once); + _pointOfCareServiceMock.Verify(repo => repo.GetInfo(oldAdmission.PointOfCareId.Value, null, false, It.IsAny()), Times.Once); + } + + [Test] + public async Task AdmitPatient_WithValidAdmission_CreatesPatientAndDeletesAdmission() + { + // Arrange + var admission = TestUtilities.CreateValidAdmission(); + var unit = TestUtilities.CreateValidUnit(); + var pointOfCare = TestUtilities.CreateValidPointOfCare(); + + + _unitServiceMock + .Setup(repo => repo.FindById(admission.UnitId)) + .ReturnsAsync(unit); + + _pointOfCareServiceMock + .Setup(repo => repo.FindById(It.IsAny())) + .ReturnsAsync((ObjectId id) => id == admission.PointOfCareId ? pointOfCare : null); + + + // Act + await _admissionService.AdmitPatient(admission); + + // Assert + _patientServiceMock.Verify(repo => repo.Insert(It.IsAny()), Times.Once); + } + + [Test] + public async Task GetAdmissionByLocation_LocationExists_ReturnsAdmissions() + { + // Arrange + var location = new PatientLocation + { + UnitName = "Test Unit", + Bed = "Test Bed", + Room = "Test Room" + }; + var expectedAdmissions = new List + { + new() { Id = ObjectId.GenerateNewId() }, + new() { Id = ObjectId.GenerateNewId() } + }; + + _admissionRepositoryMock.Setup(repo => repo.FindByLocation(location)) + .ReturnsAsync(expectedAdmissions); + + // Act + var result = await _admissionService.GetAdmissionByLocation(location); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.EqualTo(expectedAdmissions)); + } + + [Test] + public async Task GetAdmissionByLocation_LocationNotFound_ReturnsNull() + { + // Arrange + var location = new PatientLocation + { + UnitName = "Nonexistent Unit", + Bed = "Nonexistent Bed", + Room = "Nonexistent Room" + }; + + _admissionRepositoryMock.Setup(repo => repo.FindByLocation(location)) + .ThrowsAsync(new Exception()); + + // Act + var result = await _admissionService.GetAdmissionByLocation(location); + + // Assert + Assert.That(result, Is.Empty); + } + + [Test] + public async Task GetAdmissionByPointOfCareId_POCExists_ReturnsAdmissionsWithLocation() + { + // Arrange + var pocId = ObjectId.GenerateNewId(); + var expectedAdmissions = new List + { + new() { Id = ObjectId.GenerateNewId() }, + new() { Id = ObjectId.GenerateNewId() } + }; + var poc = new PointOfCare + { + Id = pocId, + UnitName = "Test Unit", + Bed = "Test Bed", + Room = "Test Room" + }; + + _admissionRepositoryMock.Setup(repo => repo.FindByPointOfCareId(pocId)) + .ReturnsAsync(expectedAdmissions); + + _pointOfCareServiceMock.Setup(repo => repo.GetInfo(pocId, null, false, It.IsAny())) + .ReturnsAsync(poc); + + // Act + var result = await _admissionService.GetAdmissionByPointOfCareId(pocId); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Count, Is.EqualTo(expectedAdmissions.Count)); + + foreach (var admission in result) + { + Assert.That(admission.PatientLocation, Is.Not.Null); + Assert.That(admission.PatientLocation?.UnitName, Is.EqualTo(poc.UnitName)); + Assert.That(admission.PatientLocation?.Bed, Is.EqualTo(poc.Bed)); + Assert.That(admission.PatientLocation?.Room, Is.EqualTo(poc.Room)); + } + } + + [Test] + public async Task GetAdmissionByPointOfCareId_POCNotFound_ReturnsNull() + { + // Arrange + var pocId = ObjectId.GenerateNewId(); + + _admissionRepositoryMock.Setup(repo => repo.FindByPointOfCareId(pocId)) + .ThrowsAsync(new Exception("Repository error")); + + // Act + var result = await _admissionService.GetAdmissionByPointOfCareId(pocId); + + // Assert + Assert.That(result, Is.Empty); + } + + [Test] + public async Task GetAdmissionByUnitIdWithOutPoC_UnitIdExists_ReturnsAdmissions() + { + // Arrange + var unitId = ObjectId.GenerateNewId(); + var expectedAdmissions = new List + { + new() { Id = ObjectId.GenerateNewId() }, + new() { Id = ObjectId.GenerateNewId() } + }; + + _admissionRepositoryMock.Setup(repo => repo.GetAdmissionByUnitIdWithOutPoC(unitId)) + .ReturnsAsync(expectedAdmissions); + + // Act + var result = await _admissionService.GetAdmissionByUnitIdWithOutPoC(unitId); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Count, Is.EqualTo(expectedAdmissions.Count)); + Assert.That(result, Is.EqualTo(expectedAdmissions)); + } + + [Test] + public async Task GetAdmissionByUnitIdWithOutPoC_UnitIdNotFound_ReturnsNull() + { + // Arrange + var unitId = ObjectId.GenerateNewId(); + + _admissionRepositoryMock.Setup(repo => repo.GetAdmissionByUnitIdWithOutPoC(unitId)) + .ThrowsAsync(new Exception("Repository error")); + + // Act + var result = await _admissionService.GetAdmissionByUnitIdWithOutPoC(unitId); + + // Assert + Assert.That(result, Is.Empty); + } + + // [Test] + // public async Task SaveRequest_NewAdmission_ValidRequest_InsertsAdmission() + // { + // // Arrange + // var apiRequest = new ApiRequest + // { + // Type = "NewAdmission", + // Admission = new Admission + // { + // Id = ObjectId.GenerateNewId(), + // Nhc = "TestNhc", + // Origin = new OptionList(), + // Diagnosis = new OptionList() + // } + // }; + // + // admissionServiceMock.Setup(service => service.InsertAdmission(apiRequest.Admission)) + // .Verifiable(); + // + // // Act + // await admissionService.SaveRequest(apiRequest); + // + // // Assert + // admissionServiceMock.Verify(service => service.InsertAdmission(apiRequest.Admission), Times.Once); + // } + + // [Test] + // public async Task SaveRequest_UpdateAdmission_ValidRequest_UpdatesAdmission() + // { + // // Arrange + // var apiRequest = new ApiRequest + // { + // Type = "UpdateAdmission", + // Admission = new Admission + // { + // Id = ObjectId.GenerateNewId(), + // Nhc = "TestNhc", + // Origin = new OptionList(), + // Diagnosis = new OptionList() + // } + // }; + // + // admissionServiceMock.Setup(service => service.UpdateAdmissionAsync(apiRequest.Admission)) + // .Verifiable(); + // + // // Act + // await admissionService.SaveRequest(apiRequest); + // + // // Assert + // admissionServiceMock.Verify(service => service.UpdateAdmissionAsync(apiRequest.Admission), Times.Once); + // } + + // [Test] + // public async Task SaveRequest_DeleteAdmission_ValidRequest_DeletesAdmission() + // { + // // Arrange + // var apiRequest = new ApiRequest + // { + // Type = "DeleteAdmission", + // Admission = new Admission { Id = ObjectId.GenerateNewId() } + // }; + // + // admissionServiceMock.Setup(service => service.DeleteAdmissionAsync(apiRequest.Admission)) + // .Verifiable(); + // + // // Act + // await admissionService.SaveRequest(apiRequest); + // + // // Assert + // admissionServiceMock.Verify(service => service.DeleteAdmissionAsync(apiRequest.Admission), Times.Once); + // } + + // [Test] + // public async Task SaveRequestAsync_Calls_SaveRequest_Method() + // { + // // Arrange + // var apiRequest = new ApiRequest + // { + // Type = "NewAdmission", + // Admission = new Admission + // { + // Id = ObjectId.GenerateNewId(), + // Nhc = "TestNhc", + // Origin = new OptionList(), + // Diagnosis = new OptionList() + // } + // }; + // + // admissionServiceMock.Setup(service => service.SaveRequest(apiRequest)) + // .Returns(Task.CompletedTask) // Since SaveRequest is async, returning completed task for simplicity + // .Verifiable(); + // + // // Act + // await admissionService.SaveRequestAsync(apiRequest); + // + // // Assert + // admissionServiceMock.Verify(service => service.SaveRequest(apiRequest), Times.Once); + // } + + [Test] + public async Task InsertAdmission_ValidAdmission_InsertsSuccessfullyWithUser() + { + // Arrange + var admission = new Admission + { + Id = ObjectId.GenerateNewId(), + AdmissionDate = DateTime.UtcNow, + Nhc = "TestNhc", + PointOfCareId = ObjectId.GenerateNewId(), + Person = new Person(), + Origin = new OptionList(), + Diagnosis = new OptionList(), + Allergies = [new OptionList()], + Insulation = new OptionList(), + LanguageBarrier = [new OptionList()] + }; + + var pointOfCare = TestUtilities.CreateValidPointOfCare(); + + _admissionRepositoryMock + .Setup(repo => repo.FindByNhc(admission.Nhc)) + .ReturnsAsync((Admission?)null); + + _pointOfCareServiceMock + .Setup(repo => repo.GetInfo(admission.PointOfCareId.Value, null, false, It.IsAny())) + .ReturnsAsync(pointOfCare); + + _admissionRepositoryMock + .Setup(repo => repo.InsertOneAsyncAndReturn(admission)) + .ReturnsAsync(admission); + + Func act = () => _admissionService.InsertAdmission(admission); + + var ex = Assert.ThrowsAsync(act); + + Assert.That(ex!.Message, + Is.EqualTo(((int)HttpEnum.ErrorMessage.NotFoundResourceMissing).ToString())); + + // Verify no insert + _admissionRepositoryMock + .Verify(repo => repo.InsertOneAsyncAndReturn(It.IsAny()), Times.Never); + + // Verify no update + _pointOfCareServiceMock + .Verify(service => service.Update(It.IsAny()), Times.Never); + } + +} \ No newline at end of file diff --git a/adas-core.Test/Services/AlarmServiceTest.cs b/adas-core.Test/Services/AlarmServiceTest.cs new file mode 100644 index 00000000..2734fdc4 --- /dev/null +++ b/adas-core.Test/Services/AlarmServiceTest.cs @@ -0,0 +1,247 @@ +using System.Security.Claims; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services; +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.MongoModels; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; +using Moq; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Services; + +public class AlarmServiceTest +{ + private readonly Mock _alarmRepositoryMock; + private readonly AlarmService _alarmService; + + + private readonly ApiSettings _apiSettings = new() + { + ConfigObservation = new ConfigObservationSettings + { + IgnoreUnknownObservation = false + }, + IntravenousLinesCode = ["10546003"], + AllergiesCode = ["473011001"], + DrainageCode = ["56868008"], + IsolationCode = ["302147001"], + PositionCode = ["386053000"] + }; + + private readonly Mock _calculatedObservationsServiceMock; + private readonly Mock _configObservationServiceMock; + private readonly Mock _observationServiceMock; + private readonly Mock _patientServiceMock; + + private readonly RecordingSettings _recordingSettings = new(); + private readonly Mock _unitServiceMock; + + public AlarmServiceTest() + { + var optionsApiSettings = Options.Create(_apiSettings); + Options.Create(_recordingSettings); + + _observationServiceMock = new Mock(); + var observationServiceMockLazy = new Lazy(() => _observationServiceMock.Object); + + _patientServiceMock = new Mock(); + _configObservationServiceMock = new Mock(); + _unitServiceMock = new Mock(); + var pocServiceMock = new Mock(); + + var beaconServiceMock = new Mock(); + var beaconServiceMockLazy = new Lazy(() => beaconServiceMock.Object); + + var relayServiceMock = new Mock(); + var relayServiceMockLazy = new Lazy(() => relayServiceMock.Object); + + var recordingServiceMock = new Mock(); + var recordingServiceMockLazy = new Lazy(() => recordingServiceMock.Object); + + var clientMessageServiceMock = new Mock(); + + var subscribersServiceMock = new Mock(); + + var httpContextAccessorMock = new Mock(); + var auditServiceMock = new Mock(); + + _calculatedObservationsServiceMock = new Mock(); + var calculatedObservationsServiceLazy = + new Lazy(() => _calculatedObservationsServiceMock.Object); + + + var logger = new Mock>(); + + _alarmRepositoryMock = new Mock(); + var userClaims = new ClaimsPrincipal(new ClaimsIdentity([ + new Claim(ClaimTypes.Name, "TestUser") + ], "mock")); + + var httpContextMock = new DefaultHttpContext + { + User = userClaims + }; + + httpContextAccessorMock.Setup(accessor => accessor.HttpContext) + .Returns(httpContextMock); + _alarmService = new AlarmService( + _alarmRepositoryMock.Object, + logger.Object, + _patientServiceMock.Object, + _configObservationServiceMock.Object, + observationServiceMockLazy, + clientMessageServiceMock.Object, + subscribersServiceMock.Object, + calculatedObservationsServiceLazy, + beaconServiceMockLazy, + recordingServiceMockLazy, + relayServiceMockLazy, + optionsApiSettings, + _unitServiceMock.Object, + pocServiceMock.Object, + httpContextAccessorMock.Object, + auditServiceMock.Object, + false // Disable the timer for testing + ); + } + + + [Test] + public async Task SaveRequest_Oru_R40_Type_Without_Alarms_Dont_Insert() + { + // Arrange + var apiRequest = new ApiRequest + { + Type = "ORU_R40", + PatientNumber = "12345", + Observation = new PatientObservation { Value = "Test" }, + Alarms = [], + Location = new PatientLocation { Bed = "Bed1", Room = "Bed1", UnitName = "Unit1" } + }; + var unit = new Unit { Configuration = new UnitConfiguration { AutoAdt = true } }; + var patient = new Patient { Id = ObjectId.GenerateNewId(), Bed = "Bed1" }; + _unitServiceMock.Setup(u => u.FindByUnitNameOrPocName(It.IsAny(), It.IsAny())) + .ReturnsAsync(unit); + _patientServiceMock.Setup(p => p.FindPatientByApiRequest(apiRequest)).ReturnsAsync(patient); + _observationServiceMock.Setup(o => o.SaveRequestAsync(It.IsAny())).Returns(Task.CompletedTask); + + // Act + await _alarmService.SaveRequest(apiRequest); + + // Assert + _patientServiceMock.Verify(p => p.FindPatientByApiRequest(apiRequest), Times.Once); + _observationServiceMock.Verify(o => o.SaveRequestAsync(It.IsAny()), Times.Never); + } + + [Test] + public async Task ProcessAlarmObservations_Should_Insert_Alarm_Observations() + { + // Arrange + var patient = new Patient { Id = ObjectId.GenerateNewId(), Bed = "Bed1" }; + var alarmObservations = new List + { + new() { Value = "Alarm1", Time = DateTime.UtcNow} + }; + var observations = new List + { + new() { Value = "Alarm1", Time = DateTime.MinValue} + }; + + _configObservationServiceMock.Setup(o => o.Map(It.IsAny(), It.IsAny())) + .ReturnsAsync((PatientObservationAlarm obs, bool _) => obs); // Correct setup + + _calculatedObservationsServiceMock.Setup(o => o.Map(It.IsAny(), It.IsAny())) + .ReturnsAsync((PatientObservationAlarm obs, bool _) => obs); // Correct setup + + // Act + await _alarmService.ProcessAlarmObservations(alarmObservations, observations, patient, DateTime.UtcNow); + + // Assert + _alarmRepositoryMock.Verify(a => a.InsertOneAsync(It.IsAny()), Times.Once); + } + + [Test] + public async Task CheckObservationAlarm_Alarm_EventPEEP_PEEP_bajo_create_Event() + { + var obs = new PatientObservation(); + + var configs = new ConfigObservation + { + Id = ObjectId.GenerateNewId(), + Name = "Event_PEEP_Low", + CodingSystem = "ADAS_EVENT", + Description = "EVT_LO and EVT_EXTR_LO" + }; + + _configObservationServiceMock.Setup(c => c.Get(It.IsAny(), false)) + .ReturnsAsync(configs); + + await _alarmService.CheckObservationAlarm(obs); + + + _observationServiceMock.Verify(o => o.InsertObservation(obs, true, true), Times.Never); + } + + [Test] + public async Task CheckObservationAlarm_Should_Launch_Red_Alarm_And_Set_BeaconColor_Red() + { + var alarmConfig = new AlarmConfig + { + Enabled = true, + Priority = 10, + Beacon = new AlarmItem + { + Enabled = true, + BeaconColor = AlarmEnum.BeaconColor.Red, // Using the enum value + EndAfter = 60 // Assuming the alarm should end after 60 seconds + } + }; + + var configs = new ConfigObservation + { + Id = ObjectId.GenerateNewId(), + Name = "Event_PEEP_Low", + CodingSystem = "ADAS_EVENT", + Description = "logs an event. No generate an alarm.", + Alarm = alarmConfig, + RequiredValue = "PEEP" + }; + + var obsConfig = new ConfigObservation + { + Id = ObjectId.GenerateNewId(), + Name = "EVT_LO", + CheckObservations = true, + CreateObservation = [configs] + }; + var patId = ObjectId.GenerateNewId(); + // Arrange + var obs = new PatientObservation + { + Id = ObjectId.GenerateNewId(), + Name = "EVT_LO", + PatientId = patId, + Value = "PEEP High", + Time = DateTime.UtcNow, + CheckObservations = true, + CreateObservation = [configs] + }; + + _configObservationServiceMock.Setup(c => c.Get(It.IsAny(), false)) + .ReturnsAsync(obsConfig); + + + // Act + await _alarmService.CheckObservationAlarm(obs); + + _observationServiceMock.Verify( + o => o.InsertObservation( + It.Is(ob => ob.Value.ToString() == "PEEP High"), true, true), + Times.Once); + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/CacheDispatcherTest.cs b/adas-core.Test/Services/CacheDispatcherTest.cs new file mode 100644 index 00000000..35717c98 --- /dev/null +++ b/adas-core.Test/Services/CacheDispatcherTest.cs @@ -0,0 +1,256 @@ +using adas_core.Application.Services.Caching; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Utils; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Moq; + +namespace adas_core.Test.Services; + + + + +public class FakeLockProvider : ILockProvider +{ + public Task AcquireAsync(string key, TimeSpan timeout) => Task.FromResult(true); + public Task ReleaseAsync(string key) => Task.CompletedTask; +} + +public class FakeLockManagerService : LockManagerService +{ + public FakeLockManagerService() : base( + Mock.Of>(), + new FakeLockProvider()) + { + } +} + + +public class FakeRedisService : RedisService, ICacheService +{ + public bool WasCalled { get; private set; } + public string? LastKey { get; private set; } + + public FakeRedisService() : base( + Options.Create(new CacheSettings()), + Mock.Of>(), + new FakeLockManagerService()) + { + } + + + Task ICacheService.GetOrSetObjectAsync( + string key, + Func> factory, + TimeSpan? ttl, + CancellationToken cancellationToken) + { + WasCalled = true; + LastKey = key; + return factory(); + } + + Task ICacheService.GetOrSetObjectAsync( + GroupedField groupedField, + ObjectId patientId, + Func> factory, + TimeSpan? ttl, + CancellationToken cancellationToken) + { + WasCalled = true; + return factory(); + } +} + + +public class FakeCacheService : CacheService, ICacheService +{ + public bool WasCalled { get; private set; } + public string? LastKey { get; private set; } + + public FakeCacheService() : base(new FakeLockManagerService()) + { + } + + Task ICacheService.GetOrSetObjectAsync( + string key, + Func> factory, + TimeSpan? ttl, + CancellationToken cancellationToken) + { + WasCalled = true; + LastKey = key; + return factory(); + } + + Task ICacheService.GetOrSetObjectAsync( + GroupedField groupedField, + ObjectId patientId, + Func> factory, + TimeSpan? ttl, + CancellationToken cancellationToken) + { + WasCalled = true; + return factory(); + } +} + + +public class FakeNoCacheService : NoCacheService, ICacheService +{ + public bool WasCalled { get; private set; } + public string? LastKey { get; private set; } + + Task ICacheService.GetOrSetObjectAsync( + string key, + Func> factory, + TimeSpan? ttl, + CancellationToken cancellationToken) + { + WasCalled = true; + LastKey = key; + return factory(); + } + + Task ICacheService.GetOrSetObjectAsync( + GroupedField groupedField, + ObjectId patientId, + Func> factory, + TimeSpan? ttl, + CancellationToken cancellationToken) + { + WasCalled = true; + return factory(); + } +} + + +[TestFixture] +public class CacheDispatcherTest +{ + private FakeRedisService _redisServiceFake = null!; + private FakeCacheService _memoryServiceFake = null!; + private FakeNoCacheService _noopServiceFake = null!; + private CacheSettings _cacheSettings = null!; + private CacheDispatcher _cacheDispatcher = null!; + + [SetUp] + public void SetUp() + { + _redisServiceFake = new FakeRedisService(); + _memoryServiceFake = new FakeCacheService(); + _noopServiceFake = new FakeNoCacheService(); + + _cacheSettings = new CacheSettings(); + + _cacheDispatcher = new CacheDispatcher( + _redisServiceFake, + _memoryServiceFake, + _noopServiceFake, + _cacheSettings); + } + + #region TC-23 + + [Test] + public async Task GetOrSetObjectAsync_UsesRedisService_WhenPatientsModeIsRedis() + { + // Arrange + var key = "patients:latestObs:abc"; + var expectedResult = new { Name = "TestPatient" }; + Func> factory = () => Task.FromResult(expectedResult); + + _cacheSettings.Patients = CacheEnum.Mode.Redis; + + // Act + var result = await _cacheDispatcher.GetOrSetObjectAsync(key, factory); + + // Assert + Assert.That(_redisServiceFake.WasCalled, Is.True, "RedisService debería haber sido invocado"); + Assert.That(_redisServiceFake.LastKey, Is.EqualTo(key)); + Assert.That(_memoryServiceFake.WasCalled, Is.False, "CacheService NO debería haber sido invocado"); + Assert.That(_noopServiceFake.WasCalled, Is.False, "NoCacheService NO debería haber sido invocado"); + } + #endregion + + #region TC-24 + [Test] + public async Task GetOrSetObjectAsync_UsesCacheService_WhenAppointmentsModeIsCache() + { + // Arrange + var key = "appointments:patient:id:20260224"; + var expectedResult = new { Name = "TestAppointment" }; + Func> factory = () => Task.FromResult(expectedResult); + + _cacheSettings.Appointments = CacheEnum.Mode.Cache; + + // Act + var result = await _cacheDispatcher.GetOrSetObjectAsync(key, factory); + + // Assert + Assert.That(_memoryServiceFake.WasCalled, Is.True, "CacheService debería haber sido invocado"); + Assert.That(_memoryServiceFake.LastKey, Is.EqualTo(key)); + Assert.That(_redisServiceFake.WasCalled, Is.False, "RedisService NO debería haber sido invocado"); + Assert.That(_noopServiceFake.WasCalled, Is.False, "NoCacheService NO debería haber sido invocado"); + } + #endregion + + #region TC-25 + [Test] + public async Task GetOrSetObjectAsync_UsesNoCacheService_WhenPumpObservationsModeIsNone() + { + // Arrange + var key = "pumpObs:latest:abc:10"; + var expectedResult = new { Name = "TestPump" }; + Func> factory = () => Task.FromResult(expectedResult); + + _cacheSettings.PumpObservations = CacheEnum.Mode.None; + + // Act + var result = await _cacheDispatcher.GetOrSetObjectAsync(key, factory); + + // Assert + Assert.That(_noopServiceFake.WasCalled, Is.True, "NoCacheService debería haber sido invocado"); + Assert.That(_noopServiceFake.LastKey, Is.EqualTo(key)); + Assert.That(_redisServiceFake.WasCalled, Is.False, "RedisService NO debería haber sido invocado"); + Assert.That(_memoryServiceFake.WasCalled, Is.False, "CacheService NO debería haber sido invocado"); + } + #endregion + + #region TC-26 + [Test] + public async Task GetOrSetObjectAsync_UsesCacheService_WhenKeyIsUnrecognized() + { + // Arrange + var key = "unknown:prefix:key"; + var expectedResult = new { Name = "TestUnknown" }; + Func> factory = () => Task.FromResult(expectedResult); + + // Act + var result = await _cacheDispatcher.GetOrSetObjectAsync(key, factory); + + // Assert - Unknown keys fallback to InMemory (Cache) + Assert.That(_memoryServiceFake.WasCalled, Is.True, "CacheService debería haber sido invocado como fallback"); + Assert.That(_memoryServiceFake.LastKey, Is.EqualTo(key)); + Assert.That(_redisServiceFake.WasCalled, Is.False, "RedisService NO debería haber sido invocado"); + Assert.That(_noopServiceFake.WasCalled, Is.False, "NoCacheService NO debería haber sido invocado"); + } + + [Test] + public void Classify_ReturnsUnknown_ForUnrecognizedPrefix() + { + // Arrange + var key = "unknown:prefix:key"; + + // Act + var result = CacheKeyClassifier.Classify(key); + + // Assert + Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Unknown)); + } + #endregion +} diff --git a/adas-core.Test/Services/CacheServiceTest.cs b/adas-core.Test/Services/CacheServiceTest.cs new file mode 100644 index 00000000..7cc408ea --- /dev/null +++ b/adas-core.Test/Services/CacheServiceTest.cs @@ -0,0 +1,208 @@ +namespace adas_core.Test.Services; +using adas_core.Application.Services.Caching; +using Microsoft.Extensions.Logging; +using Moq; +[TestFixture] +public class CacheServiceTest +{ + private CacheService _svc = null!; + + [SetUp] + public void SetUp() + { + var lockMgr = new LockManagerService( + new Mock>().Object, + new InMemoryLockProvider()); + + _svc = new CacheService(lockMgr); + } + #region TC-31 + [Test] + public async Task GetOrSetObjectAsync_ReturnsCachedValue_AndDoesNotInvokeFactory_WhenKeyAlreadyExists() + { + const string key = "patients:latestObs:abc"; + const string expected = "cached-value"; + + await _svc.SetObjectAsync(key, expected); + + var factoryCallCount = 0; + var result = await _svc.GetOrSetObjectAsync(key, () => + { + factoryCallCount++; + return Task.FromResult("factory-value"); + }); + Assert.That(factoryCallCount, Is.EqualTo(0));// por qué como la clave esta, no se ejecutó el factory + Assert.That(result, Is.EqualTo(expected)); + + } + #endregion + #region TC-32 + [Test] + public async Task GetOrSetObjectAsync_InvokesFactory_StoresResult_AndDoesNotInvokeAgainOnSecondCall() + { + const string key = "patients:latestObs:new"; + const string factoryValue = "factory-result"; + var factoryCallCount = 0; + + var firstResult = await _svc.GetOrSetObjectAsync(key, () => + { + factoryCallCount++; + return Task.FromResult(factoryValue); + }); + + Assert.That(factoryCallCount, Is.EqualTo(1)); + Assert.That(firstResult, Is.EqualTo(factoryValue)); + + var cached = await _svc.GetObjectAsync(key); + Assert.That(cached, Is.EqualTo(factoryValue)); + + var secondResult = await _svc.GetOrSetObjectAsync(key, () => + { + factoryCallCount++; + return Task.FromResult("second-call-value"); + }); + + Assert.That(factoryCallCount, Is.EqualTo(1)); + Assert.That(secondResult, Is.EqualTo(factoryValue)); + } + #endregion + + #region TC-33 + [Test] + public async Task GetOrSetObjectAsync_DoesNotCacheNullResult_AndInvokesFactoryOnSubsequentCalls() + { + const string key = "patients:latestObs:null"; + var factoryCallCount = 0; + + await _svc.GetOrSetObjectAsync(key, () => + { + factoryCallCount++; + return Task.FromResult(null); + }); + + Assert.That(factoryCallCount, Is.EqualTo(1)); + + await _svc.GetOrSetObjectAsync(key, () => + { + factoryCallCount++; + return Task.FromResult(null); + }); + + Assert.That(factoryCallCount, Is.EqualTo(2)); + } + #endregion + #region TC-34 + [Test] + public async Task GetOrSetObjectAsync_SecondCheckInLock_PreventsFactoryExecution_WhenValueAlreadyInsertedByFirstThread() + { + const string key = "patients:concurrent:abc"; + var factoryCallCount = 0; + var task1InFactory = new SemaphoreSlim(0, 1); + var task1CanFinish = new SemaphoreSlim(0, 1); + + async Task ControlledFactory() + { + Interlocked.Increment(ref factoryCallCount); + task1InFactory.Release(); + await task1CanFinish.WaitAsync(); + return "first-result"; + } + + var t1 = _svc.GetOrSetObjectAsync(key, ControlledFactory); + + await task1InFactory.WaitAsync(); + + var t2 = _svc.GetOrSetObjectAsync(key, () => + { + Interlocked.Increment(ref factoryCallCount); + return Task.FromResult("second-result"); + }); + + await Task.Delay(20); + + task1CanFinish.Release(); + + var r1 = await t1; + var r2 = await t2; + + Assert.That(factoryCallCount, Is.EqualTo(1)); + Assert.That(r1, Is.EqualTo("first-result")); + Assert.That(r2, Is.EqualTo("first-result")); + } + #endregion + + #region TC-35 + [Test] + public async Task DeleteObjectAsync_RemovesKey_AndFactoryIsInvokedOnNextCall() + { + const string key = "patients:latestObs:delete"; + await _svc.SetObjectAsync(key, "stored-value"); + + await _svc.DeleteObjectAsync(key); + + var valueAfterDelete = await _svc.GetObjectAsync(key); + Assert.That(valueAfterDelete, Is.Null); + + var factoryCallCount = 0; + var result = await _svc.GetOrSetObjectAsync(key, () => + { + factoryCallCount++; + return Task.FromResult("after-delete"); + }); + + Assert.That(factoryCallCount, Is.EqualTo(1)); + Assert.That(result, Is.EqualTo("after-delete")); + } + #endregion + + #region TC-36 + [Test] + public async Task DeleteByPatternAsync_RemovesMatchingKeys_AndKeepsNonMatchingKeys() + { + await _svc.SetObjectAsync("patients:abc", "val1"); + await _svc.SetObjectAsync("patients:def", "val2"); + await _svc.SetObjectAsync("appointments:xyz", "val3"); + + var deleted = await _svc.DeleteByPatternAsync("patients:"); + + Assert.That(deleted, Is.EqualTo(2)); + + var p1 = await _svc.GetObjectAsync("patients:abc"); + var p2 = await _svc.GetObjectAsync("patients:def"); + var a1 = await _svc.GetObjectAsync("appointments:xyz"); + + Assert.That(p1, Is.Null); + Assert.That(p2, Is.Null); + Assert.That(a1, Is.EqualTo("val3")); + } + #endregion + + #region TC-37 + [Test] + public async Task CleanCache_RemovesAllKeys_AndFactoryIsInvokedAfterClean() + { + await _svc.SetObjectAsync("patients:1", "v1"); + await _svc.SetObjectAsync("patients:2", "v2"); + await _svc.SetObjectAsync("appointments:1", "v3"); + await _svc.SetObjectAsync("configDisplays:1", "v4"); + await _svc.SetObjectAsync("pumpObs:1", "v5"); + + _svc.CleanCache(); + + Assert.That(await _svc.GetObjectAsync("patients:1"), Is.Null); + Assert.That(await _svc.GetObjectAsync("patients:2"), Is.Null); + Assert.That(await _svc.GetObjectAsync("appointments:1"), Is.Null); + Assert.That(await _svc.GetObjectAsync("configDisplays:1"), Is.Null); + Assert.That(await _svc.GetObjectAsync("pumpObs:1"), Is.Null); + + var factoryCallCount = 0; + await _svc.GetOrSetObjectAsync("patients:1", () => + { + factoryCallCount++; + return Task.FromResult("after-clean"); + }); + + Assert.That(factoryCallCount, Is.EqualTo(1)); + } + #endregion +} \ No newline at end of file diff --git a/adas-core.Test/Services/CameraServiceTest.cs b/adas-core.Test/Services/CameraServiceTest.cs new file mode 100644 index 00000000..61474b14 --- /dev/null +++ b/adas-core.Test/Services/CameraServiceTest.cs @@ -0,0 +1,59 @@ +namespace adas_core.Test.Services; + +[TestFixture] +internal class CameraServiceTest +{ + //CameraService cameraService; + + //Mock cameraServiceMock; + //Mock> logger; + + //Dictionary cameraSettings; + + + [SetUp] + public void Setup() + { + //cameraSettings = new Dictionary() + //{ + // { "camHost", "http://localhost:8080" }, + // { "camUser", "username" }, + // { "camPassword", "password" } + //}; + //cameraServiceMock = new Mock(); + + //logger = new Mock>(); + + //cameraService = new CameraService( + // logger.Object + // ); + + + // Create a mock of the singleton class subscribers + //var mockSingleton = new Mock(); + + //var subscribers = new List(); + + // Set up the mock object to return a specific value when a method is called + //mockSingleton.Setup(x => x.GetSubscribers()).Returns(subscribers); + } + + //TODO HttpWebResponse test + //[Test] + public void MaskStream_Return_Ok() + { + //bool activate = true; + //string coordinates = "100,100"; + //var expectedResponse = new HttpResponseMessage(); + + ////cameraServiceMock.Setup(c => c.GrabResponse(It.IsAny(), It.IsAny())).Returns(((string)null)); + + //// Act + //var response = cameraService.MaskStream(activate, coordinates, cameraSettings); + + //// Assert + //Assert.IsNotNull(response); + //Assert.AreEqual(expectedResponse.StatusCode, response.StatusCode); + //Assert.AreEqual(expectedResponse.Content, response.Content); + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/ConfigObservationServiceTest.cs b/adas-core.Test/Services/ConfigObservationServiceTest.cs new file mode 100644 index 00000000..9e824d5b --- /dev/null +++ b/adas-core.Test/Services/ConfigObservationServiceTest.cs @@ -0,0 +1,381 @@ +using System.Security.Claims; +using adas_core.Application.Exceptions; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services; +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.MongoModels; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Moq; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Services; + +[TestFixture] +public class ConfigObservationServiceTest +{ + private ConfigObservationService _service = null!; + private Mock _repo = null!; + private Mock _unitSvc = null!; + private Mock _auditSvc = null!; + private Mock _http = null!; + private Mock _cache = null!; + private Mock> _logger = null!; + + private IOptions _settings = null!; + private IOptions _cacheSettings = null!; + + private static readonly ObjectId Id = ObjectId.GenerateNewId(); + + private static readonly ConfigObservation ConfigList = new() + { + Id = Id, + Name = "FC", + Code = "147842", + CodingSystem = "MDC", + ParentCode = "69965", + ParentName = "MDC_DEV_MON_PHYSIO_MULTI_PARAM_MDS", + MinAlert = 60, + MaxAlert = 100, + ForceAlert = true + }; + + private readonly List _allConfigList = + [ + new() { Id = ObjectId.GenerateNewId(), Name = "Hemoglobina", Code = "12345", CodingSystem = "SNM" }, + new() { Id = ObjectId.GenerateNewId(), Name = "Glucemia", Code = "54321", CodingSystem = "SNM", OriginalName = "GLU" }, + new() { Id = ObjectId.GenerateNewId(), Name = "Sodio", OriginalName = "Sodio" }, + new() + { + Id = ObjectId.GenerateNewId(), + Name = "ph", + Code = "555", + CodingSystem = "MG4", + ParentCode = "3333", + ParentCodingSystem = "SNM" + }, + new() + { + Id = ObjectId.GenerateNewId(), + Name = "SOFA", + Code = "278061009", + OriginalName = "SOFA", + CodingSystem = "SNM", + MinAlert = 10, + MinWarn = 14 + }, + ConfigList, + new() { Id = ObjectId.GenerateNewId(), Name = "Alarm_BlueCode", CodingSystem = "ADAS_EVENT" } + ]; + + [SetUp] + public void Setup() + { + _repo = new Mock(); + _unitSvc = new Mock(); + _auditSvc = new Mock(); + _http = new Mock(); + _cache = new Mock(); + _logger = new Mock>(); + + var user = new ClaimsPrincipal( + new ClaimsIdentity([new Claim(ClaimTypes.Name, "TestUser")], "mock")); + + _http.Setup(x => x.HttpContext).Returns(new DefaultHttpContext { User = user }); + + _settings = Options.Create(new ApiSettings + { + ConfigObservation = new ConfigObservationSettings + { + IgnoreUnknownObservation = false, + Refresh = null + }, + }); + + _cacheSettings = Options.Create(new CacheSettings()); + + // KEY: mock cache to execute repository calls + _cache.Setup(c => c.GetOrSetObjectAsync( + It.IsAny(), + It.IsAny>>>(), + It.IsAny(), + It.IsAny())) + .Returns((string _, Func>> f, TimeSpan? __, CancellationToken ___) => f()); + + _cache.Setup(c => c.GetOrSetObjectAsync( + It.IsAny(), + It.IsAny>>(), + It.IsAny(), + It.IsAny())) + .Returns((string _, Func> f, TimeSpan? __, CancellationToken ___) => f()); + + _service = new ConfigObservationService( + _repo.Object, + _settings, + _cacheSettings, + _logger.Object, + _unitSvc.Object, + _http.Object, + _auditSvc.Object, + _cache.Object); + + _repo.Setup(x => x.FindAll()).ReturnsAsync([ConfigList]); + } + + // --------------------------------------------------------- + // TESTS + // --------------------------------------------------------- + + [Test] + public async Task Get_config_observation_item_by_codingSystem_only_return_null() + { + var obs = new PatientObservation + { + CodingSystem = "ADAS_EVENT", + Code = "Pump_X", + Name = "Alarm_Pump_X" + }; + + var result = await _service.Get(obs); + + Assert.That(result, Is.Null); + } + + [Test] + public async Task Get_config_observation_item_by_code_and_codingSystem() + { + _repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList); + + var obs = new BasePatientObservation + { + Name = "Hemo^GBr", + Code = "12345", + CodingSystem = "SNM" + }; + + var result = await _service.Get(obs); + + Assert.That(result, Is.Not.Null); + Assert.That(result!.Name, Is.EqualTo("Hemoglobina")); + } + + [Test] + public async Task Get_config_observation_item_by_name() + { + _repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList); + + var obs = new BasePatientObservation { Name = "Hemoglobina" }; + + var result = await _service.Get(obs); + + Assert.That(result!.Name, Is.EqualTo("Hemoglobina")); + } + + [Test] + public async Task Get_config_observation_item_by_name_not_exist_returns_null() + { + _repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList); + + var obs = new BasePatientObservation { Name = "XXX" }; + + var result = await _service.Get(obs); + + Assert.That(result, Is.Null); + } + + [Test] + public async Task Get_config_by_code_and_parent() + { + _repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList); + + var obs = new BasePatientObservation + { + Code = "555", + CodingSystem = "MG4", + ParentData = new ParentDataClass + { + Code = "3333", + CodingSystem = "SNM" + } + }; + + var result = await _service.Get(obs); + + Assert.That(result!.Name, Is.EqualTo("ph")); + } + + [Test] + public async Task Map_unknown_ignore_true_returns_null() + { + _settings.Value.ConfigObservation!.IgnoreUnknownObservation = true; + _repo.Setup(x => x.FindAll()).ReturnsAsync([]); + + var result = await _service.Map(new BasePatientObservation { Name = "XX" }); + + Assert.That(result, Is.Null); + } + + [Test] + public async Task Map_unknown_ignore_false_returns_obs() + { + _settings.Value.ConfigObservation!.IgnoreUnknownObservation = false; + _repo.Setup(x => x.FindAll()).ReturnsAsync([]); + + var obs = new BasePatientObservation { Name = "XX" }; + + var result = await _service.Map(obs); + + Assert.That(result, Is.EqualTo(obs)); + } + + [Test] + public async Task Map_threshold_Ok() + { + _repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList); + + var obs = new PatientObservation + { + Name = "SOFA", + CodingSystem = "SNM", + Value = 14 + }; + + var result = await _service.Map(obs); + + Assert.That(result!.Status, Is.EqualTo(StatusEnum.Type.Ok)); + } + + [Test] + public async Task Map_threshold_Warning() + { + _repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList); + + var obs = new PatientObservation + { + Name = "SOFA", + CodingSystem = "SNM", + Value = 13 + }; + + var result = await _service.Map(obs); + + Assert.That(result!.Status, Is.EqualTo(StatusEnum.Type.Warning)); + } + + [Test] + public async Task Map_threshold_Alert() + { + _repo.Setup(x => x.FindAll()).ReturnsAsync(_allConfigList); + + var obs = new PatientObservation + { + Name = "SOFA", + CodingSystem = "SNM", + Value = 9 + }; + + var result = await _service.Map(obs); + + Assert.That(result!.Status, Is.EqualTo(StatusEnum.Type.Alert)); + } + + [Test] + public async Task Map_parent_ok() + { + _repo.Setup(x => x.FindById(Id)).ReturnsAsync(ConfigList); + + var fc = new PatientObservation + { + Id = Id, + Name = "FC", + Value = 70 + }; + + var result = await _service.Map(fc); + + Assert.That(result!.Status, Is.EqualTo(StatusEnum.Type.Ok)); + } + + [Test] + public async Task UpdateConfig_ok() + { + var newCfg = new ConfigObservation + { + Id = Id, + Name = "New", + Code = "X", + CodingSystem = "S" + }; + + _repo.Setup(r => r.FindById(Id)).ReturnsAsync(ConfigList); + _repo.Setup(r => r.Update(It.IsAny())).ReturnsAsync(newCfg); + + var result = await _service.UpdateConfig(newCfg); + + Assert.That(result!.Name, Is.EqualTo("New")); + } + + [Test] + public async Task UpdateConfig_notfound() + { + _repo + .Setup(r => r.FindById(Id)) + .ReturnsAsync((ConfigObservation?)null); + + Func act = () => _service.UpdateConfig(new ConfigObservation { Id = Id }); + + Assert.ThrowsAsync(act); + } + + + [Test] + public async Task CreateConfig_ok() + { + _repo.Setup(r => r.InsertOneAsyncAndReturn(ConfigList)).Returns(Task.FromResult(ConfigList)); + + var result = await _service.CreateConfig(ConfigList); + + Assert.That(result, Is.EqualTo(ConfigList)); + } + + [Test] + public void CreateConfig_duplicate_throws() + { + _repo.Setup(r => r.FindById(It.IsAny())).ReturnsAsync(ConfigList); + + + Func act = () => _service.CreateConfig(ConfigList); + + Assert.ThrowsAsync(act); + + } + + [Test] + public async Task RemoveConfigItem_ok() + { + var deleted = new ConfigObservation { Id = ObjectId.GenerateNewId(), Name = "X" }; + + _repo.Setup(r => r.FindById(It.IsAny())).ReturnsAsync(ConfigList); + _repo.Setup(r => r.Delete(It.IsAny())).ReturnsAsync(deleted); + + var result = await _service.RemoveConfigItem(ObjectId.GenerateNewId()); + + Assert.That(result, Is.EqualTo(deleted)); + } + + [Test] + public async Task RemoveConfigItem_null() + { + _repo.Setup(r => r.FindById(Id)).ReturnsAsync(ConfigList); + _repo.Setup(r => r.Delete(Id)).ReturnsAsync((ConfigObservation?)null); + + var result = await _service.RemoveConfigItem(Id); + + Assert.That(result, Is.Null); + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/ConfigPumpsServiceTest.cs b/adas-core.Test/Services/ConfigPumpsServiceTest.cs new file mode 100644 index 00000000..cb4579f4 --- /dev/null +++ b/adas-core.Test/Services/ConfigPumpsServiceTest.cs @@ -0,0 +1,170 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services; +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.MongoModels; +using adas_core.Domain.Models.Pumps; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Services; + +[TestFixture] +public class ConfigPumpsServiceTest +{ + [SetUp] + public void Setup() + { + //configPumpsServiceMock = new Mock(); + _configPumpsRepositoryMock = new Mock(); + + _optionsApiSettings = Options.Create(_apiSettings); + + _logger = new Mock>(); + + _configPumpsService = new ConfigPumpsService( + _configPumpsRepositoryMock.Object, + _optionsApiSettings, + _logger.Object, + _httpContextAccessor.Object, + _auditService.Object); + } + + private ConfigPumpsService _configPumpsService; + + //Mock configPumpsServiceMock; + private Mock _configPumpsRepositoryMock; + + private readonly ApiSettings _apiSettings = new() + { + ConfigPumpsRequired = true, + ConfigPumpsKey = "PV1", + ConfigObservation = new ConfigObservationSettings + { + Refresh = null + } + }; + + private IOptions _optionsApiSettings; + + private Mock> _logger; + + private readonly Mock _httpContextAccessor = new(); + private readonly Mock _auditService = new(); + + private static readonly DateTime Now = DateTime.Now; + + [Test] + public async Task Map_configPumpsRequired_false_Return_same_pump() + { + _optionsApiSettings.Value.ConfigPumpsRequired = false; + + var pump = new PumpObservation + { + Code = "code", + Name = "name", + Time = Now + }; + + var configPumpsService = new ConfigPumpsService( + _configPumpsRepositoryMock.Object, + _optionsApiSettings, + _logger.Object, + _httpContextAccessor.Object, + _auditService.Object); + + var result = await configPumpsService.Map(pump); + + Assert.That(pump, Is.EqualTo(result)); + } + + [Test] + public async Task Map_not_alarmType_Return_same_pump() + { + _optionsApiSettings.Value.ConfigPumpsRequired = true; + + var pump = new PumpObservation + { + Code = "code", + Name = "name", + Time = Now + }; + + var result = await _configPumpsService.Map(pump); + + Assert.That(pump, Is.EqualTo(result)); + } + + [Test] + public async Task Map_Not_uiConfiguration_Return_same_pump() + { + _optionsApiSettings.Value.ConfigPumpsRequired = true; + + var pump = new PumpObservation + { + Code = "code", + Name = "name", + Time = Now, + AlarmType = PumpEnum.AlarmType.Attention + }; + + var configPumpItem = new ConfigPumpItem(); + + var configPumps = new ConfigPumps + { + Id = "PV1", + Items = [configPumpItem] + }; + _configPumpsRepositoryMock.Setup(c => c.FindById(It.IsAny())).ReturnsAsync(configPumps); + + var result = await _configPumpsService.Map(pump); + using (Assert.EnterMultipleScope()) + { + Assert.That(pump, Is.EqualTo(result)); + Assert.That(result.UiConfiguration, Is.Null); + } + } + + [Test] + public async Task Map_uiConfiguration_Return_pump_uiConfiguration() + { + _optionsApiSettings.Value.ConfigPumpsRequired = true; + + var pump = new PumpObservation + { + Code = "code", + Name = "name", + Time = Now, + AlarmType = PumpEnum.AlarmType.AirInLine + }; + + + var configPumpItem = new ConfigPumpItem + { + AlarmType = PumpEnum.AlarmType.AirInLine, + UiConfiguration = new Dictionary { { "screenAlarmLabel", PumpEnum.AlarmType.AirInLine } } + }; + + var configPumps = new ConfigPumps + { + Id = "PV1", + Items = [configPumpItem] + }; + + _configPumpsRepositoryMock.Setup(c => c.FindById(It.IsAny())).ReturnsAsync(configPumps); + + var result = await _configPumpsService.Map(pump); + + Assert.That(result.UiConfiguration, Is.Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(result.UiConfiguration, Has.Count.EqualTo(1)); + Assert.That(result.UiConfiguration!["screenAlarmLabel"], Is.EqualTo(PumpEnum.AlarmType.AirInLine)); + }; + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/ConfigUnitsServiceTest.cs b/adas-core.Test/Services/ConfigUnitsServiceTest.cs new file mode 100644 index 00000000..71a479ef --- /dev/null +++ b/adas-core.Test/Services/ConfigUnitsServiceTest.cs @@ -0,0 +1,157 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Services; + +[TestFixture] +public class ConfigUnitsServiceTest +{ + [SetUp] + public void Setup() + { + //configUnitsServiceMock = new Mock(); + _configUnitsRepositoryMock = new Mock(); + + _optionsApiSettings = Options.Create(_apiSettings); + + _logger = new Mock>(); + + _configUnitsService = new ConfigUnitsService( + _configUnitsRepositoryMock.Object, + _optionsApiSettings, + _logger.Object); + } + + private ConfigUnitsService _configUnitsService; + + //Mock configUnitsServiceMock; + private Mock _configUnitsRepositoryMock; + + private readonly ApiSettings _apiSettings = new() + { + ConfigObservation = new ConfigObservationSettings + { + Refresh = null + } + }; + + private IOptions _optionsApiSettings; + + private Mock> _logger; + + private static readonly DateTime Now = DateTime.Now; + + [Test] + public async Task Map_configUnitsRequired_false_Return_same_obs() + { + _optionsApiSettings.Value.ConfigUnitsRequired = false; + + var obs = new PatientObservation + { + Code = "code", + CodingSystem = "SNM", + Name = "name", + Time = Now + }; + + var configUnitsService = new ConfigUnitsService( + _configUnitsRepositoryMock.Object, + _optionsApiSettings, + _logger.Object); + + var result = await configUnitsService.Map(obs); + + Assert.That(obs, Is.EqualTo(result)); + } + + [Test] + public async Task Map_not_units_Return_same_obs() + { + _optionsApiSettings.Value.ConfigUnitsRequired = true; + + var obs = new PatientObservation + { + Code = "code", + CodingSystem = "SNM", + Name = "name", + Time = Now + }; + + var result = await _configUnitsService.Map(obs); + + Assert.That(obs, Is.EqualTo(result)); + } + + [Test] + public async Task Map_Not_Find_Config_Return_same_obs() + { + _optionsApiSettings.Value.ConfigUnitsRequired = true; + + var obs = new PatientObservation + { + Code = "code", + CodingSystem = "SNM", + Name = "name", + Time = Now + }; + + var configUnitItem = new ConfigUnitItem(); + + var configUnits = new ConfigUnits + { + Id = "PV1", + Items = [configUnitItem] + }; + + _configUnitsRepositoryMock.Setup(c => c.FindById(It.IsAny())).ReturnsAsync(configUnits); + + var result = await _configUnitsService.Map(obs); + using (Assert.EnterMultipleScope()) + { + Assert.That(obs, Is.EqualTo(result)); + Assert.That(result.Units, Is.Null); + }; + } + + [Test] + public async Task Map_configUnits_Return_obs_ConfigUnit() + { + _optionsApiSettings.Value.ConfigUnitsRequired = true; + + var obs = new PatientObservation + { + Code = "code", + CodingSystem = "SNM", + Name = "name", + Time = Now, + Units = "MDC_DIM_X_G_PER_KG" + }; + + + var configUnitItem = new ConfigUnitItem + { + Code = "MDC_DIM_X_G_PER_KG", + Value = "g/kg" + }; + + var configUnits = new ConfigUnits + { + Id = "PV1", + Items = [configUnitItem] + }; + + _configUnitsRepositoryMock.Setup(c => c.FindById(It.IsAny())).ReturnsAsync(configUnits); + + var result = await _configUnitsService.Map(obs); + + Assert.That(result.Units, Is.Not.Null); + Assert.That(result.Units, Is.EqualTo("g/kg")); + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/DiagnosisServiceTest.cs b/adas-core.Test/Services/DiagnosisServiceTest.cs new file mode 100644 index 00000000..9e35efab --- /dev/null +++ b/adas-core.Test/Services/DiagnosisServiceTest.cs @@ -0,0 +1,541 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services; +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.MongoModels; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Moq; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Services; + +[TestFixture] +public class DiagnosisServiceTest +{ + [SetUp] + public void Setup() + { + _patientServiceMock = new Mock(); + _patientServiceLazy = new Lazy(() => _patientServiceMock.Object); + + //var sectionServiceMock = new Mock(); + //sectionServiceLazy = new Lazy(() => sectionServiceMock.Object); + + _diagnosisRepositoryMock = new Mock(); + _diagnosisArchiveRepositoryMock = new Mock(); + _clientMessageServiceMock = new Mock(); + _subscribersServiceMock = new Mock(); + _unitServiceMock = new Mock(); + _httpContextAccessor = new Mock(); + _auditService = new Mock(); + _calculatedObservationsServiceMock = new Mock(); + _calculatedObservationsServiceLazy = + new Lazy(() => _calculatedObservationsServiceMock.Object); + _calculatedObservationsServiceMock.Setup(x => x.Map(It.IsAny())) + .ReturnsAsync((PatientDiagnosis? value) => value); + _optionsApiSettings = Options.Create(_apiSettings); + _logger = new Mock>(); + _diagnosisService = new DiagnosisService( + _patientServiceLazy, + //sectionServiceLazy, + _optionsApiSettings, + _diagnosisRepositoryMock.Object, + _diagnosisArchiveRepositoryMock.Object, + _logger.Object, + _clientMessageServiceMock.Object, + _subscribersServiceMock.Object, + _calculatedObservationsServiceLazy, + _httpContextAccessor.Object, + _auditService.Object, + _unitServiceMock.Object + ); + + + // Create a mock of the singleton class subscribers + var mockSingleton = new Mock(); + + // Set up the mock object to return a specific value when a method is called + mockSingleton.Setup(x => x.Map(It.IsAny())).ReturnsAsync((PatientDiagnosis? value) => value); + } + + private DiagnosisService _diagnosisService; + + private Lazy _patientServiceLazy; + + private Mock _patientServiceMock; + private Mock _diagnosisRepositoryMock; + private Mock _diagnosisArchiveRepositoryMock; + + private Mock _clientMessageServiceMock; + private Mock _subscribersServiceMock; + private Mock _unitServiceMock; + private Lazy _calculatedObservationsServiceLazy; + private Mock _calculatedObservationsServiceMock; + private Mock _httpContextAccessor = new(); + private Mock _auditService = new(); + + private readonly ApiSettings _apiSettings = new() + { + DiagnosisSystem = "Snomed-CT", + DiagnosisCode = { "55607006" } + }; + + private IOptions _optionsApiSettings; + + + private Mock> _logger; + + private static readonly DateTime Now = DateTime.Now; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + + // [Test] + // public void SaveRequest_patientNumber_and_pointOfCare_null_Return_not_insert() + // { + // var apiRequest = new ApiRequest(); + // + // Assert.ThrowsAsync(async () => await _diagnosisService.SaveRequest(apiRequest, null)); + // } + + // [Test] + // public void SaveRequest_Not_ORU_R01_Return_not_insert() + // { + // var patientObs = new Person + // { + // FirstName = "Miguel", + // LastName = "Villanueva", + // Ids = new Dictionary { { "MR", "437537" } } + // }; + // + // var patient = new Patient + // { + // Id = ObjectId.GenerateNewId(), + // UnitString = "UCI5C", + // Bed = "Box4", + // PatientNumber = "437537", + // Person = patientObs + // + // }; + // + // var apiRequest = new ApiRequest + // { + // Location = new PatientLocation("UCI5C", "Box4"), + // Patient = patientObs, + // PatientNumber = "437537", + // Type = "ORU_R11", + // PatientId = PatientId.ToString(), + // MessageTime = Now + // }; + // + // _patientServiceMock.Setup(cm => cm.FindPatientByApiRequest(apiRequest)).ReturnsAsync(patient); + // + // Assert.ThrowsAsync(async () => await _diagnosisService.SaveRequest(apiRequest, null)); + // } + + + [Test] + public async Task SaveRequest_Not_FindPatient_Return_not_insert() + { + var apiRequest = new ApiRequest + { + Location = new PatientLocation("UCI5C", "Box4"), + PatientNumber = "437537", + Type = "ORU_R01", + PatientId = PatientId.ToString(), + MessageTime = Now + }; + + _patientServiceMock.Setup(cm => cm.FindPatientByApiRequest(apiRequest)).ReturnsAsync((Patient?)null); + + await _diagnosisService.SaveRequest(apiRequest, null); + + _diagnosisRepositoryMock.Verify(d => d.InsertOneAsync(It.IsAny()), Times.Never); + } + + [Test] + public async Task SaveRequest_Not_Observations_Return_not_insert() + { + var patientObs = new Person + { + FirstName = "Miguel", + LastName = "Villanueva", + Ids = new Dictionary { { "MR", "437537" } } + }; + + var patient = new Patient + { + Id = ObjectId.GenerateNewId(), + UnitString = "UCI5C", + Bed = "Box4", + PatientNumber = "437537", + Person = patientObs + }; + + var apiRequest = new ApiRequest + { + Location = new PatientLocation("UCI5C", "Box4"), + ObservationData = new ObservationData + { + Code = "302147001", + CodingSystem = "SNM", + Value = "Aire; Contacto; Preventivo", + Text = "Aislamiento", + Time = Now + }, + Patient = patientObs, + PatientNumber = "437537", + Type = "ORU_R01", + PatientId = PatientId.ToString(), + MessageTime = Now + }; + + + _patientServiceMock.Setup(cm => cm.FindPatientByApiRequest(apiRequest)).ReturnsAsync(patient); + + await _diagnosisService.SaveRequest(apiRequest, null); + + _diagnosisRepositoryMock.Verify(d => d.InsertOneAsync(It.IsAny()), Times.Never); + } + + [Test] + public async Task SaveRequest_Not_DiagnosisCodeSettings_Return_not_insert() + { + var patientObs = new Person + { + FirstName = "Miguel", + LastName = "Villanueva", + Ids = new Dictionary { { "MR", "437537" } } + }; + + var patient = new Patient + { + Id = ObjectId.GenerateNewId(), + UnitString = "UCI5C", + Bed = "Box4", + PatientNumber = "437537", + Person = patientObs + }; + + var apiRequest = new ApiRequest + { + Location = new PatientLocation("UCI5C", "Box4"), + ObservationData = new ObservationData + { + Code = "302147001", + CodingSystem = "SNM", + Value = "Aire; Contacto; Preventivo", + Text = "Aislamiento", + Time = Now + }, + Observations = + [ + new PatientObservation + { + Code = "263490005", + CodingSystem = "SNM", + Name = "Estado", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "Sin alergias conocidas" + }, + + new PatientObservation + { + Code = "300916003", + CodingSystem = "SNM", + Name = "¿Alergia al látex?", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "No" + } + ], + Patient = patientObs, + PatientNumber = "437537", + Type = "ORU_R01", + PatientId = PatientId.ToString(), + MessageTime = Now + }; + + Options.Create(new ApiSettings()); + + var diagnosisService = new DiagnosisService( + _patientServiceLazy, + //sectionServiceLazy, + _optionsApiSettings, + _diagnosisRepositoryMock.Object, + _diagnosisArchiveRepositoryMock.Object, + _logger.Object, + _clientMessageServiceMock.Object, + _subscribersServiceMock.Object, + _calculatedObservationsServiceLazy, + _httpContextAccessor.Object, + _auditService.Object, + _unitServiceMock.Object); + + _patientServiceMock.Setup(cm => cm.FindPatientByApiRequest(apiRequest)).ReturnsAsync(patient); + + await diagnosisService.SaveRequest(apiRequest, null); + + _diagnosisRepositoryMock.Verify(d => d.InsertOneAsync(It.IsAny()), Times.Never); + } + + + // [Test] + // public async Task SaveRequest_DiagnosisCodeSettings_Return_insert() + // { + // var patientObs = new Person + // { + // FirstName = "Miguel", + // LastName = "Villanueva", + // Ids = new Dictionary { { "MR", "437537" } } + // }; + // + // var patient = new Patient + // { + // Id = ObjectId.GenerateNewId(), + // UnitString = "UCI5C", + // Bed = "Box4", + // PatientNumber = "437537", + // Person = patientObs + // }; + // + // var apiRequest = new ApiRequest + // { + // Location = new PatientLocation("UCI5C", "Box4"), + // ObservationData= new ObservationData + // { + // Code = "55607006", + // CodingSystem = "SNM", + // Value = "Diagnosis", + // Time = Now + // + // }, + // Observations = new List + // { + // new() + // { + // Code = "272099008", + // CodingSystem = "SNM", + // Name = "description", + // Status = ObservationStatus.Ok, + // Time = Now, + // Value = "Daño axonal difuso" + // }, + // new() + // { + // Code = "1000000013", + // CodingSystem = "SNM", + // Name = "label", + // Status = ObservationStatus.Ok, + // Time = Now, + // Value = "Daño axonal difuso" + // }, + // new() + // { + // Code = "1000000014", + // CodingSystem = "SNM", + // Name = "code", + // Status = ObservationStatus.Ok, + // Time = Now, + // Value = "262693007" + // }, + // new() + // { + // Code = "394731006", + // CodingSystem = "SNM", + // Name = "state", + // Status = ObservationStatus.Ok, + // Time = Now, + // Value = "state" + // }, + // new() + // { + // Code = "272125009", + // CodingSystem = "SNM", + // Name = "category", + // Status = ObservationStatus.Ok, + // Time = Now, + // Value = "category" + // }, + // new() + // { + // Code = "398201009", + // CodingSystem = "SNM", + // Name = "starTime", + // Status = ObservationStatus.Ok, + // Time = Now, + // Value = Now.AddHours(1) + // }, + // new() + // { + // Code = "397898000", + // CodingSystem = "SNM", + // Name = "endTime", + // Status = ObservationStatus.Ok, + // Time = Now, + // Value = Now.AddHours(2) + // } + // + // }, + // Patient = patientObs, + // PatientNumber = "437537", + // Type = "ORU_R01", + // PatientId = PatientId.ToString(), + // MessageTime = Now + // }; + // + // _diagnosisService = new DiagnosisService( + // _patientServiceLazy, + // //sectionServiceLazy, + // _optionsApiSettings, + // _diagnosisRepositoryMock.Object, + // _diagnosisArchiveRepositoryMock.Object, + // _logger.Object, + // _clientMessageServiceMock.Object, + // _subscribersServiceMock.Object, + // _calculatedObservationsServiceLazy, + // _unitServiceMock.Object); + // + // _patientServiceMock.Setup(cm => cm.FindPatientByApiRequest(apiRequest)).ReturnsAsync(patient); + // + // await _diagnosisService.SaveRequest(apiRequest, null); + // + // _diagnosisRepositoryMock.Verify(d => d.InsertOneAsync(It.IsAny()), Times.Once); + // _diagnosisRepositoryMock.Verify(d => d.UpdateOneAsync(It.IsAny(),It.IsAny()), Times.Never); + // } + + // [Test] + // public async Task SaveRequest_DiagnosisCodeSettings_Return_Update() + // { + // var patientObs = new Person + // { + // FirstName = "Miguel", + // LastName = "Villanueva", + // Ids = new Dictionary { { "MR", "437537" } } + // }; + // + // var patient = new Patient + // { + // Id = ObjectId.GenerateNewId(), + // UnitString = "UCI5C", + // Bed = "Box4", + // PatientNumber = "437537", + // Person = patientObs + // }; + // + // var apiRequest = new ApiRequest + // { + // Location = new PatientLocation("UCI5C", "Box4"), + // ObservationData= new ObservationData + // { + // Code = "55607006", + // CodingSystem = "SNM", + // Value = "Diagnosis", + // Time = Now + // + // }, + // Observations = new List + // { + // new() + // { + // Code = "272099008", + // CodingSystem = "SNM", + // Name = "description", + // Status = ObservationStatus.Ok, + // Time = Now, + // Value = "Daño axonal difuso" + // }, + // new() + // { + // Code = "1000000013", + // CodingSystem = "SNM", + // Name = "label", + // Status = ObservationStatus.Ok, + // Time = Now, + // Value = "Daño axonal difuso" + // }, + // new() + // { + // Code = "1000000014", + // CodingSystem = "SNM", + // Name = "code", + // Status = ObservationStatus.Ok, + // Time = Now, + // Value = "262693007" + // }, + // new() + // { + // Code = "394731006", + // CodingSystem = "SNM", + // Name = "state", + // Status = ObservationStatus.Ok, + // Time = Now, + // Value = "state" + // }, + // new() + // { + // Code = "272125009", + // CodingSystem = "SNM", + // Name = "category", + // Status = ObservationStatus.Ok, + // Time = Now, + // Value = "category" + // }, + // new() + // { + // Code = "398201009", + // CodingSystem = "SNM", + // Name = "starTime", + // Status = ObservationStatus.Ok, + // Time = Now, + // Value = Now.AddHours(1) + // }, + // new() + // { + // Code = "397898000", + // CodingSystem = "SNM", + // Name = "endTime", + // Status = ObservationStatus.Ok, + // Time = Now, + // Value = Now.AddHours(2) + // } + // + // }, + // Patient = patientObs, + // PatientNumber = "437537", + // Type = "ORU_R01", + // PatientId = patient.Id.ToString(), + // MessageTime = Now + // }; + // + // var patientDiagnosis = new PatientDiagnosis + // { + // Id = ObjectId.GenerateNewId(), + // Time = Now + // }; + // + // _diagnosisService = new DiagnosisService( + // _patientServiceLazy, + // //sectionServiceLazy, + // _optionsApiSettings, + // _diagnosisRepositoryMock.Object, + // _diagnosisArchiveRepositoryMock.Object, + // _logger.Object, + // _clientMessageServiceMock.Object, + // _subscribersServiceMock.Object, + // _calculatedObservationsServiceLazy, + // _unitServiceMock.Object); + // + // _patientServiceMock.Setup(cm => cm.FindPatientByApiRequest(apiRequest)).ReturnsAsync(patient); + // _diagnosisRepositoryMock.Setup(d => d.FindByPatientIdAndCode(patient.Id, "262693007", "Snomed-CT")).ReturnsAsync(patientDiagnosis); + // + // await _diagnosisService.SaveRequest(apiRequest, null); + // + // //_diagnosisRepositoryMock.Verify(d => d.InsertOneAsync(It.IsAny()), Times.Never); + // _diagnosisRepositoryMock.Verify(d => d.UpdateOneAsync(It.IsAny(), It.IsAny()), Times.Once); + // } +} \ No newline at end of file diff --git a/adas-core.Test/Services/DischargeServiceTest.cs b/adas-core.Test/Services/DischargeServiceTest.cs new file mode 100644 index 00000000..21770cba --- /dev/null +++ b/adas-core.Test/Services/DischargeServiceTest.cs @@ -0,0 +1,218 @@ +using System.Security.Claims; +using adas_core.Application.Exceptions; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.MongoModels; +using adas_core.Test.Utilities; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; +using Moq; + +namespace adas_core.Test.Services; + +public class DischargeServiceTest +{ + private readonly Mock _auditServiceMock = new(); + private readonly Mock _clientMessageServiceMock = new(); + private readonly Mock _dischargeRepositoryMock = new(); + private readonly Mock _httpContextAccessorMock = new(); + private readonly Mock> _loggerMock = new(); + private readonly Mock _masterMock = new(); + private readonly Mock> _patientServiceLazyMock = new(); + + private readonly Mock _pointOfCareServiceMock = new(); + private readonly Mock _subscribersServiceMock = new(); + private readonly Mock _unitServiceMock = new(); + private DischargeService _dischargeService = null!; + + [SetUp] + public void Setup() + { + var userClaims = new ClaimsPrincipal(new ClaimsIdentity([ + new Claim(ClaimTypes.Name, "TestUser") + ], "mock")); + + var httpContextMock = new DefaultHttpContext + { + User = userClaims + }; + + _httpContextAccessorMock.Setup(accessor => accessor.HttpContext) + .Returns(httpContextMock); + _dischargeService = new DischargeService( + _loggerMock.Object, + _subscribersServiceMock.Object, + _dischargeRepositoryMock.Object, + _patientServiceLazyMock.Object, + _clientMessageServiceMock.Object, + _pointOfCareServiceMock.Object, + _httpContextAccessorMock.Object, + _auditServiceMock.Object, + _unitServiceMock.Object, + _masterMock.Object); + } + + [Test] + public async Task DeleteDischargeAsync_WhenDischargeExists_DeletesDischarge() + { + // Arrange + var dischargeId = ObjectId.GenerateNewId(); + var discharge = new Discharge { Id = dischargeId }; + _dischargeRepositoryMock.Setup(repo => repo.FindById(dischargeId)).ReturnsAsync(discharge); + + // Act + await _dischargeService.DeleteDischargeAsync(discharge); + + // Assert + _dischargeRepositoryMock.Verify(repo => repo.Delete(dischargeId), Times.Once); + } + + //necesita el pocservice + // [Test] + // public async Task GetDischargeByIdAsync_WhenDischargeExists_ReturnsDischarge() + // { + // // Arrange + // var dischargeId = ObjectId.GenerateNewId(); + // var discharge = new Discharge { Id = dischargeId }; + // _dischargeRepositoryMock.Setup(repo => repo.FindById(dischargeId)).ReturnsAsync(discharge); + // + // // Act + // var result = await _dischargeService.GetDischargeByIdAsync(dischargeId); + // + // // Assert + // Assert.That(result, Is.EqualTo(discharge)); + // } + + [Test] + public void GetDischargeByIdAsync_WhenExceptionOccurs_ReturnsNullAndLogsError() + { + // Arrange + var dischargeId = ObjectId.GenerateNewId(); + // Act and assert + Func act = async () => await _dischargeService.GetDischargeByIdAsync(dischargeId); + Assert.ThrowsAsync(act); + } + + [Test] + public async Task GetDischargesAsync_WhenNoException_ReturnsDischarges() + { + // Arrange + var discharge = TestUtilities.CreateValidDischarge(); + var discharges = new List { discharge }; + _dischargeRepositoryMock.Setup(repo => repo.FindAll()).ReturnsAsync(discharges); + + // Act + var result = await _dischargeService.GetDischargesAsync(); + + var resultList = result.ToList(); + + // Assert + Assert.That(resultList.First().Id, Is.EqualTo(discharges.First().Id)); + } + + //necesita el pocservice + // [Test] + // public async Task GetDischargeByPatientId_WhenDischargeExists_ReturnsDischarge() + // { + // // Arrange + // var discharge = TestUtilities.CreateValidDischarge(); + // _dischargeRepositoryMock.Setup(repo => repo.GetByPatientId(discharge.PatientId)).ReturnsAsync(discharge); + // + // // Act + // var result = await _dischargeService.GetDischargeByPatientId(discharge.PatientId); + // + // // Assert + // Assert.That(result?.Id, Is.EqualTo(discharge.Id)); + // } + + [Test] + public async Task InsertDischarge_WhenInsertionSuccessful_ReturnsInsertedDischarge() + { + // Arrange + var dischargeId = ObjectId.GenerateNewId(); + var discharge = new Discharge { Id = dischargeId }; + _dischargeRepositoryMock.Setup(repo => repo.InsertOneAsync(discharge)).Returns(Task.CompletedTask); + _dischargeRepositoryMock.Setup(repo => repo.FindById(dischargeId)).ReturnsAsync(discharge); + + // Act + var result = await _dischargeService.InsertDischarge(discharge); + + // Assert + Assert.That(result, Is.EqualTo(discharge)); + } + + [Test] + public void InsertDischarge_WhenInsertionFails_ReturnsNullAndLogsError() + { + // Arrange + var discharge = new Discharge { Id = ObjectId.GenerateNewId() }; + var exception = new Exception("Failed to insert discharge"); + _dischargeRepositoryMock.Setup(repo => repo.InsertOneAsync(discharge)).ThrowsAsync(exception); + + // Assert + Func act = async () => await _dischargeService.InsertDischarge(discharge); + Assert.ThrowsAsync(act); + } + + [Test] + public async Task GetDischargeByLocation_WhenDischargeExists_ReturnsDischarge() + { + // Arrange + var location = new PatientLocation("UnitName", "Bed", "Room"); + var discharge = new Discharge(); + _dischargeRepositoryMock.Setup(repo => repo.GetDischargeByLocation(location)).ReturnsAsync(discharge); + + // Act + var result = await _dischargeService.GetDischargeByLocation(location); + + // Assert + Assert.That(result, Is.EqualTo(discharge)); + } + + [Test] + public async Task GetDischargeByLocation_WhenExceptionOccurs_ReturnsNullAndLogsError() + { + // Arrange + var location = new PatientLocation("UnitName", "Bed", "Room"); + var exception = new Exception("Failed to retrieve discharge by location"); + _dischargeRepositoryMock.Setup(repo => repo.GetDischargeByLocation(location)).ThrowsAsync(exception); + + // Act + var result = await _dischargeService.GetDischargeByLocation(location); + + // Assert + Assert.That(result, Is.Null); + } + + // [Test] + // public async Task GetDischargeByPointOfCareId_WhenDischargeExists_ReturnsDischarge() + // { + // // Arrange + // var discharge = TestUtilities.CreateValidDischarge(); + // _dischargeRepositoryMock.Setup(repo => repo.GetDischargeByPointOfCareId(discharge.PointOfCareId)).ReturnsAsync(discharge); + // + // // Act + // var result = await _dischargeService.GetDischargeByPointOfCareId(discharge.PointOfCareId); + // + // // Assert + // Assert.That(result, Is.EqualTo(discharge)); + // } + + [Test] + public async Task GetDischargeByPointOfCareId_WhenExceptionOccurs_ReturnsNullAndLogsError() + { + // Arrange + var poc = ObjectId.GenerateNewId(); + var exception = new Exception("Failed to retrieve discharge by PointOfCareId"); + _dischargeRepositoryMock.Setup(repo => repo.GetDischargeByPointOfCareId(poc)).ThrowsAsync(exception); + + // Act + var result = await _dischargeService.GetDischargeByPointOfCareId(poc); + + // Assert + Assert.That(result, Is.Null); + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/DisplayServiceTest.cs b/adas-core.Test/Services/DisplayServiceTest.cs new file mode 100644 index 00000000..fec58d39 --- /dev/null +++ b/adas-core.Test/Services/DisplayServiceTest.cs @@ -0,0 +1,375 @@ +using System.Security.Claims; +using adas_core.Application.Exceptions; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; +using Moq; +using static NUnit.Framework.Assert; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Services; + +[TestFixture] +public class DisplayServiceTest +{ + private Mock _mockCacheService = null!; + private Mock _mockDisplayRepository = null!; + private Mock _mockPointOfCareService = null!; + private Mock _mockDisplayConfigService = null!; + private Mock _mockUserRepository = null!; + private Mock _authServiceMock = null!; + private Mock _mockSubscribersService = null!; + private Mock _mockClientMessageService = null!; + private Mock> _mockUnitService = null!; + private Mock _mockPermissionService = null!; + private Lazy _lazyMockPermission = null!; + private Mock> _mockLogger = null!; + private readonly Mock _httpContextAccessorMock = new(); + private readonly Mock _auditServiceMock = new(); + + private DisplayService _displayService = null!; + + [SetUp] + public void SetUp() + { + _mockCacheService = new Mock(); + _mockDisplayRepository = new Mock(); + _mockPointOfCareService = new Mock(); + _mockDisplayConfigService = new Mock(); + _mockUserRepository = new Mock(); + _authServiceMock = new Mock(); + _mockSubscribersService = new Mock(); + _mockClientMessageService = new Mock(); + _mockUnitService = new Mock>(); + _mockPermissionService = new Mock(); + _lazyMockPermission = new Lazy(() => _mockPermissionService.Object); + _mockLogger = new Mock>(); + + var claims = new ClaimsPrincipal( + new ClaimsIdentity([new Claim(ClaimTypes.Name, "TestUser")], "mock")); + _httpContextAccessorMock.Setup(a => a.HttpContext).Returns(new DefaultHttpContext { User = claims }); + + var cacheSettings = Options.Create(new CacheSettings()); + + _displayService = new DisplayService( + _mockDisplayRepository.Object, + _mockPointOfCareService.Object, + _mockUnitService.Object, + _mockDisplayConfigService.Object, + _mockSubscribersService.Object, + _mockClientMessageService.Object, + _mockUserRepository.Object, + _authServiceMock.Object, + _mockLogger.Object, + _httpContextAccessorMock.Object, + _auditServiceMock.Object, + _lazyMockPermission, + _mockCacheService.Object, + cacheSettings + ); + + } + + // ----------------------------------------------------------- + // InsertOne + // ----------------------------------------------------------- + [Test] + public async Task InsertOne_ShouldInsertDisplay() + { + var display = new Display + { + Name = "Test Display", + Type = DisplayConfigEnums.DisplayType.DisplayNurse + }; + + var defaultCfg = new DisplayConfig { Id = ObjectId.GenerateNewId(), Type = display.Type }; + + _mockDisplayConfigService.Setup(s => s.GetDefaultConfig(display.Type)).ReturnsAsync(defaultCfg); + + _mockDisplayRepository.Setup(r => r.InsertOneAsync(display)) + .Returns(Task.CompletedTask); + + var result = await _displayService.InsertOne(display); + + That(result, Is.EqualTo(display)); + _mockDisplayRepository.Verify(r => r.InsertOneAsync(display), Times.Once); + } + + // ----------------------------------------------------------- + // GetById + // ----------------------------------------------------------- + [Test] + public async Task GetById_ShouldReturnDisplay() + { + var id = ObjectId.GenerateNewId(); + var d = new Display { Id = id, Name = "D1" }; + + _mockDisplayRepository.Setup(r => r.GetById(id)).ReturnsAsync(d); + + var result = await _displayService.GetById(id); + + That(result, Is.EqualTo(d)); + } + + // ----------------------------------------------------------- + // GetAllByUser - userName null + // ----------------------------------------------------------- + [Test] + public async Task GetAllByUser_ShouldReturnEmpty_WhenUserNameNull() + { + var result = await _displayService.GetAllByUser(null); + That(result, Is.Empty); + } + + // ----------------------------------------------------------- + // GetAllByUser - Admin case + // ----------------------------------------------------------- + + [Test] + public async Task GetAllByUser_ShouldReturnDisplays_WhenAdmin() + { + // Arrange + const string userName = "admin"; + + var adminId = ObjectId.GenerateNewId(); + var unitId = ObjectId.GenerateNewId(); + + var user = new User + { + Id = adminId, + UserName = userName, + Authorization = + [ + new Authorization { UnitId = unitId.ToString(), Rol = nameof(PermissionEnum.RolesType.AuthAdmin) } + ] + }; + + // 1) Usuario + authorities + _mockUserRepository.Setup(r => r.GetByUserAndAuthoritesName(userName)) + .ReturnsAsync(user); + + _authServiceMock.Setup(a => a.GetUserAuthorities(adminId)) + .ReturnsAsync(user.Authorization); + + // 2) Displays por UnitId + var d1 = new Display { Id = ObjectId.GenerateNewId(), Name = "D1", UnitId = unitId }; + var d2 = new Display { Id = ObjectId.GenerateNewId(), Name = "D2", UnitId = unitId }; + var displays = new List { d1, d2 }; + + _mockDisplayRepository.Setup(r => r.GetByUnitId(unitId)) + .ReturnsAsync(displays); + + // 3) GetInfo (rama base) → caché debe devolver el display haciendo el factory + _mockDisplayRepository.Setup(r => r.GetById(d1.Id)).ReturnsAsync(d1); + _mockDisplayRepository.Setup(r => r.GetById(d2.Id)).ReturnsAsync(d2); + + _mockCacheService + .Setup(c => c.GetOrSetObjectAsync( + It.IsAny(), + It.IsAny>>(), + It.IsAny(), + It.IsAny())) + .Returns((string _, Func> factory, TimeSpan? _, CancellationToken _) => factory()); + + + // 4) Permisos (si es null, el servicio NO añade el display) + _mockPermissionService + .Setup(p => p.GetPermissionsForUnit(unitId.ToString(), user)) + .ReturnsAsync(new DisplayPermissionTypes( + new UserActions(true,true,true,true,true), + new UserActions(true,true,true,true,true), + new UserActions(true,true,true,true,true), + new UserActions(true,true,true,true,true), + new UserActions(true,true,true,true,true), + new UserActions(true,true,true,true,true), + new UserActions(true,true,true,true,true), + true + )); + + // Act + var result = await _displayService.GetAllByUser(userName); + + // Assert + That(result, Has.Count.EqualTo(2)); + using (Assert.EnterMultipleScope()) + { + That(result.Any(r => r.Display != null && r.Display.Id == d1.Id), Is.True); + That(result.Any(r => r.Display != null && r.Display.Id == d2.Id), Is.True); + } + } + + // ----------------------------------------------------------- + // GetByType + // ----------------------------------------------------------- + [Test] + public async Task GetByType_ShouldReturnDisplays() + { + var cfgId = ObjectId.GenerateNewId(); + var cfg = new DisplayConfig { Id = cfgId, Type = DisplayConfigEnums.DisplayType.DisplayNurse }; + var display = new Display { Id = ObjectId.GenerateNewId(), DisplayConfigId = cfgId, Name = "D" }; + + _mockDisplayConfigService.Setup(s => s.GetByType(cfg.Type)) + .ReturnsAsync([cfg]); + + _mockDisplayRepository.Setup(r => r.GetByConfigId(cfgId)) + .ReturnsAsync([display]); + + var result = await _displayService.GetByType(cfg.Type); + + That(result.Count, Is.EqualTo(1)); + That(result[0], Is.EqualTo(display)); + } + + [Test] + public async Task GetByType_ShouldReturnEmpty_WhenNoConfigsFound() + { + _mockDisplayConfigService.Setup(s => s.GetByType(It.IsAny())) + .ReturnsAsync([]); + + var result = await _displayService.GetByType(DisplayConfigEnums.DisplayType.DisplayNurse); + + That(result, Is.Empty); + } + + // ----------------------------------------------------------- + // GetByPointOfCare + // ----------------------------------------------------------- + [Test] + public async Task GetByPointOfCare_ShouldReturnDisplays() + { + var poc = new PointOfCare { Id = ObjectId.GenerateNewId() }; + var d = new Display { Id = ObjectId.GenerateNewId(), Name = "Display" }; + + _mockDisplayRepository.Setup(r => r.GetByPointOfCare(poc)) + .ReturnsAsync([d]); + + var result = await _displayService.GetByPointOfCare(poc); + + That(result, Has.Count.EqualTo(1)); + } + + // ----------------------------------------------------------- + // GetByConfigId + // ----------------------------------------------------------- + [Test] + public async Task GetByConfigId_ShouldReturnDisplays() + { + var cfgId = ObjectId.GenerateNewId(); + var d = new Display { Id = ObjectId.GenerateNewId(), DisplayConfigId = cfgId, Name = "display1"}; + + _mockDisplayRepository.Setup(r => r.GetByConfigId(cfgId)) + .ReturnsAsync([d]); + + var result = await _displayService.GetByConfigId(cfgId); + + That(result, Has.Count.EqualTo(1)); + } + + // ----------------------------------------------------------- + // GetByName + // ----------------------------------------------------------- + [Test] + public void GetByName_ShouldThrow_WhenNotFound() + { + _mockDisplayRepository.Setup(r => r.GetByName("X")) + .ReturnsAsync((Display?)null); + + Func act = () => _displayService.GetByName("X"); + Assert.ThrowsAsync(act); + } + + // ----------------------------------------------------------- + // GetInfo + // ----------------------------------------------------------- + [Test] + public async Task GetInfo_ShouldReturnDisplayWithPoc() + { + var id = ObjectId.GenerateNewId(); + var cfgId = ObjectId.GenerateNewId(); + var pocId = ObjectId.GenerateNewId(); + + var display = new Display + { + Id = id, + Name = "Display", + DisplayConfigId = cfgId, + PointOfCareIdList = [pocId] + }; + + _mockCacheService + .Setup(c => c.GetOrSetObjectAsync( + It.IsAny(), + It.IsAny>>(), + It.IsAny(), + It.IsAny())) + .Returns((string _, Func> factory, TimeSpan? _, CancellationToken _) => factory()); + + _mockDisplayRepository.Setup(r => r.GetById(id)).ReturnsAsync(display); + _mockDisplayConfigService.Setup(s => s.GetById(cfgId)) + .ReturnsAsync(new DisplayConfig { Id = cfgId }); + + var poc = new PointOfCare { Id = pocId }; + + _mockPointOfCareService.Setup(s => s.GetInfo(pocId, null, true, It.IsAny())) + .ReturnsAsync(poc); + + var result = await _displayService.GetInfo(id, "user", [], null, true, true, false); + + That(result, Is.Not.Null); + That(result!.PointOfCares.First(), Is.EqualTo(poc)); + } + + // ----------------------------------------------------------- + // GetByUnitId + // ----------------------------------------------------------- + [Test] + public async Task GetByUnitId_ShouldReturnDisplays() + { + var id = ObjectId.GenerateNewId(); + var d = new Display { Id = ObjectId.GenerateNewId(), UnitId = id , Name = "display1"}; + + _mockDisplayRepository.Setup(r => r.GetByUnitId(id)) + .ReturnsAsync([d]); + + var result = await _displayService.GetByUnitId(id); + + That(result, Has.Count.EqualTo(1)); + } + + // ----------------------------------------------------------- + // UpdatePointOfCareList + // ----------------------------------------------------------- + [Test] + public void UpdatePointOfCareList_ShouldThrow_WhenDisplayNotFound() + { + var id = ObjectId.GenerateNewId(); + + _mockDisplayRepository.Setup(r => r.GetById(id)) + .ReturnsAsync((Display?)null); + + Func act = () => _displayService.UpdatePointOfCareList(id, []); + Assert.ThrowsAsync(act); + } + + // ----------------------------------------------------------- + // UpdateConfigPreset + // ----------------------------------------------------------- + [Test] + public void UpdateConfigPreset_ShouldThrow_WhenUpdateFails() + { + var id = ObjectId.GenerateNewId(); + var cfgId = ObjectId.GenerateNewId(); + + _mockDisplayRepository.Setup(r => r.UpdateConfigPreset(id, cfgId)) + .ReturnsAsync((Display?)null); + + Func act = () => _displayService.UpdateConfigPreset(id, cfgId); + Assert.ThrowsAsync(act); + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/GroupedObservationServiceTest.cs b/adas-core.Test/Services/GroupedObservationServiceTest.cs new file mode 100644 index 00000000..44057a4c --- /dev/null +++ b/adas-core.Test/Services/GroupedObservationServiceTest.cs @@ -0,0 +1,487 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Utils; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Moq; +using static adas_core.Domain.Enums.GroupedObservationEnum; + +namespace adas_core.Test.Services; + +[TestFixture] +public class GroupedObservationServiceTest +{ + [SetUp] + public void Setup() + { + var configObservationService = new Mock(); + var observationRepository = new Mock(); + + _logger = new Mock>(); + + _groupedObservationService = new GroupedObservationService( + observationRepository.Object, configObservationService.Object, + _logger.Object, Mock.Of(), Options.Create(new ApiSettings()), Options.Create(new CacheSettings())); + } + + private GroupedObservationService _groupedObservationService; + private Mock> _logger; + + + [Test] + public void Generate_Shift_Observations() + { + var dateTime = DateTime.Now; + if (dateTime.Day <= 2) dateTime = dateTime.AddDays(2); + + var result = new List + { + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day, 12, 30, "FC", 100, Result.Last, null), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day, 10, 30, "FC", 108, Result.Last, null), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day, 9, 30, "FC", 106, Result.Last, null), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day, 5, 30, "FC", 112, Result.Last, null), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day, 4, 30, "FC", 105, Result.Last, null), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day - 1, 23, 30, "FC", 190, Result.Last, null), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day - 1, 19, 30, "FC", 120, Result.Last, null), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day - 1, 15, 30, "FC", 130, Result.Last, null), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day - 1, 12, 30, "FC", 140, Result.Last, null), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day - 1, 09, 30, "FC", 150, Result.Last, null), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day - 2, 15, 30, "FC", 150, Result.Last, null) + }; + + var groupedField = new GroupedField + { + StartTimeShift = ["08:00", "15:00", "22:00"], + Max = 36, + Result = [Result.Last] + }; + + var shiftObservations = _groupedObservationService.GenerateShiftObservations(result, groupedField); + + var shift1ObsToday = shiftObservations.Count(s => s.Get("shift") == 0 && s.Get("day") == dateTime.Day); + + var shift1ObsYesterday = shiftObservations.Count(s => s.Get("shift") == 0 && s.Get("day") == dateTime.Day - 1); + + var shift2ObsYesterday = shiftObservations.Count(s => s.Get("shift") == 1 && s.Get("day") == dateTime.Day - 1); + + var shif3ObsYesterday = shiftObservations.Count(s => s.Get("shift") == 2 && s.Get("day") == dateTime.Day - 1); + + var shiftTwoDaysAgo = shiftObservations.Count(s => s.Get("shift") == 1 && s.Get("day") == dateTime.Day - 2); + using (Assert.EnterMultipleScope()) + { + Assert.That(shift1ObsToday, Is.EqualTo(3)); + Assert.That(shif3ObsYesterday, Is.EqualTo(3)); + Assert.That(shift2ObsYesterday, Is.EqualTo(2)); + Assert.That(shift1ObsYesterday, Is.EqualTo(2)); + Assert.That(shiftTwoDaysAgo, Is.EqualTo(1)); + }; + } + + [Test] + public void Calculate_Shift_Observations() + { + var dateTime = DateTime.Now; + + if (dateTime.Day <= 2) dateTime = dateTime.AddDays(2); + + + var groupedField = new GroupedField + { + StartTimeShift = ["08:00", "15:00", "22:00"], + Max = 36, + Result = [Result.Sum] + }; + + var result = new List + { + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day, 12, 30, "FC", 100, Result.Sum, null), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day, 10, 30, "FC", 108, Result.Sum, null), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day, 9, 30, "FC", 106, Result.Sum, null), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day, 5, 30, "FC", 112, Result.Sum, null), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day, 4, 30, "FC", 105, Result.Sum, null), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day - 1, 23, 30, "FC", 190, Result.Sum, null), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day - 1, 19, 30, "FC", 120, Result.Sum, null), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day - 1, 15, 30, "FC", 130, Result.Sum, null), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day - 1, 12, 30, "FC", 140, Result.Sum, null), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day - 1, 09, 30, "FC", 150, Result.Sum, null), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day - 2, 15, 30, "FC", 150, Result.Sum, null) + }; + + + var shiftObservations = _groupedObservationService.GenerateShiftObservations(result, groupedField); + + _groupedObservationService.CalculateShiftObservations(shiftObservations, groupedField); + var shift1ObsToday = shiftObservations.First(s => s.Get("shift") == 0 && s.Get("day") == dateTime.Day); + var shift1ObsYesterday = shiftObservations.First(s => s.Get("shift") == 0 && s.Get("day") == dateTime.Day - 1); + + var shift2ObsYesterday = shiftObservations.First(s => s.Get("shift") == 1 && s.Get("day") == dateTime.Day - 1); + + var shif3ObsYesterday = shiftObservations.First(s => s.Get("shift") == 2 && s.Get("day") == dateTime.Day - 1); + + var shiftTwoDaysAgo = shiftObservations.First(s => s.Get("shift") == 1 && s.Get("day") == dateTime.Day - 2); + using (Assert.EnterMultipleScope()) + { + Assert.That(shift1ObsToday.Get("sum")?.ToInt32(), Is.EqualTo(314)); + Assert.That(shif3ObsYesterday.Get("sum")?.ToInt32(), Is.EqualTo(407)); + + Assert.That(shift2ObsYesterday.Get("sum")?.ToInt32(), Is.EqualTo(250)); + Assert.That(shift1ObsYesterday.Get("sum")?.ToInt32(), Is.EqualTo(290)); + Assert.That(shiftTwoDaysAgo.Get("sum")?.ToInt32(), Is.EqualTo(150)); + }; + } + +// [Test] +// public void Calculate_Half_Time_Observations() +// { +// /* +// 09:18 - Toma 1: 50/100/75 +// 09:38 - Toma 2: 52/102/76 +// 10:07 - Toma 3: 54/101/72 +// 10:18 - Toma 4: 51/102/73 +// 10:58 - Toma 5: 52/107/79 +// 12:04 - Toma 6: 60/120/90 +// +// El api convierte a +// +// 09h - Toma 2: 52/102/76 +// 10h - Toma 4: 51/102/73 +// 11h - Toma 5: 52/107/79 +// 12h- Toma 6: 60/120/90 +// */ +// +// +// var dateTime = DateTime.Now; +// +// var values9 = new BsonArray { +// new BsonDocument +// { +// { "value", 50}, +// { "time", new BsonDateTime(new DateTime(2021,3,10,9,18,0))}, +// +// }, +// new BsonDocument +// { +// { "value", 100}, +// { "time", new BsonDateTime(new DateTime(2021,3,10,9,38,0))} +// } +// }; +// +// var values10 = new BsonArray { +// new BsonDocument +// { +// { "value", 50}, +// { "time", new BsonDateTime(new DateTime(2021,3,10,10,7,0))} +// }, +// new BsonDocument +// { +// { "value", 60}, +// { "time", new BsonDateTime(new DateTime(2021,3,10,10,18,0))} +// }, +// new BsonDocument +// { +// { "value", 70}, +// { "time", new BsonDateTime(new DateTime(2021,3,10,10,58,0))} +// } +// }; +// +// var values11 = new BsonArray +// { +// +// }; +// +// var values12 = new BsonArray +// { +// new BsonDocument +// { +// { "value", 50}, +// { "time", new BsonDateTime(new DateTime(2021,3,10,12,04,0))} +// } +// }; +// +// +// var result = new List() +// { +// GenerateBsonDocument(dateTime.Year,dateTime.Month,dateTime.Day,9,0,"TAM", 100, Result.HalfHour,values9), +// GenerateBsonDocument(dateTime.Year,dateTime.Month,dateTime.Day,10,0,"TAM", 100, Result.HalfHour,values10), +// GenerateBsonDocument(dateTime.Year,dateTime.Month,dateTime.Day,11,0,"TAM", 100, Result.HalfHour,values11), +// GenerateBsonDocument(dateTime.Year,dateTime.Month,dateTime.Day,12,0,"TAM", 100, Result.HalfHour,values12) +// }; +// +// var halfObservations = _groupedObservationService.CalculateHalfHourObservations(result); +// +// Assert.That(halfObservations, Has.Count.EqualTo(4)); +// Assert.Multiple(() => +// { +// Assert.That(halfObservations[0].GetByCodeSysAndCode("halfhour")?.AsBsonDocument.GetByCodeSysAndCode("value")?.AsInt32, Is.EqualTo(100)); +// Assert.That(halfObservations[1].GetByCodeSysAndCode("halfhour")?.AsBsonDocument.GetByCodeSysAndCode("value")?.AsInt32, Is.EqualTo(60)); +// Assert.That(halfObservations[2].GetByCodeSysAndCode("halfhour")?.AsBsonDocument.GetByCodeSysAndCode("value")?.AsInt32, Is.EqualTo(70)); +// Assert.That(halfObservations[3].GetByCodeSysAndCode("halfhour")?.AsBsonDocument.GetByCodeSysAndCode("value")?.AsInt32, Is.EqualTo(50)); +// }); +// } + + [Test] + public async Task Calculate_Last_Filled_Observations_should_add_Minus_4_hour_to_complete_last_five_hours() + { + var dateTime = DateTime.Now; + + //Falta el -4 tiene que crearlo el calculate last filled observation + var result = new List + { + GenerateBsonDocument(dateTime.AddHours(-5).Year, dateTime.AddHours(-5).Month, dateTime.AddHours(-5).Day, + dateTime.AddHours(-5).Hour, 0, "TAM", 100, Result.LastFilled, null), + GenerateBsonDocument(dateTime.AddHours(-3).Year, dateTime.AddHours(-3).Month, dateTime.AddHours(-3).Day, + dateTime.AddHours(-3).Hour, 0, "TAM", 100, Result.LastFilled, null), + GenerateBsonDocument(dateTime.AddHours(-2).Year, dateTime.AddHours(-2).Month, dateTime.AddHours(-2).Day, + dateTime.AddHours(-2).Hour, 0, "TAM", 100, Result.LastFilled, null), + GenerateBsonDocument(dateTime.AddHours(-1).Year, dateTime.AddHours(-1).Month, dateTime.AddHours(-1).Day, + dateTime.AddHours(-1).Hour, 0, "TAM", 100, Result.LastFilled, null) + }; + + + var groupedField = new GroupedField + { + Max = 5, + Name = "TAM", + Regularity = Regularity.Hour + }; + var patientId = ObjectId.GenerateNewId(); + + var lastFilledObservations = + await _groupedObservationService.CalculateLastFilledObservations(result, groupedField, patientId); + using (Assert.EnterMultipleScope()) + { + Assert.That(lastFilledObservations, Has.Count.EqualTo(5)); + Assert.That( + lastFilledObservations.Exists(i => i.Get("time")?.ToLocalTime().Hour == dateTime.AddHours(-5).Hour), + Is.True); + Assert.That( + lastFilledObservations.Exists(i => i.Get("time")?.ToLocalTime().Hour == dateTime.AddHours(-4).Hour), + Is.True); + Assert.That( + lastFilledObservations.Exists(i => i.Get("time")?.ToLocalTime().Hour == dateTime.AddHours(-3).Hour), + Is.True); + Assert.That( + lastFilledObservations.Exists(i => i.Get("time")?.ToLocalTime().Hour == dateTime.AddHours(-2).Hour), + Is.True); + Assert.That( + lastFilledObservations.Exists(i => i.Get("time")?.ToLocalTime().Hour == dateTime.AddHours(-1).Hour), + Is.True); + }; + } + + [Test] + public async Task Calculate_Last_Filled_Observations_should_add_Minus_1_hour_to_complete_last_five_hours() + { + var dateTime = DateTime.Now; + + //Falta el -4 tiene que crearlo el calculate last filled observation + var result = new List + { + GenerateBsonDocument(dateTime.AddHours(-5).Year, dateTime.AddHours(-5).Month, dateTime.AddHours(-5).Day, + dateTime.AddHours(-5).Hour, 0, "TAM", 100, Result.LastFilled, null), + GenerateBsonDocument(dateTime.AddHours(-4).Year, dateTime.AddHours(-4).Month, dateTime.AddHours(-4).Day, + dateTime.AddHours(-4).Hour, 0, "TAM", 100, Result.LastFilled, null), + GenerateBsonDocument(dateTime.AddHours(-3).Year, dateTime.AddHours(-3).Month, dateTime.AddHours(-3).Day, + dateTime.AddHours(-3).Hour, 0, "TAM", 100, Result.LastFilled, null), + GenerateBsonDocument(dateTime.AddHours(-2).Year, dateTime.AddHours(-2).Month, dateTime.AddHours(-2).Day, + dateTime.AddHours(-2).Hour, 0, "TAM", 100, Result.LastFilled, null) + }; + + + var groupedField = new GroupedField + { + Max = 5, + Name = "TAM", + Regularity = Regularity.Hour + }; + var patientId = ObjectId.GenerateNewId(); + + var lastFilledObservations = + await _groupedObservationService.CalculateLastFilledObservations(result, groupedField, patientId); + using (Assert.EnterMultipleScope()) + { + Assert.That(lastFilledObservations, Has.Count.EqualTo(5)); + Assert.That( + lastFilledObservations.Exists(i => i.Get("time")?.ToLocalTime().Hour == dateTime.AddHours(-5).Hour), + Is.True); + Assert.That( + lastFilledObservations.Exists(i => i.Get("time")?.ToLocalTime().Hour == dateTime.AddHours(-4).Hour), + Is.True); + Assert.That( + lastFilledObservations.Exists(i => i.Get("time")?.ToLocalTime().Hour == dateTime.AddHours(-3).Hour), + Is.True); + Assert.That( + lastFilledObservations.Exists(i => i.Get("time")?.ToLocalTime().Hour == dateTime.AddHours(-2).Hour), + Is.True); + Assert.That( + lastFilledObservations.Exists(i => i.Get("time")?.ToLocalTime().Hour == dateTime.AddHours(-1).Hour), + Is.True); + }; + } + + [Test] + public void FillHoursObservations() + { + /* + 09:18 - Toma 1: 50/100/75 + 09:38 - Toma 2: 52/102/76 + 10:07 - Toma 3: 54/101/72 + 10:18 - Toma 4: 51/102/73 + 10:58 - Toma 5: 52/107/79 + 12:04 - Toma 6: 60/120/90 + + El api convierte a + + 09h - Toma 2: 52/102/76 + 10h - Toma 4: 51/102/73 + 11h - Toma 5: 52/107/79 + 12h- Toma 6: 60/120/90 + */ + + var groupedField = new GroupedField + { + Max = 5, + Name = "TAM", + Regularity = Regularity.Hour + }; + + + var dateTime = + new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day, 12, 30, 0).ToUniversalTime(); + + var values9 = new BsonArray + { + new BsonDocument + { + { "value", 50 }, + { + "time", + new BsonDateTime( + new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, 9, 18, 0).ToUniversalTime()) + } + }, + new BsonDocument + { + { "value", 100 }, + { + "time", + new BsonDateTime( + new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, 9, 38, 0).ToUniversalTime()) + } + } + }; + + var values10 = new BsonArray + { + new BsonDocument + { + { "value", 50 }, + { + "time", + new BsonDateTime( + new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, 10, 7, 0).ToUniversalTime()) + } + }, + new BsonDocument + { + { "value", 60 }, + { + "time", + new BsonDateTime(new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, 10, 18, 0) + .ToUniversalTime()) + } + }, + new BsonDocument + { + { "value", 70 }, + { + "time", + new BsonDateTime(new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, 10, 58, 0) + .ToUniversalTime()) + } + } + }; + + var values12 = new BsonArray + { + new BsonDocument + { + { "value", 50 }, + { + "time", + new BsonDateTime(new DateTime(dateTime.Year, dateTime.Month, dateTime.Day, 12, 04, 0) + .ToUniversalTime()) + } + } + }; + + + var result = new List + { + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day, 9, 0, "TAM", 100, Result.HalfHour, + values9), + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day, 10, 0, "TAM", 100, Result.HalfHour, + values10), + + GenerateBsonDocument(dateTime.Year, dateTime.Month, dateTime.Day, 12, 0, "TAM", 100, Result.HalfHour, + values12) + }; + + var halfObservations = _groupedObservationService.CalculateHalfHourObservations(result); + + var fillHoursObservations = _groupedObservationService.FillHours(halfObservations, groupedField); + + + Assert.That(fillHoursObservations, Is.Not.Empty); + } + + + private static BsonDocument GenerateBsonDocument(int? year, int? month, int? day, int? hour, int? minute, + string name, object value, Result resultType, BsonArray? all) + { + var result = new BsonDocument + { + { + "_id", new BsonDocument + { + { "year", year ?? new DateTime().Year }, + { "month", month ?? new DateTime().Month }, + { "day", day ?? new DateTime().Day }, + { "hour", hour ?? new DateTime().Hour }, + { "minute", minute ?? new DateTime().Minute }, + { "name", name } + } + }, + { "time", new BsonDateTime(new DateTime((int)year!, (int)month!, (int)day!, (int)hour!, (int)minute!, 0)) } + }; + + + /* + {resultType.ToString().ToLower(), new BsonDocument{ + {"time", new BsonDateTime(new System.DateTime((int)year, (int)month, (int)day, (int)hour, (int)minute,0)) }, + {"value", value.ToString() } + }}, + { "time", new BsonDateTime(new System.DateTime((int)year, (int)month, (int)day, (int)hour, (int)minute,0)) }, + + }; + */ + + if (resultType != Result.HalfHour && resultType != Result.Sum && resultType != Result.Count + && resultType != Result.Min && resultType != Result.Average && resultType != Result.Max) + result.Add(resultType.ToString().ToLower(), new BsonDocument + { + { "time", new BsonDateTime(new DateTime((int)year, (int)month, (int)day, (int)hour, (int)minute, 0)) }, + { "value", value.ToString() } + }); + else + result.AddRange(new Dictionary { { resultType.ToString().ToLower(), value } }); + + if (all != null) result.Add("all", all); + + return result; + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/HistoricalConfigChangesServiceTest.cs b/adas-core.Test/Services/HistoricalConfigChangesServiceTest.cs new file mode 100644 index 00000000..3bd24303 --- /dev/null +++ b/adas-core.Test/Services/HistoricalConfigChangesServiceTest.cs @@ -0,0 +1,136 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Moq; + +namespace adas_core.Test.Services; + +[TestFixture] +[NonParallelizable] +internal class HistoricalConfigChangesServiceTest +{ + [SetUp] + public void Setup() + { + _mockRepo = new Mock(); + _mockLogger = new Mock>(); + _service = new HistoricalConfigChangesService(_mockRepo.Object, _mockLogger.Object, _httpContextAccessor.Object, + _auditService.Object); + } + + private HistoricalConfigChangesService _service; + private Mock _mockRepo; + private Mock> _mockLogger; + private readonly Mock _httpContextAccessor = new(); + private readonly Mock _auditService = new(); + + + [Test] + public async Task FindLastConfigChanges_ReturnsLastConfigChange() + { + // Arrange + var configType = DisplayConfigEnums.ConfigTypes.ObservationCfg; + var mockResult = new List + { + new() // Mock object with desired properties + }; + _mockRepo.Setup(r => r.FindLastHistoricalConfigChangesByType(configType, 10)).ReturnsAsync(mockResult); + + // Act + var result = await _service.FindLastConfigChanges(configType); + + // Assert + Assert.That(result, Is.Not.Null); + // Further assertions based on the expected result + } + + [Test] + public async Task InsertOne_WhenExceptionOccurs_ReturnsNull() + { + // Arrange + var mockChange = new HistoricalConfigChanges(); + _mockRepo.Setup(r => r.InsertOneAsync(mockChange)).Throws(new Exception()); + + // Act + var result = await _service.InsertOne(mockChange); + + // Assert + Assert.That(result, Is.Null); + } + + + [Test] + public async Task GetAll_ReturnsAllConfigChanges() + { + // Arrange + var mockResult = new List + { + new(), + new() + }; + _mockRepo.Setup(r => r.FindAll()).ReturnsAsync(mockResult); + + // Act + var result = await _service.GetAll(); + + // Assert + Assert.That(result, Has.Count.EqualTo(2)); + } + + [Test] + public async Task GetByType_ReturnsCorrectAmountOfConfigs() + { + // Arrange + var configType = DisplayConfigEnums.ConfigTypes.ObservationCfg; + var mockResult = new List + { + new(), + new(), + new() + }; + _mockRepo.Setup(r => r.FindLastHistoricalConfigChangesByType(configType, 3)).ReturnsAsync(mockResult); + + // Act + var result = await _service.GetByType(configType, 3); + + // Assert + Assert.That(result, Has.Count.EqualTo(3)); + } + + [Test] + public async Task GetByUser_ReturnsCorrectConfigs() + { + // Arrange + var user = "TestUser"; + var mockResult = new List + { + new(), + new() + }; + _mockRepo.Setup(r => r.FindLastHistoricalConfigChangesByUser(user, null, 2)).ReturnsAsync(mockResult); + + // Act + var result = await _service.GetByUser(user, null, 2); + + // Assert + Assert.That(result, Has.Count.EqualTo(2)); + } + + [Test] + public async Task UpdateHistoricalConfigChange_WhenExceptionOccurs_ReturnsNull() + { + // Arrange + var mockChange = new HistoricalConfigChanges(); + _mockRepo.Setup(r => r.Update(mockChange)).Throws(new Exception()); + + // Act + var result = await _service.UpdateHistoricalConfigChange(mockChange); + + // Assert + Assert.That(result, Is.Null); + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/InMemoryLockProviderTest.cs b/adas-core.Test/Services/InMemoryLockProviderTest.cs new file mode 100644 index 00000000..88c0fc73 --- /dev/null +++ b/adas-core.Test/Services/InMemoryLockProviderTest.cs @@ -0,0 +1,121 @@ +using System.Collections.Concurrent; +using System.Reflection; +using adas_core.Application.Services.Caching; + +namespace adas_core.Test.Services; + +[TestFixture] +public class InMemoryLockProviderTest +{ + private InMemoryLockProvider _provider = null!; + + [SetUp] + public void SetUp() + { + _provider = new InMemoryLockProvider(); + } + + private ConcurrentDictionary GetLocks() + { + var field = typeof(InMemoryLockProvider) + .GetField("_locks", BindingFlags.NonPublic | BindingFlags.Instance); + return (ConcurrentDictionary)field!.GetValue(_provider)!; + } + + #region TC-50 + [Test] + public async Task AcquireAsync_ReturnsTrue_AndCreatesEntryInLocksDictionary() + { + var result = await _provider.AcquireAsync("key", TimeSpan.FromSeconds(1)); + + Assert.That(result, Is.True); + + var locks = GetLocks(); + Assert.That(locks.ContainsKey("key"), Is.True); + } + #endregion + + #region TC-51 + [Test] + public async Task AcquireAsync_ReturnsFalse_WhenSemaphoreOccupiedAndTimeoutExpires() + { + await _provider.AcquireAsync("key", TimeSpan.FromSeconds(5)); + + var result = await _provider.AcquireAsync("key", TimeSpan.FromMilliseconds(100)); + + Assert.That(result, Is.False); + + await _provider.ReleaseAsync("key"); + + var locks = GetLocks(); + Assert.That(locks["key"].CurrentCount, Is.EqualTo(1)); + } + #endregion + + #region TC-52 + [Test] + public async Task ReleaseAsync_MakesSemaphoreAvailableForNextAcquire() + { + await _provider.AcquireAsync("key", TimeSpan.FromSeconds(1)); + await _provider.ReleaseAsync("key"); + + var result = await _provider.AcquireAsync("key", TimeSpan.FromSeconds(1)); + + Assert.That(result, Is.True); + + var locks = GetLocks(); + Assert.That(locks["key"].CurrentCount, Is.EqualTo(0)); + + await _provider.ReleaseAsync("key"); + + Assert.That(locks["key"].CurrentCount, Is.EqualTo(1)); + } + #endregion + + #region TC-53 + [Test] + public async Task ReleaseAsync_DoesNotThrow_WhenCalledWithoutPriorAcquire() + { + await _provider.ReleaseAsync("nonexistent-key"); + + await _provider.AcquireAsync("key", TimeSpan.FromSeconds(1)); + await _provider.ReleaseAsync("key"); + await _provider.ReleaseAsync("key"); + } + #endregion + + #region TC-54 + [Test] + public async Task AcquireAsync_IsThreadSafe_OnlyOneHolderAtATime_SingleSemaphorePerKey() + { + const string key = "same-key"; + const int threadCount = 10; + var acquiredCount = 0; + var currentHolders = 0; + + var tasks = Enumerable.Range(0, threadCount).Select(_ => Task.Run(async () => + { + var acquired = await _provider.AcquireAsync(key, TimeSpan.FromSeconds(10)); + + Assert.That(acquired, Is.True); + + var current = Interlocked.Increment(ref currentHolders); + Assert.That(current, Is.EqualTo(1)); + + Interlocked.Increment(ref acquiredCount); + await Task.Delay(5); + + Interlocked.Decrement(ref currentHolders); + await _provider.ReleaseAsync(key); + })).ToArray(); + + await Task.WhenAll(tasks); + + Assert.That(acquiredCount, Is.EqualTo(threadCount)); + + var locks = GetLocks(); + Assert.That(locks.ContainsKey(key), Is.True); + Assert.That(locks.Keys.Count(k => k == key), Is.EqualTo(1)); + } + #endregion +} diff --git a/adas-core.Test/Services/LightBeaconServiceTest.cs b/adas-core.Test/Services/LightBeaconServiceTest.cs new file mode 100644 index 00000000..53d34157 --- /dev/null +++ b/adas-core.Test/Services/LightBeaconServiceTest.cs @@ -0,0 +1,70 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using adas_core.module.LightBeacons.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Moq; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Services; + +[TestFixture] +public class LightBeaconServiceTest +{ + [SetUp] + public void Setup() + { + _optionsApiSettings = Options.Create(_apiSettings); + _pocService = new Mock(); + _clientMessageService = new Mock(); + _subscribersService = new Mock(); + _logger = new Mock>(); + _lightBeaconRepository = new Mock(); + + _lightBeaconService = new LightBeaconService( + _optionsApiSettings, + _logger.Object, + _clientMessageService.Object, + _subscribersService.Object, + _pocService.Object, + _lightBeaconRepository.Object + ); + } + + private LightBeaconService _lightBeaconService = null!; + + private readonly ApiSettings _apiSettings = new() + { + BalizaUrl = "http://localhost/info.html", + BalizaPassword = "password" + }; + + private IOptions _optionsApiSettings = null!; + + private Mock> _logger = null!; + private Mock _pocService = null!; + private Mock _clientMessageService = null!; + private Mock _subscribersService = null!; + private Mock _lightBeaconRepository = null!; + + [Ignore("Integration test")] + [Test] + public async Task GetBeaconColor() + { + var patient = new Patient + { + UnitId = ObjectId.GenerateNewId(), + PointOfCareId = ObjectId.GenerateNewId() + }; + + + var color = await _lightBeaconService.GetColor(patient.PointOfCareId.Value); + + Assert.That(color, Is.EqualTo( + LightBeaconColor.Off)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/MasterListServiceTest.cs b/adas-core.Test/Services/MasterListServiceTest.cs new file mode 100644 index 00000000..3031c638 --- /dev/null +++ b/adas-core.Test/Services/MasterListServiceTest.cs @@ -0,0 +1,185 @@ +using System.Security.Claims; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Masters; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Moq; + +namespace adas_core.Test.Services; + +[TestFixture] +public class MasterListServiceTest +{ + [SetUp] + public void Setup() + { + _loggerMock = new Mock>>(); + _serviceProviderMock = new Mock(); + _clientMessageServiceMock = new Mock(); + _subscribersServiceMock = new Mock(); + _unitServiceMock = new Mock(); + _displayServiceMock = new Mock(); + _repositoryMock = new Mock>(); + _patientServiceMock = new Mock(); + _dischargeServiceMock = new Mock(); + _admissionServiceMock = new Mock(); + _apiSettingsMock = new Mock>(); + var userClaims = new ClaimsPrincipal(new ClaimsIdentity([ + new Claim(ClaimTypes.Name, "TestUser") + ], "mock")); + + var httpContextMock = new DefaultHttpContext + { + User = userClaims + }; + + _httpContextAccessorMock.Setup(accessor => accessor.HttpContext) + .Returns(httpContextMock); + var apiSettings = new ApiSettings + { + PathToDisplayAssets = ["path/to/assets"] + }; + + _apiSettingsMock.Setup(ap => ap.Value).Returns(apiSettings); + + _serviceProviderMock.Setup(sp => sp.GetService(typeof(IMasterListRepository))) + .Returns(_repositoryMock.Object); + + _service = new MasterListService( + _loggerMock.Object, + _serviceProviderMock.Object, + new Lazy(() => _clientMessageServiceMock.Object), + _subscribersServiceMock.Object, + new Lazy(() => _unitServiceMock.Object), + new Lazy(() => _displayServiceMock.Object), + new Lazy(() => _patientServiceMock.Object), + new Lazy(() => _dischargeServiceMock.Object), + new Lazy(() => _admissionServiceMock.Object), + _apiSettingsMock.Object, + _httpContextAccessorMock.Object, + _auditServiceMock.Object + ); + } + + private Mock>> _loggerMock = null!; + private Mock _serviceProviderMock = null!; + private Mock _clientMessageServiceMock = null!; + private Mock _subscribersServiceMock = null!; + private Mock _unitServiceMock = null!; + private Mock _displayServiceMock = null!; + private Mock _patientServiceMock = null!; + private Mock _dischargeServiceMock = null!; + private Mock _admissionServiceMock = null!; + private Mock> _repositoryMock = null!; + private Mock> _apiSettingsMock = null!; + private readonly Mock _httpContextAccessorMock = new(); + private readonly Mock _auditServiceMock = new(); + private MasterListService _service = null!; + + + [Test] + public async Task DeleteMasterListById_ShouldCallRepositoryDelete_WhenMasterListFound() + { + // Arrange + var id = ObjectId.GenerateNewId(); + var masterList = new MasterList { Id = id }; + _repositoryMock.Setup(repo => repo.FindById(It.IsAny(), It.IsAny())) + .ReturnsAsync(masterList); + _repositoryMock.Setup(repo => repo.Delete(It.IsAny())) + .Returns(Task.CompletedTask); + _unitServiceMock.Setup(c => c.CountUnitsByMasterListId(It.IsAny(), It.IsAny())) + .ReturnsAsync(0); + // Act + await _service.DeleteMasterListById(id); + + // Assert + _repositoryMock.Verify(repo => repo.Delete(id), Times.Once); + } + + // [Test] + // public async Task GetAllMasterList_ShouldReturnAllMasterLists() + // { + // // Arrange + // var masterLists = new List { new MasterList(), new MasterList() }; + // _repositoryMock.Setup(repo => repo.GetAll()) + // .ReturnsAsync(masterLists); + // + // // Act + // var result = await _service.GetAllMasterList(); + // + // // Assert + // Assert.That(masterLists, Is.EqualTo(result)); + // } + + [Test] + public async Task GetMasterListById_ShouldReturnMasterList_WhenFound() + { + // Arrange + var id = ObjectId.GenerateNewId(); + var masterList = new MasterList { Id = id }; + _repositoryMock.Setup(repo => repo.FindById(It.IsAny(), It.IsAny())) + .ReturnsAsync(masterList); + + // Act + var result = await _service.GetMasterListById(id, LocaleEnum.Default); + + // Assert + Assert.That(masterList, Is.EqualTo(result)); + } + + [Test] + public async Task GetMasterListByName_ShouldReturnMasterList_WhenFound() + { + // Arrange + var name = "TestName"; + var masterList = new MasterList { Id = ObjectId.GenerateNewId(), Name = name }; + _repositoryMock.Setup(repo => repo.FindByName(It.IsAny())) + .ReturnsAsync(masterList); + + // Act + var result = await _service.GetMasterListByName(name); + + // Assert + Assert.That(masterList, Is.EqualTo(result)); + } + + [Test] + public async Task InsertMasterList_ShouldReturnInsertedMasterList() + { + // Arrange + var masterList = new MasterList { Id = ObjectId.GenerateNewId() }; + _repositoryMock.Setup(repo => repo.InsertOneAsync(It.IsAny())) + .Returns(Task.CompletedTask); + _repositoryMock.Setup(repo => repo.FindById(It.IsAny())) + .ReturnsAsync(masterList); + + // Act + var result = await _service.InsertMasterList(masterList); + + // Assert + Assert.That(masterList, Is.EqualTo(result)); + } + + [Test] + public async Task UpdateMasterList_ShouldReturnUpdatedMasterList() + { + // Arrange + var masterList = new MasterList { Id = ObjectId.GenerateNewId() }; + _repositoryMock.Setup(repo => repo.Update(It.IsAny())) + .Returns(Task.CompletedTask); + _repositoryMock.Setup(repo => repo.FindById(It.IsAny(), It.IsAny())) + .ReturnsAsync(masterList); + + // Act + var result = await _service.UpdateMasterList(masterList); + + // Assert + Assert.That(masterList, Is.EqualTo(result)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/MedicineServiceTest.cs b/adas-core.Test/Services/MedicineServiceTest.cs new file mode 100644 index 00000000..6ee09145 --- /dev/null +++ b/adas-core.Test/Services/MedicineServiceTest.cs @@ -0,0 +1,270 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Moq; +using static NUnit.Framework.Is; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Services; + +[TestFixture] +public class MedicineServiceTest +{ + [SetUp] + public void Setup() + { + _medicineRepositoryMock = new Mock(); + _treatmentServiceMock = new Mock(); + + _optionsApiSettings = Options.Create(_apiSettings); + + _logger = new Mock>(); + + _medicineService = new MedicineService( + _medicineRepositoryMock.Object, + _treatmentServiceMock.Object, + _optionsApiSettings, + _logger.Object, + _httpContextAccessor.Object, + _auditService.Object); + } + + private MedicineService _medicineService; + + private Mock _medicineRepositoryMock; + private Mock _treatmentServiceMock; + private readonly Mock _httpContextAccessor = new(); + private readonly Mock _auditService = new(); + + private readonly ApiSettings _apiSettings = new() + { + NotesIndicatingMedication = ["NoteMedication"] + }; + + private IOptions _optionsApiSettings; + + private Mock> _logger; + + //private static readonly DateTime now = DateTime.Now; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + + [Test] + public async Task GetMedicinesOfTreatments_Null_Medicine_Return_0() + { + var medicineList = new List(); + + List patientTreatment = []; + if (patientTreatment == null) throw new ArgumentNullException(nameof(patientTreatment)); + + _medicineRepositoryMock.Setup(m => m.GetMedicineByCodeOrNote(It.IsAny>())) + .ReturnsAsync(medicineList); + + var result = await _medicineService.GetMedicinesOfTreatments(patientTreatment); + + Assert.That(result.Count(), EqualTo(0)); + } + + [Test] + public async Task GetMedicinesOfTreatments_Medicines_Return_Medicines() + { + var treatment = + new PatientTreatment + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + OrderControl = OrderControlType.Nw, + PlacerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + RequestedGiveCodes = [new Code { Identifier = "12345" }], + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [], + Routes = [] + }; + + var treatmentList = new List + { + treatment + }; + + var medicine = + new Medicine + { + Codes = ["12345"], + Group = [MedicineEnum.Group.DoubleSignature.ToString()], + Type = [MedicineEnum.Types.MuscleRelaxant.ToString()], + Name = "cisataracurio" + }; + + + var medicineList = new List + { + medicine + }; + + _medicineRepositoryMock.Setup(m => m.GetMedicineByCodeOrNote(It.IsAny>())) + .ReturnsAsync(medicineList); + + var result = await _medicineService.GetMedicinesOfTreatments(treatmentList); + + Assert.That(result.Count(), GreaterThan(0)); + } + + [Test] + public async Task GetMedicinesOfTreatments_Not_Get_Medicines_Return_Note_Medicines() + { + var treatment = + new PatientTreatment + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + OrderControl = OrderControlType.Nw, + PlacerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + RequestedGiveCodes = [new Code { Identifier = "12345", Text = "CodeText" }], + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [new Note { Comment = "NoteMedication" }], + Routes = [] + }; + + var treatmentList = new List + { + treatment + }; + + _medicineRepositoryMock.Setup(m => m.GetMedicineByCodeOrNote(It.IsAny>())).ReturnsAsync([]); + + var result = await _medicineService.GetMedicinesOfTreatments(treatmentList); + using (Assert.EnterMultipleScope()) + { + var medicines = result as Medicine[] ?? result.ToArray(); + Assert.That(medicines.Count(), GreaterThan(0)); + Assert.That(medicines.FirstOrDefault()?.Codes.FirstOrDefault(), EqualTo("12345")); + Assert.That(medicines.FirstOrDefault()?.Name, EqualTo("CodeText")); + }; + } + + [Test] + public async Task GetMedicinesOfTreatments_Not_Get_Medicines_Not_Code_identifier_Return_Note_Medicines() + { + var treatment = + new PatientTreatment + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + OrderControl = OrderControlType.Nw, + PlacerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + RequestedGiveCodes = [new Code { Text = "CodeText" }], + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [new Note { Comment = "NoteMedication" }], + Routes = [] + }; + + var treatmentList = new List + { + treatment + }; + + + _medicineRepositoryMock.Setup(m => m.GetMedicineByCodeOrNote(It.IsAny>())).ReturnsAsync([]); + + var result = await _medicineService.GetMedicinesOfTreatments(treatmentList); + using (Assert.EnterMultipleScope()) + { + var medicines = result as Medicine[] ?? result.ToArray(); + Assert.That(medicines.Count(), GreaterThan(0)); + Assert.That(medicines.FirstOrDefault()?.Codes.FirstOrDefault(), EqualTo(string.Empty)); + Assert.That(medicines.FirstOrDefault()?.Name, EqualTo("CodeText")); + }; + } + + [Test] + public async Task CalculateParentalNutritionMedicine_Notes_NPT_Return_NPT_Medicine() + { + var treatment = + new PatientTreatment + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + OrderControl = OrderControlType.Nw, + PlacerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + RequestedGiveCodes = [new Code { Text = "CodeText" }], + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [new Note { Comment = "NPT", CommentType = "formularybaseformulation" }], + Routes = [] + }; + + var treatmentList = new List + { + treatment + }; + + + _medicineRepositoryMock.Setup(m => m.GetMedicineByCodeOrNote(It.IsAny>())).ReturnsAsync([]); + + var result = await _medicineService.GetMedicinesOfTreatments(treatmentList); + using (Assert.EnterMultipleScope()) + { + var medicines = result as Medicine[] ?? result.ToArray(); + Assert.That(medicines.Count(), GreaterThan(0)); + Assert.That(medicines.FirstOrDefault()?.Codes.FirstOrDefault(), EqualTo(null)); + Assert.That(medicines.FirstOrDefault()?.Name, EqualTo("NPT")); + Assert.That(MedicineEnum.Types.ParenteralNutrition.ToString(), + EqualTo(medicines.FirstOrDefault()?.Type.FirstOrDefault())); + }; + } + + [Test] + public async Task CalculateParentalNutritionMedicine_Notes_NPTL_Return_NPTL_Medicine() + { + var treatment = + new PatientTreatment + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + OrderControl = OrderControlType.Nw, + PlacerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + RequestedGiveCodes = [new Code { Text = "CodeText" }], + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = + [ + new Note { Comment = "NPT", CommentType = "formularybaseformulation" }, + new Note { Comment = "LÍPIDOS NEONATALES AL 20%" } + ], + Routes = [] + }; + + var treatmentList = new List + { + treatment + }; + + + _medicineRepositoryMock.Setup(m => m.GetMedicineByCodeOrNote(It.IsAny>())).ReturnsAsync([]); + + var result = await _medicineService.GetMedicinesOfTreatments(treatmentList); + using (Assert.EnterMultipleScope()) + { + var medicines = result as Medicine[] ?? result.ToArray(); + Assert.That(medicines.Count(), GreaterThan(0)); + Assert.That(medicines.FirstOrDefault()?.Codes.FirstOrDefault(), EqualTo(null)); + Assert.That(medicines.FirstOrDefault()?.Name, EqualTo("NPT")); + Assert.That(MedicineEnum.Types.ParenteralNutritionLipids.ToString(), + EqualTo(medicines.FirstOrDefault()?.Type.FirstOrDefault())); + }; + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/NoCacheServiceTest.cs b/adas-core.Test/Services/NoCacheServiceTest.cs new file mode 100644 index 00000000..4ddd51aa --- /dev/null +++ b/adas-core.Test/Services/NoCacheServiceTest.cs @@ -0,0 +1,141 @@ +using adas_core.Application.Services.Caching; + +namespace adas_core.Test.Services; + +[TestFixture] +public class NoCacheServiceTest +{ + private NoCacheService _noCacheService = null!; + + [SetUp] + public void SetUp() + { + _noCacheService = new NoCacheService(); + } + + #region TC-27 + + [Test] + public async Task GetOrSetObjectAsync_AlwaysCallsFactory_NeverUsesCache() + { + // Arrange + var callCount = 0; + var expectedResult = new { Name = "TestPatient", Id = "123" }; + + Func> factory = () => + { + callCount++; + return Task.FromResult(expectedResult); + }; + + var key = "patients:abc123"; + var ttl = TimeSpan.FromMinutes(5); + + // Act + var result1 = await _noCacheService.GetOrSetObjectAsync(key, factory, ttl); + var result2 = await _noCacheService.GetOrSetObjectAsync(key, factory, ttl); + + // Assert + Assert.That(callCount, Is.EqualTo(2), "La factory debería llamarse siempre (no hay caché)"); + Assert.That(result1, Is.EqualTo(expectedResult)); + Assert.That(result2, Is.EqualTo(expectedResult)); + } + + [Test] + public async Task GetOrSetObjectAsync_CallsFactory_ForEachUniqueKey() + { + // Arrange + var callCount = 0; + + Func> factory = () => + { + callCount++; + return Task.FromResult($"result-{callCount}"); + }; + + // Act + await _noCacheService.GetOrSetValueAsync("key1", factory); + await _noCacheService.GetOrSetValueAsync("key2", factory); + await _noCacheService.GetOrSetValueAsync("key3", factory); + + // Assert + Assert.That(callCount, Is.EqualTo(3), + "La factory debería haber sido invocada 3 veces, una por cada clave única"); + } + + #endregion + + #region TC-28 + + [Test] + public async Task GetOrSetObjectAsync_DoesNotStoreData_InAnyBackend() + { + var key = "patients:abc123"; + var expectedResult = new { Name = "TestPatient" }; + + Func> factory = () => Task.FromResult(expectedResult); + + await _noCacheService.GetOrSetObjectAsync(key, factory); + + var retrieved = await _noCacheService.GetObjectAsync(key); + Assert.That(retrieved, Is.Null); + + + Func act = async () => await _noCacheService.DeleteObjectAsync(key); + Assert.That(act, Throws.Nothing); + + } + + + #endregion + + #region TC-29 + + [Test] + public async Task DeleteByPatternAsync_ReturnsZero_DoesNotThrow() + { + // Act + var result = await _noCacheService.DeleteByPatternAsync("patients:*"); + var result2 = await _noCacheService.DeleteByPatternAsync("any:pattern:*"); + + // Assert + Assert.That(result, Is.EqualTo(0)); + Assert.That(result2, Is.EqualTo(0)); + } + + #endregion + + #region TC-30 + + [Test] + public void CleanCache_IsNoOp_DoesNotThrow() + { + + Action act = () => _noCacheService.CleanCache(); + Assert.That(act, Throws.Nothing); + + var result = _noCacheService.GetValue("any-key"); + Assert.That(result, Is.Null); + } + + + [Test] + public void SetValue_IsNoOp_DoesNotThrow() + { + + Action act = () => _noCacheService.SetValue("key", "value"); + Assert.That(act, Throws.Nothing); + + } + + + [Test] + public void GetValue_IsNoOp_ReturnsNull() + { + var result = _noCacheService.GetValue("any-key"); + + Assert.That(result, Is.Null); + } + + #endregion +} diff --git a/adas-core.Test/Services/ObservationServiceTest.cs b/adas-core.Test/Services/ObservationServiceTest.cs new file mode 100644 index 00000000..c631a7c6 --- /dev/null +++ b/adas-core.Test/Services/ObservationServiceTest.cs @@ -0,0 +1,927 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services; +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.MongoModels; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Moq; +using Options = Microsoft.Extensions.Options.Options; + +/* + * Tests shift observations + */ + +namespace adas_core.Test.Services; + +[TestFixture] +public class ObservationServiceTest +{ + [SetUp] + public void Setup() + { + //_observationServiceMock = new Mock(); + + //var medicineServiceMock = new Mock();1 + var groupedObservationServiceMock = new Mock(); + var alarmServiceMock = new Mock(); + + var clientMessageServiceMock = new Mock(); + + var subscribersServiceMock = new Mock(); + + var subscriberGroupedServiceMock = new Mock(); + + _patientServiceMock = new Mock(); + + _configObservationService = new Mock(); + + _observationRepository = new Mock(); + + var observationArchiveRepository = new Mock(); + + var calculatedObservationsServiceMock = new Mock(); + var calculatedObservationsServiceLazy = + new Lazy(() => calculatedObservationsServiceMock.Object); + calculatedObservationsServiceMock.Setup(o => o.Map(It.IsAny(), It.IsAny())) + .ReturnsAsync((PatientObservation obs, bool _) => obs); + + _configUnitsService = new Mock(); + _unitServiceMock = new Mock(); + + var diagnosisServiceMock = new Mock(); + + _optionsApiSettings = Options.Create(_apiSettings); + Options.Create(_recordingSettings); + _optionsCacheSettings = Options.Create(_cacheSettings); + + var balizaService = new Mock(); + var pocService = new Mock(); + var relayService = new Mock(); + var recordingService = new Mock(); + + _logger = new Mock>(); + + _observationService = new ObservationService( + _patientServiceMock.Object, + //Ipoc.Object, + _configObservationService.Object, + _observationRepository.Object, + observationArchiveRepository.Object, + _configUnitsService.Object, + diagnosisServiceMock.Object, + _optionsApiSettings, + _optionsCacheSettings, + balizaService.Object, + relayService.Object, + recordingService.Object, + _logger.Object, + groupedObservationServiceMock.Object, + alarmServiceMock.Object, + clientMessageServiceMock.Object, + subscribersServiceMock.Object, + subscriberGroupedServiceMock.Object, + calculatedObservationsServiceLazy, + _httpContextAccessor.Object, + _auditService.Object, + pocService.Object, + Mock.Of() + ); + + // Create a mock of the singleton class subscribers + var mockSingleton = new Mock(); + + // Set up the mock object to return a specific value when a method is called + mockSingleton.Setup(x => x.Map(It.IsAny(), It.IsAny())) + .ReturnsAsync((PatientObservation? value, bool _) => value); + var unitId = ObjectId.GenerateNewId(); + var unit = new Unit + { + Id = unitId, + Name = "UCI5C", + Title = "CONTROLC", + Configuration = new UnitConfiguration + { + AutoAdt = true + } + }; + + _unitServiceMock.Setup(u => u.FindByName(unit.Name)).ReturnsAsync(unit); + _unitServiceMock.Setup(u => u.FindById(unit.Id)).ReturnsAsync(unit); + } + + private ObservationService _observationService; + + //Mock _observationServiceMock ; + private Mock _patientServiceMock; + private Mock _configObservationService; + private Mock _configUnitsService; + private Mock _observationRepository; + private Mock _unitServiceMock; + private readonly Mock _httpContextAccessor = new(); + private readonly Mock _auditService = new(); + + + private readonly ApiSettings _apiSettings = new() + { + ConfigObservation = new ConfigObservationSettings + { + IgnoreUnknownObservation = false + }, + Customize = "H12O", + IntravenousLinesCode = ["10546003"], + AllergiesCode = ["473011001"], + DrainageCode = ["56868008"], + IsolationCode = ["302147001"], + PositionCode = ["386053000"] + }; + + private readonly CacheSettings _cacheSettings = new(); + + private IOptions _optionsApiSettings; + private IOptions _optionsCacheSettings; + + + private readonly RecordingSettings _recordingSettings = new(); + + private Mock> _logger; + + private static readonly DateTime Now = DateTime.Now; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + + [Test] + public async Task ProcessAllergiesObservation__apiRequest_No_Allergies_Return_Nothing() + { + var patientObs = new Person + { + FirstName = "Miguel", + LastName = "Villanueva", + Ids = new Dictionary { { "MR", "437537" } } + }; + + var patient = new Patient + { + Id = ObjectId.GenerateNewId(), + UnitString = "UCI5C", + Bed = "Box4", + PatientNumber = "437537", + Person = patientObs + }; + + var apiRequest = new ApiRequest + { + Location = new PatientLocation("UCI5C", "Box4"), + ObservationData = new ObservationData + { + Code = "473011001", + CodingSystem = "SNM", + Text = "Alergias", + Time = Now, + Value = "" + }, + Observations = + [ + new PatientObservation + { + Code = "263490005", + CodingSystem = "SNM", + Name = "Estado", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "Sin alergias conocidas" + }, + + new PatientObservation + { + Code = "300916003", + CodingSystem = "SNM", + Name = "�Alergia al l�tex?", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "No" + } + ], + Patient = patientObs, + PatientNumber = "437537", + Type = "ORU_R01", + PatientId = PatientId.ToString() + }; + + //PatientObservation expectedObs = new(); + + _patientServiceMock.Setup(cm => cm.FindPatientByApiRequest(apiRequest)).ReturnsAsync(patient); + + await _observationService.SaveRequest(apiRequest); + + _observationRepository.Verify(o => o.InsertOneAsync(It.Is(arg => + arg.Name == "AllergiesObs" && + arg.PatientId == PatientId + )), Times.Never); + } + + [Test] + public async Task ProcessAllergiesObservation_apiRequest_Allergies_Latex_Return_AllergiesObs_Latex() + { + //Falla CalculatedObservationService.Instance = null + + var patientObs = new Person + { + FirstName = "Miguel", + LastName = "Villanueva", + Ids = new Dictionary { { "MR", "437537" } } + }; + + var patient = new Patient + { + Id = ObjectId.GenerateNewId(), + UnitString = "UCI5C", + Bed = "Box4", + PatientNumber = "437537", + Person = patientObs, + UnitId = ObjectId.GenerateNewId(), + PointOfCareId = ObjectId.GenerateNewId() + }; + + var apiRequest = new ApiRequest + { + Location = new PatientLocation("UCI5C", "Box4"), + ObservationData = new ObservationData + { + Code = "473011001", + CodingSystem = "SNM", + Text = "Alergias", + Time = Now + }, + Observations = + [ + new PatientObservation + { + Code = "263490005", + CodingSystem = "SNM", + Name = "Estado", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "Alergias" + }, + + new PatientObservation + { + Code = "300916003", + CodingSystem = "SNM", + Name = "�Alergia al l�tex?", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "Si" + } + ], + Patient = patientObs, + PatientNumber = "437537", + Type = "ORU_R01", + PatientId = PatientId.ToString() + }; + + List expectePatientAllergiesValues = + [ + new() + { + Type = "Latex", + Value = "Si" + } + ]; + + PatientObservation expectedObs = new() + { + Time = apiRequest.ObservationData.Time.Value, + + Code = apiRequest.ObservationData.Code, + CodingSystem = apiRequest.ObservationData.CodingSystem, + PatientId = patient.Id, + ParentData = new ParentDataClass + { + Code = apiRequest.ObservationData.Code, + CodingSystem = apiRequest.ObservationData.CodingSystem, + Name = apiRequest.ObservationData.Text + }, + Value = expectePatientAllergiesValues + }; + + _patientServiceMock.Setup(cm => cm.FindPatientByApiRequest(apiRequest)).ReturnsAsync(patient); + _configObservationService.Setup(o => o.Map(It.IsAny(), false)).ReturnsAsync(expectedObs); + _configUnitsService.Setup(o => o.Map(It.IsAny())).ReturnsAsync(expectedObs); + + _configObservationService.Setup(o => o.RetentionActions(It.IsAny())) + .ReturnsAsync(new ObservatitonRetentionResult()); + _unitServiceMock.Setup(o => o.FindById(It.IsAny())).ReturnsAsync(new Unit + { Configuration = new UnitConfiguration { AutoAdt = true } }); + await _observationService.SaveRequest(apiRequest); + + _observationRepository.Verify(o => o.InsertOneAsync(expectedObs)); + } + + [Test] + public async Task ProcessAllergiesObservation_apiRequest_Allergies_Return_AllergiesObs() + { + //Falla CalculatedObservationService.Instance = null + + var patientObs = new Person + { + FirstName = "Miguel", + LastName = "Villanueva", + Ids = new Dictionary { { "MR", "437537" } } + }; + + var patient = new Patient + { + Id = ObjectId.GenerateNewId(), + UnitId = ObjectId.GenerateNewId(), + UnitString = "UCI5C", + Bed = "Box4", + PatientNumber = "437537", + Person = patientObs + }; + + var apiRequest = new ApiRequest + { + Location = new PatientLocation("UCI5C", "Box4"), + ObservationData = new ObservationData + { + Code = "473011001", + CodingSystem = "SNM", + Text = "Alergias", + Time = Now + }, + Observations = + [ + new PatientObservation + { + Code = "263490005", + CodingSystem = "SNM", + Name = "Estado", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "Alergias" + }, + + new PatientObservation + { + Code = "419199007", + CodingSystem = "SNM", + Name = "Tipo", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "Alergia ambienta" + }, + + new PatientObservation + { + Code = "277054007", + CodingSystem = "SNM", + Name = "Alergeno", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "Estacional" + }, + + new PatientObservation + { + Code = "281296001", + CodingSystem = "SNM", + Name = "Comentarios", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "epitelio de perro, mezcla de gram�neas salvajes," + }, + + new PatientObservation + { + Code = "419199007", + CodingSystem = "SNM", + Name = "Tipo", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "Alergia a f�rmacos" + }, + + new PatientObservation + { + Code = "416098002", + CodingSystem = "SNM", + Name = "F�rmacos al�rgenos", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "METILPREDNISOLONA" + }, + + new PatientObservation + { + Code = "281296001", + CodingSystem = "SNM", + Name = "Comentarios", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "Tolera dexametasona y actocortina" + }, + + new PatientObservation + { + Code = "419199007", + CodingSystem = "SNM", + Name = "Tipo", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "Alergia a f�rmacos" + }, + + new PatientObservation + { + Code = "277054007", + CodingSystem = "SNM", + Name = "Alergeno", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "Penicilina/cefalosporinas" + } + ], + Patient = patientObs, + PatientNumber = "437537", + Type = "ORU_R01", + PatientId = PatientId.ToString() + }; + + List expectePatientAllergiesValues = + [ + new() + { + Type = "Alergia ambienta", + Value = "Estacional", + Notes = "epitelio de perro, mezcla de gram�neas salvajes," + }, + + new() + { + Type = "Alergia a f�rmacos", + Value = "METILPREDNISOLONA", + Notes = "Tolera dexametasona y actocortina" + }, + + new() + { + Type = "Alergia a f�rmacos", + Value = "Penicilina/cefalosporinas" + } + ]; + + PatientObservation expectedObs = new() + { + Time = apiRequest.ObservationData.Time.Value, + + Code = apiRequest.ObservationData?.Code, + CodingSystem = apiRequest.ObservationData?.CodingSystem, + PatientId = patient.Id, + ParentData = new ParentDataClass + { + Code = apiRequest.ObservationData?.Code, + CodingSystem = apiRequest.ObservationData?.CodingSystem, + Name = apiRequest.ObservationData?.Text + }, + Value = expectePatientAllergiesValues + }; + + _patientServiceMock.Setup(cm => cm.FindPatientByApiRequest(apiRequest)).ReturnsAsync(patient); + _configObservationService.Setup(o => o.Map(It.IsAny(), false)).ReturnsAsync(expectedObs); + _configUnitsService.Setup(o => o.Map(It.IsAny())).ReturnsAsync(expectedObs); + _unitServiceMock.Setup(o => o.FindById(It.IsAny())).ReturnsAsync(new Unit + { Configuration = new UnitConfiguration { AutoAdt = true } }); + // sectionServiceMock.Setup(s => s.FindByPatient(It.Is(arg => arg == patient.id))).Returns(Task.FromResult
(null)); + _configObservationService.Setup(o => o.RetentionActions(It.IsAny())) + .ReturnsAsync(new ObservatitonRetentionResult()); + + await _observationService.SaveRequest(apiRequest); + + _observationRepository.Verify(o => o.InsertOneAsync(expectedObs)); + } + + + [Test] + public async Task ProcessDrainagesObservation_apiRequest_Drainages_Return_DrainagesObs() + { + //Falla CalculatedObservationService.Instance = null + + var patientObs = new Person + { + FirstName = "Miguel", + LastName = "Villanueva", + Ids = new Dictionary { { "MR", "437537" } } + }; + + var patient = new Patient + { + Id = ObjectId.GenerateNewId(), + UnitId = ObjectId.GenerateNewId(), + UnitString = "UCI5C", + Bed = "Box4", + PatientNumber = "437537", + Person = patientObs + }; + + var apiRequest = new ApiRequest + { + Location = new PatientLocation("UCI5C", "Box4"), + ObservationData = new ObservationData + { + Code = "56868008", + CodingSystem = "SNM", + Text = "Drenaje: Cabeza", + Time = Now + }, + Observations = + [ + new PatientObservation + { + Code = "56868008", + CodingSystem = "SNM", + Name = "Volumen", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = 100 + }, + + new PatientObservation + { + Code = "10546003", + CodingSystem = "SNM", + Name = "Localizaci�n", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "Cabeza" + }, + + new PatientObservation + { + Code = "138875005", + CodingSystem = "SNM", + Name = "Tipo de drenaje", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "Drenaje ventricular" + }, + + new PatientObservation + { + Code = "138875005", + CodingSystem = "SNM", + Name = "Altura columna(cmH2O)", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = 88 + } + ], + Patient = patientObs, + PatientNumber = "437537", + Type = "ORU_R01", + PatientId = PatientId.ToString() + }; + + List expectePatientDraingesValues = + [ + new() + { + Type = "Drenaje ventricular", + Location = "Cabeza", + Volume = 1001, + Height = 8 + } + ]; + + PatientObservation expectedObs = new() + { + Time = apiRequest.ObservationData.Time.Value, + + Code = apiRequest.ObservationData.Code, + CodingSystem = apiRequest.ObservationData.CodingSystem, + PatientId = patient.Id, + ParentData = new ParentDataClass + { + Code = apiRequest.ObservationData.Code, + CodingSystem = apiRequest.ObservationData.CodingSystem, + Name = apiRequest.ObservationData.Text + }, + Value = expectePatientDraingesValues + }; + + _patientServiceMock.Setup(cm => cm.FindPatientByApiRequest(apiRequest)).ReturnsAsync(patient); + _configObservationService + .Setup(o => o.Map(It.Is(arg => arg.PatientId == patient.Id), false)) + .ReturnsAsync(expectedObs); + _configUnitsService.Setup(o => o.Map(It.Is(arg => arg.PatientId == patient.Id))) + .ReturnsAsync(expectedObs); + + _configObservationService.Setup(o => o.RetentionActions(It.IsAny())) + .ReturnsAsync(new ObservatitonRetentionResult()); + _unitServiceMock.Setup(o => o.FindById(It.IsAny())).ReturnsAsync(new Unit + { Configuration = new UnitConfiguration { AutoAdt = true } }); + await _observationService.SaveRequest(apiRequest); + + + _observationRepository.Verify(o => o.InsertOneAsync(It.Is(arg => + arg.Value == expectedObs.Value && + arg.PatientId == expectedObs.PatientId && + arg.ParentData == expectedObs.ParentData && + arg.Time == expectedObs.Time && + arg.Code == expectedObs.Code && + arg.CodingSystem == expectedObs.CodingSystem + ))); + } + + [Test] + public async Task ProcessIsolationObservation_Return_IsolationObs() + { + var patientObs = new Person + { + FirstName = "Miguel", + LastName = "Villanueva", + Ids = new Dictionary { { "MR", "437537" } } + }; + + var patient = new Patient + { + Id = ObjectId.GenerateNewId(), + UnitId = ObjectId.GenerateNewId(), + UnitString = "UCI5C", + Bed = "Box4", + PatientNumber = "437537", + Person = patientObs + }; + + var apiRequest = new ApiRequest + { + Location = new PatientLocation("UCI5C", "Box4"), + ObservationData = new ObservationData + { + Code = "302147001", + CodingSystem = "SNM", + Value = "Aire; Contacto; Preventivo", + Text = "Aislamiento", + Time = Now + }, + Observations = + [ + new PatientObservation + { + Code = "code", + CodingSystem = "SNM", + Name = "name", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "value" + } + ], + Patient = patientObs, + PatientNumber = "437537", + Type = "ORU_R01", + PatientId = PatientId.ToString(), + MessageTime = Now + }; + + var newObs = new PatientObservation + { + Value = "Aire, Contacto, Preventivo", + Name = "Isolation", + CodingSystem = "ADAS", + PatientId = patient.Id, + MessageTime = Now, + Time = Now + }; + + _patientServiceMock.Setup(cm => cm.FindPatientByApiRequest(apiRequest)).ReturnsAsync(patient); + _configObservationService + .Setup(o => o.Map(It.Is(arg => arg.PatientId == patient.Id), false)) + .ReturnsAsync(newObs); + _configUnitsService.Setup(o => o.Map(It.Is(arg => arg.PatientId == patient.Id))) + .ReturnsAsync(newObs); + _configObservationService.Setup(o => o.RetentionActions(It.IsAny())) + .ReturnsAsync(new ObservatitonRetentionResult()); + _unitServiceMock.Setup(o => o.FindById(It.IsAny())).ReturnsAsync(new Unit + { Configuration = new UnitConfiguration { AutoAdt = true } }); + await _observationService.SaveRequest(apiRequest); + + + _observationRepository.Verify(o => o.InsertOneAsync(It.Is(arg => + arg.Value == newObs.Value && + arg.PatientId == newObs.PatientId && + arg.Name == newObs.Name && + arg.Time == newObs.Time && + arg.MessageTime == newObs.MessageTime && + arg.CodingSystem == newObs.CodingSystem + ))); + } + + + [Test] + public async Task ProcessPositionObservation_Return_PositionObs() + { + var patientObs = new Person + { + FirstName = "Miguel", + LastName = "Villanueva", + Ids = new Dictionary { { "MR", "437537" } } + }; + + var patient = new Patient + { + Id = ObjectId.GenerateNewId(), + UnitId = ObjectId.GenerateNewId(), + UnitString = "UCI5C", + Bed = "Box4", + PatientNumber = "437537", + Person = patientObs + }; + + var apiRequest = new ApiRequest + { + Location = new PatientLocation("UCI5C", "Box4"), + ObservationData = new ObservationData + { + Code = "386053000", + CodingSystem = "SNM", + Text = "CAMBIOS POSTURALES", + Value = "Cama Hill-rom", + Time = Now + }, + Observations = + [ + new PatientObservation + { + Code = "code", + CodingSystem = "SNM", + Name = "name", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "Cama Hill-rom" + } + ], + Patient = patientObs, + PatientNumber = "437537", + Type = "ORU_R01", + PatientId = PatientId.ToString(), + MessageTime = Now + }; + + var newObs = new PatientObservation + { + Value = "Cama Hill-rom", + Name = "Patient_Position", + CodingSystem = "ADAS", + PatientId = patient.Id, + MessageTime = Now, + Time = Now + }; + + _patientServiceMock.Setup(cm => cm.FindPatientByApiRequest(apiRequest)).ReturnsAsync(patient); + _configObservationService + .Setup(o => o.Map(It.Is(arg => arg.PatientId == patient.Id), false)) + .ReturnsAsync(newObs); + _configUnitsService.Setup(o => o.Map(It.Is(arg => arg.PatientId == patient.Id))) + .ReturnsAsync(newObs); + _configObservationService.Setup(o => o.RetentionActions(It.IsAny())) + .ReturnsAsync(new ObservatitonRetentionResult()); + _unitServiceMock.Setup(o => o.FindById(It.IsAny())).ReturnsAsync(new Unit + { Configuration = new UnitConfiguration { AutoAdt = true } }); + await _observationService.SaveRequest(apiRequest); + + + _observationRepository.Verify(o => o.InsertOneAsync(It.Is(arg => + arg.Value == newObs.Value && + arg.PatientId == newObs.PatientId && + arg.Name == newObs.Name && + arg.Time == newObs.Time && + arg.MessageTime == newObs.MessageTime && + arg.CodingSystem == newObs.CodingSystem + ))); + } + + [Test] + public async Task ProcessIntravenousLinesObservation_Return_PositionObs() + { + var patientObs = new Person + { + FirstName = "Miguel", + LastName = "Villanueva", + Ids = new Dictionary { { "MR", "437537" } } + }; + + var patient = new Patient + { + Id = ObjectId.GenerateNewId(), + UnitId = ObjectId.GenerateNewId(), + UnitString = "UCI5C", + Bed = "Box4", + PatientNumber = "437537", + Person = patientObs + }; + + var apiRequest = new ApiRequest + { + Location = new PatientLocation("UCI5C", "Box4"), + ObservationData = new ObservationData + { + Code = "10546003", + CodingSystem = "SNM", + Text = "Cat�ter EPICUT�NEO PERIF�RICO: Zona temporal derecha", + Time = Now + }, + Observations = + [ + new PatientObservation + { + Code = "439272007", + CodingSystem = "SNM", + Name = "Fecha inserci�n", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = Now + }, + + new PatientObservation + { + Code = "228864003", + CodingSystem = "SNM", + Name = "Duraci�n (d�as)", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = 7 + }, + + new PatientObservation + { + Code = "273248003", + CodingSystem = "SNM", + Name = "Actuaci�n", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "Insertado" + }, + + new PatientObservation + { + Code = "10546003", + CodingSystem = "SNM", + Name = "Localizaci�n", + Status = StatusEnum.Type.Ok, + Time = Now, + Value = "Zona temporal derecha" + } + ], + Patient = patientObs, + PatientNumber = "437537", + Type = "ORU_R01", + PatientId = PatientId.ToString(), + MessageTime = Now + }; + + var obsIntravenousLines = new PatientIntravenousLinesValue + { + Type = "Cat�ter EPICUT�NEO PERIF�RICO", + Location = "Zona temporal derecha", + Action = "Insertado", + InsertTime = Now, + Duration = "7" + }; + + var newObs = new PatientObservation + { + Value = obsIntravenousLines, + Code = "10546003", + CodingSystem = "SNM", + PatientId = patient.Id, + MessageTime = Now, + Time = Now + }; + + _patientServiceMock.Setup(cm => cm.FindPatientByApiRequest(apiRequest)).ReturnsAsync(patient); + _configObservationService + .Setup(o => o.Map(It.Is(arg => arg.PatientId == patient.Id), false)) + .ReturnsAsync(newObs); + _configUnitsService.Setup(o => o.Map(It.Is(arg => arg.PatientId == patient.Id))) + .ReturnsAsync(newObs); + _configObservationService.Setup(o => o.RetentionActions(It.IsAny())) + .ReturnsAsync(new ObservatitonRetentionResult()); + _unitServiceMock.Setup(o => o.FindById(It.IsAny())).ReturnsAsync(new Unit + { Configuration = new UnitConfiguration { AutoAdt = true } }); + await _observationService.SaveRequest(apiRequest); + + + _observationRepository.Verify(o => o.InsertOneAsync(It.Is(arg => + arg.Value == newObs.Value && + arg.PatientId == newObs.PatientId && + arg.Code == newObs.Code && + arg.Time == newObs.Time && + arg.MessageTime == newObs.MessageTime && + arg.CodingSystem == newObs.CodingSystem + ))); + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/PatientServiceTest.cs b/adas-core.Test/Services/PatientServiceTest.cs new file mode 100644 index 00000000..fd18ee52 --- /dev/null +++ b/adas-core.Test/Services/PatientServiceTest.cs @@ -0,0 +1,2414 @@ +using System.Security.Claims; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services; +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.Masters; +using adas_core.Domain.Models.MongoModels; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Moq; +using Options = Microsoft.Extensions.Options.Options; +using static NUnit.Framework.Assert; + +namespace adas_core.Test.Services; + +[TestFixture] +internal class PatientServiceTest +{ + [SetUp] + public void Setup() + { + _admissionService = new Mock(); + _patientRepositoryMock = new Mock(); + _patientArchiveRepository = new Mock(); + _poCMappingServiceMock = new Mock(); + + var treatmentServiceMock = new Mock(); + _treatServiceLazy = new Lazy(() => treatmentServiceMock.Object); + + + var observationServiceMock = new Mock(); + _observationServiceLazy = new Lazy(() => observationServiceMock.Object); + + _diagnosisServiceMock = new Mock(); + _appointmentServiceMock = new Mock(); + + var pumpServiceMock = new Mock(); + _pumpServiceLazy = new Lazy(() => pumpServiceMock.Object); + + _recordingAlertService = new Mock(); + + _dischargeService = new Mock { CallBase = true }; + + _unitService = new Mock { CallBase = true }; + _displayservice = new Mock { CallBase = true }; + _pocService = new Mock { CallBase = true }; + + _optionsApiSettings = Options.Create(_apiSettings); + _listSettings = Options.Create(new ListSettings()); + + _logger = new Mock>(); + + _clientMessageServiceMock = new Mock(); + _subscribersServiceMock = new Mock(); + _subscriberGroupedServiceMock = new Mock(); + _lazyAdmissionService = new Lazy(() => _admissionService.Object); + _displayConfigServiceMock = new Mock(); + _groupedServiceMock = new Mock(); + _patientCarePlanServiceMock = new Mock(); + var subscribers = new List(); + // Set up the mock object to return a specific value when a method is called + _subscribersServiceMock.Setup(x => x.GetSubscribers()).Returns(subscribers); + var userClaims = new ClaimsPrincipal(new ClaimsIdentity([ + new Claim(ClaimTypes.Name, "TestUser") + ], "mock")); + + var httpContextMock = new DefaultHttpContext + { + User = userClaims + }; + _masterListMock = new Mock(); + + _httpContextAccessorMock.Setup(accessor => accessor.HttpContext) + .Returns(httpContextMock); + _patientService = new PatientService( + _patientRepositoryMock.Object, + _patientArchiveRepository.Object, + _observationServiceLazy, + _treatServiceLazy, + _poCMappingServiceMock.Object, + _diagnosisServiceMock.Object, + _appointmentServiceMock.Object, + _pumpServiceLazy, + _recordingAlertService.Object, + _dischargeService.Object, + _optionsApiSettings, + _listSettings, + _logger.Object, + _clientMessageServiceMock.Object, + _subscribersServiceMock.Object, + _subscriberGroupedServiceMock.Object, + _unitService.Object, + _displayservice.Object, + _pocService.Object, + _lazyAdmissionService, + _displayConfigServiceMock.Object, + _groupedServiceMock.Object, + _patientCarePlanServiceMock.Object, + _httpContextAccessorMock.Object, + _auditServiceMock.Object, + _masterListMock.Object + ); + + + var unit = new Unit + { + Id = _unitId, + Name = "UCI5C", + Title = "CONTROLC", + Configuration = new UnitConfiguration + { + AutoAdt = true + } + }; + + _unitService.Setup(u => u.FindByName(unit.Name)).ReturnsAsync(unit); + _unitService.Setup(u => u.FindById(unit.Id)).ReturnsAsync(unit); + + var pocCinna1 = new PointOfCare + { + Id = _pocCinna01Id, + UnitId = _unitId, + Room = "CINA01", + Bed = "CINA01", + Status = StatusEnum.PointOfCare.InUse + }; + var pocCinna2 = new PointOfCare + { + Id = _pocCinna02Id, + UnitId = _unitId, + Room = "CINA02", + Bed = "CINA02", + Status = StatusEnum.PointOfCare.InUse + }; + var pocCinna3 = new PointOfCare + { + Id = _pocCinna03Id, + UnitId = _unitId, + Room = "CINA03", + Bed = "CINA03", + Status = StatusEnum.PointOfCare.InUse + }; + var pocCinna4 = new PointOfCare + { + Id = _pocCinna03Id, + UnitId = _unitId, + Room = "CINA04", + Bed = "CINA04", + Status = StatusEnum.PointOfCare.InUse + }; + + var pocPushed = new PointOfCare + { + Id = _pocPushedId, + UnitId = _unitId, + Room = VirtualPointOfCare.Pushed.ToString(), + Bed = VirtualPointOfCare.Pushed.ToString() + }; + var pocMoved = new PointOfCare + { + Id = _pocMovedId, + UnitId = _unitId, + Room = VirtualPointOfCare.Moved.ToString(), + Bed = VirtualPointOfCare.Moved.ToString() + }; + var pocDeleted = new PointOfCare + { + Id = _pocDeletedId, + UnitId = _unitId, + Room = VirtualPointOfCare.Deleted.ToString(), + Bed = VirtualPointOfCare.Deleted.ToString() + }; + var pocCanceled = new PointOfCare + { + Id = _pocCanceledId, + UnitId = _unitId, + Room = VirtualPointOfCare.Cancelled.ToString(), + Bed = VirtualPointOfCare.Cancelled.ToString() + }; + var pocRecovered = new PointOfCare + { + Id = _pocRecoveredId, + UnitId = _unitId, + Room = VirtualPointOfCare.Recovered.ToString(), + Bed = VirtualPointOfCare.Recovered.ToString() + }; + var pocUnknown = new PointOfCare + { + Id = _pocUnknowndId, + UnitId = _unitId, + Room = VirtualPointOfCare.Unknown.ToString(), + Bed = VirtualPointOfCare.Unknown.ToString() + }; + + // Find By Bed And Unit + _pocService.Setup(poc => poc.FindByBedAndUnitId("CINA01", unit.Id)).ReturnsAsync(pocCinna1); + _pocService.Setup(poc => poc.FindByBedAndUnitId("CINA02", unit.Id)).ReturnsAsync(pocCinna2); + _pocService.Setup(poc => poc.FindByBedAndUnitId("CINA03", unit.Id)).ReturnsAsync(pocCinna3); + _pocService.Setup(poc => poc.FindByBedAndUnitId("CINA04", unit.Id)).ReturnsAsync(pocCinna4); + + _pocService.Setup(poc => poc.FindByBedAndUnitId(VirtualPointOfCare.Pushed.ToString(), unit.Id)) + .ReturnsAsync(pocPushed); + _pocService.Setup(poc => poc.FindByBedAndUnitId(VirtualPointOfCare.Moved.ToString(), unit.Id)) + .ReturnsAsync(pocMoved); + _pocService.Setup(poc => poc.FindByBedAndUnitId(VirtualPointOfCare.Deleted.ToString(), unit.Id)) + .ReturnsAsync(pocDeleted); + _pocService.Setup(poc => poc.FindByBedAndUnitId(VirtualPointOfCare.Cancelled.ToString(), unit.Id)) + .ReturnsAsync(pocCanceled); + _pocService.Setup(poc => poc.FindByBedAndUnitId(VirtualPointOfCare.Recovered.ToString(), unit.Id)) + .ReturnsAsync(pocRecovered); + _pocService.Setup(poc => poc.FindByBedAndUnitId(VirtualPointOfCare.Unknown.ToString(), unit.Id)) + .ReturnsAsync(pocUnknown); + + + // Find By ID + _pocService.Setup(poc => poc.FindById(_pocCinna01Id)).ReturnsAsync(pocCinna1); + _pocService.Setup(poc => poc.FindById(_pocCinna02Id)).ReturnsAsync(pocCinna2); + _pocService.Setup(poc => poc.FindById(_pocCinna03Id)).ReturnsAsync(pocCinna3); + _pocService.Setup(poc => poc.FindById(_pocCinna04Id)).ReturnsAsync(pocCinna4); + + _pocService.Setup(poc => poc.FindById(pocPushed.Id)).ReturnsAsync(pocPushed); + _pocService.Setup(poc => poc.FindById(pocMoved.Id)).ReturnsAsync(pocMoved); + _pocService.Setup(poc => poc.FindById(pocDeleted.Id)).ReturnsAsync(pocDeleted); + _pocService.Setup(poc => poc.FindById(pocCanceled.Id)).ReturnsAsync(pocCanceled); + _pocService.Setup(poc => poc.FindById(pocRecovered.Id)).ReturnsAsync(pocRecovered); + _pocService.Setup(poc => poc.FindById(pocUnknown.Id)).ReturnsAsync(pocUnknown); + + // GetByCodeSysAndCode Info + _pocService.Setup(poc => poc.GetInfo(_pocCinna01Id, It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(pocCinna1); + _pocService.Setup(poc => poc.GetInfo(_pocCinna02Id, It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(pocCinna2); + _pocService.Setup(poc => poc.GetInfo(_pocCinna03Id, It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(pocCinna3); + _pocService.Setup(poc => poc.GetInfo(_pocCinna04Id, It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(pocCinna4); + + _pocService.Setup(poc => poc.GetInfo(_pocPushedId, It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(pocPushed); + _pocService.Setup(poc => poc.GetInfo(_pocMovedId, It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(pocMoved); + _pocService.Setup(poc => poc.GetInfo(_pocDeletedId, It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(pocDeleted); + _pocService.Setup(poc => poc.GetInfo(_pocCanceledId, It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(pocCanceled); + _pocService.Setup(poc => poc.GetInfo(_pocRecoveredId, It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(pocRecovered); + _pocService.Setup(poc => poc.GetInfo(_pocUnknowndId, It.IsAny(), It.IsAny(), It.IsAny())).ReturnsAsync(pocUnknown); + + // GetByCodeSysAndCode All + _pocService.Setup(poc => poc.GetAll()).ReturnsAsync([ + pocCinna1, pocCinna2, pocCinna3, pocCinna4, pocPushed, pocMoved, pocDeleted, pocCanceled, pocRecovered, + pocUnknown + ]); + _pocService.Setup(poc => poc.GetAllLocationInfo()).ReturnsAsync([ + pocCinna1, pocCinna2, pocCinna3, pocCinna4, pocPushed, pocMoved, pocDeleted, pocCanceled, pocRecovered, + pocUnknown + ]); + _unitService.Setup(poc => poc.GetAll(It.IsAny())).ReturnsAsync([unit]); + } + + private PatientService _patientService = null!; + + private Mock _patientRepositoryMock = null!; + private Mock _poCMappingServiceMock = null!; + private Mock _diagnosisServiceMock = null!; + private Mock _appointmentServiceMock = null!; + private Lazy _pumpServiceLazy = null!; + private Mock _patientArchiveRepository = null!; + private Lazy _observationServiceLazy = null!; + private Lazy _treatServiceLazy = null!; + private Mock _recordingAlertService = null!; + private Mock _dischargeService = null!; + private Mock _unitService = null!; + private Mock _displayservice = null!; + private Mock _pocService = null!; + private Mock _displayConfigServiceMock = null!; + private Mock _groupedServiceMock = null!; + private Mock _patientCarePlanServiceMock = null!; + private readonly Mock _httpContextAccessorMock = new(); + private readonly Mock _auditServiceMock = new(); + private IMock _masterListMock = null!; + + + private readonly ApiSettings _apiSettings = new() + { + IccaFacility = "Philips.CIS.CVC", + CreatePatientWithOru = false, + UpdatePatientDataWithAdtA02 = true + }; + + + private IOptions _optionsApiSettings = null!; + private IOptions _listSettings = null!; + + + private Mock> _logger = null!; + private Mock _clientMessageServiceMock = null!; + private Mock _subscribersServiceMock = null!; + private Mock _subscriberGroupedServiceMock = null!; + private Lazy _lazyAdmissionService = null!; + private Mock _admissionService = null!; + private readonly ObjectId _unitId = ObjectId.GenerateNewId(); + private readonly ObjectId _pocCinna01Id = ObjectId.GenerateNewId(); + private readonly ObjectId _pocCinna02Id = ObjectId.GenerateNewId(); + private readonly ObjectId _pocCinna03Id = ObjectId.GenerateNewId(); + private readonly ObjectId _pocCinna04Id = ObjectId.GenerateNewId(); + private readonly ObjectId _pocPushedId = ObjectId.GenerateNewId(); + private readonly ObjectId _pocMovedId = ObjectId.GenerateNewId(); + private readonly ObjectId _pocDeletedId = ObjectId.GenerateNewId(); + private readonly ObjectId _pocCanceledId = ObjectId.GenerateNewId(); + private readonly ObjectId _pocRecoveredId = ObjectId.GenerateNewId(); + private readonly ObjectId _pocUnknowndId = ObjectId.GenerateNewId(); + + [Test] + public async Task Find_Patient_By_APIRequest_Get_Patient_By_PatientNumber() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + + var apiRequest = new ApiRequest + { + Type = "ORU_R01", + PatientNumber = patientNumber, + Patient = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + }, + Location = location, + Facility = "Philips.CIS.CVC" + }; + + var patientInBd = new Patient + { + UnitId = _unitId, + PointOfCareId = _pocCinna02Id, + PatientNumber = patientNumber, + Person = new Person + { + FirstName = "Miguel", + LastName = "Villanueva", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }; + + _patientRepositoryMock.Setup(p => p.FindByPatientNumber(patientNumber)).ReturnsAsync(patientInBd); + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + var patient = await _patientService.FindPatientByApiRequest(apiRequest); + + That(patient, Is.Not.Null); + using (Assert.EnterMultipleScope()) + { + + That(patient!.PatientNumber, Is.EqualTo("12345")); + That(patient.PointOfCareId!.Value, Is.EqualTo((ObjectId?)_pocCinna02Id)); + }; + } + + [Test] + public async Task Find_Patient_By_APIRequest_Not_Exists_CreatePatientWithORU_True_Created_Patient() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + var apiRequest = new ApiRequest + { + Type = "ORU_R01", + PatientNumber = patientNumber, + Patient = person, + Location = location, + Facility = "Philips.CIS.CVC" + }; + + _optionsApiSettings.Value.CreatePatientWithOru = true; + + var patientService = new PatientService( + _patientRepositoryMock.Object, + _patientArchiveRepository.Object, + _observationServiceLazy, + _treatServiceLazy, + _poCMappingServiceMock.Object, + _diagnosisServiceMock.Object, + _appointmentServiceMock.Object, + _pumpServiceLazy, + _recordingAlertService.Object, + _dischargeService.Object, + _optionsApiSettings, + _listSettings, + _logger.Object, + _clientMessageServiceMock.Object, + _subscribersServiceMock.Object, + _subscriberGroupedServiceMock.Object, + _unitService.Object, + _displayservice.Object, + _pocService.Object, + _lazyAdmissionService, + _displayConfigServiceMock.Object, + _groupedServiceMock.Object, + _patientCarePlanServiceMock.Object, + _httpContextAccessorMock.Object, + _auditServiceMock.Object, + _masterListMock.Object + ); + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + + var patient = await patientService.FindPatientByApiRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.InsertOneAsync(It.Is(arg => + arg.UnitId == _unitId && arg.PointOfCareId == _pocCinna02Id && arg.PatientNumber == patientNumber)), + Times.Once); + + That(patient, Is.Not.Null); + using (Assert.EnterMultipleScope()) + { + That(patient!.PatientNumber, Is.EqualTo("12345")); + That(patient.UnitId!.Value, Is.EqualTo(_unitId)); + That(patient.PointOfCareId, Is.Not.Null); + }; + } + + [Test] + public async Task + Find_Patient_By_APIRequest_Not_Exists_And_Location_Ocuped_CreatePatientWithORU_True_Created_In_Temporal_Bed() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + var apiRequest = new ApiRequest + { + Type = "ORU_R01", + PatientNumber = patientNumber, + Patient = person, + Location = location, + Facility = "Philips.CIS.CVC" + }; + + _optionsApiSettings.Value.CreatePatientWithOru = true; + + var patientService = new PatientService( + _patientRepositoryMock.Object, + _patientArchiveRepository.Object, + _observationServiceLazy, + _treatServiceLazy, + _poCMappingServiceMock.Object, + _diagnosisServiceMock.Object, + _appointmentServiceMock.Object, + _pumpServiceLazy, + _recordingAlertService.Object, + _dischargeService.Object, + _optionsApiSettings, + _listSettings, + _logger.Object, + _clientMessageServiceMock.Object, + _subscribersServiceMock.Object, + _subscriberGroupedServiceMock.Object, + _unitService.Object, + _displayservice.Object, + _pocService.Object, + _lazyAdmissionService, + _displayConfigServiceMock.Object, + _groupedServiceMock.Object, + _patientCarePlanServiceMock.Object, + _httpContextAccessorMock.Object, + _auditServiceMock.Object, + _masterListMock.Object + ); + + var patientLocation = new Patient + { + PatientNumber = "11111", + Person = new Person(), + UnitId = _unitId, + PointOfCareId = _pocCinna02Id + }; + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + _patientRepositoryMock.Setup(f => f.FindByLocation(location)).ReturnsAsync(patientLocation); + _patientRepositoryMock.Setup(f => f.FindByUnitAndPocId(_unitId, _pocCinna02Id)).ReturnsAsync(patientLocation); + + + var patient = await patientService.FindPatientByApiRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.InsertOneAsync(It.Is(arg => + arg.UnitId == _unitId && arg.PointOfCareId == _pocUnknowndId && arg.PatientNumber == patientNumber)), + Times.Once); + + That(patient, Is.Not.Null); + using (Assert.EnterMultipleScope()) + { + That(patient!.PatientNumber, Is.EqualTo("12345")); + That(patient.UnitId!.Value, Is.EqualTo(_unitId)); + That(patient.PointOfCareId, Is.Not.Null); + }; + } + + [Test] + public async Task Find_Patient_By_APIRequest_Not_Exists_CreatePatientWithORU_False_Not_Created_In_Temporal_Bed() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + var apiRequest = new ApiRequest + { + Type = "ORU_R01", + PatientNumber = patientNumber, + Patient = person, + Location = location, + Facility = "Philips.CIS.CVC" + }; + + + _optionsApiSettings.Value.CreatePatientWithOru = false; + + var patientService = new PatientService( + _patientRepositoryMock.Object, + _patientArchiveRepository.Object, + _observationServiceLazy, + _treatServiceLazy, + _poCMappingServiceMock.Object, + _diagnosisServiceMock.Object, + _appointmentServiceMock.Object, + _pumpServiceLazy, + _recordingAlertService.Object, + _dischargeService.Object, + _optionsApiSettings, + _listSettings, + _logger.Object, + _clientMessageServiceMock.Object, + _subscribersServiceMock.Object, + _subscriberGroupedServiceMock.Object, + _unitService.Object, + _displayservice.Object, + _pocService.Object, + _lazyAdmissionService, + _displayConfigServiceMock.Object, + _groupedServiceMock.Object, + _patientCarePlanServiceMock.Object, + _httpContextAccessorMock.Object, + _auditServiceMock.Object, + _masterListMock.Object + ); + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + var patient = await patientService.FindPatientByApiRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.InsertOneAsync(It.Is(arg => + arg.UnitId == _unitId && arg.PointOfCareId == _pocUnknowndId && arg.PatientNumber == patientNumber)), + Times.Never); + + That(patient, Is.Null); + } + + [Test] + public async Task Find_Patient_By_APIRequest_Exists_In_Different_Location_NOT_Changes_Patient_Location() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + var apiRequest = new ApiRequest + { + Type = "ORU_R01", + PatientNumber = patientNumber, + Patient = person, + Location = location, + Facility = "Philips.CIS.CVC" + }; + + + var patientInBd = new Patient + { + UnitId = _unitId, + PointOfCareId = _pocCinna02Id, + PatientNumber = patientNumber, + Person = new Person + { + FirstName = "Miguel", + LastName = "Villanueva", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }; + + _patientRepositoryMock.Setup(p => p.FindByPatientNumber(patientNumber)).ReturnsAsync(patientInBd); + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + var patient = await _patientService.FindPatientByApiRequest(apiRequest); + + That(patient, Is.Not.Null); + using (Assert.EnterMultipleScope()) + { + That(patient!.PatientNumber, Is.EqualTo("12345")); + That(patient.UnitId!.Value, Is.EqualTo(_unitId)); + That(patient.PointOfCareId!.Value, Is.EqualTo((ObjectId?)_pocCinna02Id)); + }; + } + + [Test] + public async Task ADT_A01_New_Patient_Empty_Bed_Insert_In_Location() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + var apiRequest = new ApiRequest + { + Type = "ADT_A01", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.Update(It.Is(arg => + arg.UnitString == "UCI5C" && arg.Bed == "CINA02" && arg.PatientNumber == patientNumber)), + Times.Once); + } + + [Test] + public async Task ADT_A01_New_Patient_Unknown_Location_Insert_Nothing() + { + var location = new PatientLocation("UCI5H", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + var apiRequest = new ApiRequest + { + Type = "ADT_A01", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(() => null); + + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.InsertOneAsync(It.IsAny()), + Times.Never); + } + + [Test] + public async Task ADT_A01_New_Patient_Bed_In_Use_Moves_Existing_Patient_To_Temporal_Bed() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "123456"; + var unitId = ObjectId.GenerateNewId(); + var pocId = ObjectId.GenerateNewId(); + + var person = new Person + { + FirstName = "Miguel", + LastName = "Villanueva", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male, + Ids = new Dictionary + { + { "MR", "12345" }, + { "VN", "" } + } + }; + + var poc = new PointOfCare + { + Id = pocId, + UnitId = unitId, + Room = "CINA02", + Bed = "CINA02", + Status = StatusEnum.PointOfCare.InUse + }; + + var pocPushed = new PointOfCare + { + Id = ObjectId.GenerateNewId(), + UnitId = unitId, + Room = VirtualPointOfCare.Pushed.ToString(), + Bed = VirtualPointOfCare.Pushed.ToString() + }; + + var unit = new Unit + { + Id = unitId, + Name = "UCI5C", + Title = "CONTROLC", + Configuration = new UnitConfiguration + { + AutoAdt = true + } + }; + + var apiRequest = new ApiRequest + { + Type = "ADT_A01", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + var mongoId = ObjectId.GenerateNewId(); + + var patientInBd = new Patient + { + Id = mongoId, + PointOfCareId = pocId, + UnitId = unitId, + PatientNumber = "123456", + Person = new Person + { + FirstName = "Jose", + LastName = "Luis", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male, + Ids = new Dictionary + { + { "MR", "123456" }, + { "VN", "" } + } + } + }; + + var existingPatient = new Patient + { + Id = ObjectId.GenerateNewId(), + PointOfCareId = pocId, + UnitId = unitId, + PatientNumber = "1234567", + Person = new Person + { + FirstName = "Jose", + LastName = "Luis", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male, + Ids = new Dictionary + { + { "MR", "123456" }, + { "VN", "" } + } + } + }; + + var admissionServiceMock = new Mock(); + + admissionServiceMock.Setup(a => a.GetAdmissionByPatientNumber(patientNumber)).ReturnsAsync((Admission?)null); + + _pocService.Setup(p => p.FindByBedAndUnitId("CINA02", It.IsAny())).ReturnsAsync(poc); + + _pocService.Setup(p => p.FindByBedAndUnitId(VirtualPointOfCare.Pushed.ToString(), unit.Id)) + .ReturnsAsync(pocPushed); + _pocService.Setup(p => p.FindById(pocPushed.Id)).ReturnsAsync(pocPushed); + _pocService.Setup(p => p.FindById(pocId)).ReturnsAsync(poc); + + _unitService.Setup(u => u.FindByName(unit.Name)).ReturnsAsync(unit); + _unitService.Setup(u => u.FindById(unit.Id)).ReturnsAsync(unit); + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + _patientRepositoryMock.Setup(p => p.FindByUnitAndPocId(unit.Id, poc.Id)).ReturnsAsync(existingPatient); + _patientRepositoryMock.Setup(p => p.FindById(existingPatient.Id)).ReturnsAsync(existingPatient); + _patientRepositoryMock.Setup(p => p.FindById(patientInBd.Id)).ReturnsAsync(patientInBd); + + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.FindByPatientNumber(It.Is(arg => arg == patientInBd.PatientNumber)), Times.Once); + + _patientRepositoryMock.Verify( + p => p.Update(It.Is(arg => + arg.Bed == "CINA02" && arg.PatientNumber == patientNumber)), + Times.Once); + } + + [Test] + public async Task ADT_A01_New_Patient_Bed_In_Use_With_Same_Patient_Number_Updates_Demographic_Data() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var unitId = ObjectId.GenerateNewId(); + var pocId = ObjectId.GenerateNewId(); + + var person = new Person + { + FirstName = "Miguel", + LastName = "Villanueva", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male, + Ids = new Dictionary + { + { "MR", patientNumber }, + { "VN", "" } + } + }; + + + var apiRequest = new ApiRequest + { + Type = "ADT_A01", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + var mongoId = ObjectId.GenerateNewId(); + + var patientInBd = new Patient + { + Id = mongoId, + PointOfCareId = pocId, + UnitId = unitId, + PatientNumber = patientNumber, + Person = new Person + { + FirstName = "_", + LastName = "_", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male, + Ids = new Dictionary + { + { "MR", patientNumber }, + { "VN", "" } + } + } + }; + + + _poCMappingServiceMock.Setup(p => p.Map(location)).ReturnsAsync(location); + _patientRepositoryMock.Setup(p => p.FindById(patientInBd.Id)).ReturnsAsync(patientInBd); + _patientRepositoryMock.Setup(p => p.FindByPatientNumber(patientNumber)).ReturnsAsync(patientInBd); + _patientRepositoryMock.Setup(p => p.FindByUnitAndPocId(unitId, pocId)).ReturnsAsync(patientInBd); + + await _patientService.SaveRequest(apiRequest); + + + _patientRepositoryMock.Verify( + p => p.Update(It.Is(arg => + arg.Bed == "CINA02" && arg.PatientNumber == patientNumber && arg.Person != null && + arg.Person.Equals(person))), + Times.Once); + } + + [Test] + public async Task ADT_A05_New_Patient_New_Bed_Creates_Location() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + var apiRequest = new ApiRequest + { + Type = "ADT_A01", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.Update(It.Is(arg => + arg.UnitId == _unitId && arg.PointOfCareId == _pocCinna02Id && arg.PatientNumber == patientNumber)), + Times.Once); + } + + [Test] + public async Task ADT_A05_New_Patient_Bed_In_Use_Moves_Existing_Patient_To_Temporal_Bed() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + var apiRequest = new ApiRequest + { + Type = "ADT_A01", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + var mongoId = ObjectId.GenerateNewId(); + + var patientInBd = new Patient + { + Id = mongoId, + UnitId = _unitId, + PointOfCareId = _pocCinna02Id, + PatientNumber = "123456", + Person = new Person + { + FirstName = "Jose", + LastName = "Luis", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }; + + + _patientRepositoryMock.Setup(p => p.FindByUnitAndPocId(_unitId, _pocCinna02Id)).ReturnsAsync(patientInBd); + + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + _patientRepositoryMock.Setup(p => p.FindById(mongoId)).ReturnsAsync(patientInBd); + + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify(p => p.UpdateOneAsync(mongoId, + It.Is(arg => arg.UnitId == _unitId && arg.PointOfCareId == _pocPushedId))); + + _patientRepositoryMock.Verify( + p => p.Update(It.Is(arg => + arg.UnitId == _unitId && arg.PointOfCareId == _pocCinna02Id && arg.PatientNumber == patientNumber)), + Times.Once()); + } + + [Test] + public async Task ADT_A03_END_VISIT_PATIENT_NOT_EXISTS_DO_NOTHING() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + var apiRequest = new ApiRequest + { + Type = "ADT_A03", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.Update(It.Is(arg => + arg.PointOfCareId == _pocDeletedId && arg.UnitId == _unitId && arg.PatientNumber == patientNumber)), + Times.Never); + } + + [Test] + public async Task ADT_A03_END_VISIT_PATIENT_EXISTS() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + var mongoId = ObjectId.GenerateNewId(); + var apiRequest = new ApiRequest + { + Type = "ADT_A03", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + var patientInBd = new Patient + { + Id = mongoId, + UnitId = _unitId, + PointOfCareId = _pocCinna02Id, + Bed = location.Bed, + PatientNumber = patientNumber, + Person = person + }; + + + _patientRepositoryMock.Setup(p => p.FindByPatientNumber(patientNumber)).ReturnsAsync(patientInBd); + _patientRepositoryMock.Setup(p => p.FindById(mongoId)).ReturnsAsync(patientInBd); + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.UpdateOneAsync(mongoId, + It.Is(arg => + arg.PointOfCareId == _pocDeletedId && arg.UnitId == _unitId && arg.PatientNumber == patientNumber)), + Times.Once); + _pocService.Verify( + p => p.CheckNextAdmission(_pocCinna02Id), + Times.Once); + } + + + [Test] + public async Task ADT_A02_TRANSFER_PATIENT_FROM_EXISTS_BED_TO_NOT_EXISTS_BED_SHOULD_MOVE_TO_VIRTUAL_POC_UNKNOWN() + { + var location = new PatientLocation("UCI5C", "CINA02"); + var otherlocation = new PatientLocation("UCI5X", "CINA01"); + + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + var apiRequest = new ApiRequest + { + Type = "ADT_A02", + PatientNumber = patientNumber, + Patient = person, + OldLocation = location, + Location = otherlocation + }; + + var existingPatient = new Patient + { + Id = ObjectId.GenerateNewId(), + PatientNumber = "12345", + Person = person, + Location = location, + UnitId = _unitId, + PointOfCareId = _pocCinna02Id + }; + + // Crear una nueva instancia de ApiSettings + var testApiSettings = new ApiSettings + { + IccaFacility = "Philips.CIS.CVC", + PointOfCareMapping = new PointOfCareMapping { Required = false } + }; + + // Crear una nueva instancia de IOptions + var testOptionsApiSettings = Options.Create(testApiSettings); + + // Crear una nueva instancia de PatientService utilizando la configuración custom + var patientServiceForThisTest = new PatientService( + _patientRepositoryMock.Object, + _patientArchiveRepository.Object, + _observationServiceLazy, + _treatServiceLazy, + _poCMappingServiceMock.Object, + _diagnosisServiceMock.Object, + _appointmentServiceMock.Object, + _pumpServiceLazy, + _recordingAlertService.Object, + _dischargeService.Object, + testOptionsApiSettings, + _listSettings, + _logger.Object, + _clientMessageServiceMock.Object, + _subscribersServiceMock.Object, + _subscriberGroupedServiceMock.Object, + _unitService.Object, + _displayservice.Object, + _pocService.Object, + _lazyAdmissionService, + _displayConfigServiceMock.Object, + _groupedServiceMock.Object, + _patientCarePlanServiceMock.Object, + _httpContextAccessorMock.Object, + _auditServiceMock.Object, + _masterListMock.Object + ); + + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + _poCMappingServiceMock.Setup(i => i.Map(otherlocation)).ReturnsAsync(otherlocation); + + _patientRepositoryMock.Setup(i => i.FindByPatientNumber(patientNumber)).ReturnsAsync(existingPatient); + + _patientRepositoryMock.Setup(i => i.FindById(existingPatient.Id)).ReturnsAsync(existingPatient); + + await patientServiceForThisTest.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.UpdateOneAsync(existingPatient.Id, + It.Is(x => x.PointOfCareId == _pocUnknowndId && x.PatientNumber == patientNumber)), + Times.Once()); + } + + [Test] + public async Task ADT_A02_TRANSFER_PATIENT_FROM_NOT_EXISTS_BED_TO_BED_SHOULD_CREATE_PATIENT() + { + var location = new PatientLocation("UCI5C", "CINA02", "CINA02"); + var oldLocation = new PatientLocation("UCI5C", "CINA04", "CINA04"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + var mongoId = ObjectId.GenerateNewId(); + var apiRequest = new ApiRequest + { + Type = "ADT_A02", + PatientNumber = patientNumber, + Patient = person, + Location = location, + OldLocation = oldLocation + }; + + var newPatientInBed = new Patient + { + UnitId = _unitId, + PointOfCareId = _pocCinna02Id, + PatientNumber = patientNumber, + Person = person, + Location = location + }; + + _patientRepositoryMock.Setup(p => p.FindByPatientNumber(patientNumber)).ReturnsAsync((Patient?)null); + + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + _patientRepositoryMock.Setup(p => p.FindById(mongoId)).ReturnsAsync((Patient?)null); + + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.InsertOneAsync(It.Is(arg => arg.PatientNumber == newPatientInBed.PatientNumber)), + Times.Once); + } + + [Test] + public async Task ADT_A02_TRANSFER_PATIENT_FROM_BED_TO_IN_USE_BED() + { + // Old Location for existing patient + var oldLocation = new PatientLocation("UCI5C", "CINA03"); + // New Location for existing patient + var location = new PatientLocation("UCI5C", "CINA02"); + const string newPatientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + var mongoIdNewPatient = ObjectId.GenerateNewId(); + + // Existing patient in new patient location + var mongoIdOldPatient = ObjectId.GenerateNewId(); + var oldPatientInBed = new Patient + { + Id = mongoIdOldPatient, + UnitId = _unitId, + PointOfCareId = _pocCinna02Id, + PatientNumber = "123456", + Person = new Person + { + FirstName = "Jose", + LastName = "Luis", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }; + + var newPatientInBed = new Patient + { + Id = mongoIdNewPatient, + PointOfCareId = _pocCinna03Id, + UnitId = _unitId, + PatientNumber = newPatientNumber, + Person = person + }; + + var apiRequest = new ApiRequest + { + Type = "ADT_A02", + PatientNumber = newPatientNumber, + Patient = person, + Location = location, + OldLocation = oldLocation + }; + _patientRepositoryMock.Setup(p => p.FindByPatientNumber(newPatientNumber)).ReturnsAsync(newPatientInBed); + + _patientRepositoryMock.Setup(p => p.FindByLocation(location)).ReturnsAsync(oldPatientInBed); + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + _patientRepositoryMock.Setup(p => p.FindByUnitAndPocId(_unitId, _pocCinna02Id)).ReturnsAsync(oldPatientInBed); + + _patientRepositoryMock.Setup(p => p.FindById(oldPatientInBed.Id)).ReturnsAsync(oldPatientInBed); + _patientRepositoryMock.Setup(p => p.FindById(newPatientInBed.Id)).ReturnsAsync(newPatientInBed); + + + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.UpdateOneAsync(mongoIdOldPatient, + It.Is(arg => arg.UnitId == _unitId && arg.PointOfCareId == _pocMovedId)), Times.Once()); + _dischargeService.Verify(p => p.GetDischargeByPatientId(mongoIdOldPatient), Times.Once); + _patientRepositoryMock.Verify( + p => p.UpdateOneAsync(mongoIdNewPatient, + It.Is(arg => arg.UnitId == _unitId && arg.PointOfCareId == _pocCinna02Id)), Times.Once()); + _pocService.Verify(p => p.CheckNextAdmission(_pocMovedId), Times.Once); + } + + + [Test] + public async Task + ADT_A02_TRANSFER_PATIENT_FROM_NOT_EXISTS_TO_NOT_EMPTY_BED_SHOULD_CHANGE_LAST_PATIENT_TO_TEMPORTAL_BED() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + var mongoId = ObjectId.GenerateNewId(); + var apiRequest = new ApiRequest + { + Type = "ADT_A02", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + var oldPatientInBed = new Patient + { + Id = mongoId, + UnitId = _unitId, + PointOfCareId = _pocCinna02Id, + PatientNumber = "123456", + Person = new Person + { + FirstName = "Jose", + LastName = "Luis", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }; + + _patientRepositoryMock.Setup(p => p.FindByUnitAndPocId(_unitId, _pocCinna02Id)).ReturnsAsync(oldPatientInBed); + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + _patientRepositoryMock.Setup(p => p.FindById(mongoId)).ReturnsAsync(oldPatientInBed); + + await _patientService.SaveRequest(apiRequest); + _patientRepositoryMock.Verify( + p => p.UpdateOneAsync(mongoId, + It.Is(arg => + arg.UnitId == _unitId && arg.PointOfCareId == _pocMovedId && + arg.PatientNumber == oldPatientInBed.PatientNumber)), Times.Once); + + _patientRepositoryMock.Verify( + p => p.InsertOneAsync(It.Is(arg => + arg.UnitId == _unitId && arg.PointOfCareId == _pocCinna02Id && arg.PatientNumber == patientNumber)), + Times.Once()); + } + + [Test] + public async Task + ADT_A02_TRANSFER_PATIENT_FROM_NOT_EXISTS_BED_TO_NOT_EMPTY_BED_WITH_SAME_PATIENT_NUMBER_SHOULD_UPTADE_DEMOGRAPHIC_DATA() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + var mongoId = ObjectId.GenerateNewId(); + var apiRequest = new ApiRequest + { + Type = "ADT_A02", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + var oldPatientInBed = new Patient + { + Id = mongoId, + UnitId = _unitId, + PointOfCareId = _pocCinna02Id, + PatientNumber = patientNumber, + Person = new Person + { + FirstName = "_", + LastName = "_" + } + }; + + _patientRepositoryMock.Setup(p => p.FindByUnitAndPocId(_unitId, _pocCinna02Id)).ReturnsAsync(oldPatientInBed); + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + + _patientRepositoryMock.Setup(p => p.FindByPatientNumber(patientNumber)).ReturnsAsync(oldPatientInBed); + + _patientRepositoryMock.Setup(p => p.FindById(mongoId)).ReturnsAsync(oldPatientInBed); + + + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.UpdatePatientData(mongoId, patientNumber, It.Is(arg => arg.Equals(person)), false), + Times.Once); + + + _patientRepositoryMock.Verify( + p => p.InsertOneAsync(It.IsAny()), + Times.Never); + } + + [Test] + public async Task ADT_A04_New_Patient_Creates_Patient() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + var apiRequest = new ApiRequest + { + Type = "ADT_A04", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.InsertOneAsync(It.Is(arg => + arg.UnitId == _unitId && arg.PointOfCareId == _pocCinna02Id && arg.PatientNumber == patientNumber)), + Times.Once); + } + + [Test] + public async Task ADT_A04_New_Patient_Not_Location_Creates_Patient() + { + var location = new PatientLocation("NEONATAL", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + var apiRequest = new ApiRequest + { + Type = "ADT_A04", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync((PatientLocation?)null); + + + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.InsertOneAsync(It.Is(arg => arg.PatientNumber == patientNumber)), Times.Never); + } + + [Test] + public async Task ADT_A08_No_exist_CreatePatientWithADT_A08_True_Patient_Creates_Patient() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + var apiRequest = new ApiRequest + { + Type = "ADT_A08", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + _optionsApiSettings.Value.CreatePatientWithAdtA08 = true; + + var patientService = new PatientService( + _patientRepositoryMock.Object, + _patientArchiveRepository.Object, + _observationServiceLazy, + _treatServiceLazy, + _poCMappingServiceMock.Object, + _diagnosisServiceMock.Object, + _appointmentServiceMock.Object, + _pumpServiceLazy, + _recordingAlertService.Object, + _dischargeService.Object, + _optionsApiSettings, + _listSettings, + _logger.Object, + _clientMessageServiceMock.Object, + _subscribersServiceMock.Object, + _subscriberGroupedServiceMock.Object, + _unitService.Object, + _displayservice.Object, + _pocService.Object, + _lazyAdmissionService, + _displayConfigServiceMock.Object, + _groupedServiceMock.Object, + _patientCarePlanServiceMock.Object, + _httpContextAccessorMock.Object, + _auditServiceMock.Object, + _masterListMock.Object + ); + + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + + // Act + await patientService.SaveRequest(apiRequest); + + // Assert: Verify that the Update method is called with the correct patient + _patientRepositoryMock.Verify(p => p.Update(It.Is(arg => + arg.UnitId == _unitId && + arg.PointOfCareId == _pocCinna02Id && + arg.PatientNumber == patientNumber)), Times.Once); + + // Add additional logging for debugging + Console.WriteLine("Test completed successfully"); + } + + [Test] + public async Task ADT_A08_No_exist_Patient_Not_Creates_Patient() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + var apiRequest = new ApiRequest + { + Type = "ADT_A08", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + _optionsApiSettings.Value.CreatePatientWithAdtA08 = false; + + var patientService = new PatientService( + _patientRepositoryMock.Object, + _patientArchiveRepository.Object, + _observationServiceLazy, + _treatServiceLazy, + _poCMappingServiceMock.Object, + _diagnosisServiceMock.Object, + _appointmentServiceMock.Object, + _pumpServiceLazy, + _recordingAlertService.Object, + _dischargeService.Object, + _optionsApiSettings, + _listSettings, + _logger.Object, + _clientMessageServiceMock.Object, + _subscribersServiceMock.Object, + _subscriberGroupedServiceMock.Object, + _unitService.Object, + _displayservice.Object, + _pocService.Object, + _lazyAdmissionService, + _displayConfigServiceMock.Object, + _groupedServiceMock.Object, + _patientCarePlanServiceMock.Object, + _httpContextAccessorMock.Object, + _auditServiceMock.Object, + _masterListMock.Object + ); + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + + await patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.InsertOneAsync(It.Is(arg => + arg.UnitId == _unitId && + arg.PointOfCareId == _pocCinna02Id + && arg.PatientNumber == patientNumber)), + Times.Never); + } + + [Test] + public async Task ADT_A08_Exist_Patient_Update_Patient() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + var apiRequest = new ApiRequest + { + Type = "ADT_A08", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + var patient = new Patient + { + UnitId = _unitId, + PointOfCareId = _pocCinna02Id, + PatientNumber = patientNumber, + Person = person + }; + + _optionsApiSettings.Value.CreatePatientWithAdtA08 = false; + + var patientService = new PatientService( + _patientRepositoryMock.Object, + _patientArchiveRepository.Object, + _observationServiceLazy, + _treatServiceLazy, + _poCMappingServiceMock.Object, + _diagnosisServiceMock.Object, + _appointmentServiceMock.Object, + _pumpServiceLazy, + _recordingAlertService.Object, + _dischargeService.Object, + _optionsApiSettings, + _listSettings, + _logger.Object, + _clientMessageServiceMock.Object, + _subscribersServiceMock.Object, + _subscriberGroupedServiceMock.Object, + _unitService.Object, + _displayservice.Object, + _pocService.Object, + _lazyAdmissionService, + _displayConfigServiceMock.Object, + _groupedServiceMock.Object, + _patientCarePlanServiceMock.Object, + _httpContextAccessorMock.Object, + _auditServiceMock.Object, + _masterListMock.Object + ); + + + _patientRepositoryMock.Setup(f => f.FindByPatientNumber(patientNumber)).ReturnsAsync(patient); + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + + await patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.Update(It.Is(arg => + arg.UnitId == _unitId && arg.PointOfCareId == _pocCinna02Id && arg.PatientNumber == patientNumber)), + Times.Once); + } + + [Test] + public async Task ADT_A11_Exist_Patient_CANCELLED_Patient() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + + var apiRequest = new ApiRequest + { + Type = "ADT_A11", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + var patient = new Patient + { + UnitId = _unitId, + PointOfCareId = _pocCinna02Id, + PatientNumber = patientNumber, + Person = person, + Id = ObjectId.GenerateNewId() + }; + + var patientService = new PatientService( + _patientRepositoryMock.Object, + _patientArchiveRepository.Object, + _observationServiceLazy, + _treatServiceLazy, + _poCMappingServiceMock.Object, + _diagnosisServiceMock.Object, + _appointmentServiceMock.Object, + _pumpServiceLazy, + _recordingAlertService.Object, + _dischargeService.Object, + _optionsApiSettings, + _listSettings, + _logger.Object, + _clientMessageServiceMock.Object, + _subscribersServiceMock.Object, + _subscriberGroupedServiceMock.Object, + _unitService.Object, + _displayservice.Object, + _pocService.Object, + _lazyAdmissionService, + _displayConfigServiceMock.Object, + _groupedServiceMock.Object, + _patientCarePlanServiceMock.Object, + _httpContextAccessorMock.Object, + _auditServiceMock.Object, + _masterListMock.Object + ); + + + _patientRepositoryMock.Setup(f => f.FindByPatientNumber(patientNumber)).ReturnsAsync(patient); + // sectionServiceMock.Setup(f => f.FindByPatient(patient.id)).ReturnsAsync(null); + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + + await patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.UpdateOneAsync(patient.Id, + It.Is(arg => + arg.UnitId == _unitId && arg.PointOfCareId == _pocCanceledId && + arg.PatientNumber == patientNumber)), + Times.Once); + } + + [Test] + public async Task ADT_A11_No_Exist_Patient_Not_CANCELLED_Patient() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + var apiRequest = new ApiRequest + { + Type = "ADT_A11", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + var patientService = new PatientService( + _patientRepositoryMock.Object, + _patientArchiveRepository.Object, + _observationServiceLazy, + _treatServiceLazy, + _poCMappingServiceMock.Object, + _diagnosisServiceMock.Object, + _appointmentServiceMock.Object, + _pumpServiceLazy, + _recordingAlertService.Object, + _dischargeService.Object, + _optionsApiSettings, + _listSettings, + _logger.Object, + _clientMessageServiceMock.Object, + _subscribersServiceMock.Object, + _subscriberGroupedServiceMock.Object, + _unitService.Object, + _displayservice.Object, + _pocService.Object, + _lazyAdmissionService, + _displayConfigServiceMock.Object, + _groupedServiceMock.Object, + _patientCarePlanServiceMock.Object, + _httpContextAccessorMock.Object, + _auditServiceMock.Object, + _masterListMock.Object + ); + + + await patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.UpdateOneAsync(It.IsAny(), + It.Is(arg => + arg.UnitId == _unitId && arg.PointOfCareId == _pocCanceledId && + arg.PatientNumber == patientNumber)), + Times.Never); + } + + [Test] + public async Task ADT_A12_TRANSFER_PATIENT_NOT_EXISTS_TO_EMPTY_BED_SHOULD_CREATE_BED() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + var apiRequest = new ApiRequest + { + Type = "ADT_A12", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.InsertOneAsync(It.Is(arg => + arg.UnitId == _unitId && arg.PointOfCareId == _pocCinna02Id && arg.PatientNumber == patientNumber)), + Times.Once()); + } + + [Test] + public async Task ADT_A12_TRANSFER_PATIENT_FROM_BED_TO_NOT_EXISTS_BED_SHOULD_CREATE_BED() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + var mongoId = ObjectId.GenerateNewId(); + var apiRequest = new ApiRequest + { + Type = "ADT_A12", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + var newPatientInBed = new Patient + { + Id = mongoId, + UnitId = _unitId, + PointOfCareId = ObjectId.GenerateNewId(), + PatientNumber = patientNumber, + Person = person + }; + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + _patientRepositoryMock.Setup(p => p.FindByPatientNumber(patientNumber)).ReturnsAsync(newPatientInBed); + _patientRepositoryMock.Setup(p => p.FindById(mongoId)).ReturnsAsync(newPatientInBed); + + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.UpdateOneAsync(mongoId, + It.Is(arg => arg.UnitId == _unitId && arg.PointOfCareId == _pocCinna02Id)), Times.Once); + } + + [Test] + public async Task ADT_A12_TRANSFER_PATIENT_FROM_BED_TO_IN_USE_BED() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + var mongoIdOldPatient = ObjectId.GenerateNewId(); + var mongoIdNewPatient = ObjectId.GenerateNewId(); + var apiRequest = new ApiRequest + { + Type = "ADT_A12", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + var oldPatientInBed = new Patient + { + Id = mongoIdOldPatient, + UnitId = _unitId, + PointOfCareId = _pocCinna02Id, + PatientNumber = "123456", + Person = new Person + { + FirstName = "Jose", + LastName = "Luis", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }; + + var newPatientInBed = new Patient + { + Id = mongoIdNewPatient, + UnitId = _unitId, + PointOfCareId = _pocCinna03Id, + PatientNumber = patientNumber, + Person = person + }; + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + _patientRepositoryMock.Setup(p => p.FindByUnitAndPocId(_unitId, _pocCinna02Id)).ReturnsAsync(oldPatientInBed); + + _patientRepositoryMock.Setup(p => p.FindByPatientNumber(patientNumber)).ReturnsAsync(newPatientInBed); + + + _patientRepositoryMock.Setup(p => p.FindById(It.Is(id => id == mongoIdOldPatient))) + .ReturnsAsync(oldPatientInBed); + _patientRepositoryMock.Setup(p => p.FindById(It.Is(id => id == mongoIdNewPatient))) + .ReturnsAsync(newPatientInBed); + + + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.UpdatePatientData(mongoIdNewPatient, patientNumber, It.Is(arg => arg.Equals(person)), false), + Times.Once); + + + _patientRepositoryMock.Verify( + p => p.UpdateOneAsync(mongoIdOldPatient, + It.Is(arg => arg.UnitId == _unitId && arg.PointOfCareId == _pocMovedId)), Times.Once); + + _patientRepositoryMock.Verify(p => p.Update(It.IsAny()), Times.Never()); + } + + + [Test] + public async Task + ADT_A12_TRANSFER_PATIENT_NOT_EXISTS_TO_NOT_EMPTY_BED_SHOULD_CHANGE_LAST_PATIENT_TO_TEMPORTAL_BED() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + var mongoId = ObjectId.GenerateNewId(); + var apiRequest = new ApiRequest + { + Type = "ADT_A12", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + var oldPatientInBed = new Patient + { + Id = mongoId, + UnitId = _unitId, + PointOfCareId = _pocCinna02Id, + PatientNumber = "123456", + Person = new Person + { + FirstName = "Jose", + LastName = "Luis", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }; + + _patientRepositoryMock.Setup(p => p.FindByUnitAndPocId(_unitId, _pocCinna02Id)).ReturnsAsync(oldPatientInBed); + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + _patientRepositoryMock.Setup(p => p.FindById(mongoId)).ReturnsAsync(oldPatientInBed); + + await _patientService.SaveRequest(apiRequest); + _patientRepositoryMock.Verify( + p => p.UpdateOneAsync(mongoId, + It.Is(arg => arg.UnitId == _unitId && arg.PointOfCareId == _pocMovedId)), Times.Once); + + _patientRepositoryMock.Verify( + p => p.InsertOneAsync(It.Is(arg => + arg.UnitId == _unitId && arg.PointOfCareId == _pocCinna02Id && arg.PatientNumber == patientNumber)), + Times.Once()); + } + + [Test] + public async Task + ADT_A13_TRANSFER_PATIENT_NOT_EXISTS_TO_NOT_EMPTY_BED_SHOULD_CHANGE_LAST_PATIENT_TO_TEMPORTAL_BED() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + var mongoId = ObjectId.GenerateNewId(); + var apiRequest = new ApiRequest + { + Type = "ADT_A13", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + var patient = new Patient + { + Id = mongoId, + UnitId = _unitId, + PointOfCareId = _pocCinna02Id, + PatientNumber = patientNumber, + Person = person + }; + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + _patientRepositoryMock.Setup(p => p.FindByPatientNumber(patientNumber)).ReturnsAsync(patient); + _patientRepositoryMock.Setup(p => p.FindById(mongoId)).ReturnsAsync(patient); + + await _patientService.SaveRequest(apiRequest); + _patientRepositoryMock.Verify( + p => p.UpdateOneAsync(mongoId, + It.Is(arg => arg.UnitId == _unitId && arg.PointOfCareId == _pocCinna02Id)), + Times.Once); + } + + [Test] + public async Task ADT_A13_TRANSFER_PATIENT_NOT_EXISTS_TO_EMPTY_BED_SHOULD_CHANGE_LAST_PATIENT_TO_TEMPORTAL_BED() + { + var location = new PatientLocation("UCI5C", ""); + const string patientNumber = "12345"; + var recoveredLocation = new PatientLocation("UCI5C", "Recovered", "Recovered"); + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + var mongoId = ObjectId.GenerateNewId(); + var apiRequest = new ApiRequest + { + Type = "ADT_A13", + PatientNumber = patientNumber, + Patient = person, + Location = location + }; + + var patient = new Patient + { + Id = mongoId, + UnitId = _unitId, + PointOfCareId = _pocDeletedId, + PatientNumber = patientNumber, + Person = person + }; + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + _poCMappingServiceMock + .Setup(i => i.Map(It.Is(l => l.UnitName == recoveredLocation.UnitName && l.Bed == recoveredLocation.Bed))) + .ReturnsAsync(recoveredLocation); + + _patientRepositoryMock.Setup(p => p.FindByPatientNumber(patientNumber)).ReturnsAsync(patient); + _patientRepositoryMock.Setup(p => p.FindById(mongoId)).ReturnsAsync(patient); + _patientRepositoryMock.Setup(p => p.FindByUnitAndPocId(_unitId, _pocDeletedId)).ReturnsAsync(patient); + + await _patientService.SaveRequest(apiRequest); + _patientRepositoryMock.Verify( + p => p.UpdateOneAsync(mongoId, It.Is(arg => + arg.UnitId == _unitId && + arg.PointOfCareId == _pocRecoveredId)), + Times.Once); + } + + [Test] + public async Task ADT_A31_Upadate_Patient() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var person = new Person { FirstName = "Miguel", LastName = "Vill", Gender = PatientEnum.Gender.Male }; + var mongoId = ObjectId.GenerateNewId(); + var neWperson = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + var apiRequest = new ApiRequest + { + Type = "ADT_A31", + PatientNumber = patientNumber, + Patient = neWperson, + Location = location + }; + + + var patient = new Patient + { + Id = mongoId, + UnitId = _unitId, + PointOfCareId = _pocCinna02Id, + PatientNumber = patientNumber, + Person = person + }; + + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + _patientRepositoryMock.Setup(p => p.FindByPatientNumber(patientNumber)).ReturnsAsync(patient); + + await _patientService.SaveRequest(apiRequest); + _patientRepositoryMock.Verify(p => p.Update(It.Is(arg => arg.UnitId == _unitId && + arg.PointOfCareId == _pocCinna02Id + && arg.Person != null && + arg.Person.LastName == neWperson.LastName + && arg.Person.BirthDate == + neWperson.BirthDate)), Times.Once); + } + + [Test] + public async Task ADT_A31_Upadate_Patient_Not_Found_Not_Update() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + var neWperson = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + var apiRequest = new ApiRequest + { + Type = "ADT_A31", + PatientNumber = patientNumber, + Patient = neWperson, + Location = location + }; + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + await _patientService.SaveRequest(apiRequest); + _patientRepositoryMock.Verify(p => p.Update(It.Is(arg => arg.UnitId == _unitId && + arg.PointOfCareId == _pocCinna02Id + && arg.Person != null && + arg.Person.LastName == neWperson.LastName + && arg.Person.BirthDate == + neWperson.BirthDate)), Times.Never); + } + + [Test] + public async Task ADT_A39_Merge_Patient() + { + var location = new PatientLocation("UCI5C", "CINA02"); + var oldLocation = new PatientLocation("XXXXXX", "XXXXXX"); + const string patientNumber = "12345"; + const string oldPatientNumber = "54321"; + var person = new Person { FirstName = "Miguel", LastName = "Vill", Gender = PatientEnum.Gender.Male }; + var mongoId = ObjectId.GenerateNewId(); + var oldPerson = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + var apiRequest = new ApiRequest + { + Type = "ADT_A39", + PatientNumber = patientNumber, + OldPatientNumber = oldPatientNumber, + Location = location + }; + + var patient = new Patient + { + Id = mongoId, + UnitId = _unitId, + PointOfCareId = _pocCinna02Id, + PatientNumber = patientNumber, + Person = person + }; + + var oldPatient = new Patient + { + Id = ObjectId.GenerateNewId(), + UnitString = oldLocation.UnitName, + Bed = oldLocation.Bed, + PatientNumber = oldPatientNumber, + Person = oldPerson + }; + + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + _patientRepositoryMock.Setup(p => p.FindByPatientNumber(patientNumber)).ReturnsAsync(patient); + _patientRepositoryMock.Setup(p => p.FindByPatientNumber(oldPatientNumber)).ReturnsAsync(oldPatient); + + await _patientService.SaveRequest(apiRequest); + + + _patientRepositoryMock.Verify(p => p.Delete(oldPatient.Id), Times.Once); + } + + [Test] + public async Task ADT_A39_Not_OldPatient_Not_Merge_Patient() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + const string oldPatientNumber = "54321"; + var person = new Person { FirstName = "Miguel", LastName = "Vill", Gender = PatientEnum.Gender.Male }; + var mongoId = ObjectId.GenerateNewId(); + + var apiRequest = new ApiRequest + { + Type = "ADT_A39", + PatientNumber = patientNumber, + OldPatientNumber = oldPatientNumber, + Location = location + }; + + var patient = new Patient + { + Id = mongoId, + UnitId = _unitId, + PointOfCareId = _pocCinna02Id, + PatientNumber = patientNumber, + Person = person + }; + + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + _patientRepositoryMock.Setup(p => p.FindByPatientNumber(patientNumber)).ReturnsAsync(patient); + + await _patientService.SaveRequest(apiRequest); + + + _patientRepositoryMock.Verify(p => p.Delete(It.IsAny()), Times.Never); + } + + [Test] + public async Task ADT_A39_Not_Patient_Not_Merge_Patient() + { + var location = new PatientLocation("NEONATAL", "CINA02"); + var oldLocation = new PatientLocation("XXXXXX", "XXXXXX"); + const string patientNumber = "12345"; + const string oldPatientNumber = "54321"; + var oldPerson = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + var apiRequest = new ApiRequest + { + Type = "ADT_A39", + PatientNumber = patientNumber, + OldPatientNumber = oldPatientNumber, + Location = location + }; + + + var oldPatient = new Patient + { + Id = ObjectId.GenerateNewId(), + UnitString = oldLocation.UnitName, + Bed = oldLocation.Bed, + PatientNumber = oldPatientNumber, + Person = oldPerson + }; + + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + _patientRepositoryMock.Setup(p => p.FindByPatientNumber(oldPatientNumber)).ReturnsAsync(oldPatient); + + + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify(p => p.Delete(oldPatient.Id), Times.Never); + } + + [Test] + public async Task ADT_A44_Upadate_Patient() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + const string oldPatientNumber = "54321"; + var person = new Person { FirstName = "Miguel", LastName = "Vill", Gender = PatientEnum.Gender.Male }; + var newPerson = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + var apiRequest = new ApiRequest + { + Type = "ADT_A44", + PatientNumber = patientNumber, + Patient = newPerson, + Location = location, + OldPatientNumber = oldPatientNumber + }; + + var oldPatient = new Patient + { + Id = ObjectId.GenerateNewId(), + UnitId = _unitId, + PointOfCareId = _pocCinna02Id, + Person = person + }; + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + _patientRepositoryMock.Setup(p => p.FindByPatientNumber(oldPatientNumber)).ReturnsAsync(oldPatient); + + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify(p => p.Update(It.Is(arg => arg.PatientNumber == patientNumber)), + Times.Once); + } + + [Test] + public async Task ADT_A44_Not_OldPatient_Not__Upadate_Patient() + { + var location = new PatientLocation("UCI5C", "CINA02"); + const string patientNumber = "12345"; + const string oldPatientNumber = "54321"; + var newPerson = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + var apiRequest = new ApiRequest + { + Type = "ADT_A44", + PatientNumber = patientNumber, + Patient = newPerson, + Location = location, + OldPatientNumber = oldPatientNumber + }; + + _poCMappingServiceMock.Setup(i => i.Map(location)).ReturnsAsync(location); + + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify(p => p.Update(It.Is(arg => arg.PatientNumber == patientNumber)), + Times.Never); + } + + [Test] + public async Task ICCA_Process_No_Patient_BD_Insert_ICCA_Patients() + { + var iccaPatients = new List + { + new() + { + Location = new PatientLocation("UCI5C", "CINA01"), + PatientNumber = "patientNumber_1", + PatientId = "patientId_1", + AdmitTime = DateTime.Now + }, + new() + { + Location = new PatientLocation("UCI5C", "CINA02"), + PatientNumber = "patientNumber_2", + PatientId = "patientId_2", + AdmitTime = DateTime.Now + }, + new() + { + Location = new PatientLocation("UCI5C", "CINA03"), + PatientNumber = "patientNumber_3", + PatientId = "patientId_3", + AdmitTime = DateTime.Now + }, + new() + { + Location = new PatientLocation("UCI5C", "CINA04"), + PatientNumber = "patientNumber_4", + PatientId = "patientId_4", + AdmitTime = DateTime.Now + } + }; + + var apiRequest = new ApiRequest + { + Type = "ICCA", + IccaPatients = iccaPatients + }; + + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.InsertOneAsync(It.Is(arg => + arg.UnitId == _unitId && + arg.PointOfCareId == _pocCinna01Id && arg.PatientNumber == iccaPatients[0].PatientNumber)), + Times.Once); + _patientRepositoryMock.Verify( + p => p.InsertOneAsync(It.Is(arg => + arg.UnitId == _unitId && + arg.PointOfCareId == _pocCinna02Id && arg.PatientNumber == iccaPatients[1].PatientNumber)), + Times.Once); + _patientRepositoryMock.Verify( + p => p.InsertOneAsync(It.Is(arg => + arg.UnitId == _unitId && + arg.PointOfCareId == _pocCinna03Id && arg.PatientNumber == iccaPatients[2].PatientNumber)), + Times.Once); + _patientRepositoryMock.Verify( + p => p.InsertOneAsync(It.Is(arg => + arg.UnitId == _unitId && + arg.PointOfCareId == _pocCinna03Id && arg.PatientNumber == iccaPatients[3].PatientNumber)), + Times.Once); + } + + [Test] + public async Task ICCA_Process_Patient_BD_Insert_ICCA_Patient_Pushed_Actual_Patient() + { + var location1 = new PatientLocation("UCI5C", "CINA01"); + + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + var iccaPatients = new List + { + new() + { + Location = location1, + PatientNumber = "patientNumber_1", + PatientId = "patientId_1", + AdmitTime = DateTime.Now + } + }; + + var apiRequest = new ApiRequest + { + Type = "ICCA", + IccaPatients = iccaPatients + }; + + var patientInBd = new Patient + { + Id = ObjectId.GenerateNewId(), + UnitId = _unitId, + PointOfCareId = _pocCinna01Id, + PatientNumber = patientNumber, + Person = person + }; + + _patientRepositoryMock.Setup(s => s.FindByPointOfCareId(_pocCinna01Id)).ReturnsAsync(patientInBd); + _patientRepositoryMock.Setup(p => p.FindById(patientInBd.Id)).ReturnsAsync(patientInBd); + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.UpdateOneAsync(It.Is(arg => arg == patientInBd.Id), + It.Is(arg => arg.UnitId == _unitId && arg.PointOfCareId == _pocMovedId)), + Times.Once); + _patientRepositoryMock.Verify( + p => p.InsertOneAsync(It.Is(arg => + arg.UnitId == _unitId && arg.PointOfCareId == _pocCinna01Id && + arg.PatientNumber == iccaPatients[0].PatientNumber)), + Times.Once); + } + + [Test] + public async Task ICCA_Process_Patient_BD_No_Insert_ICCA_Patient_UNKNOWN_Actual_Patient() + { + const string patientNumber = "12345"; + var person = new Person + { + FirstName = "Miguel", LastName = "Villanueva", BirthDate = new DateTime(), Gender = PatientEnum.Gender.Male + }; + + var iccaPatients = new List(); + + var apiRequest = new ApiRequest + { + Type = "ICCA", + IccaPatients = iccaPatients + }; + + var patientInBd = new Patient + { + Id = ObjectId.GenerateNewId(), + UnitId = _unitId, + PointOfCareId = _pocCinna01Id, + PatientNumber = patientNumber, + Person = person + }; + + _patientRepositoryMock.Setup(s => s.FindByPointOfCareId(_pocCinna01Id)).ReturnsAsync(patientInBd); + _patientRepositoryMock.Setup(p => p.FindById(patientInBd.Id)).ReturnsAsync(patientInBd); + + await _patientService.SaveRequest(apiRequest); + + _patientRepositoryMock.Verify( + p => p.UpdateOneAsync(It.Is(arg => arg == patientInBd.Id), + It.Is(arg => arg.UnitId == _unitId && arg.PointOfCareId == _pocUnknowndId)), Times.Once); + _patientRepositoryMock.Verify( + p => p.InsertOneAsync(It.Is(arg => + arg.UnitId == _unitId && + arg.PointOfCareId == _pocCinna01Id && + arg.PatientNumber == iccaPatients[0].PatientNumber)), + Times.Never); + } + + //TODO GeTBoxTest +} \ No newline at end of file diff --git a/adas-core.Test/Services/PointOfCareServiceTest.cs b/adas-core.Test/Services/PointOfCareServiceTest.cs new file mode 100644 index 00000000..b77c5f76 --- /dev/null +++ b/adas-core.Test/Services/PointOfCareServiceTest.cs @@ -0,0 +1,291 @@ +using System.Security.Claims; +using adas_core.Application.Exceptions; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.MongoModels; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; +using Moq; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Services; + +[TestFixture] +public class PointOfCareServiceTests +{ + [SetUp] + public void SetUp() + { + _pointOfCareRepositoryMock = new Mock(); + _unitServiceMock = new Mock(); + _admissionServiceMock = new Mock(); + _cacheServiceMock = new Mock(); + _cacheServiceMock + .Setup(c => c.GetOrSetObjectAsync( + It.IsAny(), + It.IsAny>>(), + It.IsAny(), + It.IsAny())) + .Returns((string _, Func> factory, TimeSpan? _, CancellationToken _) => factory()); + + var loggerMock = new Mock>(); + var userClaims = new ClaimsPrincipal(new ClaimsIdentity([ + new Claim(ClaimTypes.Name, "TestUser") + ], "mock")); + + var httpContextMock = new DefaultHttpContext + { + User = userClaims + }; + + _httpContextAccessorMock.Setup(accessor => accessor.HttpContext) + .Returns(httpContextMock); + _pointOfCareService = new PointOfCareService( + loggerMock.Object, + _pointOfCareRepositoryMock.Object, + new Lazy(Mock.Of), + new Lazy(() => _unitServiceMock.Object), + Mock.Of(), + Mock.Of>(), + new Lazy(() => _admissionServiceMock.Object), + _httpContextAccessorMock.Object, + _auditServiceMock.Object, + _cacheServiceMock.Object, + Options.Create(new CacheSettings()) + ); + } + + private PointOfCareService _pointOfCareService = null!; + private Mock _pointOfCareRepositoryMock = null!; + private Mock _unitServiceMock = null!; + private Mock _cacheServiceMock = null!; + private Mock _admissionServiceMock = null!; + private readonly Mock _httpContextAccessorMock = new(); + private readonly Mock _auditServiceMock = new(); + + [Test] + public async Task Delete_ValidObjectId_DeletesPointOfCare() + { + // Arrange + var id = ObjectId.GenerateNewId(); + _pointOfCareRepositoryMock.Setup(m => m.FindByIdAllConfig(id)) + .ReturnsAsync(new PointOfCare { Id = id, AdmissionId = null }); + _pointOfCareRepositoryMock.Setup(m => m.Delete(id)).Verifiable(); + + // Act + await _pointOfCareService.Delete(id); + + // Assert + _pointOfCareRepositoryMock.Verify(); + } + + [Test] + public async Task Update_ValidPointOfCare_UpdatesPointOfCare() + { + // Arrange + var pointOfCare = new PointOfCare { Id = ObjectId.GenerateNewId(), UnitName = "TestUnit" }; + _pointOfCareRepositoryMock.Setup(m => m.Update(pointOfCare)).Verifiable(); + + // Act + await _pointOfCareService.Update(pointOfCare); + + // Assert + _pointOfCareRepositoryMock.Verify(); + } + + [Test] + public async Task GetAll_NoArguments_ReturnsListOfPointOfCare() + { + // Arrange + var pointOfCareList = new List { new(), new() }; + _pointOfCareRepositoryMock.Setup(m => m.GetAll()).ReturnsAsync(pointOfCareList); + + // Act + var result = await _pointOfCareService.GetAll(); + + // Assert + Assert.That(result, Is.EqualTo(pointOfCareList)); + } + + [Test] + public void UpdateConfiguration_ValidIdAndConfiguration_InvokesRepositoryUpdateConfiguration() + { + // Arrange + var id = ObjectId.GenerateNewId(); + var configuration = new PointOfCareConfiguration(); + + // Act & Assert + Assert.That(_pointOfCareService, Is.Not.Null); + Assert.That(_pointOfCareRepositoryMock, Is.Not.Null); + + Func act = async () => await _pointOfCareService.UpdateConfiguration(id, configuration); + Assert.ThrowsAsync(act); + } + + [Test] + public async Task FindById_ValidId_ReturnsPointOfCare() + { + // Arrange + var id = ObjectId.GenerateNewId(); + var expectedPointOfCare = new PointOfCare { Id = id }; + _pointOfCareRepositoryMock.Setup(m => m.FindByIdAllConfig(id)).ReturnsAsync(expectedPointOfCare); + + // Act + var result = await _pointOfCareService.FindById(id); + + // Assert + Assert.That(result, Is.EqualTo(expectedPointOfCare)); + } + + // [Test] + // public async Task FindByUnit_ValidUnit_ReturnsListOfPointOfCare() + // { + // // Arrange + // var id = ObjectId.GenerateNewId(); + // var pointOfCare = new PointOfCare { Id = id, UnitId = ObjectId.GenerateNewId() }; + // var unit = new Unit { Id = pointOfCare.UnitId, Name = "TestUnit" }; + // var expectedPointOfCareList = new List + // { + // new PointOfCare { Unit = unit }, + // new PointOfCare { Unit = unit }, + // }; + // _pointOfCareRepositoryMock.Setup(m => m.FindByUnit(unit)).ReturnsAsync(expectedPointOfCareList); + // + // // Act + // var result = await _pointOfCareService.FindByUnit(unit); + // + // // Assert + // Assert.That(result, Is.EqualTo(expectedPointOfCareList)); + // } + + [Test] + public async Task FindByUnitAndStatus_ValidUnitIdAndStatus_ReturnsListOfPointOfCare() + { + // Arrange + var unitId = ObjectId.GenerateNewId(); + var pocStatus = StatusEnum.PointOfCare.Available; + var expectedPointOfCareList = new List + { + new(), + new() + }; + _pointOfCareRepositoryMock.Setup(m => m.FindByUnitAndStatus(unitId, pocStatus, false)) + .ReturnsAsync(expectedPointOfCareList); + + // Act + var result = await _pointOfCareService.FindByUnitAndStatus(unitId, pocStatus); + + // Assert + Assert.That(result, Is.EqualTo(expectedPointOfCareList)); + } + + [Test] + public void CheckNextAdmission_ValidPatientLocation_UpdatesPoCStatus_AndCallsUpdate() + { + // Arrange + var patientLocationId = ObjectId.GenerateNewId(); + + var pocToCheck = new PointOfCare + { + Id = patientLocationId, + AdmissionId = null, // Forzamos la rama que busca la siguiente admisión + Status = StatusEnum.PointOfCare.Locked // Locked → no debe actualizar. Cambia para probar actualización: + // Usa Available o Reserved para que se ejecute el update. + }; + + // Para que ejecute la lógica de actualización, cambiamos el estado inicial: + pocToCheck.Status = StatusEnum.PointOfCare.Available; + + var nextAdmission = new Admission { Id = ObjectId.GenerateNewId() }; + + // 1) Cache: debe ejecutar el factory y devolver el PoC + _cacheServiceMock + .Setup(c => c.GetOrSetObjectAsync( + It.IsAny(), + It.IsAny>>(), + It.IsAny(), + It.IsAny())) + .Returns((string _, Func> factory, TimeSpan? _, CancellationToken _) => factory()); + + // 2) Repo: FindById (usado por el factory del caché) + _pointOfCareRepositoryMock + .Setup(m => m.FindById(patientLocationId)) + .ReturnsAsync(pocToCheck); + + // 3) AdmissionService (ruta AdmissionId == null → buscar siguiente) + _admissionServiceMock + .Setup(m => m.GetAdmissionByPointOfCareId(pocToCheck.Id)) + .ReturnsAsync([nextAdmission]); + + // 4) Update(poc) → queremos esperar a que se invoque + var updatedSignal = new ManualResetEventSlim(false); + _pointOfCareRepositoryMock + .Setup(m => m.Update(It.IsAny())) + .Callback(_ => updatedSignal.Set()) + .Returns(Task.CompletedTask); + + // Update llama internamente a FindById otra vez (en Update) + _pointOfCareRepositoryMock + .Setup(m => m.FindById(pocToCheck.Id)) + .ReturnsAsync(pocToCheck); + + // 5) Ejecutar (async void) y esperar a que se dispare el update + _pointOfCareService.CheckNextAdmission(patientLocationId); + + // Espera razonable a que termine la operación asíncrona interna + var completed = updatedSignal.Wait(TimeSpan.FromSeconds(1)); + Assert.That(completed, Is.True, "CheckNextAdmission no disparó Update a tiempo."); + + // Assert + // a) Se llamó a FindById (vía caché) + _pointOfCareRepositoryMock.Verify(m => m.FindById(patientLocationId), Times.AtLeastOnce); + + // b) Se llamó a Update con el PoC actualizado: Status Reserved + Admission asignada + _pointOfCareRepositoryMock.Verify(m => m.Update( + It.Is(p => + p.Id == pocToCheck.Id && + p.Status == StatusEnum.PointOfCare.Reserved && + p.AdmissionId == nextAdmission.Id && + p.Admission == nextAdmission) + ), Times.Once); + + // c) NO verifiques FindByIdAllConfig: en CheckNextAdmission no se usa esa ruta + } + + [Test] + public async Task FindByRoom_ValidRoom_CallsRepositoryFindByRoom() + { + // Arrange + const string room = "TestRoom"; + var expectedPointOfCares = new List(); + _pointOfCareRepositoryMock.Setup(m => m.FindByRoom(room)).ReturnsAsync(expectedPointOfCares); + + // Act + var result = await _pointOfCareService.FindByRoom(room); + + // Assert + Assert.That(result, Is.EqualTo(expectedPointOfCares)); + _pointOfCareRepositoryMock.Verify(m => m.FindByRoom(room), Times.Once); + } + + [Test] + public async Task FindByBed_ValidBed_CallsRepositoryFindByBed() + { + // Arrange + const string bed = "TestBed"; + var expectedPointOfCares = new List(); + _pointOfCareRepositoryMock.Setup(m => m.FindByBed(bed)).ReturnsAsync(expectedPointOfCares); + + // Act + var result = await _pointOfCareService.FindByBed(bed); + + // Assert + Assert.That(result, Is.EqualTo(expectedPointOfCares)); + _pointOfCareRepositoryMock.Verify(m => m.FindByBed(bed), Times.Once); + } + +} \ No newline at end of file diff --git a/adas-core.Test/Services/PublisherServiceTest.cs b/adas-core.Test/Services/PublisherServiceTest.cs new file mode 100644 index 00000000..2ad8e4f0 --- /dev/null +++ b/adas-core.Test/Services/PublisherServiceTest.cs @@ -0,0 +1,62 @@ +using adas_core.Domain.Models.AppSettings; +using adas_core.Infrastructure.Services; +using EasyNetQ; +using EasyNetQ.SystemMessages; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; + +namespace adas_core.Test.Services; + +[TestFixture] +public class PublisherServiceTest +{ + //private static readonly DateTime now = DateTime.Now; + //private static readonly ObjectId patientId = ObjectId.GenerateNewId(); + + [SetUp] + public void Setup() + { + //advancedBusMock = new Mock(); + + _optionsRabbitMqSettings = Options.Create(_rabbitMqSettings); + + _logger = new Mock>(); + + _publisherService = new PublisherService( + _optionsRabbitMqSettings, + _logger.Object); + } + + private PublisherService _publisherService = null!; + //private Mock advancedBusMock; + + private readonly RabbitMqSettings _rabbitMqSettings = new() + { + ObservationsQueue = "Observations" + }; + + private IOptions _optionsRabbitMqSettings = null!; + + private Mock> _logger = null!; + + [Test] + public void SendMessage_Return_true() + { + var newMessage = new Message(); + + var result = _publisherService.SendMessage(newMessage, _rabbitMqSettings.ObservationsQueue); + + Assert.That(result, Is.Not.Null); + } + + [Test] + public void SendMessage_Error_Return_true() + { + var newMessage = new Message(); + + var result = _publisherService.SendMessageError(newMessage, _rabbitMqSettings.ObservationsQueue); + + Assert.That(result, Is.Not.Null); + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/PumpServiceTest.cs b/adas-core.Test/Services/PumpServiceTest.cs new file mode 100644 index 00000000..5c44f3ef --- /dev/null +++ b/adas-core.Test/Services/PumpServiceTest.cs @@ -0,0 +1,461 @@ +using System.Security.Claims; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services; +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 Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; +using Moq; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Services; + +[TestFixture] +public class PumpServiceTest +{ + private PumpService _service = null!; + private Mock _obsRepo = null!; + private Mock _stateRepo = null!; + private Mock _alarmEventRepo = null!; + private Mock _alarmStateRepo = null!; + private Mock _archiveRepo = null!; + private Mock _patientSvc = null!; + private Mock _configPumps = null!; + private Mock _subs = null!; + private Mock _clientMsg = null!; + private Mock _configUnits = null!; + private Mock _calcObs = null!; + private Lazy _lazyCalc = null!; + private Mock _http = null!; + private Mock _audit = null!; + private Mock> _logger = null!; + + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + private static readonly DateTime Now = DateTime.UtcNow; + + [SetUp] + public void Setup() + { + _obsRepo = new Mock(); + _stateRepo = new Mock(); + _alarmEventRepo = new Mock(); + _alarmStateRepo = new Mock(); + _archiveRepo = new Mock(); + _patientSvc = new Mock(); + _configPumps = new Mock(); + _subs = new Mock(); + _clientMsg = new Mock(); + _calcObs = new Mock(); + _lazyCalc = new Lazy(() => _calcObs.Object); + _http = new Mock(); + _audit = new Mock(); + _logger = new Mock>(); + _configUnits = new Mock(); + + var principal = new ClaimsPrincipal( + new ClaimsIdentity([new Claim(ClaimTypes.Name, "test")], "mock")); + + _http.Setup(x => x.HttpContext) + .Returns(new DefaultHttpContext { User = principal }); + + _calcObs.Setup(x => x.Map(It.IsAny())) + .ReturnsAsync((PumpObservation o) => o); + + var api = Options.Create(new ApiSettings + { + PumpExpiresSeconds = 5, + SendPumpsZero = false + }); + + _service = new PumpService( + _obsRepo.Object, + _stateRepo.Object, + _alarmEventRepo.Object, + _alarmStateRepo.Object, + _archiveRepo.Object, + _patientSvc.Object, + _configPumps.Object, + api, + _logger.Object, + _subs.Object, + _clientMsg.Object, + _lazyCalc, + _http.Object, + _audit.Object, + _configUnits.Object + ); + + _configPumps.Setup(x => x.Map(It.IsAny())) + .ReturnsAsync((PumpObservation o) => o); + _configUnits.Setup(x => x.Map(It.IsAny())) + .ReturnsAsync((PumpObservation o) => o); + } + + // -------------------------------------------------------------- + // SAVE REQUEST — casos básicos + // -------------------------------------------------------------- + [Test] + public async Task SaveRequest_Returns_When_No_Observations() + { + var req = new ApiRequest { Type = "ORU_R01" }; + await _service.SaveRequest(req); + _obsRepo.Verify(r => r.InsertAsync(It.IsAny()), Times.Never); + } + + [Test] + public async Task SaveRequest_UnknownType_DoesNotInsert() + { + var req = new ApiRequest + { + Type = "UNKNOWN", + PumpObservation = new PumpObservation { Time = Now } + }; + + await _service.SaveRequest(req); + _obsRepo.Verify(r => r.InsertAsync(It.IsAny()), Times.Never); + } + + [Test] + public async Task SaveRequest_Converts_SingleObservation_ToList() + { + var obs = new PumpObservation { Time = Now }; + var req = new ApiRequest + { + Type = "ORU_R01", + PumpObservation = obs, + PatientNumber = "123" + }; + + _patientSvc.Setup(p => p.FindPatientByApiRequest(req)) + .ReturnsAsync(new Patient { Id = PatientId }); + + await _service.SaveRequest(req); + Assert.That(req.PumpObservations, Has.Count.EqualTo(1)); + } + + [Test] + public async Task SaveRequest_SetsExpires_BeforeInsert() + { + var obs = new PumpObservation + { + Time = Now, + DeviceId = "D1" + }; + + var req = new ApiRequest + { + Type = "ORU_R01", + PumpObservation = obs, + PatientNumber = "1" + }; + + _patientSvc.Setup(x => x.FindPatientByApiRequest(req)) + .ReturnsAsync(new Patient { Id = PatientId }); + + await _service.SaveRequest(req); + + _obsRepo.Verify(r => r.InsertAsync( + It.Is(o => o.Expires == 5)), Times.Once); + } + // -------------------------------------------------------------- + // PROCESS ALARM + // -------------------------------------------------------------- + [Test] + public async Task SaveRequest_ORU_R40_CreatesAlarmEvent() + { + var obs = new PumpObservation + { + Time = Now, + DeviceId = "D1", + AlarmType = PumpEnum.AlarmType.Occlusion, + EventPhase = PumpEnum.EventPhase.Start + }; + + var req = new ApiRequest { Type = "ORU_R40", PumpObservation = obs }; + _patientSvc.Setup(x => x.FindPatientByApiRequest(req)) + .ReturnsAsync(new Patient { Id = PatientId }); + + await _service.SaveRequest(req); + + _alarmEventRepo.Verify(r => r.InsertAsync(It.IsAny()), Times.Once); + _obsRepo.Verify(r => r.InsertAsync(It.IsAny()), Times.Never); + } + + [Test] + public async Task ProcessAlarm_End_RemovesAlarmState() + { + var obs = new PumpObservation + { + Time = Now, + DeviceId = "DX", + AlarmType = PumpEnum.AlarmType.Occlusion, + AlarmTypeMdc = "H1", + EventPhase = PumpEnum.EventPhase.End + }; + + var req = new ApiRequest { Type = "ORU_R40", PumpObservation = obs }; + _patientSvc.Setup(x => x.FindPatientByApiRequest(req)) + .ReturnsAsync(new Patient { Id = PatientId }); + + await _service.SaveRequest(req); + + _alarmStateRepo.Verify(r => r.RemoveAsync("DX", PumpEnum.AlarmType.Occlusion, "H1"), Times.Once); + } + + // -------------------------------------------------------------- + // SNAPSHOT DE BOMBA + // -------------------------------------------------------------- + [Test] + public async Task SaveRequest_CreatesPumpState_IfNotExists() + { + var obs = new PumpObservation { Time = Now, DeviceId = "P1" }; + var req = new ApiRequest { Type = "ORU_R01", PumpObservation = obs, PatientNumber = "1" }; + + _patientSvc.Setup(x => x.FindPatientByApiRequest(req)) + .ReturnsAsync(new Patient { Id = PatientId }); + + _stateRepo.Setup(r => r.FindByDeviceIdAsync("P1")) + .ReturnsAsync((PumpState?)null); + + await _service.SaveRequest(req); + + _stateRepo.Verify(r => r.UpsertAsync(It.Is(s => s.DeviceId == "P1")), Times.Once); + } + + // -------------------------------------------------------------- + // BROADCAST + // -------------------------------------------------------------- + [Test] + public async Task SaveRequest_NoSubscribers_NoBroadcast() + { + var obs = new PumpObservation { Time = Now, DeviceId = "BR1", PatientId = PatientId }; + var patient = new Patient + { + Id = PatientId, + Location = new PatientLocation("U1", "B1") + }; + + var req = new ApiRequest + { + Type = "ORU_R01", + PumpObservation = obs, + PatientNumber = "1" + }; + + _subs.Setup(x => x.GetSubscribers()).Returns([]); + _patientSvc.Setup(x => x.FindPatientByApiRequest(req)) + .ReturnsAsync(patient); + + await _service.SaveRequest(req); + + _clientMsg.Verify(r => r.SendAsync(It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + + [Test] + public async Task SaveRequest_WithSubscribers_UsesReqLocation_AndSendsBroadcast() + { + // Arrange + var obs = new PumpObservation + { + Time = Now, + DeviceId = "D22", + PatientId = null // <- clave para que se use req.Location + }; + + var req = new ApiRequest + { + Type = "ORU_R01", + PumpObservation = obs, + Location = new PatientLocation("UCI5C", "Box4", "Room1"), + PatientNumber = "437537" + }; + + var subscriber = new WsSubscriber("sub1") + { + Locations = [ new PatientLocation("UCI5C", "Box4", "Room1") ] + }; + + // 1) NO devolver paciente en FindPatientByApiRequest -> fuerza uso de req. Location + _patientSvc.Setup(p => p.FindPatientByApiRequest(req)) + .ReturnsAsync((Patient?)null); + + // 2) Suscriptores con la misma ubicación que req.Location + _subs.Setup(x => x.GetSubscribers()) + .Returns([subscriber]); + + // 3) Evitar null en alarmas activas al enumerar (foreach) + _alarmStateRepo.Setup(r => r.FindAllActiveByDeviceAsync("D22")) + .ReturnsAsync(Enumerable.Empty()); + + // 4) Snapshot: por claridad, fuerza que se cree uno nuevo + _stateRepo.Setup(r => r.FindByDeviceIdAsync("D22")) + .ReturnsAsync((PumpState?)null); + + // Map mínimo para que avance el flujo + _configPumps.Setup(m => m.Map(It.IsAny())) + .ReturnsAsync((PumpObservation o) => o); + _configUnits.Setup(m => m.Map(It.IsAny())) + .ReturnsAsync((PumpObservation o) => o); + + // Act + await _service.SaveRequest(req); + + // Assert + _clientMsg.Verify(x => + x.SendAsync("sub1", OperationType.Pump, It.IsAny()), + Times.Once); + + // No se envían PumpAlarm si no hay activas + _clientMsg.Verify(x => + x.SendAsync("sub1", OperationType.PumpAlarm, It.IsAny()), + Times.Never); + } + + // -------------------------------------------------------------- + // RETENCIÓN — DeleteOlderDays + // -------------------------------------------------------------- + [Test] + public async Task SaveRequest_Retention_DeleteOlderDays_CallsRepo() + { + var obs = new PumpObservation { Time = Now, DeviceId = "R1", PatientId = PatientId }; + var req = new ApiRequest { Type = "ORU_R01", PumpObservation = obs, PatientNumber = "1" }; + + _patientSvc.Setup(x => x.FindPatientByApiRequest(req)) + .ReturnsAsync(new Patient { Id = PatientId }); + + _configPumps.Setup(x => x.RetentionActions(It.IsAny())) + .ReturnsAsync(new ObservatitonRetentionResult + { + RetentionPolicy = RetentionPolicy.DeleteOlderDays, + RetentionPolicyValue = 7 + }); + + await _service.SaveRequest(req); + + _obsRepo.Verify(r => r.DeleteOlderThanDaysAsync(7, null), Times.Once); + } + // -------------------------------------------------------------- + // PAGINACIÓN + // -------------------------------------------------------------- + [Test] + public async Task GetPaginatedPump_ByPatient_Works() + { + var obs = new List + { + new() { Time = Now.AddMinutes(-1), PatientId = PatientId }, + new() { Time = Now.AddMinutes(-5), PatientId = PatientId } + }; + + _obsRepo.Setup(r => r.FindByPatientId(PatientId)) + .ReturnsAsync(obs); + + var fileredRequest = new FilteredRequest + { + PatientId = PatientId.ToString() + }; + + var filter = new PaginationFilter(1, 1, fileredRequest); + + var result = await _service.GetPaginatedPump(filter); + + using (Assert.EnterMultipleScope()) + { + Assert.That(result!.Data, Has.Count.EqualTo(1)); + Assert.That(result.Data[0].Time, Is.EqualTo(Now.AddMinutes(-1)).Within(TimeSpan.FromMilliseconds(2))); + } + } + + // -------------------------------------------------------------- + // ARCHIVO + // -------------------------------------------------------------- + [Test] + public async Task ArchiveByPatientId_InsertsArchive_ThenDeletes() + { + var list = new List + { + new() { Id = ObjectId.GenerateNewId(), PatientId = PatientId } + }; + + _obsRepo.Setup(r => r.FindByPatientId(PatientId)).ReturnsAsync(list); + + await _service.ArchiveByPatientId(PatientId); + + _archiveRepo.Verify(r => r.InsertManyAsync(list), Times.Once); + _obsRepo.Verify(r => r.DeleteByPatientId(PatientId), Times.Once); + _alarmEventRepo.Verify(r => r.DeleteByPatientId(PatientId), Times.Once); + _alarmStateRepo.Verify(r => r.DeleteByPatientId(PatientId), Times.Once); + } + + // -------------------------------------------------------------- + // UPDATE MANY + // -------------------------------------------------------------- + [Test] + public async Task UpdateManyObjectId_UpdatesObs_Alarms_States() + { + var oldId = ObjectId.GenerateNewId(); + var newId = ObjectId.GenerateNewId(); + + _obsRepo.Setup(r => r.UpdateManyObjectIdByFieldAsync("patientId", newId, oldId)) + .ReturnsAsync(3); + _alarmEventRepo.Setup(r => r.UpdateManyObjectIdByFiledNameAsync("patientId", newId, oldId)) + .ReturnsAsync(1); + _alarmStateRepo.Setup(r => r.UpdateManyObjectIdByFieldNameAsync("patientId", newId, oldId)) + .ReturnsAsync(2); + + await _service.UpdateManyObjectId("patientId", newId, oldId); + + } + + // -------------------------------------------------------------- + // FIND LAST OBSERVATIONS + // -------------------------------------------------------------- + [Test] + public async Task FindLastPumpObservations_ReturnsOrdered() + { + var items = new List + { + new() { Time = Now.AddMinutes(-10) }, + new() { Time = Now.AddMinutes(-1) } + }; + + _obsRepo.Setup(r => r.FindByPatientId(PatientId)) + .ReturnsAsync(items); + + var result = await _service.FindLastPumpObservations(PatientId, 1); + + Assert.That(result, Has.Count.EqualTo(1)); + Assert.That(result[0].Time, Is.EqualTo(items[1].Time)); + } + + // -------------------------------------------------------------- + // INSERT MANUAL + // -------------------------------------------------------------- + [Test] + public async Task InsertPumpObservation_Inserts_AndBroadcasts() + { + var obs = new PumpObservation + { + DeviceId = "D11", + PatientId = PatientId, + Time = Now + }; + + _stateRepo.Setup(r => r.FindByDeviceIdAsync("D11")) + .ReturnsAsync((PumpState?)null); + + _subs.Setup(x => x.GetSubscribers()).Returns([]); + + await _service.InsertPumpObservation(obs); + + _obsRepo.Verify(r => r.InsertAsync(It.IsAny()), Times.Once); + _stateRepo.Verify(r => r.UpsertAsync(It.IsAny()), Times.Once); + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/RecordingAlertServiceTest.cs b/adas-core.Test/Services/RecordingAlertServiceTest.cs new file mode 100644 index 00000000..7f0fd702 --- /dev/null +++ b/adas-core.Test/Services/RecordingAlertServiceTest.cs @@ -0,0 +1,173 @@ +using System.Security.Claims; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.MongoModels; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; +using Moq; + +namespace adas_core.Test.Services; + +[TestFixture] +public class RecordingAlertServiceTest +{ + [SetUp] + public void Setup() + { + _patientServiceMock = new Mock(); + _patientServiceLazy = new Lazy(() => _patientServiceMock.Object); + _configObservationServiceMock = new Mock(); + _recordingAlertRepositoryMock = new Mock(); + _recordingAlertArchiveRepositoryMock = new Mock(); + _clientMessageServiceMock = new Mock(); + _subscribersServiceMock = new Mock(); + var userClaims = new ClaimsPrincipal(new ClaimsIdentity([ + new Claim(ClaimTypes.Name, "TestUser") + ], "mock")); + + var httpContextMock = new DefaultHttpContext + { + User = userClaims + }; + + _httpContextAccessorMock.Setup(accessor => accessor.HttpContext) + .Returns(httpContextMock); + + //optionsApiSettings = Microsoft.Extensions.Options.Options.Create(apiSettings); + + _logger = new Mock>(); + + _recordingAlertService = new RecordingAlertService( + _patientServiceLazy, + _configObservationServiceMock.Object, + _recordingAlertRepositoryMock.Object, + _recordingAlertArchiveRepositoryMock.Object, + //optionsApiSettings, + _logger.Object, + _clientMessageServiceMock.Object, + _subscribersServiceMock.Object, + _httpContextAccessorMock.Object, + _auditServiceMock.Object + ); + } + + private RecordingAlertService _recordingAlertService = null!; + private Mock _patientServiceMock = null!; + private Lazy _patientServiceLazy = null!; + private Mock _configObservationServiceMock = null!; + private Mock _recordingAlertRepositoryMock = null!; + private Mock _recordingAlertArchiveRepositoryMock = null!; + private Mock _clientMessageServiceMock = null!; + private Mock _subscribersServiceMock = null!; + private readonly Mock _httpContextAccessorMock = new(); + + private readonly Mock _auditServiceMock = new(); + //private readonly ApiSettings apiSettings = new () + //{ + + //}; + //IOptions optionsApiSettings; + + + private Mock> _logger = null!; + + private static readonly DateTime Now = DateTime.Now; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + + + [Test] + public async Task SaveRequest_Not_RecordingAlert_Not_ORU_R01_Return_not_insert() + { + var apiRequest = new ApiRequest(); + + await _recordingAlertService.SaveRequest(apiRequest); + + _recordingAlertRepositoryMock.Verify(d => d.InsertOneAsync(It.IsAny()), Times.Never); + } + + [Test] + public async Task SaveRequest_patientNumber_and_pointOfCare_null_Return_not_insert() + { + var apiRequest = new ApiRequest + { + Type = "ORU_R11", + MessageTime = Now + }; + + await _recordingAlertService.SaveRequest(apiRequest); + + _recordingAlertRepositoryMock.Verify(d => d.InsertOneAsync(It.IsAny()), Times.Never); + } + + [Test] + public async Task SaveRequest_Not_Find_Patient_Return_not_insert() + { + var person = new Person + { + FirstName = "Miguel", + LastName = "Villanueva", + Ids = new Dictionary { { "MR", "437537" } } + }; + + var apiRequest = new ApiRequest + { + Location = new PatientLocation("UCI5C", "Box4"), + Patient = person, + PatientNumber = "437537", + Type = "ORU_R01", + PatientId = PatientId.ToString(), + MessageTime = Now + }; + + await _recordingAlertService.SaveRequest(apiRequest); + + _recordingAlertRepositoryMock.Verify(d => d.InsertOneAsync(It.IsAny()), Times.Never); + } + + [Test] + public async Task SaveRequest_Alert_Return_insert() + { + var person = new Person + { + FirstName = "Miguel", + LastName = "Villanueva", + Ids = new Dictionary { { "MR", "437537" } } + }; + + var patient = new Patient + { + Id = ObjectId.GenerateNewId(), + UnitString = "UCI5C", + Bed = "Box4", + PatientNumber = "437537", + Person = person + }; + + var recordingAlert = new PatientRecordingAlert + { + IsRecording = true + }; + + var apiRequest = new ApiRequest + { + Location = new PatientLocation("UCI5C", "Box4"), + Patient = person, + PatientNumber = "437537", + Type = "ORU_R01", + PatientId = PatientId.ToString(), + MessageTime = Now, + RecordingAlert = recordingAlert + }; + + _patientServiceMock.Setup(p => p.FindPatientByApiRequest(apiRequest)).ReturnsAsync(patient); + _configObservationServiceMock.Setup(c => c.RetentionActions(It.IsAny())) + .ReturnsAsync((ObservatitonRetentionResult?)null); + + await _recordingAlertService.SaveRequest(apiRequest); + + _recordingAlertRepositoryMock.Verify(d => d.InsertOneAsync(It.IsAny()), Times.Once); + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/RecordingServiceTest.cs b/adas-core.Test/Services/RecordingServiceTest.cs new file mode 100644 index 00000000..9bb3f246 --- /dev/null +++ b/adas-core.Test/Services/RecordingServiceTest.cs @@ -0,0 +1,187 @@ +using System.Net; +using System.Text; +using adas_core.Application.Services; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Recording; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Moq.Protected; +using Newtonsoft.Json; +using Error = EasyNetQ.SystemMessages.Error; +using Options = Microsoft.Extensions.Options.Options; + + +namespace adas_core.Test.Services; + +[TestFixture] +public class RecordingServiceTest +{ + [SetUp] + public void Setup() + { + _optionsApiSettings = Options.Create(_apiSettings); + _optionsRabbitMqSettings = Options.Create(_rabbitMqSettings); + _optionsRecordingSettings = Options.Create(_recordingSettings); + _publisherServiceMock = new Mock(); + + _logger = new Mock>(); + _authServiceMock = new Mock(); + + _httpClientFactoryMock = new Mock(); + _httpMessageHandlerMock = new Mock(); + _clientMessageServiceMock = new Mock(); + _subscribersServiceMock = new Mock(); + _patientServiceMock = new Mock>(); + + var client = new HttpClient(_httpMessageHandlerMock.Object); + + _httpClientFactoryMock.Setup(c => c.CreateClient(It.IsAny())).Returns(client); + + _recordingService = new RecordingService( + _optionsRabbitMqSettings, + _optionsRecordingSettings, + _logger.Object, + _httpClientFactoryMock.Object, + _publisherServiceMock.Object, + _authServiceMock.Object, + _optionsApiSettings, + _clientMessageServiceMock.Object, + _subscribersServiceMock.Object, + _patientServiceMock.Object + ); + + + // Set up the mock object to return a specific value when a method is called + _publisherServiceMock.Setup(x => x.SendMessage(It.IsAny(), It.IsAny())).ReturnsAsync(true); + _publisherServiceMock.Setup(x => x.SendMessageError(It.IsAny(), It.IsAny())).ReturnsAsync(true); + } + + private RecordingService _recordingService = null!; + + //Mock mockSingleton = null!; + private Mock _httpClientFactoryMock = null!; + private Mock _httpMessageHandlerMock = null!; + private Mock _publisherServiceMock = null!; + private Mock _authServiceMock = null!; + private Mock> _patientServiceMock = null!; + + private readonly RabbitMqSettings _rabbitMqSettings = new() + { + RecordingQueue = "recordings" + }; + + private IOptions _optionsRabbitMqSettings = null!; + + private readonly RecordingSettings _recordingSettings = new() + { + RecordingApiUrl = "http://localhost:8082" + }; + + private IOptions _optionsRecordingSettings = null!; + + private readonly ApiSettings _apiSettings = new() + { + StartRecordingWithoutPatientNumber = false + }; + + private IOptions _optionsApiSettings = null!; + + private Mock> _logger = null!; + private Mock _clientMessageServiceMock = null!; + private Mock _subscribersServiceMock = null!; + + [Test] + public async Task GetRecordings_Return_Empty() + { + var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.Unauthorized, + Content = new StringContent("Content text.") + }; + + var recordingData = new List(); + + _authServiceMock.Setup(s => s.GetToken()).ReturnsAsync("abc"); + + _httpMessageHandlerMock.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(httpResponseMessage); + + var result = await _recordingService.GetRecordings(4); + + Assert.That(result, Is.Not.Null); + //Assert.AreEqual(result, recordingData); + Assert.That(result, Is.EqualTo(recordingData)); + } + + [Test] + public async Task GetRecordings_Return_RecordingData() + { + var patient = new Patient + { + Id = "id", + FirstName = "firstName", + LastName = "lastName" + }; + + var recordingData = new List + { + new() + { + Patient = patient, + RoomId = 4, + Status = "INITIALIZED" + } + }; + + var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent(JsonConvert.SerializeObject(recordingData), Encoding.UTF8, "application/json") + }; + + _httpMessageHandlerMock.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(httpResponseMessage); + + _authServiceMock.Setup(s => s.GetToken()).ReturnsAsync("abc"); + + var result = await _recordingService.GetRecordings(4); + + Assert.That(result, Is.Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(recordingData[0].Status, Is.EqualTo(result![0].Status)); + Assert.That(recordingData[0].Patient?.Id, Is.EqualTo(result[0].Patient?.Id)); + Assert.That(recordingData[0].Patient?.LastName, Is.EqualTo(result[0].Patient?.LastName)); + Assert.That(recordingData[0].Patient?.FirstName, Is.EqualTo(result[0].Patient?.FirstName)); + Assert.That(recordingData[0].RoomId, Is.EqualTo(result[0].RoomId)); + } + } + + [Test] + public Task SaveRequest() + { + ApiRequest apiRequest = new(); + + var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Content text.") + }; + + _httpMessageHandlerMock.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(httpResponseMessage); + + Func act = async () => await _recordingService.SaveRequest(apiRequest); + Assert.ThrowsAsync(act); + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/RedisLockProviderTest.cs b/adas-core.Test/Services/RedisLockProviderTest.cs new file mode 100644 index 00000000..4096552a --- /dev/null +++ b/adas-core.Test/Services/RedisLockProviderTest.cs @@ -0,0 +1,175 @@ +using System.Diagnostics; +using adas_core.Application.Services.Caching; +using Moq; +using StackExchange.Redis; + +namespace adas_core.Test.Services; + +[TestFixture] +public class RedisLockProviderTest +{ + private Mock _mockDb = null!; + private RedisLockProvider _provider = null!; + + [SetUp] + public void SetUp() + { + _mockDb = new Mock(); + _provider = new RedisLockProvider(() => _mockDb.Object); + } + + #region TC-45 + [Test] + public async Task AcquireAsync_ReturnsTrue_AndStoresToken_WhenRedisAcceptsNxSet() + { + _mockDb + .Setup(db => db.StringSetAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(true); + + _mockDb + .Setup(db => db.ScriptEvaluateAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Task.FromResult(null!)); + + var result = await _provider.AcquireAsync("key", TimeSpan.FromSeconds(5)); + + Assert.That(result, Is.True); + + _mockDb.Verify(db => db.StringSetAsync( + It.Is(k => k == (RedisKey)"lock:key"), + It.IsAny(), + (TimeSpan?)TimeSpan.FromSeconds(5), + When.NotExists), Times.Once()); + + await _provider.ReleaseAsync("key"); + + _mockDb.Verify(db => db.ScriptEvaluateAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Once()); + } + #endregion + + #region TC-46 + [Test] + public async Task AcquireAsync_RetriesWithDelays_UntilNxSucceeds() + { + _mockDb + .SetupSequence(db => db.StringSetAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(false) + .ReturnsAsync(false) + .ReturnsAsync(false) + .ReturnsAsync(true); + + var sw = Stopwatch.StartNew(); + var result = await _provider.AcquireAsync("key", TimeSpan.FromSeconds(1)); + sw.Stop(); + + Assert.That(result, Is.True); + + _mockDb.Verify(db => db.StringSetAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Exactly(4)); + + Assert.That(sw.ElapsedMilliseconds, Is.GreaterThan(100)); + } + #endregion + + #region TC-47 + [Test] + public async Task AcquireAsync_ReturnsFalse_AfterTimeoutWithMultipleRetries() + { + _mockDb + .Setup(db => db.StringSetAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .ReturnsAsync(false); + + var sw = Stopwatch.StartNew(); + var result = await _provider.AcquireAsync("key", TimeSpan.FromMilliseconds(200)); + sw.Stop(); + + Assert.That(result, Is.False); + Assert.That(sw.ElapsedMilliseconds, Is.GreaterThan(100)); + + _mockDb.Verify(db => db.StringSetAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.AtLeast(2)); + } + #endregion + + #region TC-48 + [Test] + public async Task ReleaseAsync_CallsScriptEvaluateWithCorrectKeyAndToken() + { + RedisValue capturedToken = default; + + _mockDb + .Setup(db => db.StringSetAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Callback( + (_, v, _, _) => capturedToken = v) + .ReturnsAsync(true); + + _mockDb + .Setup(db => db.ScriptEvaluateAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Task.FromResult(null!)); + + await _provider.AcquireAsync("key", TimeSpan.FromSeconds(5)); + await _provider.ReleaseAsync("key"); + + _mockDb.Verify(db => db.ScriptEvaluateAsync( + It.IsAny(), + It.Is(keys => keys != null && keys[0] == (RedisKey)"lock:key"), + It.Is(vals => vals != null && vals[0] == capturedToken), + It.IsAny()), Times.Once()); + } + #endregion + + #region TC-49 + [Test] + public async Task ReleaseAsync_IsIdempotent_WhenCalledWithoutPriorAcquire() + { + _mockDb + .Setup(db => db.ScriptEvaluateAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Task.FromResult(null!)); + + await _provider.ReleaseAsync("key"); + + _mockDb.Verify(db => db.ScriptEvaluateAsync( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny()), Times.Never()); + } + #endregion +} diff --git a/adas-core.Test/Services/RedisServiceTest.cs b/adas-core.Test/Services/RedisServiceTest.cs new file mode 100644 index 00000000..7314a977 --- /dev/null +++ b/adas-core.Test/Services/RedisServiceTest.cs @@ -0,0 +1,251 @@ +using System.Reflection; +using adas_core.Application.Services.Caching; +using adas_core.Domain.Models.AppSettings; +using Microsoft.Extensions.Logging; +using Moq; +using Newtonsoft.Json; +using StackExchange.Redis; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Services; + +[TestFixture] +public class RedisServiceTest +{ + private record TestModel(string Name); + + private Mock _mockDb = null!; + private LockManagerService _lockMgr = null!; + + [SetUp] + public void SetUp() + { + _mockDb = new Mock(); + _lockMgr = new LockManagerService( + new Mock>().Object, + new InMemoryLockProvider()); + } + + private RedisService CreateSut(CacheSettings? settings = null, bool redisAvailable = true) + { + var sut = new RedisService( + Options.Create(settings ?? new CacheSettings()), + new Mock>().Object, + _lockMgr); + + SetField(sut, "_database", _mockDb.Object); + SetField(sut, "_isRedisAvailable", redisAvailable); + + return sut; + } + + private static void SetField(object target, string name, object? value) + => typeof(RedisService) + .GetField(name, BindingFlags.NonPublic | BindingFlags.Instance)! + .SetValue(target, value); + + private void SetupStringSetAsync() + => _mockDb + .Setup(db => db.StringSetAsync( + It.IsAny(), It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny(), It.IsAny())) + .ReturnsAsync(true); + + private void SetupKeyExpire() + => _mockDb + .Setup(db => db.KeyExpire( + It.IsAny(), It.IsAny(), + It.IsAny(), It.IsAny())) + .Returns(true); + + #region TC-38 + [Test] + public async Task GetOrSetObjectAsync_ExecutesFactory_WhenRedisUnavailable() + { + var sut = CreateSut(redisAvailable: false); + var factoryInvoked = false; + + var result = await sut.GetOrSetObjectAsync( + "patients:latestObs:abc", + () => { factoryInvoked = true; return Task.FromResult("result"); }); + + Assert.That(factoryInvoked, Is.True); + Assert.That(result, Is.EqualTo("result")); + _mockDb.Verify(db => db.StringGetAsync(It.IsAny(), It.IsAny()), Times.Never); + _mockDb.Verify(db => db.StringSetAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + #endregion + + #region TC-39 + [Test] + public async Task GetOrSetObjectAsync_ReturnsCachedObject_WhenRedisHit() + { + var cached = new TestModel("cached"); + + _mockDb.Setup(db => db.StringGetAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((RedisValue)JsonConvert.SerializeObject(cached)); + SetupKeyExpire(); + + var sut = CreateSut(); + var factoryInvoked = false; + + var result = await sut.GetOrSetObjectAsync( + "patients:latestObs:abc", + () => { factoryInvoked = true; return Task.FromResult(new TestModel("fresh")); }); + + Assert.That(factoryInvoked, Is.False); + Assert.That(result?.Name, Is.EqualTo("cached")); + _mockDb.Verify(db => db.StringSetAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Never); + } + #endregion + + #region TC-40 + [Test] + public async Task GetObjectAsync_CallsKeyExpire_WhenUpdateExpirationIsTrue() + { + var settings = new CacheSettings + { + Redis = new RedisSettings { Ttl = new TtlSettings { PatientsSeconds = 300 } } + }; + + _mockDb.Setup(db => db.StringGetAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((RedisValue)JsonConvert.SerializeObject(new TestModel("hit"))); + SetupKeyExpire(); + + var sut = CreateSut(settings); + + await sut.GetObjectAsync("patients:latestObs:abc", updateExpiration: true); + + _mockDb.Verify( + db => db.KeyExpire( + It.IsAny(), + It.Is(t => t == TimeSpan.FromSeconds(300)), + It.IsAny(), + It.IsAny()), + Times.Once); + } + + [Test] + public async Task GetObjectAsync_DoesNotCallKeyExpire_WhenUpdateExpirationIsFalse() + { + _mockDb.Setup(db => db.StringGetAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((RedisValue)JsonConvert.SerializeObject(new TestModel("hit"))); + + var sut = CreateSut(); + + await sut.GetObjectAsync("patients:latestObs:abc", updateExpiration: false); + + _mockDb.Verify( + db => db.KeyExpire(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + #endregion + + #region TC-41 + [Test] + public async Task GetOrSetObjectAsync_InvokesFactoryAndPersists_WhenCacheMiss() + { + var settings = new CacheSettings + { + Redis = new RedisSettings { Ttl = new TtlSettings { PatientsSeconds = 300, PatientObservationsSeconds = 300} } + }; + + _mockDb.Setup(db => db.StringGetAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(RedisValue.Null); + SetupStringSetAsync(); + + var sut = CreateSut(settings); + var expected = new TestModel("fresh"); + var factoryInvoked = false; + + var result = await sut.GetOrSetObjectAsync( + "patients:latestObs:abc", + () => { factoryInvoked = true; return Task.FromResult(expected); }); + + var expectedJson = JsonConvert.SerializeObject(expected); + + Assert.That(factoryInvoked, Is.True); + Assert.That(result?.Name, Is.EqualTo("fresh")); + _mockDb.Verify( + db => db.StringSetAsync( + It.IsAny(), + It.IsAny(), + It.Is(t => t == TimeSpan.FromSeconds(300)), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Once); + It.Is(v => v.Equals(expectedJson)); + } + #endregion + + #region TC-42 + [Test] + public async Task GetOrSetObjectAsync_DoesNotPersist_WhenFactoryReturnsNull() + { + _mockDb.Setup(db => db.StringGetAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync(RedisValue.Null); + + var sut = CreateSut(); + + var result = await sut.GetOrSetObjectAsync( + "patients:latestObs:abc", + () => Task.FromResult(null)); + + Assert.That(result, Is.Null); + _mockDb.Verify( + db => db.StringSetAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + #endregion + + #region TC-43 + [Test] + public async Task GetObjectAsync_ReturnsObject_AndSkipsKeyExpire_WhenUpdateExpirationFalse() + { + var expected = new TestModel("data"); + + _mockDb.Setup(db => db.StringGetAsync(It.IsAny(), It.IsAny())) + .ReturnsAsync((RedisValue)JsonConvert.SerializeObject(expected)); + + var sut = CreateSut(); + + var result = await sut.GetObjectAsync("patients:latestObs:abc", updateExpiration: false); + + Assert.That(result?.Name, Is.EqualTo("data")); + _mockDb.Verify( + db => db.KeyExpire(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), + Times.Never); + } + #endregion + + #region TC-44 + [Test] + public async Task SetObjectAsync_SerializesToJson_AndPersistsWithEntityTtl() + { + var settings = new CacheSettings + { + Redis = new RedisSettings { Ttl = new TtlSettings { PatientsSeconds = 600 } } + }; + + SetupStringSetAsync(); + + var sut = CreateSut(settings); + var obj = new TestModel("save-me"); + var expectedJson = JsonConvert.SerializeObject(obj); + + await sut.SetObjectAsync("patients:latestObs:abc", obj, null, true); + _mockDb.Verify( + db => db.StringSetAsync( + It.IsAny(), + It.IsAny(), + It.Is(t => t == TimeSpan.FromSeconds(600)), + It.IsAny(), + It.IsAny(), + It.IsAny()), + Times.Once); + It.Is(v => v.Equals(expectedJson)); + + } + #endregion +} diff --git a/adas-core.Test/Services/RelayServiceTest.cs b/adas-core.Test/Services/RelayServiceTest.cs new file mode 100644 index 00000000..ac6b1f4c --- /dev/null +++ b/adas-core.Test/Services/RelayServiceTest.cs @@ -0,0 +1,241 @@ +using System.Net; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using adas_core.Infrastructure.Services; +using adas_core.module.Relays.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using Moq.Protected; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Services; + +[TestFixture] +public class RelayServiceTest +{ + [SetUp] + public void Setup() + { + _logger = new Mock>(); + _httpClientFactoryMock = new Mock(); + _httpMessageHandlerMock = new Mock(); + _relaySettings.RecordingOrApiUrl = "http://localhost:8080"; + _relaySettings.Cache = true; + _optionsRelaySettings = Options.Create(_relaySettings); + _pocServiceMock = new Mock(); + _relayRepositoryMock = new Mock(); + + _relayService = new RelayService( + //optionsApiSettings, + _logger.Object, + _httpClientFactoryMock.Object, + _optionsRelaySettings, + _pocServiceMock.Object, + _relayRepositoryMock.Object); + } + + private RelayService _relayService = null!; + private Mock _httpClientFactoryMock = null!; + private Mock _httpMessageHandlerMock = null!; + private readonly RelaySettings _relaySettings = new(); + private IOptions _optionsRelaySettings = null!; + private Mock _pocServiceMock = null!; + private Mock _relayRepositoryMock = null!; + + //readonly ApiSettings apiSettings = new () + //{ + //}; + //IOptions optionsApiSettings; + + private Mock> _logger = null!; + + [Test] + public async Task PowerOffAsync() + { + var relay = new Relay + { + Driver = "KMTronicV2", + Ip = "10.133.10.169", + Port = 80, + RelayNumber = 1, + RelayName = "Relay1", + Total = 8, + Username = "adas", + Password = "!HULPM22" + }; + + + var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Test content") + }; + + _httpMessageHandlerMock.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(httpResponseMessage); + + + var client = new HttpClient(_httpMessageHandlerMock.Object); + + _httpClientFactoryMock.Setup(c => c.CreateClient(It.IsAny())).Returns(client); + + await _relayService.PowerOff(relay); + } + + [Test] + public async Task PowerOnAsync() + { + var relay = new Relay + { + Driver = "KMTronicV2", + Ip = "10.133.10.169", + Port = 80, + RelayNumber = 1, + RelayName = "Relay1", + Total = 8, + Username = "adas", + Password = "!HULPM22" + }; + + + var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("Test content") + }; + + _httpMessageHandlerMock.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(httpResponseMessage); + + + var client = new HttpClient(_httpMessageHandlerMock.Object); + + _httpClientFactoryMock.Setup(c => c.CreateClient(It.IsAny())).Returns(client); + + await _relayService.PowerOn(relay); + } + + [Test] + public async Task CheckRelayStatus_Return_On() + { + var relay = new Relay + { + Driver = "KMTronicV2", + Ip = "10.133.10.169", + Port = 80, + RelayNumber = 1, + RelayName = "Relay1", + Total = 8, + Username = "adas", + Password = "!HULPM22" + }; + + var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("\"On") + }; + + _httpMessageHandlerMock.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(httpResponseMessage); + + //httpMessageHandlerMock.Protected() + // .Setup>("ReadAsStringAsync", ItExpr.IsAny(), ItExpr.IsAny()) + // .ReturnsAsync("\\On"); + + + var client = new HttpClient(_httpMessageHandlerMock.Object); + + _httpClientFactoryMock.Setup(c => c.CreateClient(It.IsAny())).Returns(client); + + await _relayService.PowerOn(relay); + var result = await _relayService.CheckRelayStatus(relay); + + //Assert.That(result, Is.Not.Null); + Assert.That(result, Is.EqualTo(RelayEnum.Status.On)); + } + + [Test] + public async Task CheckRelayStatus_Return_Off() + { + var relay = new Relay + { + Driver = "KMTronicV2", + Ip = "10.133.10.169", + Port = 80, + RelayNumber = 1, + RelayName = "Relay1", + Total = 8, + Username = "adas", + Password = "!HULPM22" + }; + + var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("\"Off") + }; + + _httpMessageHandlerMock.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(httpResponseMessage); + + + var client = new HttpClient(_httpMessageHandlerMock.Object); + + _httpClientFactoryMock.Setup(c => c.CreateClient(It.IsAny())).Returns(client); + await _relayService.PowerOff(relay); + var result = await _relayService.CheckRelayStatus(relay); + + //Assert.That(result, Is.Not.Null); //result nunca es null aquí + Assert.That(result, Is.EqualTo(RelayEnum.Status.Off)); + } + + [Test] + public async Task CheckRelayStatus_Return_Unknown() + { + var relay = new Relay + { + Driver = "KMTronicV2", + Ip = "10.133.10.169", + Port = 80, + RelayNumber = 1, + RelayName = "Relay1", + Total = 8, + Username = "adas", + Password = "!HULPM22" + }; + + + var httpResponseMessage = new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + Content = new StringContent("\\Off") + }; + + _httpMessageHandlerMock.Protected() + .Setup>("SendAsync", ItExpr.IsAny(), + ItExpr.IsAny()) + .ReturnsAsync(httpResponseMessage); + + + var client = new HttpClient(_httpMessageHandlerMock.Object); + + _httpClientFactoryMock.Setup(c => c.CreateClient(It.IsAny())).Returns(client); + + var result = await _relayService.CheckRelayStatus(relay); + + //Assert.That(result, Is.Not.Null); + Assert.That(result, Is.EqualTo(RelayEnum.Status.Unknown)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/SchedulerServiceTest.cs b/adas-core.Test/Services/SchedulerServiceTest.cs new file mode 100644 index 00000000..584caf07 --- /dev/null +++ b/adas-core.Test/Services/SchedulerServiceTest.cs @@ -0,0 +1,590 @@ +using System.Collections.Specialized; +using adas_core.Application.Services; +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.MongoModels; +using adas_core.Domain.Models.Providers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Moq; +using Quartz; +using Quartz.Impl; +using Options = Microsoft.Extensions.Options.Options; + +namespace adas_core.Test.Services; + +[TestFixture] +public class SchedulerServiceTest +{ + [OneTimeSetUp] + public async Task SetUp() + { + #region [Services Mock seUp] + + _treatmentServiceMock = new Mock(); + _treatmentServiceLazy = new Lazy(() => _treatmentServiceMock.Object); + + _patientServiceMock = new Mock(); + _patientServiceLazy = new Lazy(() => _patientServiceMock.Object); + + _medicineServiceMock = new Mock(); + _medicineServiceLazy = new Lazy(() => _medicineServiceMock.Object); + + _observationServiceMock = new Mock(); + _observationServiceLazy = new Lazy(() => _observationServiceMock.Object); + + _configObservationServiceMock = new Mock(); + var configObservationServiceLazy = + new Lazy(() => _configObservationServiceMock.Object); + + _logger = new Mock>(); + + _httpClientFactoryMock = new Mock(); + _httpMessageHandlerMock = new Mock(); + + _calculatedObservationsServiceMock = new Mock(); + _calculatedObservationsServiceLazy = + new Lazy(() => _calculatedObservationsServiceMock.Object); + + _appointmentServiceServiceMock = new Mock(); + _appointmentServiceServiceLazy = + new Lazy(() => _appointmentServiceServiceMock.Object); + _diagnosisServiceMock = new Mock(); + _diagnosisServiceLazy = + new Lazy(() => _diagnosisServiceMock.Object); + + var client = new HttpClient(_httpMessageHandlerMock.Object); + + _httpClientFactoryMock.Setup(c => c.CreateClient(It.IsAny())).Returns(client); + + _optionsApiSettings = Options.Create(_apiSettings); + _optionsProvidersSettings = Options.Create(_providersSettings); + + #endregion + + var unused = new SchedulerService( + _optionsApiSettings, + _optionsProvidersSettings, + _patientServiceLazy, + _treatmentServiceLazy, + _appointmentServiceServiceLazy, + _diagnosisServiceLazy, + _medicineServiceLazy, + _observationServiceLazy, + configObservationServiceLazy, + _logger.Object, + _httpClientFactoryMock.Object, + _calculatedObservationsServiceLazy, + _patientCarePlanLazy + ); + + #region [Service item fill] + + _sinceDate = Now.AddHours(-24); + _sinceDischargeTimeToArchive = 48; + _archivePatientsWithoutObservationsSinceHours = 28; + + _jobDataMap = new JobDataMap + { + { "sinceDate", _sinceDate }, + { "sinceDischargeTime", _sinceDischargeTimeToArchive }, + { "archivePatientsWithoutObservationsSinceHours", _archivePatientsWithoutObservationsSinceHours }, + { "checkNewsJobIntervalMinutes", 15 } + }; + + // Grab the Scheduler instance from the Factory + var properties = new NameValueCollection + { + { "quartz.scheduler.instanceName", "MyUniqueSchedulerName" } + }; + _factory = new StdSchedulerFactory(properties); + if (_scheduler == null) + { + var getFactory = await _factory.GetScheduler(); + _scheduler = getFactory; + } + + // Set up observation list + var itemList = new List + { + new() + { + Name = "Resp_Rate_Calculated", + CodingSystem = "ADAS", + MinAlert = 14, + MaxAlert = 25, + Expires = 10, + ShowOnExpired = false + }, + new() + { + Name = "SpO2", + Code = "150456", + OriginalName = "MDC_PULS_OXIM_SAT_O2", + CodingSystem = "MDC", + ParentCode = "69965", + ParentName = "MDC_DEV_MON_PHYSIO_MULTI_PARAM_MDS", + MinAlert = 94, + MaxAlert = 100, + ParentCodingSystem = "MDC", + Expires = 10, + ShowOnExpired = false + }, + new() + { + Name = "Temperature", + Code = "386053000", + OriginalName = "Temperatura", + CodingSystem = "SNM", + ParentCode = "386053000", + ParentName = "Temperatura(ºC)", + MinAlert = 34.5, + MinWarn = 35.5, + MaxWarn = 37.4, + MaxAlert = 38, + ParentCodingSystem = "SNM", + Expires = 60 + }, + new() + { + CodingSystem = "MDC", + Code = "150033", + OriginalName = "MDC_PRESS_BLD_ART_SYS", + Name = "TAs", + Expires = 10 + }, + new() + { + Name = "FC", + Code = "149514", + OriginalName = "MDC_PULS_RATE", + CodingSystem = "MDC", + MinAlert = 60, + MaxAlert = 100, + Expires = 10, + ShowOnExpired = false + } + }; + var observations = new List + { + new() + { + Name = "Resp_Rate_Calculated", Value = 18, Time = DateTime.UtcNow.AddMinutes(-60), PatientId = PatientId + }, + new() { Name = "SpO2", Value = 93, Time = DateTime.UtcNow.AddMinutes(-60), PatientId = PatientId }, + new() { Name = "Temperature", Value = 36.5, Time = DateTime.UtcNow, PatientId = PatientId }, + new() { Name = "TAs", Value = 103, Time = DateTime.UtcNow, PatientId = PatientId }, + new() { Name = "FC", Value = 80, Time = DateTime.UtcNow.AddMinutes(-60), PatientId = PatientId }, + new() + { + Name = "Resp_Rate_Calculated", Value = 18, Time = DateTime.UtcNow.AddMinutes(-60), + PatientId = PatientId2 + }, + new() { Name = "SpO2", Value = 93, Time = DateTime.UtcNow.AddMinutes(-60), PatientId = PatientId2 }, + new() + { + Name = "Temperature", Value = 36.5, Time = DateTime.UtcNow.AddMinutes(-60), PatientId = PatientId2 + }, + new() { Name = "TAs", Value = 103, Time = DateTime.UtcNow, PatientId = PatientId2 }, + new() { Name = "FC", Value = 40, Time = DateTime.UtcNow, PatientId = PatientId2 } + }; + + // Set Up patientList + var patientList = new List + { + new() + { + Id = PatientId, + UnitId = ObjectId.GenerateNewId(), + PointOfCareId = ObjectId.GenerateNewId(), + PatientNumber = "patientNumber1", + Person = new Person + { + FirstName = "firstName1", + LastName = "lastName1", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }, + new() + { + Id = PatientId2, + UnitId = ObjectId.GenerateNewId(), + PointOfCareId = ObjectId.GenerateNewId(), + PatientNumber = "patientNumber2", + Person = new Person + { + FirstName = "firstName2", + LastName = "lastName2", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + } + }; + + #endregion + + #region [Service behavior] + + // Set up configObservationServiceMock to return desire item + _configObservationServiceMock + .Setup(service => service.Get(It.IsAny(), It.IsAny())) + .ReturnsAsync((BasePatientObservation obs, bool _) => + { + return itemList.FirstOrDefault(obsConfig => obsConfig.Name == obs.Name); + }); + // Set up observationService behavior + _observationServiceMock + .Setup(service => service.FindLastObservations(It.IsAny(), 1, It.IsAny>())) + .ReturnsAsync((ObjectId patientId, int _, List? filterObservations) => + { + var filteredObservations = observations + .Where(obs => + filterObservations != null && obs.Name != null && filterObservations.Contains(obs.Name) && + obs.PatientId == patientId) + .ToList(); + + return filteredObservations; + }); + // Set up patientServiceMock to return patientList for FindInActivePoC + _patientServiceMock.Setup(service => service.FindInActivePoC()).ReturnsAsync(patientList); + + #endregion + + // Asignación de dependencias estáticas para el job + CalculateNewsJob.PatientService = _patientServiceMock.Object; + CalculateNewsJob.ObservationService = _observationServiceMock.Object; + CheckExpiredAlertsJob.ObservationService = _observationServiceMock.Object; + CheckExpiredObservationsJob.ObservationService = _observationServiceMock.Object; + + CalculateNewsJob.ConfigObservationService = _configObservationServiceMock.Object; + // and start it off + _scheduler?.Start(); + } + + + //[TearDown] + [OneTimeTearDown] + public void TearDown() + { + // Detener el motor de Quartz.NET después de las pruebas + //scheduler.Shutdown().Wait(); + _scheduler?.Shutdown(); + } + + private Mock? _treatmentServiceMock; + private Mock? _patientServiceMock; + private Mock? _medicineServiceMock; + private Mock? _observationServiceMock; + private Mock? _configObservationServiceMock; + private Mock? _httpClientFactoryMock; + private Mock? _httpMessageHandlerMock; + private Mock? _calculatedObservationsServiceMock; + private Mock? _appointmentServiceServiceMock; + private Mock? _diagnosisServiceMock; + + private Lazy? _treatmentServiceLazy; + private Lazy? _patientServiceLazy; + private Lazy? _medicineServiceLazy; + private Lazy? _observationServiceLazy; + + private Lazy _calculatedObservationsServiceLazy; + private Lazy _appointmentServiceServiceLazy; + private Lazy _diagnosisServiceLazy; + private readonly Lazy _patientCarePlanLazy = new(); + + private readonly ApiSettings _apiSettings = new(); + private IOptions? _optionsApiSettings; + + private readonly List _providersSettings = + [ + new() + { + Name = "Adas", + Url = "http://localhost/info.html" + } + ]; + + private IOptions>? _optionsProvidersSettings; + + private Mock>? _logger; + private JobDataMap _jobDataMap = new(); + private DateTime _sinceDate; + private int _sinceDischargeTimeToArchive; + private int _archivePatientsWithoutObservationsSinceHours; + + private StdSchedulerFactory? _factory; + private IScheduler? _scheduler; + + private static readonly DateTime Now = DateTime.Now; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + private static readonly ObjectId PatientId2 = ObjectId.GenerateNewId(); + + [Test] + public void CheckInactivePatientsJob_ShouldExecuteOnTrigger() + { + var checkInactivePatientSchedulerIntervalHours = 12; + + // Arrange + var checkInactivePatientsJob = JobBuilder.Create() + .UsingJobData(_jobDataMap) + .Build(); + + // Crear un disparador personalizado que incremente el contador + var checkInactivePatientsTrigger = TriggerBuilder.Create() + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInHours(checkInactivePatientSchedulerIntervalHours) + .RepeatForever()) + .Build(); + + + _patientServiceMock?.Setup(p => p.DischargeInactivePatients(_sinceDate, _sinceDischargeTimeToArchive)); + + // Asociar el trabajo y el desencadenador en el motor de Quartz.NET + _scheduler?.ScheduleJob(checkInactivePatientsJob, checkInactivePatientsTrigger).Wait(); + + // Act + // Esperar un tiempo suficiente para que el trabajo se ejecute varias veces + Thread.Sleep(TimeSpan.FromSeconds(1)); + + + _patientServiceMock?.Verify(p => p.DischargeInactivePatients(It.IsAny(), It.IsAny()), + Times.AtLeastOnce()); + } + + + [Test] + public void CheckActiveTreatmentsJob_ShouldExecuteOnTrigger() + { + var patient = new Patient + { + PatientId = PatientId.ToString(), + UnitString = "NEONATAL", + Bed = "CINA02", + PatientNumber = "123456", + Person = new Person + { + FirstName = "Jose", + LastName = "Luis", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }; + + const int checkActiveTreatmentsSchedulerIntervalMinutes = 10; + + var treatmentsJob = JobBuilder.Create() + .Build(); + + // Trigger the job to run now, and then repeat every 10 seconds + var treatmentsTrigger = TriggerBuilder.Create() + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInMinutes(checkActiveTreatmentsSchedulerIntervalMinutes) + .RepeatForever()) + .Build(); + + _patientServiceMock?.Setup(p => p.FindAll(It.IsAny())).ReturnsAsync([patient]); + + // Create a mock of the singleton class subscribers + var mockSingleton = new Mock(); + + // Set up the mock object to return a specific value when a method is called + var patientTreatmentList = new List + { + new() + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + OrderControl = OrderControlType.Nw, + PlacerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" }, + OrderStatus = "A", + OrderTime = Now, + Notes = [], + Routes = [] + } + }; + + mockSingleton.Setup(x => x.GetActiveTreatmentsByPatient(PatientId)).ReturnsAsync(patientTreatmentList); + + + _medicineServiceMock?.Setup(m => m.GetMedicinesOfTreatments(It.IsAny>())) + .ReturnsAsync(new List()); + + // Asociar el trabajo y el desencadenador en el motor de Quartz.NET + _scheduler?.ScheduleJob(treatmentsJob, treatmentsTrigger).Wait(); + + // Act + // Esperar un tiempo suficiente para que el trabajo se ejecute varias veces + Thread.Sleep(TimeSpan.FromSeconds(10)); + + _medicineServiceMock?.Verify(x => x.GetMedicinesOfTreatments(It.IsAny>()), + Times.Once()); + } + + [Test] + public void CheckExpiredObservationsJob_ShouldExecuteOnTrigger() + { + var checkExpiredObservationsIntervalMinutes = 3; + + // Arrange + var checkExpiredObservationsJob = JobBuilder.Create() + .UsingJobData(_jobDataMap) + .Build(); + + // Crear un disparador personalizado que incremente el contador + var checkExpiredObservationsTrigger = TriggerBuilder.Create() + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInHours(checkExpiredObservationsIntervalMinutes) + .RepeatForever()) + .Build(); + + // Asociar el trabajo y el desencadenador en el motor de Quartz.NET + _scheduler?.ScheduleJob(checkExpiredObservationsJob, checkExpiredObservationsTrigger).Wait(); + + // Act + // Esperar un tiempo suficiente para que el trabajo se ejecute varias veces + Thread.Sleep(TimeSpan.FromSeconds(1)); + + + _observationServiceMock?.Verify(p => p.ExpireObservationsAndRecalculateAsync(), Times.AtLeastOnce()); + } + + + [Test] + public void CheckExpiredAlertsJob_ShouldExecuteOnTrigger() + { + var checkExpiredAlertsIntervalMinutes = 3; + + // Arrange + var checkExpiredAlertsJob = JobBuilder.Create() + .UsingJobData(_jobDataMap) + .Build(); + + // Crear un disparador personalizado que incremente el contador + var checkExpiredAlertsTrigger = TriggerBuilder.Create() + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInHours(checkExpiredAlertsIntervalMinutes) + .RepeatForever()) + .Build(); + + // Asociar el trabajo y el desencadenador en el motor de Quartz.NET + _scheduler?.ScheduleJob(checkExpiredAlertsJob, checkExpiredAlertsTrigger).Wait(); + + // Act + // Esperar un tiempo suficiente para que el trabajo se ejecute varias veces + Thread.Sleep(TimeSpan.FromSeconds(1)); + + + _observationServiceMock?.Verify(p => p.ExpireAlertsAndPowerOffAsync(), Times.AtLeastOnce()); + } + + [Test] + public void GetProvidersObservationsJob_ShouldExecuteOnTrigger_Result_GetType_null() + { + var getProviderObservationsIntervalMinutes = 5; + + var patient = new Patient + { + PatientId = PatientId.ToString(), + UnitString = "NEONATAL", + Bed = "CINA02", + PatientNumber = "123456", + Person = new Person + { + FirstName = "Jose", + LastName = "Luis", + BirthDate = new DateTime(), + Gender = PatientEnum.Gender.Male + } + }; + + // Arrange + var getProvidersObservationsJob = JobBuilder.Create() + .UsingJobData(_jobDataMap) + .Build(); + + // Crear un disparador personalizado que incremente el contador + var getProvidersObservationsTrigger = TriggerBuilder.Create() + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInHours(getProviderObservationsIntervalMinutes) + .RepeatForever()) + .Build(); + + _patientServiceMock?.Setup(p => p.FindAll(It.IsAny())).ReturnsAsync([patient]); + + // Asociar el trabajo y el desencadenador en el motor de Quartz.NET + _scheduler?.ScheduleJob(getProvidersObservationsJob, getProvidersObservationsTrigger).Wait(); + + // Act + // Esperar un tiempo suficiente para que el trabajo se ejecute varias veces + Thread.Sleep(TimeSpan.FromSeconds(1)); + + //observationServiceMock.Verify(p => p.InsertObservation(It.IsAny(),true,true), Times.AtLeastOnce()); + _observationServiceMock?.Verify(p => p.InsertObservation( + It.Is(obs => obs.Name != "NEWS"), true, true), Times.Never()); + } + + [Test] + public void CheckCalculateNewsJob_ShouldExecuteOnTrigger() + { + var checkExpiredAlertsIntervalMinutes = 3; + + // Arrange + var calculateNewsJob = JobBuilder.Create() + .UsingJobData(_jobDataMap) + .Build(); + + // Crear un disparador personalizado que incremente el contador + var checkExpiredAlertsTrigger = TriggerBuilder.Create() + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInHours(checkExpiredAlertsIntervalMinutes) + .RepeatForever()) + .Build(); + + // Asociar el trabajo y el desencadenador en el motor de Quartz.NET + _scheduler?.ScheduleJob(calculateNewsJob, checkExpiredAlertsTrigger).Wait(); + + // Act + // Esperar un tiempo suficiente para que el trabajo se ejecute varias veces + Thread.Sleep(TimeSpan.FromSeconds(6)); + + + _observationServiceMock?.Verify(p => p.InsertObservation(It.IsAny(), true, true), + Times.AtLeastOnce()); + } + + [Test] + public async Task CalculateNewsJob_Insert_ObsFull_MultiplePatient_With_Correct_Value() + { + var job = new CalculateNewsJob(); + + // Act + await job.Execute(Mock.Of()); + + // Assert + _observationServiceMock?.Verify( + service => service.InsertObservation( + It.Is(obs => + obs.Name == "NEWS" && (int)obs.Value == 1 && obs.PatientId == PatientId), + true, true), + Times.Once); + + _observationServiceMock?.Verify( + service => service.InsertObservation( + It.Is(obs => + obs.Name == "NEWS" && (int)obs.Value == 4 && obs.PatientId == PatientId2), + true, true), + Times.Once); + } + //Se deshabilita el mensaje de warning porque lo detecta como no usado y sugiere suprimirlo siendo necesario +} \ No newline at end of file diff --git a/adas-core.Test/Services/SendAlertServiceTest.cs b/adas-core.Test/Services/SendAlertServiceTest.cs new file mode 100644 index 00000000..380b5536 --- /dev/null +++ b/adas-core.Test/Services/SendAlertServiceTest.cs @@ -0,0 +1,115 @@ +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.SystemAlerts; +using adas_core.Infrastructure.Services; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; + +namespace adas_core.Test.Services; + +[TestFixture] +public class SendAlertServiceTest +{ + [SetUp] + public void Setup() + { + _optionsRabbitMqSettings = Options.Create(_rabbitMqSettings); + + _logger = new Mock>(); + + _sendAlertService = new SendAlertService( + _optionsRabbitMqSettings, + _logger.Object + ); + + _windowsPlatforms = + [ + PlatformID.Win32NT, + PlatformID.Win32S, + PlatformID.Win32Windows, + PlatformID.WinCE + ]; + } + + private SendAlertService _sendAlertService = null!; + + private readonly RabbitMqSettings _rabbitMqSettings = new(); + private IOptions _optionsRabbitMqSettings = null!; + + private Mock> _logger = null!; + + private PlatformID[] _windowsPlatforms = null!; + + [Ignore("Integration test to pull request")] + [Test] + public void GetConsumedCpu_Return_PerformanceCpu_data() + { + if (_windowsPlatforms.Contains(Environment.OSVersion.Platform)) + { + var result = _sendAlertService.GetConsumedCpu(); + //result = await sendAlertService.GetConsumedCpu(); + + Assert.That(result, Is.Not.Null); + Assert.That(result.ValueTotal, Is.GreaterThan(0)); + } + else + { + Assert.Ignore("This test can only be run on Windows."); + } + } + + + [Ignore("Integration test to pull request")] + [Test] + public void GetConsumedRAM_Return_PerformanceRAM_data() + { + if (_windowsPlatforms.Contains(Environment.OSVersion.Platform)) + { + var result = _sendAlertService.GetConsumedRam(); + //result = await sendAlertService.GetConsumedRAM(); + + Assert.That(result, Is.Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(result.PercentageConsumed, Is.GreaterThan(0)); + Assert.That(result.ValueConsumed, Is.GreaterThan(0)); + Assert.That(result.ValueTotal, Is.GreaterThan(0)); + } + } + else + { + Assert.Ignore("This test can only be run on Windows."); + } + } + + [Ignore("Integration test to pull request")] + [Test] + public void GetConsumedStorage_Return_PerformanceStorage_data() + { + if (_windowsPlatforms.Contains(Environment.OSVersion.Platform)) + { + List performanceList = []; + + foreach (var drive in DriveInfo.GetDrives()) + if (drive.IsReady) + { + var performance = _sendAlertService.GetConsumedStorage(drive); + + performanceList.Add(performance); + } + + Assert.That(performanceList, Is.Not.Null); + using (Assert.EnterMultipleScope()) + { + Assert.That(performanceList, Is.Not.Empty); + Assert.That(performanceList[0].PercentageConsumed, Is.GreaterThan(0)); + Assert.That(performanceList[0].ValueConsumed, Is.GreaterThan(0)); + Assert.That(performanceList[0].ValueTotal, Is.GreaterThan(0)); + } + } + else + { + Assert.Ignore("This test can only be run on Windows."); + } + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/SeviceConfigServiceTest.cs b/adas-core.Test/Services/SeviceConfigServiceTest.cs new file mode 100644 index 00000000..bf334a78 --- /dev/null +++ b/adas-core.Test/Services/SeviceConfigServiceTest.cs @@ -0,0 +1,63 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Domain.Models.MongoModels; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; +using Moq; +using ServiceConfigService = adas_core.Application.Services.ServiceConfigService; + +namespace adas_core.Test.Services; + +[TestFixture] +public class ServiceConfigServiceTest +{ + [SetUp] + public void Setup() + { + _serviceConfigRepositoryMock = new Mock(); + + _logger = new Mock>(); + + _serviceConfigService = new ServiceConfigService( + _serviceConfigRepositoryMock.Object, + _logger.Object + ); + } + + private ServiceConfigService _serviceConfigService; + + private Mock _serviceConfigRepositoryMock; + private Mock> _logger; + + private static ObjectId _id = ObjectId.GenerateNewId(); + + [Test] + public async Task Get_Find_Id_Return_ServiceConfig() + { + var serviceConfig = new ServiceConfig + { + StrId = _id.ToString() + }; + + _serviceConfigRepositoryMock.Setup(s => s.FindById(_id.ToString())).ReturnsAsync(serviceConfig); + + var result = await _serviceConfigService.Get(serviceConfig.StrId); + + Assert.That(result, Is.Not.Null); + } + + [Test] + public async Task Get_Not_Find_Id_Return_ServiceConfig() + { + var serviceConfig = new ServiceConfig + { + StrId = _id.ToString() + }; + + _serviceConfigRepositoryMock.Setup(s => s.FindById(_id)).ReturnsAsync((ServiceConfig?)null); + _serviceConfigRepositoryMock.Setup(s => s.FindById(It.IsAny())).ReturnsAsync(serviceConfig); + + var result = await _serviceConfigService.Get(_id.ToString()); + + Assert.That(result, Is.Not.Null); + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/TreatmentServiceTest.cs b/adas-core.Test/Services/TreatmentServiceTest.cs new file mode 100644 index 00000000..a96309e3 --- /dev/null +++ b/adas-core.Test/Services/TreatmentServiceTest.cs @@ -0,0 +1,277 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.MongoModels; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; +using Moq; +using static NUnit.Framework.Assert; + +namespace adas_core.Test.Services; + +[TestFixture] +public class TreatmentServiceTest +{ + [SetUp] + public void Setup() + { + _treatmentRepositoryMock = new Mock(); + _treatmentArchiveRepositoryMock = new Mock(); + //pocMappingServiceMock = new Mock(); + _patientServiceMock = new Mock(); + //calculatedObservationsServiceMock = new Mock(); + _configObservationServiceMock = new Mock(); + _clientMessageServiceMock = new Mock(); + _subscribersServiceMock = new Mock(); + _calculatedObservationsServiceMock = new Mock(); + _unitServiceMock = new Mock(); + _calculatedObservationsServiceLazy = + new Lazy(() => _calculatedObservationsServiceMock.Object); + + //optionsApiSettings = Microsoft.Extensions.Options.Options.Create(apiSettings); + + _logger = new Mock>(); + + + _treatmentService = new TreatmentService( + _treatmentRepositoryMock.Object, + _treatmentArchiveRepositoryMock.Object, + _patientServiceMock.Object, + _configObservationServiceMock.Object, + //optionsApiSettings, + _logger.Object, + _clientMessageServiceMock.Object, + _subscribersServiceMock.Object, + _calculatedObservationsServiceLazy, + _unitServiceMock.Object, + _httpContextAccessor.Object, + _auditService.Object + ); + } + + private TreatmentService _treatmentService = null!; + + private Mock _treatmentRepositoryMock = null!; + + private Mock _treatmentArchiveRepositoryMock = null!; + + //Mock pocMappingServiceMock = null!; + private Mock _patientServiceMock = null!; + + //Mock calculatedObservationsServiceMock = null!; + private Mock _configObservationServiceMock = null!; + + private Mock _clientMessageServiceMock = null!; + private Mock _subscribersServiceMock = null!; + private Lazy _calculatedObservationsServiceLazy = null!; + private Mock _calculatedObservationsServiceMock = null!; + private Mock _unitServiceMock = null!; + private readonly Mock _httpContextAccessor = new(); + private readonly Mock _auditService = new(); + + //readonly ApiSettings apiSettings = new () + //{ + // ConfigObservation = new ConfigObservationSettings + // { + // IgnoreUnknownObservation = false + // } + //}; + //IOptions optionsApiSettings; + + + private Mock> _logger = null!; + + private static readonly DateTime Now = DateTime.Now; + private static readonly ObjectId PatientId = ObjectId.GenerateNewId(); + + //3 Nuevos tratamiento3,5,6 + private readonly List _patientTreatments = + [ + new() + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + OrderControl = OrderControlType.Nw, + PlacerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" }, + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [], + Routes = [] + }, + + new() + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + OrderControl = OrderControlType.Dc, + PlacerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "3690", NamespaceId = "CareVue" }, + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [], + Routes = [] + }, + + new() + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + OrderControl = OrderControlType.Nw, + PlacerOrder = new Entity { EntityIdentifier = "4590", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "4590", NamespaceId = "CareVue" }, + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [], + Routes = [] + }, + + new() + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + OrderControl = OrderControlType.Xo, + PlacerOrder = new Entity { EntityIdentifier = "4590", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "4590", NamespaceId = "CareVue" }, + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [], + Routes = [] + }, + + + new() + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + OrderControl = OrderControlType.Nw, + PlacerOrder = new Entity { EntityIdentifier = "2100", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "2100", NamespaceId = "CareVue" }, + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [], + Routes = [] + }, + + + new() + { + Id = ObjectId.GenerateNewId(), + PatientId = PatientId, + OrderControl = OrderControlType.Nw, + PlacerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + FillerOrder = new Entity { EntityIdentifier = "100", NamespaceId = "CareVue" }, + OrderStatus = "A", + OrderTime = DateTime.Now, + Notes = [], + Routes = [] + } + ]; + + [Test] + public void SaveRequest_Not_Case_option_Return_not_insert() + { + var apiRequest = new ApiRequest(); + + Func act = async () => await _treatmentService.SaveRequest(apiRequest); + Assert.ThrowsAsync(act); + } + + [Test] + public void SaveRequest_patientNumber_and_pointOfCare_null_Return_not_insert() + { + var apiRequest = new ApiRequest + { + Type = "ORU_R01", + MessageTime = Now + }; + + Func act = async () => await _treatmentService.SaveRequest(apiRequest); + Assert.ThrowsAsync(act); + } + + [Test] + public async Task SaveRequest_Not_Find_Patient_Return_not_insert() + { + var person = new Person + { + FirstName = "Miguel", + LastName = "Villanueva", + Ids = new Dictionary { { "MR", "437537" } } + }; + + var apiRequest = new ApiRequest + { + Location = new PatientLocation("UCI5C", "Box4"), + Patient = person, + PatientNumber = "437537", + Type = "OMP_O09", + PatientId = PatientId.ToString(), + MessageTime = Now + }; + + await _treatmentService.SaveRequest(apiRequest); + + _treatmentArchiveRepositoryMock.Verify(d => d.InsertOneAsync(It.IsAny()), Times.Never); + } + + [Test] + public async Task SaveRequest_Find_Patient_Return_insert() + { + var person = new Person + { + FirstName = "Miguel", + LastName = "Villanueva", + Ids = new Dictionary { { "MR", "437537" } } + }; + + var patient = new Patient + { + Id = ObjectId.GenerateNewId(), + UnitString = "UCI5C", + Bed = "Box4", + PatientNumber = "437537", + Person = person + }; + + var apiRequest = new ApiRequest + { + Location = new PatientLocation("UCI5C", "Box4"), + Patient = person, + PatientNumber = "437537", + Type = "OMP_O09", + PatientId = PatientId.ToString(), + MessageTime = Now + }; + + _patientServiceMock.Setup(p => p.FindByPatientNumber(apiRequest.PatientNumber, false)).ReturnsAsync(patient); + + await _treatmentService.SaveRequest(apiRequest); + + _treatmentArchiveRepositoryMock.Verify(d => d.InsertOneAsync(It.IsAny()), Times.Never); + } + + [Test] + public async Task GetActiveTreatmentsByPatient() + { + _treatmentRepositoryMock.Setup(t => t.GetByPatientId(PatientId)) + .ReturnsAsync(_patientTreatments.AsEnumerable()); + + var treatments = await _treatmentService.GetActiveTreatmentsByPatient(PatientId); + var result = treatments.ToList(); + + using (Assert.EnterMultipleScope()) + { + That(result, Is.Not.Empty); + That(result, Has.Count.EqualTo(4)); + That(result.Find(t => t?.PlacerOrder?.EntityIdentifier == "100"), Is.Not.Null); + That(result.Find(t => t?.PlacerOrder?.EntityIdentifier == "2100"), Is.Not.Null); + That(result.Find(t => t?.PlacerOrder?.EntityIdentifier == "4590"), Is.Not.Null); + That(result.Find(t => t?.PlacerOrder?.EntityIdentifier == "3690"), Is.Not.Null); + }; + } +} \ No newline at end of file diff --git a/adas-core.Test/Services/UnitServiceTest.cs b/adas-core.Test/Services/UnitServiceTest.cs new file mode 100644 index 00000000..4bcb9dc3 --- /dev/null +++ b/adas-core.Test/Services/UnitServiceTest.cs @@ -0,0 +1,317 @@ +using System.Security.Claims; +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services; +using adas_core.Application.Services.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.MongoModels; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using MongoDB.Bson; +using Moq; + +namespace adas_core.Test.Services; + +public class UnitServiceTest +{ + private readonly Mock _auditServiceMock = new(); + private readonly Mock _httpContextAccessorMock = new(); + private Mock _clientMessageServiceMock = null!; + private Mock> _loggerMock = null!; + private Mock _masterListServiceFactoryMock = null!; + private Mock _patientServiceMock = null!; + private Mock _pointOfCareServiceMock = null!; + private Mock _subscribersServiceMock = null!; + private Mock _unitRepositoryMock = null!; + private UnitService _unitService = null!; + + [SetUp] + public void SetUp() + { + _unitRepositoryMock = new Mock(); + _patientServiceMock = new Mock(); + _loggerMock = new Mock>(); + _clientMessageServiceMock = new Mock(); + _subscribersServiceMock = new Mock(); + _masterListServiceFactoryMock = new Mock(); + _pointOfCareServiceMock = new Mock(); + var userClaims = new ClaimsPrincipal(new ClaimsIdentity([ + new Claim(ClaimTypes.Name, "TestUser") + ], "mock")); + + var httpContextMock = new DefaultHttpContext + { + User = userClaims + }; + + _httpContextAccessorMock.Setup(accessor => accessor.HttpContext) + .Returns(httpContextMock); + _unitService = new UnitService( + _unitRepositoryMock.Object, + new Lazy(() => _patientServiceMock.Object), + _loggerMock.Object, + _masterListServiceFactoryMock.Object, + _subscribersServiceMock.Object, + new Lazy(() => _clientMessageServiceMock.Object), + _pointOfCareServiceMock.Object, + _httpContextAccessorMock.Object, + _auditServiceMock.Object + ); + } + + [Test] + public async Task GetAll_ShouldReturnAllUnits() + { + // Arrange + var units = new List { new() { Id = ObjectId.GenerateNewId() } }; + _unitRepositoryMock.Setup(repo => repo.GetAll()).ReturnsAsync(units); + + // Act + var result = await _unitService.GetAll(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Count, Is.EqualTo(1)); + Assert.That(result, Is.EqualTo(units)); + } + + [Test] + public async Task Get_ShouldReturnUnitById() + { + // Arrange + var unitId = ObjectId.GenerateNewId(); + var unit = new Unit { Id = unitId }; + _unitRepositoryMock.Setup(repo => repo.FindById(unitId)).ReturnsAsync(unit); + + // Act + var result = await _unitService.Get(unitId.ToString()); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.EqualTo(unit)); + } + + [Test] + public async Task Get_ShouldReturnUnitByName_WhenIdIsNotObjectId() + { + // Arrange + var unitName = "TestUnit"; + var units = new List + { + new() { Title = unitName }, + new() { Name = unitName } + }; + _unitRepositoryMock.Setup(repo => repo.GetAll()).ReturnsAsync(units); + + // Act + var result = await _unitService.Get(unitName); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result?.Title ?? result?.Name, Is.EqualTo(unitName)); + } + + + [Test] + public async Task GetAllUnits_ShouldReturnAllUnits() + { + // Arrange + var units = new List + { + new() { Id = ObjectId.GenerateNewId(), Name = "Unit1" }, + new() { Id = ObjectId.GenerateNewId(), Name = "Unit2" } + }; + + _unitRepositoryMock.Setup(repo => repo.GetAll()).ReturnsAsync(units); + + // Act + var result = await _unitService.GetAll(); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result.Count(), Is.EqualTo(units.Count)); + Assert.That(result, Is.EquivalentTo(units)); + } + + [Test] + public async Task GetUnitById_ShouldReturnUnit_WhenFound() + { + // Arrange + var unitId = ObjectId.GenerateNewId(); + var unit = new Unit { Id = unitId, Name = "Unit1" }; + + _unitRepositoryMock.Setup(repo => repo.FindById(unitId)).ReturnsAsync(unit); + + // Act + var result = await _unitService.FindById(unitId); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.EqualTo(unit)); + } + + [Test] + public async Task GetUnitByName_ShouldReturnUnit_WhenFound() + { + // Arrange + var unitName = "TestUnit"; + var expectedUnit = new Unit { Id = ObjectId.GenerateNewId(), Name = unitName }; + + _unitRepositoryMock.Setup(repo => repo.FindByName(unitName)).ReturnsAsync(expectedUnit); + + // Act + var result = await _unitService.GetByName(unitName); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.EqualTo(expectedUnit)); + } + + [Test] + public async Task FindUnitByPatientId_ShouldReturnUnit_WhenPatientFoundInActivePoC() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var unitId = ObjectId.GenerateNewId(); + var pocId = ObjectId.GenerateNewId(); + var patient = new Patient { Id = patientId, PointOfCareId = pocId, UnitId = unitId }; + var unit = new Unit + { + Id = unitId, + PointOfCares = [new PointOfCare { Id = pocId, UnitId = unitId, Status = StatusEnum.PointOfCare.InUse }] + }; + + _unitRepositoryMock.Setup(repo => repo.FindById(unitId)).ReturnsAsync(unit); + _patientServiceMock.Setup(service => service.FindById(patientId, false)).ReturnsAsync(patient); + + // Act + var result = await _unitService.FindByPatientId(patientId); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result?.PointOfCares?.Any(c => c.Id == patient.PointOfCareId && c.UnitId == patient.UnitId), + Is.True); + } + + + [Test] + public async Task FindUnitsByMasterListId_ShouldReturnUnits_WhenUnitsFound() + { + // Arrange + var masterListId = ObjectId.GenerateNewId(); + var masterListType = MasterListType.AltableOptionList; + var units = new List { new(), new() }; + + _unitRepositoryMock.Setup(repo => repo.FindByMasterListId(masterListId, masterListType)).ReturnsAsync(units); + + // Act + var result = await _unitService.FindUnitsByMasterListId(masterListId, masterListType); + + // Assert + if (result != null) + { + var actual = result.ToList(); + Assert.That(actual, Is.Not.Null); + Assert.That(actual, Is.EqualTo(units)); + } + } + + [Test] + public async Task FindByLocation_ShouldReturnUnits_WhenUnitsFound() + { + // Arrange + var location = new PatientLocation("TestUnit", "TestBed", "TestRoom"); + var units = new List + { + new() + { + PointOfCares = [new PointOfCare { UnitName = "TestUnit", Bed = "TestBed" }] + } + }; + _unitRepositoryMock.Setup(repo => repo.GetAll()).ReturnsAsync(units); + + // Act + var result = await _unitService.FindByLocation(location); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result?.Count, Is.EqualTo(1)); + Assert.That(result?.First().PointOfCares?.First().UnitName, Is.EqualTo("TestUnit")); + Assert.That(result?.First().PointOfCares?.First().Bed, Is.EqualTo("TestBed")); + } + + [Test] + public async Task FindByLocation_ShouldReturnEmptyList_WhenNoUnitsFound() + { + // Arrange + var location = new PatientLocation("NonExistentUnit", "NonExistentBed", "NonExistentRoom"); + _unitRepositoryMock.Setup(repo => repo.GetAll()).ReturnsAsync([]); + + // Act + var result = await _unitService.FindByLocation(location); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result?.Count, Is.EqualTo(0)); + } + + [Test] + public async Task FindById_ShouldReturnUnit_WhenValidIdProvided() + { + // Arrange + var unitId = ObjectId.GenerateNewId(); + var expectedUnit = new Unit { Id = unitId }; + _unitRepositoryMock.Setup(repo => repo.FindById(unitId)).ReturnsAsync(expectedUnit); + + // Act + var result = await _unitService.FindById(unitId); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.EqualTo(expectedUnit)); + } + + [Test] + public async Task FindById_ShouldReturnNull_WhenNullIdProvided() + { + // Arrange + + // Act + var result = await _unitService.FindById(null); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public async Task FindByName_ShouldReturnUnit_WhenValidNameProvided() + { + // Arrange + var unitName = "Unit1"; + var expectedUnit = new Unit { Name = unitName }; + _unitRepositoryMock.Setup(repo => repo.FindByName(unitName)).ReturnsAsync(expectedUnit); + + // Act + var result = await _unitService.FindByName(unitName); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.EqualTo(expectedUnit)); + } + + + [Test] + public async Task InsertOne_ShouldReturnInsertedUnit() + { + // Arrange + var unit = new Unit { Id = ObjectId.GenerateNewId(), Name = "TestUnit" }; + _unitRepositoryMock.Setup(repo => repo.InsertOneUnit(unit)).ReturnsAsync(unit); + + // Act + var result = await _unitService.InsertOne(unit); + + // Assert + Assert.That(result, Is.Not.Null); + Assert.That(result, Is.EqualTo(unit)); + } +} \ No newline at end of file diff --git a/adas-core.Test/Usings.cs b/adas-core.Test/Usings.cs new file mode 100644 index 00000000..cefced49 --- /dev/null +++ b/adas-core.Test/Usings.cs @@ -0,0 +1 @@ +global using NUnit.Framework; \ No newline at end of file diff --git a/adas-core.Test/Utilities/CacheKeyClassifierTest.cs b/adas-core.Test/Utilities/CacheKeyClassifierTest.cs new file mode 100644 index 00000000..80e072d6 --- /dev/null +++ b/adas-core.Test/Utilities/CacheKeyClassifierTest.cs @@ -0,0 +1,634 @@ +using adas_core.Application.Services.Caching; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Utils; +using MongoDB.Bson; +using Moq; +using Microsoft.Extensions.Logging; + + +namespace adas_core.Test.Utilities; + +[TestFixture] +public class CacheKeyClassifierTest +{ + #region TC-01 + [Test] + public void Classify_ReturnsPatients_ForKeyStartingWithPatients() + { + + // Arrange + var key = "patients:latestObs:abc123:HR:0"; + + // Act + var result = CacheKeyClassifier.Classify(key); + + // Assert + Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Patients)); + } + + [Test] + public void Classify_ReturnsPatients_ForKeyStartingWithPatientsXYZ() + { + // Arrange + var key = "patients:xyz:any:key"; + + // Act + var result = CacheKeyClassifier.Classify(key); + + // Assert + Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Patients)); + } + + + + #endregion + + #region TC-02 + [Test] + public void Classify_ReturnsDisplays_ForKeyStartingWithConfigDisplaysBase() + { + // Arrange + //var key2 = "configDisplays:display:id:base"; + var key = "configDisplays:display:690b341c4a349b5898898241:base "; + + // Act + var result = CacheKeyClassifier.Classify(key); + + // Assert + Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Displays)); + + } + + [Test] + public void Classify_ReturnsDisplays_ForKeyStartingWithConfigDisplaysConfig() + { + // Arrange + var key = "configDisplays:display:id:config"; + + // Act + var result = CacheKeyClassifier.Classify(key); + + // Assert + Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Displays)); + } + #endregion + + #region TC-03 + [Test] + public void Classify_ReturnsPumpObservations_ForKeyStartingWithPumpObs() + { + // Arrange + var key = "pumpObs:latest:patientId:10"; + + // Act + var result = CacheKeyClassifier.Classify(key); + + // Assert + Assert.That(result, Is.EqualTo(CacheEnum.EntityType.PumpObservations)); + } + #endregion + + + #region TC-04 + [Test] + public void Classify_ReturnsAppointments_ForKeyStartingWithAppointmentsPatient() + { + // Arrange + var key = "appointments:patient:id:20260224"; + + // Act + var result = CacheKeyClassifier.Classify(key); + + // Assert + Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Appointments)); + } + + [Test] + public void Classify_ReturnsAppointments_ForKeyStartingWithAppointmentsPoc() + { + // Arrange + var key = "appointments:PoC:pocId:20260224"; + + // Act + var result = CacheKeyClassifier.Classify(key); + + // Assert + Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Appointments)); + } + #endregion + + #region TC-05 + [Test] + public void Classify_ReturnsGroupedObservations_ForKeyStartingWithGroupedObs() + { + // Arrange + var key = "groupedObs:HR:patient:abc123"; + + // Act + var result = CacheKeyClassifier.Classify(key); + + // Assert + Assert.That(result, Is.EqualTo(CacheEnum.EntityType.GroupedObservations)); + } + #endregion + + #region TC-06 + [Test] + public void Classify_ReturnsConfigObservations_ForKeyStartingWithConfigObservations() + { + // Arrange + var key = "configObservations:all"; + + // Act + var result = CacheKeyClassifier.Classify(key); + + // Assert + Assert.That(result, Is.EqualTo(CacheEnum.EntityType.ConfigObservations)); + } + #endregion + + #region TC-07 + [Test] + public void Classify_ReturnsUnknown_ForUnrecognizedPrefixDiagnostics() + { + // Arrange + var key = "diagnostics:patient:abc"; + + // Act + var result = CacheKeyClassifier.Classify(key); + + // Assert + Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Unknown)); + } + + [Test] + public void Classify_ReturnsUnknown_ForUnrecognizedPrefixRandom() + { + // Arrange + var key = "random:key"; + + // Act + var result = CacheKeyClassifier.Classify(key); + + // Assert + Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Unknown)); + } + #endregion + + #region TC-08 + [Test] + public void Classify_ReturnsPatients_ForKeyInUpperCase() + { + // Arrange + var key = "PATIENTS:latestObs:abc"; + + // Act + var result = CacheKeyClassifier.Classify(key); + + // Assert + Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Patients)); + } + + [Test] + public void Classify_ReturnsAppointments_ForKeyInMixedCase() + { + // Arrange + var key = "Appointments:PoC:id:date"; + + // Act + var result = CacheKeyClassifier.Classify(key); + + // Assert + Assert.That(result, Is.EqualTo(CacheEnum.EntityType.Appointments)); + } + #endregion + + #region TC-09 + [Test] + public void ResolveForEntity_ReturnsNull_WhenGlobalCacheDisabled() + { + // Arrange + var settings = new CacheSettings + { + IsEnabled = false, + Patients = CacheEnum.Mode.Redis, + Redis = new RedisSettings + { + Ttl = new TtlSettings { PatientsSeconds = 600 } + } + }; + + // Act + var result = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Patients); + + // Assert + Assert.That(result, Is.Null); + } + + [Test] + public void ResolveForEntity_ReturnsNull_WhenGlobalDisabled_ForAllEntities() + { + // Arrange + var entities = new[] + { + CacheEnum.EntityType.Patients, + CacheEnum.EntityType.Displays, + CacheEnum.EntityType.PumpObservations, + CacheEnum.EntityType.Appointments, + CacheEnum.EntityType.PontOfCare, + CacheEnum.EntityType.GroupedObservations, + CacheEnum.EntityType.PatientObservations, + CacheEnum.EntityType.ConfigObservations + }; + + var settings = new CacheSettings { IsEnabled = false }; + + // Act & Assert + foreach (var entity in entities) + { + var result = CacheKeyTtl.ResolveForEntity(settings, entity); + Assert.That(result, Is.Null, $"Expected null for entity {entity}"); + } + } + #endregion + + #region TC-10 + [Test] + public void ResolveForEntity_ReturnsNull_WhenEntityModeIsNone() + { + // Arrange + var settings = new CacheSettings + { + IsEnabled = true, + PumpObservations = CacheEnum.Mode.None, + InMemory = new InMemorySettings + { + Ttl = new TtlSettings { PumpObservationsSeconds = 300 } + } + }; + + // Act + var result = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PumpObservations); + + // Assert + Assert.That(result, Is.Null); + } + #endregion + + #region TC-11 + [Test] + public void ResolveForEntity_UsesEntitySpecificTtl_WhenModeIsCache() + { + // Arrange + var settings = new CacheSettings + { + IsEnabled = true, + Patients = CacheEnum.Mode.Cache, + InMemory = new InMemorySettings + { + Ttl = new TtlSettings + { + GlobalSeconds = 3600, + PatientsSeconds = 600 + } + } + }; + + // Act + var result = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Patients); + + // Assert + Assert.That(result, Is.EqualTo(TimeSpan.FromSeconds(600))); + Assert.That(result, Is.Not.EqualTo(TimeSpan.FromSeconds(3600))); + } + #endregion + + #region TC-12 + [Test] + public void ResolveForEntity_UsesRedisTtl_WhenModeIsRedis() + { + // Arrange + var settings = new CacheSettings + { + IsEnabled = true, + Appointments = CacheEnum.Mode.Redis, + Redis = new RedisSettings + { + Ttl = new TtlSettings + { + GlobalSeconds = 3600, + AppointmentsSeconds = 900 + } + } + }; + + // Act + var result = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Appointments); + + // Assert + Assert.That(result, Is.EqualTo(TimeSpan.FromSeconds(900))); + } + #endregion + + #region TC-13 + [Test] + public void ResolveForEntity_UsesGlobalSeconds_WhenNoEntitySpecificTtl() + { + // Arrange + var settings = new CacheSettings + { + IsEnabled = true, + Patients = CacheEnum.Mode.Cache, + InMemory = new InMemorySettings + { + Ttl = new TtlSettings + { + GlobalSeconds = 3600 + // No PatientsSeconds defined + } + } + }; + + // Act + var result = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Patients); + + // Assert + Assert.That(result, Is.EqualTo(TimeSpan.FromSeconds(3600))); + } + #endregion + + #region TC-14 + [Test] + public void ResolveForEntity_ReturnsNull_WhenNoTtlConfigured() + { + // Arrange + var settings = new CacheSettings + { + IsEnabled = true, + Patients = CacheEnum.Mode.Cache, + InMemory = new InMemorySettings + { + Ttl = new TtlSettings() + // No GlobalSeconds, no PatientsSeconds + } + }; + + // Act + var result = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Patients); + + // Assert + Assert.That(result, Is.Null); + } + #endregion + + #region TC-15 + [Test] + public void PatientAppointmentsTodayKeyWithTtl_GeneratesCorrectFormat() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var today = DateTime.UtcNow.ToString("yyyyMMdd"); + var settings = new CacheSettings + { + IsEnabled = true, + Appointments = CacheEnum.Mode.Cache, + InMemory = new InMemorySettings + { + Ttl = new TtlSettings { AppointmentsSeconds = 120 } + } + }; + + // Act + var (key, ttl) = CacheKeys.PatientAppointmentsTodayKeyWithTtl(settings, patientId); + + // Assert + Assert.That(key, Is.EqualTo($"appointments:patient:{patientId}:{today}")); + Assert.That(ttl, Is.EqualTo(TimeSpan.FromSeconds(120))); + } + #endregion + + #region TC-16 + [Test] + public void PocAppointmentsTodayKeyWithTtl_GeneratesCorrectFormat() + { + // Arrange + var pocId = ObjectId.GenerateNewId(); + var today = DateTime.UtcNow.ToString("yyyyMMdd"); + var settings = new CacheSettings + { + IsEnabled = true, + PointOfCares = CacheEnum.Mode.Cache, + InMemory = new InMemorySettings + { + Ttl = new TtlSettings { PointOfCaresSeconds = 180 } + } + }; + + // Act + var (key, ttl) = CacheKeys.PocAppointmentsTodayKeyWithTtl(settings, pocId); + + // Assert + Assert.That(key, Is.EqualTo($"appointments:PoC:{pocId}:{today}")); + Assert.That(ttl, Is.EqualTo(TimeSpan.FromSeconds(180))); + } + #endregion + + #region TC-17 + [Test] + public void LatestObservationsKeyWithTtl_SortsFieldNamesAlphabetically() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var settings = new CacheSettings(); + + // Act + var (key1, _) = CacheKeys.LatestObservationsKeyWithTtl( + settings, patientId, new[] { "Systolic", "HR", "Diastolic" }); + + var (key2, _) = CacheKeys.LatestObservationsKeyWithTtl( + settings, patientId, new[] { "HR", "Diastolic", "Systolic" }); + + // Assert + Assert.That(key1, Is.EqualTo(key2)); + Assert.That(key1, Does.Contain("Diastolic|HR|Systolic")); + } + #endregion + + #region TC-18 + [Test] + public void LatestObservationsKeyWithTtl_ExcludesEmptyFields() + { + // Arrange + var patientId = ObjectId.GenerateNewId(); + var settings = new CacheSettings(); + + // Act + var (key, _) = CacheKeys.LatestObservationsKeyWithTtl( + settings, patientId, new[] { "HR", "", "Diastolic", null! }); + + // Assert + Assert.That(key.Contains("||"), Is.False); + Assert.That(key, Does.Contain("Diastolic|HR")); + } + #endregion + + #region TC-19 + [Test] + public void DisplayBaseKeyWithTtl_GeneratesCorrectFormat() + { + // Arrange + var displayId = ObjectId.GenerateNewId(); + var settings = new CacheSettings + { + IsEnabled = true, + Displays = CacheEnum.Mode.Cache, + InMemory = new InMemorySettings + { + Ttl = new TtlSettings { DisplaysSeconds = 300 } + } + }; + + // Act + var (key, ttl) = CacheKeys.DisplayBaseKeyWithTtl(settings, displayId); + + // Assert + Assert.That(key, Is.EqualTo($"configDisplays:display:{displayId}:base")); + Assert.That(ttl, Is.EqualTo(TimeSpan.FromSeconds(300))); + } + #endregion + + #region TC-20 + [Test] + public void DisplayWithConfigKeyWithTtl_GeneratesCorrectFormat() + { + // Arrange + var displayId = ObjectId.GenerateNewId(); + var settings = new CacheSettings(); + + // Act + var (keyBase, _) = CacheKeys.DisplayBaseKeyWithTtl(settings, displayId); + var (keyConfig, _) = CacheKeys.DisplayWithConfigKeyWithTtl(settings, displayId); + + // Assert + Assert.That(keyConfig, Is.EqualTo($"configDisplays:display:{displayId}:config")); + Assert.That(keyBase, Is.Not.EqualTo(keyConfig)); + } + #endregion + + #region TC-21 + [Test] + public void PointOfCareBaseKeyWithTtl_GeneratesCorrectFormat() + { + // Arrange + var pocId = ObjectId.GenerateNewId(); + var settings = new CacheSettings + { + IsEnabled = true, + PointOfCares = CacheEnum.Mode.Cache, + InMemory = new InMemorySettings + { + Ttl = new TtlSettings { PointOfCaresSeconds = 240 } + } + }; + + // Act + var (key, ttl) = CacheKeys.PointOfCareBaseKeyWithTtl(settings, pocId); + + // Assert + Assert.That(key, Is.EqualTo($"pointOfCare:{pocId}:base")); + Assert.That(ttl, Is.EqualTo(TimeSpan.FromSeconds(240))); + } + #endregion + + #region TC-22 + [Test] + public void ConfigObservationsAllKeyWithTtl_GeneratesCorrectFormat() + { + // Arrange + var settings1 = new CacheSettings(); + var settings2 = new CacheSettings + { + IsEnabled = true, + ConfigObservations = CacheEnum.Mode.Cache, + InMemory = new InMemorySettings + { + Ttl = new TtlSettings { ConfigObservationsSeconds = 600 } + } + }; + + // Act + var (key1, _) = CacheKeys.ConfigObservationsAllKeyWithTtl(settings1); + var (key2, _) = CacheKeys.ConfigObservationsAllKeyWithTtl(settings2); + + // Assert + Assert.That(key1, Is.EqualTo("configObservations:all")); + Assert.That(key2, Is.EqualTo("configObservations:all")); + Assert.That(key1, Is.EqualTo(key2)); + } + #endregion + + #region TC-59 + [TestCase(CacheEnum.EntityType.Patients, "patients:*")] + [TestCase(CacheEnum.EntityType.Displays, "displays:*")] + [TestCase(CacheEnum.EntityType.PumpObservations, "pumpObs:*")] + [TestCase(CacheEnum.EntityType.Appointments, "appointments:*")] + [TestCase(CacheEnum.EntityType.GroupedObservations, "groupedObs:*")] + [TestCase(CacheEnum.EntityType.ConfigObservations, "configObservation:*")] + [TestCase(CacheEnum.EntityType.Unknown, "*")] + public void ForEntity_ReturnsCorrectPatternForEachEntityType( + CacheEnum.EntityType entity, string expectedPattern) + { + var result = CacheKeyPatterns.ForEntity(entity); + + Assert.That(result, Is.EqualTo(expectedPattern)); + } + #endregion + + #region TC-60 + [Test] + public void ByPatient_ReturnsPatternContainingPrefixAndPatientId() + { + const string prefix = "patients:latestObs"; + const string patientId = "abc123"; + + var pattern = CacheKeyPatterns.ByPatient(prefix, patientId); + + Assert.That(pattern, Is.EqualTo($"{prefix}:*:{patientId}*")); + Assert.That(pattern, Does.Contain(prefix)); + Assert.That(pattern, Does.Contain(patientId)); + } + + [Test] + public async Task ByPatient_DeleteByPatternAsync_OnlyRemovesMatchingPatientKeys() + { + var lockMgr = new LockManagerService( + new Mock>().Object, + new InMemoryLockProvider()); + var svc = new CacheService(lockMgr); + + const string prefix = "patients:latestObs"; + const string patientId = "abc123"; + const string otherId = "def456"; + + var pattern = CacheKeyPatterns.ByPatient(prefix, patientId); + var effectiveSubstring = pattern.Replace("*", ""); + + var matchingKey = $"{effectiveSubstring}:HR:0"; + var nonMatchingKey = $"patients:latestObs::{otherId}:HR:0"; + var differentEntityKey = $"appointments::{patientId}:data"; + + await svc.SetObjectAsync(matchingKey, "val1"); + await svc.SetObjectAsync(nonMatchingKey, "val2"); + await svc.SetObjectAsync(differentEntityKey, "val3"); + + var deleted = await svc.DeleteByPatternAsync(pattern); + + Assert.That(deleted, Is.EqualTo(1)); + Assert.That(await svc.GetObjectAsync(matchingKey), Is.Null); + Assert.That(await svc.GetObjectAsync(nonMatchingKey), Is.EqualTo("val2")); + Assert.That(await svc.GetObjectAsync(differentEntityKey), Is.EqualTo("val3")); + } + #endregion + +} diff --git a/adas-core.Test/Utilities/TestUtilities.cs b/adas-core.Test/Utilities/TestUtilities.cs new file mode 100644 index 00000000..08c72600 --- /dev/null +++ b/adas-core.Test/Utilities/TestUtilities.cs @@ -0,0 +1,263 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using MongoDB.Bson; + +namespace adas_core.Test.Utilities; + +public class TestUtilities +{ + public static PointOfCare CreateValidPointOfCare() + { + return new PointOfCare + { + Id = ObjectId.GenerateNewId(), + Room = "Test Room", + Bed = "Test Bed", + UnitId = ObjectId.GenerateNewId(), + Configuration = new PointOfCareConfiguration(), + Status = StatusEnum.PointOfCare.Available, + AdmissionId = ObjectId.GenerateNewId(), + Unit = CreateValidUnit(), + UnitName = "Test Unit" + }; + } + + public static Discharge CreateValidDischarge() + { + return new Discharge + { + Id = ObjectId.GenerateNewId(), + PointOfCareId = ObjectId.GenerateNewId(), + UnitId = ObjectId.GenerateNewId(), + DischargeDate = DateTime.UtcNow, + PatientId = ObjectId.GenerateNewId(), + Destination = "Test Destination", + DestinationOption = CreateValidOptionList(), + ServiceOption = CreateValidOptionList(), + Service = "Test Service", + MedicalDischarge = DateTime.UtcNow, + AdminDischarge = DateTime.UtcNow, + NurseDischarge = DateTime.UtcNow + }; + } + + public static Patient CreateValidPatient() + { + return new Patient + { + Id = ObjectId.GenerateNewId(), + PatientNumber = "TestPatientNumber", + PatientId = "TestPatientId", + AdmTime = DateTime.UtcNow, + DischargeStatus = new OptionList + { + OptionType = "TestDischargeStatus", + Name = "TestDischargeStatusName", + IconDefault = "TestIcon", + Color = "TestColor", + Description = "TestDescription", + InitDate = DateTime.UtcNow, + EndDate = DateTime.UtcNow.AddDays(7) // Example end date + }, + Altable = new OptionList + { + OptionType = "TestAltable", + Name = "TestAltableName", + IconDefault = "TestAltableIcon", + Color = "TestAltableColor", + Description = "TestAltableDescription", + InitDate = DateTime.UtcNow, + EndDate = DateTime.UtcNow.AddDays(7) // Example end date + }, + Origin = new OptionList + { + OptionType = "TestOrigin", + Name = "TestOriginName", + IconDefault = "TestOriginIcon", + Color = "TestOriginColor", + Description = "TestOriginDescription", + InitDate = DateTime.UtcNow, + EndDate = DateTime.UtcNow.AddDays(7) // Example end date + } + // Initialize other properties as needed + }; + } + + public static OptionList CreateValidOptionList() + { + return new OptionList + { + OptionType = "Test Option", + Name = "Test Name", + IconDefault = "Test Icon", + Color = "Test Color", + Description = "Test Description", + InitDate = DateTime.UtcNow, + EndDate = DateTime.UtcNow.AddDays(1) + }; + } + + private static PatientLocation CreateValidPatientLocation() + { + return new PatientLocation + { + UnitName = "Test Unit", + Bed = "Test Bed", + Room = "Test Room" + }; + } + + public static Unit CreateValidUnit() + { + return new Unit + { + Id = ObjectId.GenerateNewId(), + Title = "Test Unit", + Name = "Test Unit", + LastUpdate = null, + PointOfCareIds = [], + Status = StatusEnum.Type.Ok, + Configuration = new UnitConfiguration + { + AutoAdt = true, + } + }; + } + + public static Admission CreateValidAdmission() + { + return new Admission + { + //Id = ObjectId.GenerateNewId(), + AdmissionDate = DateTime.UtcNow, + Nhc = ObjectId.GenerateNewId().ToString(), + UnitId = ObjectId.GenerateNewId(), + PointOfCareId = ObjectId.GenerateNewId(), + Person = new Person + { + FirstName = "Test First Name", + LastName = "Test Last Name" + // Initialize other properties as needed + }, + Origin = CreateValidOptionList(), + OriginAux = "Test Origin Aux", + Diagnosis = CreateValidOptionList(), + DiagnosisAux = "Test Diagnosis Aux", + Allergies = + [ + CreateValidOptionList(), + CreateValidOptionList() + // Add more allergies as needed + ], + Insulation = CreateValidOptionList(), + LanguageBarrier = [CreateValidOptionList()], + PatientLocation = CreateValidPatientLocation() + // Initialize other properties as needed + }; + } + + public static CardConfig CreateValidCardConfig(ObjectId? id = null) + { + return new CardConfig + { + Id = id ?? ObjectId.GenerateNewId(), + Rows = new List + { + new() + { + Cells = new List + { + new() { Type = DisplayConfigEnums.CellType.DemographicCell } + }, + GrowPriority = 1, + Type = DisplayConfigEnums.CellType.CellWrapper + } + } + }; + } + + public static CardDetailsConfig CreateValidCardDetailsNurseConfig(ObjectId? id = null) + { + return new CardDetailsConfig + { + Id = id ?? ObjectId.GenerateNewId(), + NurseRows = new List + { + new() + { + Type = DisplayConfigEnums.RowType.Demographic, + Title = "Title details", + Cells = new List + { + new() + { + Type = DisplayConfigEnums.CellType.CellWrapper, + Cells = new List + { + new() + { + Type = DisplayConfigEnums.CellType.AllergyCell + }, + new() + { + Type = DisplayConfigEnums.CellType.DemographicCell + } + } + } + } + } + } + }; + } + + public static CardDetailsConfig CreateValidCardDetailsSmartConfig(ObjectId? id = null) + { + return new CardDetailsConfig + { + Id = id ?? ObjectId.GenerateNewId(), + SmartSections = new List + { + new() + { + Type = DisplayConfigEnums.RowType.General, + Name = "Title details", + Rows = new List + { + new() + { + Type = DisplayConfigEnums.CellType.CellWrapper, + Observations = new List + { + new() + { + Type = DisplayConfigEnums.CellType.BeaconColor + }, + new() + { + Type = DisplayConfigEnums.CellType.Isolation + } + } + } + } + } + } + }; + } + + public static DisplayConfig CreateValidDisplayConfig(DisplayConfigEnums.DisplayType type, ObjectId? id, + ObjectId? idCard, ObjectId? idDetail) + { + return new DisplayConfig + { + Id = id ?? ObjectId.GenerateNewId(), + CardConfigId = idCard ?? ObjectId.GenerateNewId(), + DetailConfigId = idDetail ?? ObjectId.GenerateNewId(), + Type = type, + FieldList = new List { new() { Name = "FR", Last = 2 } }, + Hospital = "Test Hospital" + }; + } +} \ No newline at end of file diff --git a/adas-core.Test/adas-core.Test.csproj b/adas-core.Test/adas-core.Test.csproj new file mode 100644 index 00000000..5a88c1b4 --- /dev/null +++ b/adas-core.Test/adas-core.Test.csproj @@ -0,0 +1,51 @@ + + + + net8.0 + adas_core.Test + enable + enable + + false + + adas-core.Test + + + + True + + + + True + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + diff --git a/adas-core.Test/adas-core.Test.csproj.bak b/adas-core.Test/adas-core.Test.csproj.bak new file mode 100644 index 00000000..c988b141 --- /dev/null +++ b/adas-core.Test/adas-core.Test.csproj.bak @@ -0,0 +1,52 @@ + + + + net8.0 + adas_core.Test + enable + enable + + false + + adas_core.Test + + + + True + + + + True + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + > + + + + + + + + + + + + + diff --git a/adas-core.module.LightBeacons/Devices/LightBeacon.cs b/adas-core.module.LightBeacons/Devices/LightBeacon.cs new file mode 100644 index 00000000..15f25ea3 --- /dev/null +++ b/adas-core.module.LightBeacons/Devices/LightBeacon.cs @@ -0,0 +1,19 @@ +using adas_core.Domain.Models; +using adas_core.Domain.Utils; + +namespace adas_core.module.LightBeacons.Devices; + +public abstract class LightBeacon(EquatableDictionary options) +{ + protected readonly EquatableDictionary Options = options; + + + public abstract Task BlueCode(); + public abstract Task RedCode(); + public abstract Task YellowCode(); + public abstract Task PowerOffLed(); + public abstract Task GenerateColorAlert(PatientObservation obs); + + + public abstract Task GetBeaconColor(); +} \ No newline at end of file diff --git a/adas-core.module.LightBeacons/Devices/LightBeaconAbstract.cs b/adas-core.module.LightBeacons/Devices/LightBeaconAbstract.cs new file mode 100644 index 00000000..0841929d --- /dev/null +++ b/adas-core.module.LightBeacons/Devices/LightBeaconAbstract.cs @@ -0,0 +1,34 @@ +using Serilog; + +namespace adas_core.module.LightBeacons.Devices; + +public abstract class LightBeaconAbstract(string host, string password) +{ + public string Host = host; + public string Password = password; + + + public abstract void CodeBlue(object entry); + + public abstract void CodeRed(object entry); + + public abstract void CodeYellow(object entry); + + public abstract void PowerOffLed(object entry); + + public static LightBeaconAbstract? GetBeaconDevice(string driver, string host, string password) + { + var modelType = Type.GetType($"adas_core.Drivers.LightBeacon.{driver}"); + if (modelType == null) + { + Log.Debug($"LightBeacon modelType not found: driver {driver}, host {host}, password {password}"); + return null; + } + + var ctor = modelType.GetConstructor([typeof(string), typeof(string)]); + if (ctor == null) + return null; + + return (LightBeaconAbstract)ctor.Invoke([host, password]); + } +} \ No newline at end of file diff --git a/adas-core.module.LightBeacons/Devices/Turktbens2LightBeacon.cs b/adas-core.module.LightBeacons/Devices/Turktbens2LightBeacon.cs new file mode 100644 index 00000000..e39ea3c6 --- /dev/null +++ b/adas-core.module.LightBeacons/Devices/Turktbens2LightBeacon.cs @@ -0,0 +1,279 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Utils; +using HtmlAgilityPack; +using Serilog; + +namespace adas_core.module.LightBeacons.Devices; + +public class Turktbens2LightBeacon : LightBeacon +{ + private readonly HttpClient _client; + private readonly int _code = 75; + private readonly bool _emulate; + private readonly ILogger _logger; + private readonly string _password = "password"; + private readonly int _port = 1; + + private readonly Dictionary _portMappings = new() + { + { 1, [0, 1] }, + { 2, [2, 3] }, + { 3, [4, 5] }, + { 4, [6, 7] } + }; + + private readonly Dictionary _ports = new() + { + { 1, ["|1|1|1|2", "|12|0|1|0", "|1|0|1|2"] }, + { 2, ["|1|3|1|2", "|12|1|1|0", "|1|2|1|2"] }, + { 3, ["|1|5|1|2", "|12|2|1|0", "|1|4|1|2"] }, + { 4, ["|1|7|1|2", "|12|3|1|0", "|1|6|1|2"] } + }; + + private readonly int _timeout = 30; + + private readonly string _url; + + public Turktbens2LightBeacon(EquatableDictionary options) : base(options) + { + _logger = Log.ForContext(); + if (options == null) throw new ArgumentException("TURKTBENS2Beacon has no options"); + if (!options.TryGetValue("url", out var url)) + throw new ArgumentException("TURKTBENS2Beacon has no valid url option"); + if (url == null) throw new ArgumentException("TURKTBENS2Beacon has no valid url option"); + _url = url.ToString()!; + if (options.TryGetValue("password", out var password)) _password = password.ToString() ?? "password"; + if (options.TryGetValue("port", out var port)) _port = Convert.ToInt32(port); + if (options.TryGetValue("timeout", out var timeout)) _timeout = Convert.ToInt32(timeout); + if (options.TryGetValue("code", out var code)) _code = Convert.ToInt32(code); + if (options.TryGetValue("emulate", out var emulate)) _emulate = Convert.ToBoolean(emulate); + _client = new HttpClient + { + Timeout = TimeSpan.FromSeconds(_timeout) + }; + } + + + /// + /// GetByCodeSysAndCode beacon color downloading the html of the web. and using AgilityPackHtml to access. + /// The web is bugged and sometimes you are logged or not. The information is in two urls, the color is formed by 3 + /// bits, Vaux html changes if logged/not logged + /// returns OFF when some error appears or try to parse the bits to BalizaColors enum + /// + /// + public override async Task GetBeaconColor() + { + if (_emulate) + return LightBeaconColor.Off; + + var httpClient = new HttpClient(); + + try + { + //we dont need to login to check status + var vauxValue = 0; + var output1Value = 0; + var output2Value = 0; + var outputUri = new Uri($"{_url}/IO01_03.html"); + var vauxUri = new Uri($"{_url}/IO12_03.html"); + + var outputs = await httpClient.GetAsync(outputUri); + var vauxs = await httpClient.GetAsync(vauxUri); + if (!outputs.IsSuccessStatusCode || !vauxs.IsSuccessStatusCode) + return LightBeaconColor.Off; + + var outputsHtml = new HtmlDocument(); + outputsHtml.LoadHtml(await outputs.Content.ReadAsStringAsync()); + + var vauxHtml = new HtmlDocument(); + vauxHtml.LoadHtml(await vauxs.Content.ReadAsStringAsync()); + + //GetByCodeSysAndCode Vaux info from vauxUri can be on off and we translate it to 0,1 + var trElements = vauxHtml.DocumentNode.SelectNodes("//tr"); + if (trElements == null || trElements.Count < _port) + return LightBeaconColor.Off; + + var portNode = trElements[_port - 1]; + var tdNodes = portNode.SelectNodes(".//td"); + var vauxResult = tdNodes[1]; + switch (vauxResult.InnerText) + { + case "on": + vauxValue = 1; + break; + //sometimes it display a select option when logged and other times its a label. this case is for option, take selected option + case "offon": + { + var selected = vauxResult.SelectSingleNode("select").SelectSingleNode("option[@selected]"); + if (selected.InnerText.Equals("on")) vauxValue = 1; + + break; + } + } + + //GetByCodeSysAndCode outputs info + var checkboxElements = outputsHtml.DocumentNode.SelectNodes("//input[@type='checkbox']"); + if (checkboxElements == null) + return LightBeaconColor.Off; + + var portsParsed = _portMappings.TryGetValue(_port, out var mappedValues); + if (!portsParsed) return LightBeaconColor.Off; + var output1 = checkboxElements[mappedValues![0]]; + var output2 = checkboxElements[mappedValues[1]]; + + if (output1.Attributes["checked"] != null) output1Value = 1; + if (output2.Attributes["checked"] != null) output2Value = 1; + + var finalValue = $"{output2Value}{vauxValue}{output1Value}"; + var parsed = Enum.TryParse(finalValue, out LightBeaconColor parsedColor); + return parsed ? parsedColor : LightBeaconColor.Off; + } + catch (Exception e) + { + _logger.Error( + "Exception trying to get beacon color for beacon: {url} port: {port} ex: {eMessage}, trace: {eStackTrace}", + _url, _port, e.Message, e.StackTrace); + return LightBeaconColor.Off; + } + finally + { + httpClient.Dispose(); + } + } + + public override async Task BlueCode() + { + try + { + await Send(ColorsCodes.Blue); + } + catch (Exception e) + { + _logger.Error("An error occurred while sending the Blue code: {Message}", e.Message); + throw; + } + } + + public override async Task RedCode() + { + try + { + await Send(ColorsCodes.Red); + } + catch (Exception e) + { + _logger.Error("An error occurred while sending the Red code: {Message}", e.Message); + throw; + } + } + + public override async Task YellowCode() + { + try + { + await Send(ColorsCodes.Yellow); + } + catch (Exception e) + { + _logger.Error("An error occurred while sending the Yellow code: {Message}", e.Message); + throw; + } + } + + public override async Task PowerOffLed() + { + try + { + await Send(ColorsCodes.Off); + } + catch (Exception e) + { + _logger.Error("An error occurred while sending the power off code: {Message}", e.Message); + throw; + } + } + + public override Task GenerateColorAlert(PatientObservation obs) + { + throw new NotImplementedException(); + } + + + private async Task Send(ColorsCodes colorCodes) + { + var clr = ((int)colorCodes).ToString().PadLeft(3, '0'); + var passwordEncoded = Uri.EscapeDataString(_password); + var body = new Dictionary + { + { "access_password", passwordEncoded }, + { _code + _ports[_port][0], clr[0].ToString() }, + { _code + _ports[_port][1], clr[1].ToString() }, + { _code + _ports[_port][2], clr[2].ToString() } + }; + + if (!_emulate) + try + { + await SendMessage(body); + } + catch (Exception e) + { + _logger.Error("Failed to send color codes: {errorMessage}", e.Message); + throw; + } + } + + + protected async Task SendMessage(Dictionary body) + { + if (string.IsNullOrEmpty(_url)) throw new InvalidOperationException("URL is not set."); + + var retryCount = 0; + const int maxRetries = 3; // Número máximo de intentos + var delayMilliseconds = 500; // Tiempo de espera inicial entre reintentos + + while (retryCount < maxRetries) + try + { + var uri = new Uri(_url); + var content = new FormUrlEncodedContent(body); + var b = await content.ReadAsStringAsync(); + _logger.Debug("Sending to {url} body {b}", _url, b); + var response = await _client.PostAsync(uri, content); + + if (!response.IsSuccessStatusCode) + throw new Exception( + $"HTTP error: {response.StatusCode} while sending message to {_url} with body {b}"); + break; + } + catch (Exception e) + { + retryCount++; + _logger.Warning( + "Error sending to url: {url}, message: {eMessage}, stack trace: {eStackTrace}, attempt: {retryCount}", + _url, e.Message, e.StackTrace, retryCount); + + if (retryCount >= maxRetries) + { + _logger.Error("Max retry attempts reached. Failing operation."); + throw; + } + + await Task.Delay(delayMilliseconds); + delayMilliseconds *= 2; // Incrementa el tiempo de espera para cada reintento + } + } + + private enum ColorsCodes + { + Off = 000, + Red = 001, + Yellow = 011, + Green = 010, + Cyan = 110, + Blue = 100, + Magenta = 101, + White = 111 + } +} \ No newline at end of file diff --git a/adas-core.module.LightBeacons/Services/LightBeaconService.cs b/adas-core.module.LightBeacons/Services/LightBeaconService.cs new file mode 100644 index 00000000..d5b9bbf5 --- /dev/null +++ b/adas-core.module.LightBeacons/Services/LightBeaconService.cs @@ -0,0 +1,396 @@ +using System.Collections.Concurrent; +using System.Text; +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.MongoModels; +using adas_core.Domain.Models.Responses; +using adas_core.Domain.Utils; +using adas_core.module.LightBeacons.Devices; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using MongoDB.Driver; +using LightBeaconDevice = adas_core.module.LightBeacons.Devices.LightBeacon; +using LightBeacon = adas_core.Domain.Models.MongoModels.LightBeacon; + +namespace adas_core.module.LightBeacons.Services; + +public class LightBeaconService : ILightBeaconService +{ + private readonly IClientMessageService _clientMessageService; + + private readonly ConcurrentDictionary _locationsWithColor = new(); + + private readonly ILogger _logger; + private readonly IPointOfCareService _pointOfCareService; + private readonly ILightBeaconRepository _lightBeaconRepository; + private readonly ISubscribersService _subscribersService; + + public LightBeaconService( + IOptions apiSettings, + ILogger logger, + IClientMessageService clientMessageService, + ISubscribersService subscribersService, + IPointOfCareService pointOfCareService, + ILightBeaconRepository lightBeaconRepository) + { + _logger = logger; + _clientMessageService = clientMessageService; + _subscribersService = subscribersService; + _pointOfCareService = pointOfCareService; + _lightBeaconRepository = lightBeaconRepository; + + try + { + Url = apiSettings.Value.BalizaUrl; + } + catch (Exception ex) + { + _logger.LogError("Error BalizaService apiSettings values: {exMessage} ", ex); + } + + //_ = SetLocationsWithColor(); + } + + private string? Url { get; } + + public async Task PowerOffLed(ObjectId pocId) + { + var poc = await _pointOfCareService.FindById(pocId); + if (poc == null) + { + _logger.LogError("Power off led did not find poc by id: {PocId}", pocId); + return; + } + + _ = PowerOffLed(poc); + } + + public async Task InsertOne(LightBeacon beacon) + { + var beaconFound = await _lightBeaconRepository.GetByName(beacon.Name); + if(beaconFound != null) throw new Exception($"Light beacon with name {beacon.Name} already exists"); + return await _lightBeaconRepository.InsertOneAsyncAndReturn(beacon); + } + + public async Task> GetPaginatedBeacons(PaginationFilter filter) + { + var usedRelayIds = await _pointOfCareService.FindAllIdBeaconsInUse(); + + var fluentQuery = _lightBeaconRepository.GetPaginatedRelays(filter); + + if (filter.FilteredRequest?.InUse != null) + { + bool filterInUse = filter.FilteredRequest.InUse.Value; + var filterBuilder = Builders.Filter; + + var idFilter = filterInUse + ? filterBuilder.In(c => c.Id, usedRelayIds) + : filterBuilder.Not(filterBuilder.In(c => c.Id, usedRelayIds)); + + fluentQuery.Filter = filterBuilder.And(fluentQuery.Filter, idFilter); + } + + var count = await fluentQuery.CountDocumentsAsync(); + var data = await fluentQuery + .Skip((filter.PageNumber - 1) * filter.PageSize) + .Limit(filter.PageSize) + .ToListAsync(); + + if(data == null) return new PaginationResponse([], filter.PageNumber, filter.PageSize, count); + + foreach (var camera in data) + { + if (camera == null) continue; + + bool isInUse = usedRelayIds.Contains(camera.Id); + + // Asignación mediante reflexión para el private set + camera.GetType().GetProperty(nameof(Relay.InUse)) + ?.SetValue(camera, isInUse); + } + + return new PaginationResponse(data, filter.PageNumber, filter.PageSize, count); + } + + public async Task> GetSearchByName(string textToSearch) + { + return await _lightBeaconRepository.GetSearchByName(textToSearch); + } + + public async Task UpdateOne(LightBeacon beacon) + { + await _lightBeaconRepository.UpdateOneAsync(beacon.Id, beacon); + return await _lightBeaconRepository.GetById(beacon.Id); + } + + public async Task PowerOffLed(PointOfCare poc) + { + if (_locationsWithColor.TryGetValue(poc.Id, out var lightBeaconColor) && + lightBeaconColor == LightBeaconColor.Off) + return; + + var beaconConfig = poc.Configuration?.BeaconIdList; + if (beaconConfig == null || !beaconConfig.Any()) return; + foreach (var beacon in beaconConfig.Select(GetBeacon)) + if (beacon != null) + await SendColor(poc.Id, LightBeaconColor.Off); + + lock (_locationsWithColor) + { + if (_locationsWithColor.ContainsKey(poc.Id)) + _locationsWithColor[poc.Id] = LightBeaconColor.Off; + + else + _locationsWithColor.TryAdd(poc.Id, LightBeaconColor.Off); + } + + _logger.LogDebug("Locations with color cached: {dct}", DictionaryToString(_locationsWithColor)); + } + + public void GenerateColorAlert(PatientObservation obs) + { + try + { + LightBeaconColor colorEnum; + var colorNumbers = new char[3]; + + switch (obs.Status) + { + case StatusEnum.Type.Warning: + if (obs.WarnColor == null) return; + colorEnum = (LightBeaconColor)Enum.Parse(typeof(LightBeaconColor), + obs.WarnColor); + var colorValue = (int)colorEnum; + var valuesChart = colorValue.ToString().ToCharArray(); + if (valuesChart.Length is < 3 and < 2) + { + var c = valuesChart[0]; + colorNumbers[0] = '0'; + colorNumbers[1] = '0'; + colorNumbers[2] = c; + } + else if (valuesChart.Length < 3) + { + var c = valuesChart[0]; + var c2 = valuesChart[1]; + colorNumbers[0] = '0'; + colorNumbers[1] = c; + colorNumbers[2] = c2; + } + + break; + + case StatusEnum.Type.Alert: + if (obs.AlertColor == null) return; + colorEnum = (LightBeaconColor)Enum.Parse(typeof(LightBeaconColor), + obs.AlertColor); + var value = (int)colorEnum; + var vchart = value.ToString().ToCharArray(); + if (vchart.Length is < 3 and < 2) + { + var c = vchart[0]; + colorNumbers[0] = '0'; + colorNumbers[1] = '0'; + colorNumbers[2] = c; + } + else if (vchart.Length < 3) + { + var c = vchart[0]; + var c2 = vchart[1]; + colorNumbers[0] = '0'; + colorNumbers[1] = c; + colorNumbers[2] = c2; + } + + break; + + + default: + + colorNumbers[0] = '0'; + colorNumbers[1] = '0'; + colorNumbers[2] = '0'; + + break; + } + + var passwordEncoded = Uri.EscapeDataString("password"); + var body = + $"access_password={passwordEncoded}&58|12|1|1|0={Uri.EscapeDataString(colorNumbers[0].ToString())}&58|1|3|1|2={Uri.EscapeDataString(colorNumbers[1].ToString())}&58|1|2|1|2={Uri.EscapeDataString(colorNumbers[2].ToString())}"; + _ = SendMessage(body); + } + catch (Exception ex) + { + _logger.LogError("Error GenerateColorAlert {obs}: {exMessage}", obs, ex); + } + } + + public async Task SendColor(ObjectId pocId, LightBeaconColor color) + { + var poc = await _pointOfCareService.FindById(pocId); + if (poc == null) return; + _ = SendColor(poc, color); + } + + public async Task SendColor(PointOfCare poc, LightBeaconColor color) + { + try + { + if (_locationsWithColor.TryGetValue(poc.Id, out var lightBeaconColor) && lightBeaconColor == color) + return; + + var beaconConfig = poc.Configuration?.BeaconIdList; + if (beaconConfig == null || !beaconConfig.Any()) return; + foreach (var beaconId in beaconConfig) + { + var beacon = await GetBeacon(beaconId); + switch (color) + { + case LightBeaconColor.Red: + beacon?.RedCode(); + break; + case LightBeaconColor.Blue: + beacon?.BlueCode(); + break; + case LightBeaconColor.Yellow: + beacon?.YellowCode(); + break; + case LightBeaconColor.Off: + beacon?.PowerOffLed(); + break; + } + + await SendBeaconBroadcast(poc, color); + + lock (_locationsWithColor) + { + if (_locationsWithColor.ContainsKey(poc.Id)) + _locationsWithColor[poc.Id] = color; + else + _locationsWithColor.TryAdd(poc.Id, color); + } + } + + _logger.LogDebug("Locations with color cached: {dct}", DictionaryToString(_locationsWithColor)); + } + catch (Exception e) + { + _logger.LogError("An error occurred on sending color {color} while sending the Beacon code: {Message}", + color.ToString(), e.Message); + } + } + + public async Task GetColor(ObjectId pocId) + { + var poc = await _pointOfCareService.FindById(pocId); + if (poc == null) return LightBeaconColor.Off; + return await GetColor(poc); + } + + public async Task GetColor(PointOfCare poc) + { + try + { + if (_locationsWithColor.TryGetValue(poc.Id, out var lightBeaconColor)) return lightBeaconColor; + + var lightBeaconConfig = poc.Configuration?.BeaconIdList; + if (lightBeaconConfig == null || !lightBeaconConfig.Any()) return LightBeaconColor.Off; + foreach (var beaconId in lightBeaconConfig) + { + var beacon = await GetBeacon(beaconId); + if (beacon != null) + return await beacon.GetBeaconColor(); + } + + return LightBeaconColor.Off; + } + catch (Exception ex) + { + _logger.LogError("Exception getting color from beacon patient poc Id: {poc} Exception: {ex}", poc.Id, ex); + return LightBeaconColor.Off; + } + } + + public async Task SendBeaconBroadcast(PointOfCare poc, LightBeaconColor color) + { + var subscribers = _subscribersService.GetSubscribers() + .Where(s => s.LocationIds.Any(lid => lid == poc.Id)) + .ToList(); + BeaconResponse bc = new(color.ToString(), poc.Id, poc.UnitId); + + foreach (var subscriber in subscribers) + await _clientMessageService.SendAsync(subscriber.Id, OperationType.Beacon, bc); + } + + public async Task SendBeaconBroadcast(ObjectId pocId, LightBeaconColor color) + { + var poc = await _pointOfCareService.FindById(pocId); + if (poc == null) return; + _ = SendBeaconBroadcast(poc, color); + } + + private async Task SendMessage(string body) + { + if (string.IsNullOrEmpty(Url)) + { + _logger.LogError("Url string is null or empty. Message:{body}", body); + return; + } + + HttpClient client = new(); + var content = new StringContent(body, Encoding.UTF8, "application/x-www-form-urlencoded"); + + try + { + var response = await client.PostAsync(Url, content); + + if (response.IsSuccessStatusCode) + _logger.LogDebug("Message sent successfully: {body}", body); + else + _logger.LogError("Error SendMessage {body}: {responseStatusCode}", body, response.StatusCode); + } + catch (Exception ex) + { + _logger.LogError("Error SendMessage {body}: {exMessage}", body, ex); + } + } + + private async Task GetBeacon(ObjectId lightBeaconId) + { + var cfg = await _lightBeaconRepository.GetById(lightBeaconId); + if (cfg == null) return null; + if (string.IsNullOrEmpty(cfg.Options.Url) || cfg.Options.Port == null) + return null; + switch (cfg.Type) + { + case "TURKTBENS2": + return new Turktbens2LightBeacon(new EquatableDictionary + { + { "url", cfg.Options.Url }, + { "port", cfg.Options.Port }, + { "password", cfg.Options.Password ?? "password" }, + { "emulate", cfg.Options.Emulate } + }); + } + + return null; + } + + public static string DictionaryToString(ConcurrentDictionary dictionary) + { + var builder = new StringBuilder(); + + foreach (var pair in dictionary) builder.AppendLine($"Location: {pair.Key}, Color: {pair.Value}"); + + return builder.ToString(); + } + + + //TODO:Almacenar en memoria los estados de los colores de la baliza + //Array con el puerto y color +} \ No newline at end of file diff --git a/adas-core.module.LightBeacons/adas-core.module.LightBeacons.csproj b/adas-core.module.LightBeacons/adas-core.module.LightBeacons.csproj new file mode 100644 index 00000000..01a7e2dd --- /dev/null +++ b/adas-core.module.LightBeacons/adas-core.module.LightBeacons.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + adas_core.module.LightBeacons + enable + enable + + + + + + + + + + + + diff --git a/adas-core.module.ProxyDevices/Devices/HttpDevice.cs b/adas-core.module.ProxyDevices/Devices/HttpDevice.cs new file mode 100644 index 00000000..7b61b81b --- /dev/null +++ b/adas-core.module.ProxyDevices/Devices/HttpDevice.cs @@ -0,0 +1,74 @@ +using System.Net; +using Serilog; + +namespace adas_core.module.ProxyDevices.Devices; + +public class HttpDevice : IProxyDevice +{ + private string _domain = ""; + + private bool _isInitialized; + private string _name = ""; + private string _password = ""; + private Uri _uri = null!; + private string _username = ""; + + public void Initialize(Dictionary deviceParameters) + { + if (_isInitialized) throw new InvalidOperationException("Device already initialized"); + if (!deviceParameters.TryGetValue("url", out var urlobj) || urlobj is not string url || + string.IsNullOrEmpty(url.Trim()) || !Uri.TryCreate(url, UriKind.Absolute, out var uri)) + throw new ArgumentNullException(nameof(deviceParameters), + "Device parameter 'url' is required and must be a valid ur"); + _uri = uri; + if (deviceParameters.TryGetValue("name", out var name)) _name = name?.ToString() ?? ""; + if (deviceParameters.TryGetValue("username", out var username)) _username = username?.ToString() ?? ""; + if (deviceParameters.TryGetValue("password", out var password)) _password = password?.ToString() ?? ""; + if (deviceParameters.TryGetValue("domain", out var domain)) _domain = domain?.ToString() ?? ""; + _isInitialized = true; + } + + public async Task Process() + { + try + { + var client = GetHttpClient(); + return await client.GetAsync(_uri); + } + catch (TaskCanceledException ex) + { + Log.Error("Error getting Http Client get async. Exception:{ex}", ex.ToString()); + throw new TimeoutException("La solicitud fue cancelada debido al tiempo de espera.", ex); + } + } + + public async Task Stream() + { + try + { + var client = GetHttpClient(); + var response = await client.GetAsync(_uri, HttpCompletionOption.ResponseHeadersRead); + return response; + } + catch (TaskCanceledException ex) + { + Log.Error("Exception getting response. Ex: {ex}", ex.ToString()); + throw new TimeoutException("La solicitud fue cancelada debido al tiempo de espera.", ex); + } + } + + private HttpClient GetHttpClient() + { + if (!_isInitialized) throw new InvalidOperationException("Device not initialized"); + // get content via http with digest auth + var clientHandler = new HttpClientHandler(); + if (!string.IsNullOrEmpty(_username)) + clientHandler.Credentials = new NetworkCredential(_username, _password, _domain); + return new HttpClient(clientHandler); + } + + public override string ToString() + { + return $"[HttpDevice] ({_name} {_uri} {_username} {_password} {_domain})"; + } +} \ No newline at end of file diff --git a/adas-core.module.ProxyDevices/Devices/IProxyDevice.cs b/adas-core.module.ProxyDevices/Devices/IProxyDevice.cs new file mode 100644 index 00000000..a3e25ea5 --- /dev/null +++ b/adas-core.module.ProxyDevices/Devices/IProxyDevice.cs @@ -0,0 +1,8 @@ +namespace adas_core.module.ProxyDevices.Devices; + +public interface IProxyDevice +{ + void Initialize(Dictionary deviceParameters); + Task Process(); + Task Stream(); +} \ No newline at end of file diff --git a/adas-core.module.ProxyDevices/ProxyDeviceSettings.cs b/adas-core.module.ProxyDevices/ProxyDeviceSettings.cs new file mode 100644 index 00000000..949f9d1d --- /dev/null +++ b/adas-core.module.ProxyDevices/ProxyDeviceSettings.cs @@ -0,0 +1,13 @@ +namespace adas_core.module.ProxyDevices; + +public class ProxyDeviceSettings : List +{ +} + +public class ProxyDevice +{ + public string Id { get; set; } = null!; + public string Type { get; set; } = null!; + public bool Enabled { get; set; } = true; + public Dictionary Parameters { get; set; } = new(); +} \ No newline at end of file diff --git a/adas-core.module.ProxyDevices/Services/IProxyDeviceService.cs b/adas-core.module.ProxyDevices/Services/IProxyDeviceService.cs new file mode 100644 index 00000000..e69eb71f --- /dev/null +++ b/adas-core.module.ProxyDevices/Services/IProxyDeviceService.cs @@ -0,0 +1,7 @@ +namespace adas_core.module.ProxyDevices.Services; + +public interface IProxyDeviceService +{ + Task Process(string deviceId); + Task Stream(string deviceId); +} \ No newline at end of file diff --git a/adas-core.module.ProxyDevices/Services/ProxyDeviceService.cs b/adas-core.module.ProxyDevices/Services/ProxyDeviceService.cs new file mode 100644 index 00000000..fcaa02de --- /dev/null +++ b/adas-core.module.ProxyDevices/Services/ProxyDeviceService.cs @@ -0,0 +1,75 @@ +using System.Net; +using adas_core.module.ProxyDevices.Devices; +using Microsoft.Extensions.Options; + +namespace adas_core.module.ProxyDevices.Services; + +public class ProxyDeviceService(IOptions settings) : IProxyDeviceService +{ + private readonly Dictionary _cache = new(); + + public async Task Process(string deviceId) + { + try + { + var device = GetDevice(deviceId); + if (device == null) + return new HttpResponseMessage(HttpStatusCode.NotFound) + { ReasonPhrase = $"Proxy device not found. deviceId:{deviceId}" }; + + return await device.Process(); + } + catch (HttpRequestException e) + { + return new HttpResponseMessage(e.StatusCode ?? HttpStatusCode.NotFound) { ReasonPhrase = e.Message }; + } + } + + public async Task Stream(string deviceId) + { + try + { + var device = GetDevice(deviceId); + if (device == null) + return new HttpResponseMessage(HttpStatusCode.NotFound) + { ReasonPhrase = $"Proxy device not found. deviceId:{deviceId}" }; + + return await device.Stream(); + } + catch (HttpRequestException e) + { + return new HttpResponseMessage(e.StatusCode ?? HttpStatusCode.NotFound) { ReasonPhrase = e.Message }; + } + } + + private IProxyDevice? GetDevice(string deviceId) + { + var device = settings.Value.FirstOrDefault(d => d.Id == deviceId); + if (device is not { Enabled: true }) + throw new HttpRequestException("Device not found or not enabled", null, HttpStatusCode.NotFound); + IProxyDevice? deviceInstance = null; + lock (_cache) + { + if (_cache.TryGetValue(deviceId, out var value)) + { + deviceInstance = value; + } + else + { + var deviceType = Type.GetType("adas_core.module.ProxyDevices.Devices." + device.Type + "Device") ?? + throw new HttpRequestException("Device not implemented", null, + HttpStatusCode.NotImplemented); + var instance = Activator.CreateInstance(deviceType); + if (instance is IProxyDevice proxyDevice) + { + deviceInstance = proxyDevice; + deviceInstance.Initialize(device.Parameters); + } + + _cache.Add(deviceId, deviceInstance); + } + } + + return deviceInstance; + } +} \ No newline at end of file diff --git a/adas-core.module.ProxyDevices/adas-core.module.ProxyDevices.csproj b/adas-core.module.ProxyDevices/adas-core.module.ProxyDevices.csproj new file mode 100644 index 00000000..0c44716c --- /dev/null +++ b/adas-core.module.ProxyDevices/adas-core.module.ProxyDevices.csproj @@ -0,0 +1,26 @@ + + + + net8.0 + adas_core.module.ProxyDevices + enable + enable + + + + + + + + + + + + + + + + + + + diff --git a/adas-core.module.Relays/Devices/FakeRelay.cs b/adas-core.module.Relays/Devices/FakeRelay.cs new file mode 100644 index 00000000..6ea27ac0 --- /dev/null +++ b/adas-core.module.Relays/Devices/FakeRelay.cs @@ -0,0 +1,62 @@ +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using adas_core.module.Relays.Models; + +namespace adas_core.module.Relays.Devices; + +internal class FakeRelay(Relay relay, RelaySettings relaySettings) : RelayDevice(relay, relaySettings) +{ + public override bool CheckDevice() + { + return true; + } + + public override bool GetStatusRelay() + { + return true; + } + + public override void PowerOffRelay(int outletId) + { + } + + public override void PowerOnRelay(int outletId) + { + } + + public override void PowerOffAll() + { + } + + public override void PowerOnAll() + { + } + + public override RelayEnum.Status GetStatusRelay(int outletId) + { + const string status = "00000000"; + + if (status.Length == 0) return RelayEnum.Status.On; + return status.Substring(outletId - 1, 1) == "0" ? RelayEnum.Status.Off : RelayEnum.Status.On; + } + + public override void RebootOutlet(int outletId) + { + } + + public override void Reboot() + { + PowerOffAll(); + if (Relay.RebootDelay is > 0) Thread.Sleep(Relay.RebootDelay!.Value); + PowerOnAll(); + } + + public override Task GetStatusRelayAsync() + { + return Task.FromResult(true); + } + + public override void CheckStatus() + { + } +} \ No newline at end of file diff --git a/adas-core.module.Relays/Devices/KMTronicRelay.cs b/adas-core.module.Relays/Devices/KMTronicRelay.cs new file mode 100644 index 00000000..f0872f9c --- /dev/null +++ b/adas-core.module.Relays/Devices/KMTronicRelay.cs @@ -0,0 +1,209 @@ +using System.Net.Sockets; +using System.Text; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using adas_core.module.Relays.Models; +using Serilog; + +namespace adas_core.module.Relays.Devices; + +public class KmTronicRelay(Relay relay, RelaySettings relaySettings) + : RelayDevice(relay, relaySettings) +{ + private UdpClient? _udpClient; + + public bool DeviceStatus { get; set; } + + public override bool CheckDevice() + { + CheckStatus(); + var result = GetStatusRelay(1); + return result != RelayEnum.Status.Unknown; + } + + public override bool GetStatusRelay() + { + var status = Send("FF0000"); + //while (!status.IsCompleted) + //{ + + //} + //return !string.IsNullOrEmpty(status.Result); + + return !string.IsNullOrEmpty(status); + } + + public override RelayEnum.Status GetStatusRelay(int outletId) + { + var status = Send("FF0000"); + + //while (!status.IsCompleted) + //{ + + //} + + + //if (!string.IsNullOrEmpty(status.Result)) + //{ + // if (status.Result.Substring(outlet.outlet - 1, 1) == "0") + // { + // return true; + // } + //} + + if (!string.IsNullOrEmpty(status)) + try + { + if (status.Substring(outletId - 1, 1) == "0") + //return RelayStatus.Off; + return Relay.Mode == RelayEnum.Mode.OpenedOffClosedOn + ? RelayEnum.Status.On + : RelayEnum.Status.Off; + } + catch (ArgumentOutOfRangeException e) + { + Log.Error( + "There isn't an outlet {Outlet} in the rele {Ip}. Please check the configuration.\nException: {Ex}", + outletId, Relay.Ip, e.Message); + } + + //return RelayStatus.On; + return Relay.Mode == RelayEnum.Mode.OpenedOffClosedOn ? RelayEnum.Status.Off : RelayEnum.Status.On; + } + + public override void PowerOffRelay(int outletId) + { + if (Relay.Mode == RelayEnum.Mode.OpenedOffClosedOn) OpenRelay(outletId); + else CloseRelay(outletId); + } + + public void OpenRelay(int outletId) + { + //Activa el relé por lo que corta la corriente del dispositivo conectado + Send("FF0" + outletId + "01"); + } + + public override void PowerOnRelay(int outletId) + { + if (Relay.Mode == RelayEnum.Mode.OpenedOffClosedOn) CloseRelay(outletId); + else OpenRelay(outletId); + } + + public void CloseRelay(int outletId) + { + //Desactiva el relé por lo que deja pasar la corriente del dispositivo conectado + Send("FF0" + outletId + "00"); + } + + public override void PowerOffAll() + { + //Activa el relé por lo que corta la corriente de todos los dispositivo conectados + Send("FFE0FF"); + } + + public override void PowerOnAll() + { + //Desactiva el relé por lo que deja pasar la corriente a todos dispositivo conectados + Send("FFE000"); + } + + public override void CheckStatus() + { + DeviceStatus = GetStatusRelay(); + } + + + public override void RebootOutlet(int outletId) + { + //Reinicia el relé del dispositivo conectado + PowerOnOffRelay(outletId, 2000); + } + + public override void Reboot() + { + PowerOffAll(); + if (Relay.RebootDelay is > 0) Thread.Sleep(Relay.RebootDelay!.Value); + PowerOnAll(); + } + + public override Task GetStatusRelayAsync() + { + throw new NotImplementedException(); + } + + protected virtual void Connect() + { + if (_udpClient == null) + try + { + if (!string.IsNullOrEmpty(Relay.Ip)) _udpClient = new UdpClient(Relay.Ip, Relay.Port); + //var ipaddress = IPAddress.Parse(RelayConfig.Ip); + //sourceipendpoint = new IPEndPoint(ipaddress, port); //source port IPEndpoint + } + catch (Exception e) + { + Log.Debug("UdpClient not connect {Host}: {Port} Ex: {Ex}", Relay.Ip, Relay.Port, e.Message); + + try + { + if (_udpClient != null) + { + _udpClient.Close(); + _udpClient = null; + } + } + catch + { + // ignored + } + } + else + try + { + _udpClient.Close(); + _udpClient = null; + } + catch + { + // ignored + } + } + + protected virtual string Send(string datagrama) + { + if (_udpClient == null) Connect(); + + if (_udpClient == null) return string.Empty; + + var command = Encoding.ASCII.GetBytes(datagrama.ToUpper()); + _udpClient.Send(command, command.Length); + + var returnData = string.Empty; + try + { + var t = Task.Run(() => + { + var taskReceiveBytes = _udpClient.ReceiveAsync(); + returnData = Encoding.ASCII.GetString(taskReceiveBytes.Result.Buffer); + }); + var ts = TimeSpan.FromSeconds(5); + if (!t.Wait(ts)) + { + if (!t.IsCompletedSuccessfully) returnData = string.Empty; + if (t.Exception != null) throw t.Exception; + } + + if (t.Status == TaskStatus.RanToCompletion) t.Dispose(); + } + catch (Exception ex) + { + Log.Debug("UdpClient not connect {Host}: {Port} Ex: {Ex}", Relay.Ip, Relay.Port, ex.Message); + returnData = string.Empty; + } + + if (returnData == string.Empty) + Log.Debug("UdpClient not connect {Host}: {Port}", Relay.Ip, Relay.Port); + + return returnData; + } +} \ No newline at end of file diff --git a/adas-core.module.Relays/Devices/KMTronicV2Relay.cs b/adas-core.module.Relays/Devices/KMTronicV2Relay.cs new file mode 100644 index 00000000..df2ada07 --- /dev/null +++ b/adas-core.module.Relays/Devices/KMTronicV2Relay.cs @@ -0,0 +1,187 @@ +using System.Net; +using System.Xml; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using adas_core.module.Relays.Models; +using adas_core.module.Relays.Utils; +using Serilog; + +namespace adas_core.module.Relays.Devices; + +public class KmTronicV2Relay : RelayDevice +{ + public KmTronicV2Relay(Relay relay, RelaySettings relaySettings) : base(relay, relaySettings) + { + CheckDevice(); + } + + public sealed override bool CheckDevice() + { + CheckStatus(); + var result = GetStatusRelay(1); + return result != RelayEnum.Status.Unknown; + } + + public override void CheckStatus() + { + try + { + var httpResponse = SendRequest("status.xml", HttpMethod.Get); + + if (httpResponse.StatusCode.Equals(HttpStatusCode.OK)) + { + var responseContent = httpResponse.Content; + var responseString = responseContent.ReadAsStringAsync().Result; + if (!string.IsNullOrEmpty(responseString)) + { + var response = new XmlDocument(); + response.LoadXml(responseString); + foreach (XmlNode node in response.SelectNodes("//response")!) + foreach (XmlNode childNode in node.ChildNodes) + { + if (!childNode.Name.StartsWith("relay") || string.IsNullOrEmpty(childNode.InnerText)) continue; + var id = int.Parse(childNode.Name[5..]); + if (id <= 0 || id > Relay.Total) continue; + var value = int.Parse(childNode.InnerText); + var relayStatus = value.Equals(0) ? RelayEnum.Status.Off : + value.Equals(1) ? RelayEnum.Status.On : RelayEnum.Status.Unknown; + Tuple.Create(Relay.Ip, Relay.Port, id); + SetStatusRelay(id, relayStatus); + } + } + + Log.Debug(responseContent.ReadAsStringAsync().Result); + } + else + { + Log.Debug("Status code = " + httpResponse.StatusCode); + } + } + catch (Exception ex) + { + Log.Error(ex.ToString()); + Enumerable.Range(1, Relay.Total).ToList().ForEach(outletId => + { + SetStatusRelay(outletId, RelayEnum.Status.Unknown); + }); + } + } + + + public override bool GetStatusRelay() + { + return GetStatusRelay(1) == RelayEnum.Status.On; + } + + public override Task GetStatusRelayAsync() + { + var result = GetStatusRelay(); + return Task.FromResult(result); + } + + public override void PowerOnRelay(int outletId) + { + PowerRelay(outletId, RelayEnum.Status.On); + } + + public override void PowerOffRelay(int outletId) + { + PowerRelay(outletId, RelayEnum.Status.Off); + } + + private Uri GetUri(string path, string? query = null) + { + var uriBuilder = new UriBuilder + { Scheme = "http", Host = Relay.Ip, Port = Relay.Port, Path = path }; + if (!string.IsNullOrEmpty(query)) uriBuilder.Query = query; + return uriBuilder.Uri; + } + + private HttpRequestMessage GetRequest(string path, HttpMethod method, string? query = null) + { + var uri = GetUri(path, query); + var request = new HttpRequestMessage + { + RequestUri = uri, + Method = method + }; + + if (!string.IsNullOrEmpty(Relay.Username) && !string.IsNullOrEmpty(Relay.Password)) + HttpUtils.AddBasicAuth(Relay.Username, Relay.Password, request.Headers); + return request; + } + + private HttpResponseMessage SendRequest(string path, HttpMethod method, string? query = null) + { + using var client = new HttpClient(); + client.Timeout = TimeSpan.FromSeconds(15); + try + { + return client.Send(GetRequest(path, method, query)); + } + catch (Exception e) + { + Log.Warning("Error sending request to kmtronic ip {IP} port {Port}: {Message}", Relay.Ip, + Relay.Port, e.Message); + return new HttpResponseMessage(HttpStatusCode.Gone); + } + } + + private void PowerRelay(int outletId, RelayEnum.Status toState, bool checkStatus = true) + { + if (Relay.Mode == RelayEnum.Mode.OpenedOffClosedOn) + toState = toState == RelayEnum.Status.Off ? RelayEnum.Status.On : RelayEnum.Status.Off; + + var status = GetStatusRelay(outletId); + + if (status == toState) return; + + + //No entiendo esto así, siempre enciende/apaga el mismo uriBuilder.Query = $"relay={outlet.outlet}"; + //me quedo con el nombre y solo con el número del rele que es la llamada que el hace realmente desde la web + //No uso una regex porque es mucho más lento + + try + { + SendRequest("relays.cgi", HttpMethod.Post, $"relay={outletId}"); + } + catch (Exception ex) + { + Log.Debug(ex.Message); + } + finally + { + if (checkStatus) CheckStatus(); + } + } + + public override void PowerOnAll() + { + PowerAll(RelayEnum.Status.On); + } + + public override void PowerOffAll() + { + PowerAll(RelayEnum.Status.Off); + } + + private void PowerAll(RelayEnum.Status toState) + { + Enumerable.Range(1, Relay.Total).ToList().ForEach(outletId => { PowerRelay(outletId, toState, false); }); + CheckStatus(); + } + + public override void Reboot() + { + PowerOffAll(); + if (Relay.RebootDelay is > 0) Thread.Sleep(Relay.RebootDelay!.Value); + PowerOnAll(); + } + + public override void RebootOutlet(int outletId) + { + PowerOffRelay(outletId); + Thread.Sleep(30000); + PowerOnRelay(outletId); + } +} \ No newline at end of file diff --git a/adas-core.module.Relays/Devices/RelayDevice.cs b/adas-core.module.Relays/Devices/RelayDevice.cs new file mode 100644 index 00000000..6a0bf4ab --- /dev/null +++ b/adas-core.module.Relays/Devices/RelayDevice.cs @@ -0,0 +1,86 @@ +using System.Collections.Concurrent; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using adas_core.module.Relays.Models; +using Timer = System.Timers.Timer; + +namespace adas_core.module.Relays.Devices; + +public abstract class RelayDevice +{ + private readonly ConcurrentDictionary _relaysStatus = new(); + private readonly Timer? _timer; + public readonly Relay Relay; + public readonly RelaySettings RelaySettings; + + //string host, int port, int relays, int? refreshTime, EventHandler refreshEvent + protected RelayDevice(Relay relay, RelaySettings relaySettings) + { + Relay = relay; + RelaySettings = relaySettings; + if (relaySettings.RefreshTime is > 0) + { + _timer = new Timer(); + _timer.Interval = relaySettings.RefreshTime.Value * 1000; + _timer.Elapsed += delegate + { + _timer.Stop(); + CheckStatus(); + _timer.Start(); + }; + _timer.Start(); + } + } + + public event EventHandler? RelayStatusChanged; + + public abstract bool CheckDevice(); + public abstract void CheckStatus(); + public abstract bool GetStatusRelay(); + + public abstract Task GetStatusRelayAsync(); + + public virtual RelayEnum.Status GetStatusRelay(int outletId) + { + lock (_relaysStatus) + { + return _relaysStatus.GetValueOrDefault(outletId, RelayEnum.Status.Unknown); + } + } + + public virtual void SetStatusRelay(int outletId, RelayEnum.Status status) + { + lock (_relaysStatus) + { + if (!_relaysStatus.ContainsKey(outletId) || _relaysStatus[outletId] != status) + RelayStatusChanged?.Invoke(this, outletId); + _relaysStatus[outletId] = status; + } + } + + public virtual void PowerOnOffRelay(int outletId, int milliseconds) + { + Task.Run(() => + { + PowerOffRelay(outletId); + Task.Delay(milliseconds); + PowerOnRelay(outletId); + }); + } + + public virtual void Refresh() + { + } + + public abstract void PowerOnRelay(int outletId); + + public abstract void PowerOffRelay(int outletId); + + public abstract void PowerOnAll(); + + public abstract void PowerOffAll(); + + public abstract void RebootOutlet(int outletId); + + public abstract void Reboot(); +} \ No newline at end of file diff --git a/adas-core.module.Relays/Models/PowerOutletsConfig.cs b/adas-core.module.Relays/Models/PowerOutletsConfig.cs new file mode 100644 index 00000000..87347ac9 --- /dev/null +++ b/adas-core.module.Relays/Models/PowerOutletsConfig.cs @@ -0,0 +1,24 @@ +using adas_core.Domain.Enums; + +namespace adas_core.module.Relays.Models; + +public class PowerOutletsConfig +{ + public bool PowerOffEnabled = false; + public bool ResetEnabled = true; + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public int Outlet { get; set; } + public string Type { get; set; } = RelayEnum.OutletType.Pdu.ToString(); + public string? IconType { get; set; } + public bool ShowPowerOutlet { get; set; } = false; + + + public override string ToString() + { + return "PowerOutletsConfig id" + Id + + "name=" + Name + + " outlet=" + Outlet + + " type=" + Type; + } +} \ No newline at end of file diff --git a/adas-core.module.Relays/Models/RelaySettings.cs b/adas-core.module.Relays/Models/RelaySettings.cs new file mode 100644 index 00000000..a4a28746 --- /dev/null +++ b/adas-core.module.Relays/Models/RelaySettings.cs @@ -0,0 +1,8 @@ +namespace adas_core.module.Relays.Models; + +public class RelaySettings +{ + public bool Cache { get; set; } + public int? RefreshTime { get; set; } + public string? RecordingOrApiUrl { get; set; } +} \ No newline at end of file diff --git a/adas-core.module.Relays/Utils/HttpUtils.cs b/adas-core.module.Relays/Utils/HttpUtils.cs new file mode 100644 index 00000000..f08941ac --- /dev/null +++ b/adas-core.module.Relays/Utils/HttpUtils.cs @@ -0,0 +1,15 @@ +using System.Net.Http.Headers; +using System.Text; + +namespace adas_core.module.Relays.Utils; + +public static class HttpUtils +{ + public static HttpRequestHeaders AddBasicAuth(string username, string password, HttpRequestHeaders headers) + { + var auth = Encoding.ASCII.GetBytes($"{username}:{password}"); + var clientAuthorizationHeader = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(auth)); + headers.Add("Authorization", clientAuthorizationHeader.ToString()); + return headers; + } +} \ No newline at end of file diff --git a/adas-core.module.Relays/adas-core.module.Relays.csproj b/adas-core.module.Relays/adas-core.module.Relays.csproj new file mode 100644 index 00000000..6ad62b26 --- /dev/null +++ b/adas-core.module.Relays/adas-core.module.Relays.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + adas_core.module.Relays + enable + enable + + + + + + + + + + + + diff --git a/adas-core.sln b/adas-core.sln new file mode 100644 index 00000000..0938804d --- /dev/null +++ b/adas-core.sln @@ -0,0 +1,481 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.3.32721.290 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "adas-core", "adas-core\adas-core.csproj", "{CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "adas-core.Test", "adas-core.Test\adas-core.Test.csproj", "{2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F8DD6248-80AE-4BF1-A579-CEEE735BC2D1}" + ProjectSection(SolutionItems) = preProject + .dockerignore = .dockerignore + .gitattributes = .gitattributes + .gitignore = .gitignore + .teralevel-ci.yml = .teralevel-ci.yml + Dockerfile = Dockerfile + README.md = README.md + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "adas-core.module.ProxyDevices", "adas-core.module.ProxyDevices\adas-core.module.ProxyDevices.csproj", "{769CE8C8-5441-4BF9-82B8-824660192BF9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "adas-core.Domain", "adas-core.Domain\adas-core.Domain.csproj", "{7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "adas-core.Infrastructure", "adas-core.Infrastructure\adas-core.Infrastructure.csproj", "{B402EB15-46D9-485B-B8AB-6E2618514570}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "adas-core.Application", "adas-core.Application\adas-core.Application.csproj", "{12D28524-6C55-461E-9872-AD87C660207C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "adas-core.module.LightBeacons", "adas-core.module.LightBeacons\adas-core.module.LightBeacons.csproj", "{556CF91F-CF7B-4C84-ADDA-9334223F4EE1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "adas-core.module.Relays", "adas-core.module.Relays\adas-core.module.Relays.csproj", "{E81E564E-0EDF-42EE-917D-6BAE74F13A05}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "adas-core.Authentication", "adas-core.Authentication\adas-core.Authentication.csproj", "{6C2D6D63-26E9-4196-BE84-E4B62F9D443F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "adas-core.LocalLogin", "adas-core.LocalLogin\adas-core.LocalLogin.csproj", "{D470C6A5-0B93-467A-9C54-846DDF08DD1D}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "adas-core.LdapLogin", "adas-core.LdapLogin\adas-core.LdapLogin.csproj", "{37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + DebugNoMedia|Any CPU = DebugNoMedia|Any CPU + DebugNoMedia|x64 = DebugNoMedia|x64 + DebugNoMedia|x86 = DebugNoMedia|x86 + H12O-Release|Any CPU = H12O-Release|Any CPU + H12O-Release|x64 = H12O-Release|x64 + H12O-Release|x86 = H12O-Release|x86 + HRYC-Release|Any CPU = HRYC-Release|Any CPU + HRYC-Release|x64 = HRYC-Release|x64 + HRYC-Release|x86 = HRYC-Release|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + ReleaseNoMedia|Any CPU = ReleaseNoMedia|Any CPU + ReleaseNoMedia|x64 = ReleaseNoMedia|x64 + ReleaseNoMedia|x86 = ReleaseNoMedia|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.Debug|x64.ActiveCfg = Debug|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.Debug|x64.Build.0 = Debug|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.Debug|x86.ActiveCfg = Debug|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.Debug|x86.Build.0 = Debug|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.DebugNoMedia|Any CPU.ActiveCfg = Debug|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.DebugNoMedia|Any CPU.Build.0 = Debug|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.DebugNoMedia|x64.ActiveCfg = Debug|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.DebugNoMedia|x64.Build.0 = Debug|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.DebugNoMedia|x86.ActiveCfg = Debug|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.DebugNoMedia|x86.Build.0 = Debug|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.H12O-Release|Any CPU.ActiveCfg = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.H12O-Release|Any CPU.Build.0 = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.H12O-Release|x64.ActiveCfg = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.H12O-Release|x64.Build.0 = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.H12O-Release|x86.ActiveCfg = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.H12O-Release|x86.Build.0 = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.HRYC-Release|Any CPU.ActiveCfg = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.HRYC-Release|Any CPU.Build.0 = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.HRYC-Release|x64.ActiveCfg = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.HRYC-Release|x64.Build.0 = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.HRYC-Release|x86.ActiveCfg = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.HRYC-Release|x86.Build.0 = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.Release|Any CPU.Build.0 = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.Release|x64.ActiveCfg = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.Release|x64.Build.0 = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.Release|x86.ActiveCfg = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.Release|x86.Build.0 = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.ReleaseNoMedia|Any CPU.ActiveCfg = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.ReleaseNoMedia|Any CPU.Build.0 = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.ReleaseNoMedia|x64.ActiveCfg = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.ReleaseNoMedia|x64.Build.0 = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.ReleaseNoMedia|x86.ActiveCfg = Release|Any CPU + {CD78C3FB-9F4C-4ECF-B4C0-7248BAD1BB3D}.ReleaseNoMedia|x86.Build.0 = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.Debug|x64.ActiveCfg = Debug|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.Debug|x64.Build.0 = Debug|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.Debug|x86.ActiveCfg = Debug|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.Debug|x86.Build.0 = Debug|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.DebugNoMedia|Any CPU.ActiveCfg = Debug|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.DebugNoMedia|Any CPU.Build.0 = Debug|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.DebugNoMedia|x64.ActiveCfg = Debug|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.DebugNoMedia|x64.Build.0 = Debug|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.DebugNoMedia|x86.ActiveCfg = Debug|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.DebugNoMedia|x86.Build.0 = Debug|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.H12O-Release|Any CPU.ActiveCfg = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.H12O-Release|Any CPU.Build.0 = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.H12O-Release|x64.ActiveCfg = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.H12O-Release|x64.Build.0 = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.H12O-Release|x86.ActiveCfg = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.H12O-Release|x86.Build.0 = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.HRYC-Release|Any CPU.ActiveCfg = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.HRYC-Release|Any CPU.Build.0 = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.HRYC-Release|x64.ActiveCfg = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.HRYC-Release|x64.Build.0 = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.HRYC-Release|x86.ActiveCfg = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.HRYC-Release|x86.Build.0 = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.Release|Any CPU.Build.0 = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.Release|x64.ActiveCfg = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.Release|x64.Build.0 = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.Release|x86.ActiveCfg = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.Release|x86.Build.0 = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.ReleaseNoMedia|Any CPU.ActiveCfg = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.ReleaseNoMedia|Any CPU.Build.0 = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.ReleaseNoMedia|x64.ActiveCfg = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.ReleaseNoMedia|x64.Build.0 = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.ReleaseNoMedia|x86.ActiveCfg = Release|Any CPU + {2F4DE0CA-1B6F-40C2-991D-8743B62EA9D1}.ReleaseNoMedia|x86.Build.0 = Release|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.Debug|x64.ActiveCfg = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.Debug|x64.Build.0 = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.Debug|x86.ActiveCfg = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.Debug|x86.Build.0 = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.DebugNoMedia|Any CPU.ActiveCfg = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.DebugNoMedia|Any CPU.Build.0 = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.DebugNoMedia|x64.ActiveCfg = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.DebugNoMedia|x64.Build.0 = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.DebugNoMedia|x86.ActiveCfg = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.DebugNoMedia|x86.Build.0 = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.H12O-Release|Any CPU.ActiveCfg = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.H12O-Release|Any CPU.Build.0 = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.H12O-Release|x64.ActiveCfg = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.H12O-Release|x64.Build.0 = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.H12O-Release|x86.ActiveCfg = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.H12O-Release|x86.Build.0 = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.HRYC-Release|Any CPU.ActiveCfg = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.HRYC-Release|Any CPU.Build.0 = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.HRYC-Release|x64.ActiveCfg = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.HRYC-Release|x64.Build.0 = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.HRYC-Release|x86.ActiveCfg = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.HRYC-Release|x86.Build.0 = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.Release|Any CPU.Build.0 = Release|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.Release|x64.ActiveCfg = Release|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.Release|x64.Build.0 = Release|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.Release|x86.ActiveCfg = Release|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.Release|x86.Build.0 = Release|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.ReleaseNoMedia|Any CPU.ActiveCfg = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.ReleaseNoMedia|Any CPU.Build.0 = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.ReleaseNoMedia|x64.ActiveCfg = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.ReleaseNoMedia|x64.Build.0 = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.ReleaseNoMedia|x86.ActiveCfg = Debug|Any CPU + {769CE8C8-5441-4BF9-82B8-824660192BF9}.ReleaseNoMedia|x86.Build.0 = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.Debug|x64.ActiveCfg = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.Debug|x64.Build.0 = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.Debug|x86.ActiveCfg = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.Debug|x86.Build.0 = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.DebugNoMedia|Any CPU.ActiveCfg = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.DebugNoMedia|Any CPU.Build.0 = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.DebugNoMedia|x64.ActiveCfg = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.DebugNoMedia|x64.Build.0 = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.DebugNoMedia|x86.ActiveCfg = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.DebugNoMedia|x86.Build.0 = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.H12O-Release|Any CPU.ActiveCfg = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.H12O-Release|Any CPU.Build.0 = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.H12O-Release|x64.ActiveCfg = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.H12O-Release|x64.Build.0 = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.H12O-Release|x86.ActiveCfg = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.H12O-Release|x86.Build.0 = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.HRYC-Release|Any CPU.ActiveCfg = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.HRYC-Release|Any CPU.Build.0 = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.HRYC-Release|x64.ActiveCfg = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.HRYC-Release|x64.Build.0 = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.HRYC-Release|x86.ActiveCfg = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.HRYC-Release|x86.Build.0 = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.Release|Any CPU.Build.0 = Release|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.Release|x64.ActiveCfg = Release|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.Release|x64.Build.0 = Release|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.Release|x86.ActiveCfg = Release|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.Release|x86.Build.0 = Release|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.ReleaseNoMedia|Any CPU.ActiveCfg = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.ReleaseNoMedia|Any CPU.Build.0 = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.ReleaseNoMedia|x64.ActiveCfg = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.ReleaseNoMedia|x64.Build.0 = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.ReleaseNoMedia|x86.ActiveCfg = Debug|Any CPU + {7AD9D3ED-D46B-4E05-8826-654BB30EC5DA}.ReleaseNoMedia|x86.Build.0 = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.Debug|x64.ActiveCfg = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.Debug|x64.Build.0 = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.Debug|x86.ActiveCfg = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.Debug|x86.Build.0 = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.DebugNoMedia|Any CPU.ActiveCfg = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.DebugNoMedia|Any CPU.Build.0 = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.DebugNoMedia|x64.ActiveCfg = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.DebugNoMedia|x64.Build.0 = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.DebugNoMedia|x86.ActiveCfg = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.DebugNoMedia|x86.Build.0 = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.H12O-Release|Any CPU.ActiveCfg = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.H12O-Release|Any CPU.Build.0 = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.H12O-Release|x64.ActiveCfg = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.H12O-Release|x64.Build.0 = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.H12O-Release|x86.ActiveCfg = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.H12O-Release|x86.Build.0 = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.HRYC-Release|Any CPU.ActiveCfg = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.HRYC-Release|Any CPU.Build.0 = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.HRYC-Release|x64.ActiveCfg = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.HRYC-Release|x64.Build.0 = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.HRYC-Release|x86.ActiveCfg = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.HRYC-Release|x86.Build.0 = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.Release|Any CPU.Build.0 = Release|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.Release|x64.ActiveCfg = Release|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.Release|x64.Build.0 = Release|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.Release|x86.ActiveCfg = Release|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.Release|x86.Build.0 = Release|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.ReleaseNoMedia|Any CPU.ActiveCfg = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.ReleaseNoMedia|Any CPU.Build.0 = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.ReleaseNoMedia|x64.ActiveCfg = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.ReleaseNoMedia|x64.Build.0 = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.ReleaseNoMedia|x86.ActiveCfg = Debug|Any CPU + {B402EB15-46D9-485B-B8AB-6E2618514570}.ReleaseNoMedia|x86.Build.0 = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.Debug|x64.ActiveCfg = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.Debug|x64.Build.0 = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.Debug|x86.ActiveCfg = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.Debug|x86.Build.0 = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.DebugNoMedia|Any CPU.ActiveCfg = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.DebugNoMedia|Any CPU.Build.0 = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.DebugNoMedia|x64.ActiveCfg = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.DebugNoMedia|x64.Build.0 = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.DebugNoMedia|x86.ActiveCfg = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.DebugNoMedia|x86.Build.0 = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.H12O-Release|Any CPU.ActiveCfg = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.H12O-Release|Any CPU.Build.0 = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.H12O-Release|x64.ActiveCfg = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.H12O-Release|x64.Build.0 = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.H12O-Release|x86.ActiveCfg = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.H12O-Release|x86.Build.0 = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.HRYC-Release|Any CPU.ActiveCfg = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.HRYC-Release|Any CPU.Build.0 = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.HRYC-Release|x64.ActiveCfg = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.HRYC-Release|x64.Build.0 = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.HRYC-Release|x86.ActiveCfg = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.HRYC-Release|x86.Build.0 = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.Release|Any CPU.Build.0 = Release|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.Release|x64.ActiveCfg = Release|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.Release|x64.Build.0 = Release|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.Release|x86.ActiveCfg = Release|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.Release|x86.Build.0 = Release|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.ReleaseNoMedia|Any CPU.ActiveCfg = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.ReleaseNoMedia|Any CPU.Build.0 = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.ReleaseNoMedia|x64.ActiveCfg = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.ReleaseNoMedia|x64.Build.0 = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.ReleaseNoMedia|x86.ActiveCfg = Debug|Any CPU + {12D28524-6C55-461E-9872-AD87C660207C}.ReleaseNoMedia|x86.Build.0 = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.Debug|x64.ActiveCfg = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.Debug|x64.Build.0 = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.Debug|x86.ActiveCfg = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.Debug|x86.Build.0 = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.DebugNoMedia|Any CPU.ActiveCfg = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.DebugNoMedia|Any CPU.Build.0 = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.DebugNoMedia|x64.ActiveCfg = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.DebugNoMedia|x64.Build.0 = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.DebugNoMedia|x86.ActiveCfg = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.DebugNoMedia|x86.Build.0 = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.H12O-Release|Any CPU.ActiveCfg = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.H12O-Release|Any CPU.Build.0 = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.H12O-Release|x64.ActiveCfg = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.H12O-Release|x64.Build.0 = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.H12O-Release|x86.ActiveCfg = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.H12O-Release|x86.Build.0 = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.HRYC-Release|Any CPU.ActiveCfg = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.HRYC-Release|Any CPU.Build.0 = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.HRYC-Release|x64.ActiveCfg = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.HRYC-Release|x64.Build.0 = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.HRYC-Release|x86.ActiveCfg = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.HRYC-Release|x86.Build.0 = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.Release|Any CPU.Build.0 = Release|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.Release|x64.ActiveCfg = Release|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.Release|x64.Build.0 = Release|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.Release|x86.ActiveCfg = Release|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.Release|x86.Build.0 = Release|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.ReleaseNoMedia|Any CPU.ActiveCfg = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.ReleaseNoMedia|Any CPU.Build.0 = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.ReleaseNoMedia|x64.ActiveCfg = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.ReleaseNoMedia|x64.Build.0 = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.ReleaseNoMedia|x86.ActiveCfg = Debug|Any CPU + {556CF91F-CF7B-4C84-ADDA-9334223F4EE1}.ReleaseNoMedia|x86.Build.0 = Debug|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.Debug|x64.ActiveCfg = Debug|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.Debug|x64.Build.0 = Debug|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.Debug|x86.ActiveCfg = Debug|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.Debug|x86.Build.0 = Debug|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.DebugNoMedia|Any CPU.ActiveCfg = Debug|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.DebugNoMedia|Any CPU.Build.0 = Debug|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.DebugNoMedia|x64.ActiveCfg = Debug|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.DebugNoMedia|x64.Build.0 = Debug|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.DebugNoMedia|x86.ActiveCfg = Debug|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.DebugNoMedia|x86.Build.0 = Debug|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.H12O-Release|Any CPU.ActiveCfg = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.H12O-Release|Any CPU.Build.0 = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.H12O-Release|x64.ActiveCfg = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.H12O-Release|x64.Build.0 = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.H12O-Release|x86.ActiveCfg = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.H12O-Release|x86.Build.0 = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.HRYC-Release|Any CPU.ActiveCfg = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.HRYC-Release|Any CPU.Build.0 = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.HRYC-Release|x64.ActiveCfg = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.HRYC-Release|x64.Build.0 = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.HRYC-Release|x86.ActiveCfg = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.HRYC-Release|x86.Build.0 = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.Release|Any CPU.Build.0 = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.Release|x64.ActiveCfg = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.Release|x64.Build.0 = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.Release|x86.ActiveCfg = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.Release|x86.Build.0 = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.ReleaseNoMedia|Any CPU.ActiveCfg = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.ReleaseNoMedia|Any CPU.Build.0 = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.ReleaseNoMedia|x64.ActiveCfg = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.ReleaseNoMedia|x64.Build.0 = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.ReleaseNoMedia|x86.ActiveCfg = Release|Any CPU + {E81E564E-0EDF-42EE-917D-6BAE74F13A05}.ReleaseNoMedia|x86.Build.0 = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.Debug|x64.ActiveCfg = Debug|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.Debug|x64.Build.0 = Debug|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.Debug|x86.ActiveCfg = Debug|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.Debug|x86.Build.0 = Debug|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.DebugNoMedia|Any CPU.ActiveCfg = Debug|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.DebugNoMedia|Any CPU.Build.0 = Debug|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.DebugNoMedia|x64.ActiveCfg = Debug|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.DebugNoMedia|x64.Build.0 = Debug|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.DebugNoMedia|x86.ActiveCfg = Debug|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.DebugNoMedia|x86.Build.0 = Debug|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.H12O-Release|Any CPU.ActiveCfg = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.H12O-Release|Any CPU.Build.0 = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.H12O-Release|x64.ActiveCfg = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.H12O-Release|x64.Build.0 = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.H12O-Release|x86.ActiveCfg = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.H12O-Release|x86.Build.0 = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.HRYC-Release|Any CPU.ActiveCfg = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.HRYC-Release|Any CPU.Build.0 = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.HRYC-Release|x64.ActiveCfg = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.HRYC-Release|x64.Build.0 = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.HRYC-Release|x86.ActiveCfg = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.HRYC-Release|x86.Build.0 = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.Release|Any CPU.Build.0 = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.Release|x64.ActiveCfg = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.Release|x64.Build.0 = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.Release|x86.ActiveCfg = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.Release|x86.Build.0 = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.ReleaseNoMedia|Any CPU.ActiveCfg = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.ReleaseNoMedia|Any CPU.Build.0 = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.ReleaseNoMedia|x64.ActiveCfg = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.ReleaseNoMedia|x64.Build.0 = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.ReleaseNoMedia|x86.ActiveCfg = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.ReleaseNoMedia|x86.Build.0 = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.SmacsServer|Any CPU.ActiveCfg = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.SmacsServer|Any CPU.Build.0 = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.SmacsServer|x64.ActiveCfg = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.SmacsServer|x64.Build.0 = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.SmacsServer|x86.ActiveCfg = Release|Any CPU + {6C2D6D63-26E9-4196-BE84-E4B62F9D443F}.SmacsServer|x86.Build.0 = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.Debug|x64.ActiveCfg = Debug|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.Debug|x64.Build.0 = Debug|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.Debug|x86.ActiveCfg = Debug|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.Debug|x86.Build.0 = Debug|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.DebugNoMedia|Any CPU.ActiveCfg = Debug|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.DebugNoMedia|Any CPU.Build.0 = Debug|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.DebugNoMedia|x64.ActiveCfg = Debug|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.DebugNoMedia|x64.Build.0 = Debug|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.DebugNoMedia|x86.ActiveCfg = Debug|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.DebugNoMedia|x86.Build.0 = Debug|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.H12O-Release|Any CPU.ActiveCfg = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.H12O-Release|Any CPU.Build.0 = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.H12O-Release|x64.ActiveCfg = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.H12O-Release|x64.Build.0 = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.H12O-Release|x86.ActiveCfg = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.H12O-Release|x86.Build.0 = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.HRYC-Release|Any CPU.ActiveCfg = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.HRYC-Release|Any CPU.Build.0 = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.HRYC-Release|x64.ActiveCfg = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.HRYC-Release|x64.Build.0 = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.HRYC-Release|x86.ActiveCfg = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.HRYC-Release|x86.Build.0 = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.Release|Any CPU.Build.0 = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.Release|x64.ActiveCfg = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.Release|x64.Build.0 = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.Release|x86.ActiveCfg = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.Release|x86.Build.0 = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.ReleaseNoMedia|Any CPU.ActiveCfg = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.ReleaseNoMedia|Any CPU.Build.0 = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.ReleaseNoMedia|x64.ActiveCfg = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.ReleaseNoMedia|x64.Build.0 = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.ReleaseNoMedia|x86.ActiveCfg = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.ReleaseNoMedia|x86.Build.0 = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.SmacsServer|Any CPU.ActiveCfg = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.SmacsServer|Any CPU.Build.0 = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.SmacsServer|x64.ActiveCfg = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.SmacsServer|x64.Build.0 = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.SmacsServer|x86.ActiveCfg = Release|Any CPU + {D470C6A5-0B93-467A-9C54-846DDF08DD1D}.SmacsServer|x86.Build.0 = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.Debug|Any CPU.Build.0 = Debug|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.Debug|x64.ActiveCfg = Debug|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.Debug|x64.Build.0 = Debug|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.Debug|x86.ActiveCfg = Debug|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.Debug|x86.Build.0 = Debug|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.DebugNoMedia|Any CPU.ActiveCfg = Debug|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.DebugNoMedia|Any CPU.Build.0 = Debug|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.DebugNoMedia|x64.ActiveCfg = Debug|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.DebugNoMedia|x64.Build.0 = Debug|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.DebugNoMedia|x86.ActiveCfg = Debug|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.DebugNoMedia|x86.Build.0 = Debug|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.H12O-Release|Any CPU.ActiveCfg = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.H12O-Release|Any CPU.Build.0 = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.H12O-Release|x64.ActiveCfg = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.H12O-Release|x64.Build.0 = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.H12O-Release|x86.ActiveCfg = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.H12O-Release|x86.Build.0 = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.HRYC-Release|Any CPU.ActiveCfg = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.HRYC-Release|Any CPU.Build.0 = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.HRYC-Release|x64.ActiveCfg = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.HRYC-Release|x64.Build.0 = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.HRYC-Release|x86.ActiveCfg = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.HRYC-Release|x86.Build.0 = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.Release|Any CPU.ActiveCfg = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.Release|Any CPU.Build.0 = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.Release|x64.ActiveCfg = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.Release|x64.Build.0 = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.Release|x86.ActiveCfg = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.Release|x86.Build.0 = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.ReleaseNoMedia|Any CPU.ActiveCfg = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.ReleaseNoMedia|Any CPU.Build.0 = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.ReleaseNoMedia|x64.ActiveCfg = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.ReleaseNoMedia|x64.Build.0 = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.ReleaseNoMedia|x86.ActiveCfg = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.ReleaseNoMedia|x86.Build.0 = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.SmacsServer|Any CPU.ActiveCfg = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.SmacsServer|Any CPU.Build.0 = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.SmacsServer|x64.ActiveCfg = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.SmacsServer|x64.Build.0 = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.SmacsServer|x86.ActiveCfg = Release|Any CPU + {37B9C8E3-9D31-4CEF-B943-A9DF45FD7693}.SmacsServer|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A934931F-021F-4526-8FA6-501530175DED} + EndGlobalSection +EndGlobal diff --git a/adas-core/.config/dotnet-tools.json b/adas-core/.config/dotnet-tools.json new file mode 100644 index 00000000..b8c6c1f4 --- /dev/null +++ b/adas-core/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "6.0.9", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/adas-core/.gitignore b/adas-core/.gitignore new file mode 100644 index 00000000..983058fd --- /dev/null +++ b/adas-core/.gitignore @@ -0,0 +1,198 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +#NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc +log*.txt + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# If using the old MSBuild-Integrated Package Restore, uncomment this: +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ +Documentation/Resources/Diagrams.vsd +Documentation/.~lock.HL7Fuse Developer guide.doc# +Documentation/Resources/logo dib0 full.png +Documentation/Resources/logo dib0 small.png + +# JetBrains Rider +.idea/ + +#Test Files +Test Files +.vs/ +*/bin/Debug +*/bin/Release + diff --git a/adas-core/Configurations/ServiceExtension.cs b/adas-core/Configurations/ServiceExtension.cs new file mode 100644 index 00000000..8230ff05 --- /dev/null +++ b/adas-core/Configurations/ServiceExtension.cs @@ -0,0 +1,78 @@ +using System.Text; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Providers; +using adas_core.module.ProxyDevices; +using adas_core.module.ProxyDevices.Services; +using adas_core.module.Relays.Models; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.IdentityModel.Tokens; +using Serilog; + +namespace adas_core.Configurations; + +public static class ServiceExtension +{ + public static void CreateConfigEntitiesFromAppSettingsSections(this WebApplicationBuilder builder) + { + builder.Services.Configure(builder.Configuration.GetSection("DatabaseSettings")); + builder.Services.Configure(builder.Configuration.GetSection("RabbitMQSettings")); + builder.Services.Configure>(builder.Configuration.GetSection("ProvidersSettings")); + builder.Services.Configure(builder.Configuration.GetSection("RecordingSettings")); + builder.Services.Configure(builder.Configuration.GetSection("RelaySettings")); + builder.Services.Configure(builder.Configuration.GetSection("ApiSettings")); + builder.Services.Configure(builder.Configuration.GetSection("AuthSettings")); + builder.Services.Configure(builder.Configuration.GetSection("RedisSettings")); + builder.Services.Configure(builder.Configuration.GetSection("CacheSettings")); + builder.Services.Configure(builder.Configuration.GetSection("ListSettings")); + } + + public static void UseProxyDevices( + this WebApplicationBuilder builder) + { + if (builder == null) + throw new ArgumentNullException(nameof(builder)); + + builder.Services.AddSingleton(); + builder.Services.Configure(builder.Configuration.GetSection("ProxyDevicesSettings")); + } + + public static void ConfigureJwtAuthentication(this WebApplicationBuilder builder) + { + var auth = new AuthSettings(); + builder.Configuration.GetSection("AuthSettings").Bind(auth); + var jwt = auth.JwtConfig; + if (jwt == null) + return; + + if (string.IsNullOrEmpty(jwt.Key)) + { + Log.Error("Jwt secret is empty on appSettings.json"); + throw new Exception("Cant configure jwt without secret"); + } + + builder.Services.AddAuthentication(x => + { + x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + x.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(options => + { + options.RequireHttpsMetadata = false; + options.SaveToken = true; + SymmetricSecurityKey symmetricSecurityKey = new(Encoding.UTF8.GetBytes(jwt.Key)); + options.TokenValidationParameters = new TokenValidationParameters + { + ValidateIssuer = jwt.ValidateIssuer, + ValidateAudience = jwt.ValidateAudience, + ValidateIssuerSigningKey = true, + ValidAudience = jwt.Audience, + ValidIssuer = jwt.Issuer, + IssuerSigningKey = symmetricSecurityKey + }; + + }); + + builder.Services.AddAuthorization(); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/AdmissionController.cs b/adas-core/Controllers/AdmissionController.cs new file mode 100644 index 00000000..dc32a972 --- /dev/null +++ b/adas-core/Controllers/AdmissionController.cs @@ -0,0 +1,183 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using adas_core.Logging; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; + +namespace adas_core.Controllers; + +[Route("admissions")] +[ApiController] +public class AdmissionController : ControllerBase +{ + private readonly IAdmissionService _admissionService; + private readonly ILogger _logger; + private readonly IPatientService _patientService; + private readonly IUnitService _unitService; + + public AdmissionController(ILogger logger, IAdmissionService admissionService, + IPatientService patientService, IUnitService unitService) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + _logger = logger; + _admissionService = admissionService; + _patientService = patientService; + _unitService = unitService; + } + + [HttpGet] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetAllAdmissions() + { + var admissions = await _admissionService.GetAdmissionsAsync(); + + return Ok(admissions); + } + + [HttpGet("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetAdmissionbyId(string id) + { + if (!ObjectId.TryParse(id, out var admissionId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var admission = await _admissionService.GetAdmissionByIdAsync(admissionId); + if (admission != null) return Ok(admission); + + _logger.LogDebug("Admission id: {id} not Found", id); + + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + } + + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task CreateNewAdmission([FromBody] Admission admission) + { + if (admission == null || string.IsNullOrEmpty(admission.Nhc)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var newAdmission = await _admissionService.InsertAdmission(admission); + return Ok(newAdmission); + } + + [HttpPut] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateAdmission([FromBody] Admission admission) + { + if (string.IsNullOrEmpty(admission.Nhc)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + await _admissionService.UpdateAdmissionAsync(admission); + return Ok(); + } + + [HttpDelete("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DeleteAdmissionById(string id) + { + if (!ObjectId.TryParse(id, out var admissionId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var admission = await _admissionService.GetAdmissionByIdAsync(admissionId); + if (admission == null) throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + await _admissionService.DeleteAdmissionAsync(admission); + return Ok(); + } + + [HttpPost("{id}/admission")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task AdmitPatient(string id) + { + if (!ObjectId.TryParse(id, out var admissionId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var admission = await _admissionService.GetAdmissionByIdAsync(admissionId); + if (admission == null) throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + + + var unitConfig = await _unitService.FindById(admission.UnitId); + switch (unitConfig) + { + case null: + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + case { Configuration.ManualAdmit: false }: + throw new ConflictException(HttpEnum.ErrorMessage.ServiceUnavailableCensusInAutomaticMode); + } + + await _admissionService.AdmitPatient(admission); + var patient = await _patientService.FindByPatientNumber(admission.Nhc) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictOperationIncompleted); + return Ok(patient); + } + + [HttpPost("admit/new-patient")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task AdmitNewPatient(Admission admission) + { + if (admission == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + await _admissionService.UpdateAdmissionAsync(admission); + + var unitConfig = await _unitService.FindById(admission.UnitId); + switch (unitConfig) + { + case null: + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + case { Configuration.ManualAdmit: false }: + throw new ConflictException(HttpEnum.ErrorMessage.ServiceUnavailableCensusInAutomaticMode); + } + + await _admissionService.AdmitPatient(admission); + var patient = await _patientService.FindByPatientNumber(admission.Nhc) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictOperationIncompleted); + return Ok(patient); + } + + [HttpPost("{id}/patient-to-admission")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ReturnPatientToAdmission(string id) + { + if (!ObjectId.TryParse(id, out var patientId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var patient = await _patientService.FindById(patientId); + var unitConfig = await _unitService.FindById(patient!.UnitId); + switch (unitConfig) + { + case null: + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + case { Configuration.ManualMove: false }: + throw new ConflictException(HttpEnum.ErrorMessage.ServiceUnavailableCensusInAutomaticMode); + default: + await _admissionService.ReturnPatientToAdmissions(patient.Id); + return Ok(); + } + } + + [HttpPost("{id}/temporal-bed")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ReturnToAdmissionFromTemporalBed([FromBody] Admission admission, string id) + { + if (!ObjectId.TryParse(id, out var patientId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var patient = await _patientService.FindById(patientId); + await _admissionService.ReturnPatientToAdmissions(patient!.Id, admission); + return Ok(); + } + + [HttpGet("{patientNumber}/{unitId}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ReturnPatientToAdmission(string patientNumber, string unitId) + { + if (!ObjectId.TryParse(unitId, out var unitObjectId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + return Ok(await _admissionService.SearchByPatientNumberAndDistinctUnit(patientNumber, unitObjectId)); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/AlarmController.cs b/adas-core/Controllers/AlarmController.cs new file mode 100644 index 00000000..6696150c --- /dev/null +++ b/adas-core/Controllers/AlarmController.cs @@ -0,0 +1,51 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.DTO; +using adas_core.Logging; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace adas_core.Controllers; + +[Route("alarm-observations")] +[ApiController] +public class AlarmController : ControllerBase +{ + private readonly IAlarmService _alarmService; + private readonly ILogger _logger; + + public AlarmController(ILogger logger, IAlarmService alarmService) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + _logger = logger; + + _alarmService = alarmService; + } + + [AuthorizeRoles(PermissionEnum.RolesType.AuthAdmin)] + [HttpPost] + [Route("")] + public IActionResult ProcessRequest([FromBody] ObservationAlarmDto alarmDto) + { + var request = new ApiRequest + { + TraceId = Guid.NewGuid().ToString(), + Type = "ORU_R40", + PatientNumber = alarmDto.PatientNumber, + Location = alarmDto.Location, + Alarms = alarmDto.Alarms, + MessageTime = alarmDto.MessageTime, + WaitResult = alarmDto.WaitResult, + ObsertationData = alarmDto.ObservationData + }; + + _logger.LogDebug("Alarm received: {request}", request.ToString()); + + if (request.WaitResult) _alarmService.SaveRequest(request); + else _alarmService.SaveRequestAsync(request); + return Ok(request); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/ApplicationController.cs b/adas-core/Controllers/ApplicationController.cs new file mode 100644 index 00000000..6e671f21 --- /dev/null +++ b/adas-core/Controllers/ApplicationController.cs @@ -0,0 +1,102 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Logging; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace adas_core.Controllers; + +[Route("application")] +[ApiController] +public class ApplicationController : ControllerBase +{ + private readonly IClientMessageService _clientMessageService; + + //Logger logger + //private readonly ILogger _loggerADMPanel; + private readonly IFileService _fileService; + private readonly ICacheService? _redisService; + private readonly ISubscriberGroupedService _subscriberGroupedService; + private readonly ISubscribersService _subscribersService; + + public ApplicationController( + IFileService fileService, + ISubscribersService subscribersService, + ISubscriberGroupedService subscriberGroupedService, + IClientMessageService clientMessageService, + ICacheService? redisService) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + _fileService = fileService; + _subscribersService = subscribersService; + _subscriberGroupedService = subscriberGroupedService; + _clientMessageService = clientMessageService; + _redisService = redisService; + } + + + [HttpGet("subscribers")] + [AllowAnonymous] + public IActionResult GetSubscribersVersion() + { + return Ok(_subscribersService.GetSubscribers()); + } + + [HttpGet("subscribers-group")] + [AllowAnonymous] + public IActionResult GetSubscribersGroup() + { + return Ok(_subscriberGroupedService.GetGrouped()); + } + + [AuthorizeRoles(PermissionEnum.RolesType.AuthAdmin)] + [HttpPost] + public IActionResult UpdateApplications([FromBody] List boxes) + { + _clientMessageService.SendUpdateMessageToBoxes(boxes); + return Ok(); + } + + [AuthorizeRoles(PermissionEnum.RolesType.AuthAdmin)] + [HttpPost("upload")] + [RequestSizeLimit(300_000_000)] + public async Task UploadFile(List files) + { + await _fileService.CopyUpdateFiles(files); + return Ok(); + } + + [AuthorizeRoles(PermissionEnum.RolesType.AuthAdmin)] + [HttpPost("upload/{theme}/{category}/asset")] + [RequestSizeLimit(300_000_000)] + public async Task UploadAssetFile(string theme, List files) + { + var isParsed = Enum.TryParse(theme, out var themeParsed); + if (!isParsed) throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + await _fileService.UploadAssetFiles(files, themeParsed); + + return Ok(); + } + + [AuthorizeRoles(PermissionEnum.RolesType.AuthAdmin)] + [HttpGet("{theme}/{category}/asset")] + public ActionResult UGetAssetFile(string theme) + { + var isParsed = Enum.TryParse(theme, out var themeParse); + if (!isParsed) throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + return Ok(_fileService.GetAllAssetFile(themeParse)); + } + + [AuthorizeRoles(PermissionEnum.RolesType.AuthAdmin)] + [HttpGet("cleaning")] + public ActionResult CleanCache() + { + if (_redisService == null) throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + _redisService.CleanCache(); + return Ok(); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/AuditController.cs b/adas-core/Controllers/AuditController.cs new file mode 100644 index 00000000..8ba7645b --- /dev/null +++ b/adas-core/Controllers/AuditController.cs @@ -0,0 +1,49 @@ +using adas_core.Application.Exceptions; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using audit_logs.Models.DTO; +using audit_logs.Services.Interfaces; +using Microsoft.AspNetCore.Mvc; + +namespace adas_core.Controllers; + +[Route("audits")] +[ApiController] +public class AuditController(IAuditService auditService) : ControllerBase // Hereda de ControllerBase +{ + /// + /// Recupera registros de auditoria basándose en criterios de búsqueda en cualquier campo de texto y fechas. + /// + /// + /// DTO que contiene los parámetros de búsqueda para los registros de auditoria. Incluye: + /// - pageNumber: Número de página para la paginación de resultados. + /// - pageSize: Cantidad de registros por página. + /// - searchText: Texto libre para buscar en todos los campos textuales (RecordId, EntityType, ActionType)de los + /// registros de auditoria. Opcional + /// - actionTime: Fecha concreta en la que se busca un registro de auditoria, para algún contexto puede ser + /// util. Opcional + /// - startDate: Fecha de inicio para filtrar los registros por rango de fechas. Opcional + /// - endDate: Fecha de fin para el filtro por rango de fechas. Opcional + /// + /// + /// Una respuesta de acción que puede ser una lista de registros de auditoria o un mensaje de error. + /// Retorna Ok con los registros encontrados o BadRequest si hay un error en los parámetros. + /// + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + [HttpPost] + public async Task GetAuditRecordsByAnyTextField( + [FromBody] AuditLogTextOrDateSearchDTO auditRequestDto) + { + try + { + var result = await auditService.GetAuditLogsMatchingTextOrDateAsync(auditRequestDto); + return Ok(result); + } + catch (ArgumentException ex) + { + Console.WriteLine(ex.Message); + throw new CustomArgumentException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + } + } +} \ No newline at end of file diff --git a/adas-core/Controllers/BoxController.cs b/adas-core/Controllers/BoxController.cs new file mode 100644 index 00000000..45d8b854 --- /dev/null +++ b/adas-core/Controllers/BoxController.cs @@ -0,0 +1,67 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Authentication.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.Masters; +using Microsoft.AspNetCore.Mvc; + +namespace adas_core.Controllers; + +[Route("Box")] +[ApiController] +public class BoxController : ControllerBase +{ + private readonly IUnitService _unitService; + private readonly IPatientService _patientService; + private readonly IMasterListService _isulationListService; + private readonly IPointOfCareService _pointOfCareService; + private readonly IUserService _userService; + + public BoxController(IUnitService unitService, IPatientService patientService, IMasterListService isulationListService, IPointOfCareService pointOfCareService, IUserService userService) + { + _unitService = unitService; + _patientService = patientService; + _isulationListService = isulationListService; + _pointOfCareService = pointOfCareService; + _userService = userService; + } + + [HttpGet("{boxNumber}/{elementSwitjAction}/switch-data")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task SwitchData(string boxNumber, string elementSwitjAction) + { + // Necesito buscar el por el boxNumber la unidad a la que pertenece + // Encontrar la lista que actua sobre la observacion que quiero cambiar en base al action + // Enviar un valor aleatorio diferente del que este en uso + var userNameContext = HttpContext.User.Identity?.Name ?? + throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedNoPermission); + var user = await _userService.GetUserByUserName(userNameContext); + var unit = await _unitService.FindByUnitNameOrPocName(null, boxNumber); + switch (elementSwitjAction) + { + case "Isolation": + var list = await _isulationListService.GetMasterListById(unit!.InsulationListId!.Value, null); + var option = list!.Options[Random.Shared.Next(list.Options.Count)]; + var poc = await _pointOfCareService.FindByBedAndUnitId(boxNumber, unit.Id); + var data = await _pointOfCareService.GetInfo(poc!.Id, null, true); + var options = new List + { + new() + { + Id = option.Id, + Name = option.Name, + Color = option.Color, + BgColor = option.BgColor, + } + }; + await _patientService.UpdatePatientMasterList(data!.Patient!.Id, MasterListType.InsulationList, options, user, options); + + + break; + case "Mobility": + break; + } + return Ok(); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/CameraController.cs b/adas-core/Controllers/CameraController.cs new file mode 100644 index 00000000..0877298f --- /dev/null +++ b/adas-core/Controllers/CameraController.cs @@ -0,0 +1,54 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; + +namespace adas_core.Controllers; + +[Route("camera")] +[ApiController] +public class CameraController(ICameraService cameraService) : ControllerBase +{ + [HttpPost("paginated")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task GetPaginatedCamera([FromBody] PaginationFilter request, bool withPoCs) + { + if (request == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var cameras = await cameraService.GetPaginatedCameras(request); + return Ok(cameras); + } + [HttpGet("{textToSearch}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task GetSearchByName(string textToSearch) + { + var cameras = await cameraService.GetSearchByNameCameras(textToSearch); + return Ok(cameras); + } + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task CreateCamera([FromBody] Camera camera) + { + var cam = await cameraService.InsertCamera(camera); + return Ok(cam); + } + [HttpPut("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task UpdateCamera(string id, [FromBody] Camera camera) + { + if (!ObjectId.TryParse(id, out var idParsed)) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var cameraUpdated = await cameraService.UpdateCameraById(idParsed, camera); + return Ok(cameraUpdated); + } + [HttpDelete("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task DeleteCamera(string id) + { + if (!ObjectId.TryParse(id, out var idParsed)) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var cameraUpdated = await cameraService.DeleteCamera(idParsed); + return Ok(cameraUpdated); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/ConfigDisplayController.cs b/adas-core/Controllers/ConfigDisplayController.cs new file mode 100644 index 00000000..d08b4155 --- /dev/null +++ b/adas-core/Controllers/ConfigDisplayController.cs @@ -0,0 +1,437 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.DTO.Display; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Utils; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; +using Newtonsoft.Json; + +namespace adas_core.Controllers; + +[Route("config-displays")] +[ApiController] +public class ConfigDisplayController( + IDisplayConfigService displayConfigService) + : ControllerBase +{ + + /// + /// GetByCodeSysAndCode all display config list + /// + /// + /// List + /// + [HttpGet] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task List() + { + return Ok(await displayConfigService.GetAll()); + } + + /// + /// GetByCodeSysAndCode all display config compact paginated + /// + /// + /// List + /// + [HttpPost("paginated")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ListCompactPaginated([FromBody] PaginationFilter request) + { + return Ok(await displayConfigService.GetAllCompactPaginated(request)); + } + + /// + /// GetByCodeSysAndCode all display config compact + /// + /// + /// List + /// + [HttpGet("compact")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task ListCompact() + { + return Ok(await displayConfigService.GetAllCompact()); + } + + /// + /// GetByCodeSysAndCode all display config list by type + /// + /// + /// List + /// + [HttpGet("{type}/type")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ListByType(DisplayConfigEnums.DisplayType type) + { + return Ok(await displayConfigService.GetByType(type)); + } + + /// + /// GetByCodeSysAndCode display config by it object id + /// + /// + /// displayConfig + /// + [HttpGet("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ListById(string id) + { + if (!ObjectId.TryParse(id, out var objectId)) + throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var result = await displayConfigService.GetById(objectId); + return Ok(result); + } + + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task Insert([FromBody] DisplayConfig config) + { + var result = await displayConfigService.InsertOne(config); + return Ok(result); + } + + [HttpPost("compact")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task InsertMinimal([FromBody] CreateDisplayConfigDto config) + { + var result = config.DisplayConfigIdTemplate != null + ? await displayConfigService.InsertOneWithTemplate(config.DisplayConfigIdTemplate, config.Type, + config.Hospital) + : await displayConfigService.InsertOneMinimal(config); + return Ok(result); + } + + [HttpGet("insertion-test")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task InsertNurse() + { + var result = await displayConfigService.InsertOneTest(); + return Ok(result); + } + + /// + /// Update display Config and notify subscribers + /// + /// + /// display + /// + [HttpPut("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateConfig(string id, [FromBody] object newDisplayConfig) + { + if (!ObjectId.TryParse(id, out var objectId)) + throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + var result = await displayConfigService.UpdateConfig(objectId, newDisplayConfig); + return Ok(result); + } + + [HttpPut("{idConfig}/color-config")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateConfigColor(string idConfig, ColorConfig colorConfig) + { + if (!ObjectId.TryParse(idConfig, out var objectIdConfigDisplay)) + throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var result = await displayConfigService.UpdateConfigColor(objectIdConfigDisplay, colorConfig); + + return result ? Ok(result) : Ok((int)HttpEnum.ErrorMessage.OkResouceNotModified); + } + + [HttpPut("{idConfig}/header-config")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateHeaderConfig(string idConfig, HeaderConfig headerConfig) + { + if (!ObjectId.TryParse(idConfig, out var objectIdConfigDisplay)) + throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var result = await displayConfigService.UpdateHeaderConfig(objectIdConfigDisplay, headerConfig); + + return result ? Ok(result) : Ok((int)HttpEnum.ErrorMessage.OkResouceNotModified); + } + + [HttpPut("{idConfig}/home-banner")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateSetHomeBanner(string idConfig, List bannerItems) + { + if (!ObjectId.TryParse(idConfig, out var objectIdConfigDisplay)) + throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var displayConfig = await displayConfigService.GetById(objectIdConfigDisplay) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + if (displayConfig.Type != DisplayConfigEnums.DisplayType.DisplayNurse) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + var result = await displayConfigService.UpdateSetHomeBanner(objectIdConfigDisplay, bannerItems); + return result ? Ok(result) : Ok((int)HttpEnum.ErrorMessage.OkResouceNotModified); + } + + [HttpPut("update/base-config")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateBaseConfig(DisplayConfig baseConfig) + { + var result = await displayConfigService.UpdateBaseConfig(baseConfig); + return result ? Ok(result) : Ok(HttpEnum.ErrorMessage.OkResouceNotModified); + } + + [HttpPut("update/card-config")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateCardConfig(CardConfig baseConfig) + { + var result = await displayConfigService.UpdateCardConfig(baseConfig); + return result ? Ok(result) : Ok(HttpEnum.ErrorMessage.OkResouceNotModified); + } + + [HttpPut("update/detail-config")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateDetailConfig(CardDetailsConfig baseConfig) + { + var result = await displayConfigService.UpdateDetailConfig(baseConfig); + return result ? Ok(result) : Ok(HttpEnum.ErrorMessage.OkResouceNotModified); + } + + [HttpPut("update/chart-config")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateChartConfig(ChartConfig baseConfig) + { + var result = await displayConfigService.UpdateChartConfig(baseConfig); + return result ? Ok(result) : Ok(HttpEnum.ErrorMessage.OkResouceNotModified); + } + + [HttpPut("{idConfig}/field-list")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateFieldList(string idConfig, List fields) + { + if (!ObjectId.TryParse(idConfig, out var objectIdConfigDisplay)) + throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var result = await displayConfigService.UpdateFieldList(objectIdConfigDisplay, fields); + return Ok(result); + } + + [HttpPost("{idConfig}/name")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateName(string idConfig, UpdateDisplayConfigNameDto updateDisplayConfigNameDto) + { + if (!ObjectId.TryParse(idConfig, out var objectIdConfigDisplay)) + throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var displayConfigName = updateDisplayConfigNameDto.DisplayName; + if (displayConfigName.IsEmpty()) + throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var result = + await displayConfigService.UpdateDisplayConfigHospitalName(objectIdConfigDisplay, displayConfigName); + return result ? Ok(result) : Ok((int)HttpEnum.ErrorMessage.OkResouceNotModified); + } + + [HttpPost("card-config")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task InsertCardConfig(CreateDisplayConfigCardDto updateDisplayConfigNameDto) + { + var result = await displayConfigService.InsertCardConfig(updateDisplayConfigNameDto); + return Ok(result); + } + + [HttpPost("detail-config")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task InsertCardDetailConfig(CreateDisplayConfigCardDto updateDisplayConfigNameDto) + { + var result = await displayConfigService.InsertCardDetailConfig(updateDisplayConfigNameDto); + return Ok(result); + } + + [HttpPost("chart-config")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task InsertChartConfig(CreateDisplayConfigCardDto updateDisplayConfigNameDto) + { + var result = await displayConfigService.InsertChartConfig(updateDisplayConfigNameDto); + return Ok(result); + } + + [HttpGet("get-all-card-config")] + [AllowAnonymous] + public async Task GetAllCardConfig() + { + var result = await displayConfigService.GetCardConfigAll(); + return Ok(result); + } + + [HttpDelete("{idConfig}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DeleteDisplayConfig(string idConfig) + { + if (!ObjectId.TryParse(idConfig, out var objectIdConfigDisplay)) + throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var result = await displayConfigService.DeleteDisplayConfig(objectIdConfigDisplay); + if (result) return Ok(result); + + throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); + } + + [HttpDelete("chart-config/{idConfig}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DeleteChartConfig(string idConfig) + { + if (!ObjectId.TryParse(idConfig, out var objectIdConfigDisplay)) + throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var result = await displayConfigService.DeleteChartConfig(objectIdConfigDisplay); + if (result) return Ok(result); + + throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); + } + + [HttpGet("chart-config/{idConfig}")] + [AllowAnonymous] + public async Task GetChartConfig(string idConfig) + { + if (!ObjectId.TryParse(idConfig, out var objectIdConfigDisplay)) + throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var result = await displayConfigService.GetChartConfig(objectIdConfigDisplay); + if (result != null) return Ok(result); + + throw new NotFoundException(); + } + + [HttpGet("{idConfig}/locations")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetDisplayConfigLocations(string idConfig) + { + if (!ObjectId.TryParse(idConfig, out var objectIdConfigDisplay)) + throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + return Ok(await displayConfigService.GetDisplayConfigLocations(objectIdConfigDisplay)); + } + + + /// + /// Search media element + /// + /// display config id + /// + /// File stream + /// + //[AuthorizeRoles(RolesType.Some)] + //[TypeFilter(type: typeof(AuthorizePermissionsAttribute), Arguments = new object[] { "DisplayId", AuthorizeUserByServiceType.CONFIGDISPLAY })] + [HttpGet] + [AllowAnonymous] + [Route("{id}/getMedia/{filename}")] + public async Task StreamMediaById(string filename, string id) + { + try + { + // Validar el ID + if (!ObjectId.TryParse(id, out var configDisplayId)) + throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + // Obtener la configuraci�n y ruta + var config = await displayConfigService.GetById(configDisplayId); + var sharedFolderPath = config.MediaFolder; + + // Validar existencia de carpeta y archivo + if (!Directory.Exists(sharedFolderPath)) return NotFound("Shared Folder Not Exists"); + + var filePath = Path.Combine(sharedFolderPath, filename); + if (!System.IO.File.Exists(filePath)) return NotFound("File not found"); + + var fileInfo = new FileInfo(filePath); + var fileLength = fileInfo.Length; + + // Leer el encabezado "Range" + var rangeHeader = Request.Headers["Range"].ToString(); + var end = fileLength - 1; + + if (!string.IsNullOrEmpty(rangeHeader)) + { + // Parsear el rango solicitado + var range = rangeHeader.Replace("bytes=", "").Split('-'); + var start = long.Parse(range[0]); + if (range.Length > 1 && !string.IsNullOrEmpty(range[1])) end = long.Parse(range[1]); + + // Validar el rango solicitado + if (start >= fileLength || end >= fileLength || start > end) + return StatusCode(416, $"Requested Range Not Satisfiable. File length: {fileLength}"); + } + + // Crear un flujo continuo para transmitir los datos + return new FileStreamResult(new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read), + MediaUtils.GetMimeType(filePath)) + { + EnableRangeProcessing = true + }; + } + catch (Exception ex) + { + return StatusCode(500, $"Error al procesar la solicitud: {ex.Message}"); + } + } + + + /// + /// GetByCodeSysAndCode all media files in a shared folder + /// + /// display config id + /// File details list + /// + [AllowAnonymous] + [HttpGet("allMediaFiles/{id}")] + public async Task GetAllMediaFiles(string id) + { + try + { + if (!ObjectId.TryParse(id, out var configDisplayId)) + throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + var config = await displayConfigService.GetById(configDisplayId); + + var sharedFolderPath = config.MediaFolder; // @"\\10.0.20.75\smartuci-nurseplan\ad-media"; + + // Verificar si la carpeta existe + if (!Directory.Exists(sharedFolderPath)) return NotFound("Shared Folder Not Exists"); + + // Obtener todos los archivos en la carpeta + var files = Directory.GetFiles(sharedFolderPath); + + var fileDetails = files.Select(file => new + { + Name = Path.GetFileName(file), + Type = MediaUtils.GetMimeType(file), + SizeInBytes = new FileInfo(file).Length, + Duration = MediaUtils.IsVideoFile(file) + ? MediaUtils.GetVideoDuration(file) + : null // S�lo calcula duraci�n para videos + }); + + return Ok(JsonConvert.SerializeObject(fileDetails, new JsonSerializerSettings + { + Formatting = Formatting.None + })); + } + catch (Exception ex) + { + return StatusCode(500, $"Error al procesar la solicitud: {ex.Message}"); + } + } +} \ No newline at end of file diff --git a/adas-core/Controllers/ConfigObservationController.cs b/adas-core/Controllers/ConfigObservationController.cs new file mode 100644 index 00000000..deb711c0 --- /dev/null +++ b/adas-core/Controllers/ConfigObservationController.cs @@ -0,0 +1,227 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using adas_core.Logging; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; + +namespace adas_core.Controllers; + +[Route("config-observations")] +[ApiController] +public class ConfigObservationController : ControllerBase +{ + private readonly IConfigObservationService _configObservationService; + private readonly IHistoricalConfigChangesService _historicalConfigChangesService; + + public ConfigObservationController(IConfigObservationService configObservationService, + IHistoricalConfigChangesService historicalConfigChangesService) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + _configObservationService = configObservationService; + _historicalConfigChangesService = historicalConfigChangesService; + } + + [HttpGet] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetActiveConfigObservation() + { + var configObservation = await _configObservationService.GetAllConfigs() ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + return Ok(configObservation); + } + + /// + /// GetByCodeSysAndCode compact configs + /// + /// + [HttpGet("compact")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetAllCompact() + { + var configObservation = await _configObservationService.GetAllCompact(); + return Ok(configObservation); + } + + + /// + /// Get config by id + /// + /// + /// + [HttpGet("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task FindConfigObservationById(string id) + { + if (!ObjectId.TryParse(id, out var objectId)) + throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var configObservation = await _configObservationService.GetConfigById(objectId) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + return Ok(configObservation); + } + + #region ADMPanel + + /// + /// Get a list of paginated items + /// + /// + [HttpPost("paginated-items")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetPaginatedItems([FromBody] PaginationFilter filter) + { + var configObservation = await _configObservationService.GetPaginatedItems(filter); + + return Ok(configObservation); + } + + + /// + /// Post new config observation + /// + /// + /// + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task CreateConfigObservation([FromBody] ConfigObservation configObservation) + { + var newConfig = await _configObservationService.CreateConfig(configObservation); + if (newConfig == null) + return Conflict($"config with id: {configObservation.Id} already exists"); + + await LogHistoricalConfigChanged(configObservation, null); + + return Ok(newConfig); + } + + /// + /// Post new observation + /// + /// + /// + [HttpPost("item")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task CreateObservation([FromBody] ConfigObservation configObservationItem) + { + var newObservation = await _configObservationService.CreateConfig(configObservationItem) + ?? throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed); + + return Ok(newObservation); + } + + + + /// + /// Update config observation item + /// + /// + /// + /// + /// + [HttpPut("{configId}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateConfigObservationItem(string configId, + [FromBody] ConfigObservation configObservationItem) + { + if (!ObjectId.TryParse(configId, out var objectId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + + var oldConfigObservation = await _configObservationService.GetConfigById(objectId) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + var configObservation = await _configObservationService.UpdateConfig(configObservationItem); + await LogHistoricalConfigChanged(configObservation, oldConfigObservation); + return Ok(true); + } + + /// + /// Get by name config observation item list + /// + /// + /// + [HttpGet("itemName/{itemName}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetConfigObservationItemByName(string itemName) + { + var oldConfigObservation = await _configObservationService.GetConfigObservationItemsByName(itemName); + return Ok(oldConfigObservation); + } + + /// + /// GetByCodeSysAndCode config observation item + /// + /// + /// + [HttpGet("item/by-code-sys-and-code/{data}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetSingleConfigObservationItem(string data) + { + var parts = data.Split('-'); + var code = parts.Length > 0 ? parts[0] : string.Empty; + var codingSystem = parts.Length > 1 ? parts[1] : string.Empty; + var name = parts.Length > 2 ? parts[2] : string.Empty; + var originalName = parts.Length > 3 ? parts[3] : string.Empty; + var item = await _configObservationService.GetSingleConfigObservationItem(code, codingSystem, name, + originalName); + return Ok(item); + } + + + [HttpDelete("delete/item")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DeleteSingleConfigObservationItem( + [FromBody] ConfigObservation configObservationItem) + { + if (configObservationItem == null) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var item = await _configObservationService.DeleteSingleConfigObservationItem(configObservationItem); + return Ok(item); + } + + /// + /// GetByCodeSysAndCode config all observation items by name + /// + /// + /// + [HttpGet("{itemName}/name")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetConfigObservationItemsByName(string itemName) + { + if (itemName.Length < 2) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectLength); + var items = await _configObservationService.GetConfigObservationItemsByName(itemName); + return Ok(items); + } + + [HttpGet("get-name-obs")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetConfigNames() + { + var items = await _configObservationService.GetConfigNames(); + return Ok(items); + } + + private async Task LogHistoricalConfigChanged(ConfigObservation? newConfigObservation, + ConfigObservation? oldConfigObservation) + { + var user = HttpContext.User.Identity?.Name ?? HttpContext.Connection.RemoteIpAddress?.ToString(); + await _historicalConfigChangesService.LogConfigChanged(user!, DisplayConfigEnums.ConfigTypes.ObservationCfg, + newConfigObservation?.ToJson() ?? string.Empty, oldConfigObservation?.ToJson() ?? string.Empty); + } + + #endregion +} \ No newline at end of file diff --git a/adas-core/Controllers/DeviceController.cs b/adas-core/Controllers/DeviceController.cs new file mode 100644 index 00000000..bf5c9192 --- /dev/null +++ b/adas-core/Controllers/DeviceController.cs @@ -0,0 +1,57 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.DTO; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; + +namespace adas_core.Controllers; + +[Route("device")] +[ApiController] +public class DeviceController : ControllerBase +{ + private readonly IDeviceService _deviceService; + + public DeviceController(IDeviceService deviceService) + { + _deviceService = deviceService; + } + + [HttpPost("add")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task CreateDevice([FromBody] DeviceDto device) + { + var result = await _deviceService.Create(device); + if(result is null) Conflict("Unable to create device"); + return Ok(result); + } + + [HttpDelete("delete/{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task CreateDevice(string id) + { + if(!ObjectId.TryParse(id, out ObjectId deviceId)) return BadRequest("Invalid device Id"); + var result = await _deviceService.Delete(deviceId); + if(result is false) Conflict("Unable to delete device"); + return Ok(result); + } + + [HttpPut("edit")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task EditDevice([FromBody] DeviceDto device) + { + var result = await _deviceService.Update(device); + if(result is null) Conflict("Unable to update device"); + return Ok(result); + } + + [HttpPost("event")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task ReceiveEventDevice([FromBody] DeviceDto device) + { + var result = await _deviceService.ReceiveEvent(device); + if(result is null) Conflict("Unable to create device"); + return Ok(); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/DiagnosisController.cs b/adas-core/Controllers/DiagnosisController.cs new file mode 100644 index 00000000..2315f0bb --- /dev/null +++ b/adas-core/Controllers/DiagnosisController.cs @@ -0,0 +1,49 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.DTO; +using adas_core.Logging; +using Microsoft.AspNetCore.Mvc; + +namespace adas_core.Controllers; + +[Route("diagnosis")] +[ApiController] +public class DiagnosisController : ControllerBase +{ + private readonly IDiagnosisService _diagnosisService; + + public DiagnosisController(IDiagnosisService diagnosisService) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + _diagnosisService = diagnosisService; + } + + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public IActionResult ProcessRequest([FromBody] DiagnosisDto diagnosisDto) + { + var request = new ApiRequest + { + TraceId = Guid.NewGuid().ToString(), + Type = "ORU_R01", + PatientNumber = diagnosisDto.PatientNumber, + Location = diagnosisDto.Location, + ObservationData = diagnosisDto.ObservationData, + Observations = diagnosisDto.Observations, + Diagnosis = diagnosisDto.Diagnosis, + MessageTime = diagnosisDto.MessageTime, + WaitResult = diagnosisDto.WaitResult + }; + + if (request.WaitResult) + _diagnosisService.SaveRequest(request); + else + _diagnosisService.SaveRequestAsync(request); + + return Ok(); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/DischargeController.cs b/adas-core/Controllers/DischargeController.cs new file mode 100644 index 00000000..1c3afe7e --- /dev/null +++ b/adas-core/Controllers/DischargeController.cs @@ -0,0 +1,102 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using adas_core.Logging; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; + +namespace adas_core.Controllers; + +[Route("discharges")] +[ApiController] +public class DischargeController : ControllerBase +{ + private readonly IDischargeService _dischargeService; + private readonly IPatientService _patientService; + + public DischargeController(IDischargeService dischargeService, IPatientService patientService) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + _dischargeService = dischargeService; + _patientService = patientService; + } + + [HttpGet] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetAllDischarges() + { + var discharges = await _dischargeService.GetDischargesAsync(); + return Ok(discharges); + } + + [HttpGet("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetDischargebyId(string id) + { + if (!ObjectId.TryParse(id, out var dischargeId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var discharge = await _dischargeService.GetDischargeByIdAsync(dischargeId); + return Ok(discharge); + } + + [HttpPost("patient")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DischargePatient([FromBody] Discharge discharge) + { + if (discharge == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var patient = await _patientService.FindById(discharge.PatientId); + discharge.Patient = patient; + + await _dischargeService.InsertDischarge(discharge); + return Ok(); + } + + [HttpPut] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateDischarge([FromBody] Discharge discharge) + { + if (discharge == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var c = await _dischargeService.GetDischargeByIdAsync(discharge.Id); + if (discharge.UnitId == null && c?.UnitId != null) discharge.UnitId = c.UnitId; + await _dischargeService.UpdateDischargeAsync(discharge); + + return Ok(); + } + + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task InsertDischarge([FromBody] Discharge discharge) + { + if (discharge == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var dis = await _dischargeService.InsertDischarge(discharge); + + return Ok(dis); + } + + [HttpDelete("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DeleteDischargeById(string id) + { + if (!ObjectId.TryParse(id, out var dischargeId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var discharge = await _dischargeService.GetDischargeByIdAsync(dischargeId); + var patient = await _patientService.FindById(discharge!.PatientId); + discharge.Patient = patient; + if ("Altable".Equals(patient!.Altable?.OptionType)) + { + await _dischargeService.DeleteDischargeAsync(discharge); + return Ok(); + } + + throw new ConflictException(HttpEnum.ErrorMessage.ConflictOperationIncompleted); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/DisplayController.cs b/adas-core/Controllers/DisplayController.cs new file mode 100644 index 00000000..8e1308d2 --- /dev/null +++ b/adas-core/Controllers/DisplayController.cs @@ -0,0 +1,307 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Utils; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; + +namespace adas_core.Controllers; + +[Route("displays")] +[ApiController] +public class DisplayController( + IDisplayService displayService, + IDisplayConfigService displayConfigService) + : ControllerBase +{ + /// + /// Get all display list + /// + /// + /// List + /// + [HttpGet] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + //[PermissionAuthorize("Source", "DisplayId")] + public async Task List() + { + var username = JwtHelper.GetUsernameFromPrincipal(HttpContext.User); + return Ok(await displayService.GetAllByUser(username)); + } + + [HttpGet("compact")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ListCompact() + { + return Ok(await displayService.GetAllCompact()); + } + + [HttpPost("paginated")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetPaginatedDisplays([FromBody] PaginationFilter request) + { + if (request == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var displays = await displayService.GetPaginatedDisplays(request); + + return Ok(displays); + } + + /// + /// Get By type all display list + /// + /// + /// List + /// + [HttpGet("{type}/type")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ListByType(DisplayConfigEnums.DisplayType type) + { + return Ok(await displayService.GetByType(type)); + } + + /// + /// Get By type all minimal display list on a DTO + /// + /// + /// List + /// + [HttpGet("{type}/compact")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetAllMinimalListByType() + { + return Ok(await displayService.GetAllDisplaySection()); + } + + /// + /// Get all display list by pointOfCare + /// + /// + /// List + /// + [HttpPost("poc")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ListByPointOfCare([FromBody] PointOfCare pointOfCare) + { + return Ok(await displayService.GetByPointOfCare(pointOfCare)); + } + + /// + /// Get display by name + /// + /// + /// display + /// + [HttpGet("{name}/name")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ListByPointOfCare(string name) + { + var result = await displayService.GetByName(name); + return Ok(result); + } + + /// + /// Get display by object id + /// + /// + /// display + /// + [HttpGet("{localeEnum}/{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetById(string id, string localeEnum) + { + if (!ObjectId.TryParse(id, out var objectId) || !Enum.TryParse(localeEnum, out var locale)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var result = await displayService.GetByIdWithPermissions(objectId, locale); + return Ok(result); + } + + [HttpGet("{id}/adm-panel")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetByIdWithNoPermissions(string id) + { + if (!ObjectId.TryParse(id, out var objectId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + var result = await displayService.GetById(objectId); + return Ok(result); + } + + /// + /// Get display list by it unit object id + /// + /// + /// List + /// + [HttpGet("{id}/unitId")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ListUnitById(string id) + { + if (!ObjectId.TryParse(id, out var objectId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var result = await displayService.GetByUnitId(objectId); + return Ok(result); + } + + /// + /// Get display info by it object id and fill options + /// + /// + /// display + /// + [HttpGet("{id}/info")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ListInfoId(string id, [FromQuery] bool? fillPointOfCare, + [FromQuery] bool? fillDisplayList) + { + if (!ObjectId.TryParse(id, out var objectId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var username = JwtHelper.GetUsernameFromPrincipal(HttpContext.User); + var result = + await displayService.GetInfo(objectId, username, null,null, fillPointOfCare ?? true, fillDisplayList ?? true); + return result == null ? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing) : Ok(result); + } + + [HttpGet("{id}/unitId/{type}/type")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DefaultConfig(string id, DisplayConfigEnums.DisplayType type) + { + if (!ObjectId.TryParse(id, out var objectId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + //var username = JwtHelper.GetUsernameFromPrincipal(HttpContext.User); + return Ok(await displayConfigService.GetById(new ObjectId("66191fbb91b99fb5ac42e714"), objectId, type)); + } + + [HttpPost("poc-available")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetPocAvailablebyDisplayId([FromBody] List displayIds) + { + var listToReturn = await displayService.GetAllAvailablePoc(displayIds, true); + return Ok(listToReturn); + } + + [HttpGet("{displayId}/pocs")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetPocByDisplayId(string displayId) + { + if (!ObjectId.TryParse(displayId, out var objectId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var pocList = await displayService.GetAllPocsByDisplayId(objectId); + return Ok(pocList); + } + + + /// + /// Update display PointOfCare id and notify subscribers + /// + /// + /// display + /// + [HttpPut("{id}/pocs-ids")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + // [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdatePointOfCares(string id, [FromBody] List pointOfCareIdList) + { + if (!ObjectId.TryParse(id, out var objectId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var listPocObId = new List(); + foreach (var poc in pointOfCareIdList) + { + if (!ObjectId.TryParse(poc, out var pocObjectId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + listPocObId.Add(pocObjectId); + } + + return Ok(await displayService.UpdatePointOfCareList(objectId, listPocObId)); + } + + + /// + /// Update display config id and notify subscribers + /// + /// + /// display + /// + [HttpPut("{idDisplay}/config-preset/{idConfig}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + // [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateConfig(string idDisplay, string idConfig) + { + if (!ObjectId.TryParse(idDisplay, out var objectIdDisplay) || + !ObjectId.TryParse(idConfig, out var objectIdConfigDisplay) + ) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var display = await displayService.GetInfo(objectIdDisplay, null, null, null, false); + if (display == null) throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + await displayConfigService.GetById(objectIdConfigDisplay); + var result = await displayService.UpdateConfigPreset(objectIdDisplay, objectIdConfigDisplay); + + return Ok(result); + } + + /// + /// Update display name + /// + /// + /// display + /// + [HttpPut("{id}/{name}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + // [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateDisplayName(string id, string name) + { + if (!ObjectId.TryParse(id, out var displayId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var result = await displayService.UpdateName(displayId, name); + return Ok(result); + } + + /// + /// Insert display + /// + /// + /// display + /// + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + // [PermissionAuthorize("Source", "DisplayId")] + public async Task CreateDisplay([FromBody] Display display) + { + var result = await displayService.InsertOne(display); + return Ok(result); + } + + + /// + /// Delete display + /// + /// + /// display + /// + [HttpDelete("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + // [PermissionAuthorize("Source", "DisplayId")] + public async Task DeleteDisplay(string id) + { + if (!ObjectId.TryParse(id, out var displayId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var result = await displayService.DeleteDisplay(displayId); + + return Ok(result); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/GroupedObservationsController.cs b/adas-core/Controllers/GroupedObservationsController.cs new file mode 100644 index 00000000..4bd58dac --- /dev/null +++ b/adas-core/Controllers/GroupedObservationsController.cs @@ -0,0 +1,36 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.GroupedObservations; +using adas_core.Logging; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; + +namespace adas_core.Controllers; + +[Route("grouped-observations")] +[ApiController] +public class GroupedObservationsController : ControllerBase +{ + private readonly IGroupedObservationService _groupedObservationService; + + public GroupedObservationsController(IGroupedObservationService groupedObservationService) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + _groupedObservationService = groupedObservationService; + } + + + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetGroupedObservationAsync(string id, GroupedField groupedConfig) + { + if (string.IsNullOrEmpty(id) || !ObjectId.TryParse(id, out var objectId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var groupedObs = await _groupedObservationService.GenerateGroupedObservation(objectId, groupedConfig); + return Ok(groupedObs); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/LightBeaconController.cs b/adas-core/Controllers/LightBeaconController.cs new file mode 100644 index 00000000..86f08c3a --- /dev/null +++ b/adas-core/Controllers/LightBeaconController.cs @@ -0,0 +1,118 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using adas_core.Logging; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; + +namespace adas_core.Controllers; + +[Route("beacons")] +public class LightBeaconController : ControllerBase +{ + private readonly ILightBeaconService _lightBeaconService; + private readonly IPatientService _patientService; + + + public LightBeaconController(ILightBeaconService lightBeaconService, IPatientService patientService) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + _lightBeaconService = lightBeaconService; + _patientService = patientService; + } + + [HttpPost("paginated")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task GetPaginatedUnits([FromBody] PaginationFilter request) + { + if (request == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var beacons = await _lightBeaconService.GetPaginatedBeacons(request); + return Ok(beacons); + } + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task CreateCamera([FromBody] LightBeacon beac) + { + var beacon = await _lightBeaconService.InsertOne(beac); + return Ok(beacon); + } + [HttpPut("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task UpdateCamera(string id, [FromBody] LightBeacon beacon) + { + if (!ObjectId.TryParse(id, out var idParsed)) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + beacon.Id = idParsed; + var cameraUpdated = await _lightBeaconService.UpdateOne(beacon); + return Ok(cameraUpdated); + } + [HttpGet("{textToSearch}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task GetSearchByName(string textToSearch) + { + var beacon = await _lightBeaconService.GetSearchByName(textToSearch); + return Ok(beacon); + } + [HttpPost("on/{color}")] + public async Task On([FromBody] string patientId, string color) + { + var parsed = ObjectId.TryParse(patientId, out var patientObjectId); + if (!parsed) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var patient = await _patientService.FindById(patientObjectId); + if (patient == null || !patient.PointOfCareId.HasValue) + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + switch (color.ToUpper()) + { + case "YELLOW": + await _lightBeaconService.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Yellow); + break; + case "BLUE": + await _lightBeaconService.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Blue); + break; + case "RED": + await _lightBeaconService.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Red); + break; + case "OFF": + await _lightBeaconService.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Off); + break; + case "GREEN": + await _lightBeaconService.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Green); + break; + case "CYAN": + await _lightBeaconService.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Cyan); + break; + case "MAGENTA": + await _lightBeaconService.SendColor(patient.PointOfCareId.Value, LightBeaconColor.Magenta); + break; + case "WHITE": + await _lightBeaconService.SendColor(patient.PointOfCareId.Value, LightBeaconColor.White); + break; + } + + return Ok(); + } + + + [HttpPost("off")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public IActionResult Off([FromBody] string pocId) + { + var isParse = ObjectId.TryParse(pocId, out var pocObjectId); + if (!isParse) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + _lightBeaconService.PowerOffLed(pocObjectId); + + return Ok(); + } + [HttpPost("create")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task Create([FromBody] adas_core.Domain.Models.MongoModels.LightBeacon beacon) + { + var beaconInserted = await _lightBeaconService.InsertOne(beacon); + if(beaconInserted == null) return Conflict(); + + return Ok(beaconInserted); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/MasterListController.cs b/adas-core/Controllers/MasterListController.cs new file mode 100644 index 00000000..e84415f0 --- /dev/null +++ b/adas-core/Controllers/MasterListController.cs @@ -0,0 +1,513 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.Masters; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using MongoDB.Bson; + +namespace adas_core.Controllers; + +[Route("master-lists")] +[ApiController] +public class MasterListController( + IMasterListServiceFactory serviceFactory, + ILogger logger, + IOptions listSettings) + : ControllerBase +{ + [HttpGet("list-settings")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public IActionResult GetMasterListSettings() + { + return Ok(listSettings.Value); + } + + [HttpGet("{typeName}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetMasterList(string typeName) + { + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + dynamic service = serviceFactory.GetService(masterListType); + var result = await service.GetAllMasterList(); + return Ok(result); + } + + + [HttpGet("no-options")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetAllMasterListWithoutOptions() + { + List listToReturn = []; + foreach (MasterListType listType in Enum.GetValues(typeof(MasterListType))) + { + dynamic service = serviceFactory.GetService(listType); + listToReturn.AddRange(await service.GetAllMasterListWithoutOptions()); + } + + return Ok(listToReturn); + } + + [HttpPost("{typeName}/paginated-options")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetMasterListWithPaginatedOptions(string typeName, + [FromBody] PaginationFilter request) + { + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + dynamic service = serviceFactory.GetService(masterListType); + var result = await service.GetAllMasterListWithPaginatedOptions(request); + return Ok(result); + } + + + [HttpGet("{typeName}/count")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetMasterListCount(string typeName) + { + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + dynamic service = serviceFactory.GetService(masterListType); + var result = await service.GetAllMasterListCount(); + return Ok(result); + } + + [HttpGet("count")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetAllMasterListCounts() + { + var result = new Dictionary(); + foreach (MasterListType listType in Enum.GetValues(typeof(MasterListType))) + { + dynamic service = serviceFactory.GetService(listType); + result.Add(listType, await service.GetAllMasterListCount()); + } + + return Ok(result); + } + + [HttpGet("{typeName}/{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetMasterListById(string typeName, string id) + { + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + if (!ObjectId.TryParse(id, out var masterListId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + dynamic service = serviceFactory.GetService(masterListType); + + var result = await service.GetMasterListById(masterListId); + return Ok(result); + } + + [HttpPost("{typeName}/{id}/master-list-paginated-options")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetMasterListByIdWithPaginatedOptions(string typeName, string id, + [FromBody] PaginationFilter request) + { + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + if (!ObjectId.TryParse(id, out var masterListId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + dynamic service = serviceFactory.GetService(masterListType); + var result = await service.GetMasterListByIdWithPaginatedOptions(masterListId, request); + return Ok(result); + } + + [HttpPost("{typeName}/paginated-master-list-paginated-options")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetPaginatedMasterListWithPaginatedOptions([FromBody] PaginationPairDto filters, + string typeName) + { + logger.LogDebug("Start GetPaginatedMasterList"); + var start = DateTime.Now; + + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + dynamic service = serviceFactory.GetService(masterListType); + + var result = await service.GetPaginatedMasterListWithPaginatedOptions(filters.ListFilter, filters.OptionFilter); + + var end = DateTime.Now; + logger.LogDebug("Finished GetPaginatedMasterList in {TotalSeconds:F1} seconds", (end - start).TotalSeconds); + return Ok(result); + } + + + [HttpPost("{typeName}/{id}/paginated-options")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetPaginatedOptions(string typeName, string id, + [FromBody] PaginationFilter request) + { + logger.LogDebug("Start GetPaginatedOptions"); + var start = DateTime.Now; + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + if (!ObjectId.TryParse(id, out var masterListId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + dynamic service = serviceFactory.GetService(masterListType); + + var result = await service.GetPaginatedOptions(request, masterListId); + + var end = DateTime.Now; + logger.LogDebug("Finished GetPaginatedOptions from list {id} in {TotalSeconds:F1} seconds", id, + (end - start).TotalSeconds); + + return Ok(result); + } + + [HttpGet("{typeName}/{id}/master-list-options-names")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetMasterListOptionsNamesById(string typeName, string id) + { + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + if (!ObjectId.TryParse(id, out var masterListId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + dynamic service = serviceFactory.GetService(masterListType); + var result = await service.GetMasterListOptionsNamesById(masterListId); + return Ok(result); + } + + + [HttpGet("{typeName}/{name}/name")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetMasterListByName(string typeName, string name) + { + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + dynamic service = serviceFactory.GetService(masterListType); + + var result = await service.GetMasterListByName(name); + + return Ok(result); + } + + [HttpGet("{typeName}/{listId}/related-options/{targetCategory}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetOptionTypeList(string typeName, string listId, string targetCategory) + { + if (!Enum.TryParse(typeName, out MasterListType masterListType1) || + !Enum.TryParse(targetCategory, out MasterListType masterListType2)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + if (!ObjectId.TryParse(listId, out var masterListId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + dynamic service1 = serviceFactory.GetService(masterListType1); + + ObjectId requestedListId = await service1.GetAssociatedList(masterListId, masterListType1, masterListType2); + dynamic service2 = serviceFactory.GetService(masterListType2); + + MasterList options = await service2.GetMasterListById(requestedListId, LocaleEnum.Default); + return Ok(options); + } + + + /// + /// Busca las opciones que contengan un texto de una lista determinada + /// + /// Tipo de Lista + /// id de la lista + /// + /// + [HttpPost("{typeName}/{id}/options")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetMasterListByIdAndSearchOptions(string typeName, string id, + [FromBody] FilterOptionListElement searchOptions) + { + logger.LogDebug("Start GetMasterListByIdAndSearchOptions"); + var start = DateTime.Now; + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + if (!ObjectId.TryParse(id, out var masterListId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + dynamic service = serviceFactory.GetService(masterListType); + + var result = await service.GetMasterListByIdAndSearchOptions(masterListId, searchOptions); + + var end = DateTime.Now; + logger.LogDebug("Finished GetMasterListByIdAndSearchOptions in {TotalSeconds:F1} seconds", + (end - start).TotalSeconds); + + return Ok(result); + } + + [HttpGet("{typeName}/{textSearch}/text")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetMasterListByTextSearch(string typeName, string? textSearch) + { + logger.LogDebug("Start GetMasterListByTextSearch"); + var start = DateTime.Now; + + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + if (textSearch is { Length: < 3 }) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectLength); + dynamic service = serviceFactory.GetService(masterListType); + + var result = await service.GetMasterListByTextSearch(textSearch); + var end = DateTime.Now; + logger.LogDebug("Finished GetMasterListByTextSearch in {TotalSeconds:F1} seconds", (end - start).TotalSeconds); + + return Ok(result); + } + + [HttpPost("{typeName}/paginated")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetPaginatedMasterList([FromBody] PaginationFilter request, string typeName) + { + logger.LogDebug("Start GetPaginatedMasterList"); + var start = DateTime.Now; + + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + dynamic service = serviceFactory.GetService(masterListType); + + var result = await service.GetPaginatedMasterList(request); + var end = DateTime.Now; + logger.LogDebug("Finished GetPaginatedMasterList in {TotalSeconds:F1} seconds", (end - start).TotalSeconds); + return Ok(result); + } + + + [HttpPost("{typeName}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task InsertMasterListFromPanel(string typeName, [FromBody] MasterList master) + { + if (master == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + var result = await serviceFactory.InsertMasterList(masterListType, master); + return Ok(result); + } + + [HttpPut("{typeName}/{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateMasterList(string typeName, string id, [FromBody] MasterList master) + { + if (master == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + if (!ObjectId.TryParse(id, out var masterListId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + master.Id = masterListId; + var result = await serviceFactory.UpdateMasterList(masterListType, master); + return Ok(result); + } + + [HttpPost("{typeName}/{id}/item")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task AddOptionToMasterList(string typeName, string id, + [FromBody] FilterOptionListElement opt) + { + var start = DateTime.Now; + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + if (!ObjectId.TryParse(id, out var masterListId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + dynamic service = serviceFactory.GetService(masterListType); + + var result = await service.AddOptionToMasterList(masterListId, opt); + + var end = DateTime.Now; + logger.LogDebug("Finished AddOptionToMasterList in {TotalSeconds:F1} seconds", (end - start).TotalSeconds); + return Ok(result); + } + + [HttpPut("{locale}/{typeName}/{id}/item")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateOptionFromMasterList(string locale, string typeName, string id, + [FromBody] OptionList opt) + { + var start = DateTime.Now; + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + if (!Enum.TryParse(locale, out LocaleEnum localeEnum)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + if (!ObjectId.TryParse(id, out var masterListId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + dynamic service = serviceFactory.GetService(masterListType); + + var result = await service.UpdateMasterListOption(masterListId, opt, typeName, localeEnum); + + var end = DateTime.Now; + logger.LogDebug("Finished UpdateOptionToMasterList in {TotalSeconds:F1} seconds", (end - start).TotalSeconds); + return result == null ? Conflict("Error while updating option in list") : (IActionResult)Ok(result); + } + + [HttpPut("{typeName}/{id}/item")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateOptionMasterListFromAdmPanel(string typeName, string id, + [FromBody] OptionList opt) + { + var start = DateTime.Now; + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + if (!ObjectId.TryParse(id, out var masterListId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + dynamic service = serviceFactory.GetService(masterListType); + + var result = await service.UpdateMasterListOption(masterListId, opt, typeName); + + var end = DateTime.Now; + logger.LogDebug("Finished UpdateOptionToMasterList in {TotalSeconds:F1} seconds", (end - start).TotalSeconds); + return result == null ? Conflict("Error while updating option in list") : (IActionResult)Ok(result); + } + + [HttpPut("{typeName}/{id}/details")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateOptionDetailsFromMasterList(string typeName, string id, + [FromBody] UpdateMasterListDetailsDto opt) + { + var start = DateTime.Now; + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + if (!ObjectId.TryParse(id, out var masterListId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + dynamic service = serviceFactory.GetService(masterListType); + + var result = await service.UpdateOptionDetailsToMasterList(masterListId, opt); + + var end = DateTime.Now; + logger.LogDebug("Finished UpdateOptionToMasterList in {TotalSeconds:F1} seconds", (end - start).TotalSeconds); + return result == null ? Conflict("Error while updating list details") : (IActionResult)Ok(result); + } + + [HttpPut("{typeName}/{id}/name")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateMasterListName(string typeName, string id, + [FromBody] UpdateListNameAndDescriptionDto udpateListNameAndDescriptionDto) + { + var start = DateTime.Now; + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + if (!ObjectId.TryParse(id, out var masterListId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + if (udpateListNameAndDescriptionDto.Name == null) return BadRequest($"Invalid name: {typeName}."); + dynamic service = serviceFactory.GetService(masterListType); + + var result = await service.UpdateMasterListName(masterListId, udpateListNameAndDescriptionDto.Name); + + var end = DateTime.Now; + logger.LogDebug("Finished UpdateMasterListName in {TotalSeconds:F1} seconds", (end - start).TotalSeconds); + return !result ? Conflict("Error while updating list name") : (IActionResult)Ok(result); + } + + [HttpPut("{typeName}/{id}/description")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateMasterListDescription(string typeName, string id, + [FromBody] UpdateListNameAndDescriptionDto udpateListNameAndDescriptionDto) + { + var start = DateTime.Now; + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + if (!ObjectId.TryParse(id, out var masterListId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + if (udpateListNameAndDescriptionDto.Description == null) return BadRequest($"Description is null: {typeName}."); + dynamic service = serviceFactory.GetService(masterListType); + + var result = + await service.UpdateMasterListDescription(masterListId, udpateListNameAndDescriptionDto.Description); + + var end = DateTime.Now; + logger.LogDebug("Finished UpdateMasterListDescription in {TotalSeconds:F1} seconds", + (end - start).TotalSeconds); + return !result ? Conflict("Error while updating list description") : (IActionResult)Ok(result); + } + + + [HttpDelete("{typeName}/{id}/item/{itemId}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DeleteOptionFromMasterList(string typeName, string id, string itemId) + { + var start = DateTime.Now; + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + if (!ObjectId.TryParse(id, out var masterListId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + if (!ObjectId.TryParse(itemId, out var masterListItemId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + dynamic service = serviceFactory.GetService(masterListType); + + var result = await service.DeleteMasterListOption(masterListId, masterListItemId, typeName); + + var end = DateTime.Now; + logger.LogDebug("Finished DeleteOptionToMasterList in {TotalSeconds:F1} seconds", (end - start).TotalSeconds); + if (result == null) throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); + return Ok(result); + } + + [HttpDelete("{typeName}/{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DeleteMasterList(string typeName, string id) + { + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + if (!ObjectId.TryParse(id, out var masterListId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + dynamic service = serviceFactory.GetService(masterListType); + + await service.DeleteMasterListById(masterListId); + return Ok(); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/MedicinesController.cs b/adas-core/Controllers/MedicinesController.cs new file mode 100644 index 00000000..48196e14 --- /dev/null +++ b/adas-core/Controllers/MedicinesController.cs @@ -0,0 +1,130 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.Filter; +using adas_core.Logging; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; + +namespace adas_core.Controllers; + +[Route("medicines")] +public class MedicinesController : ControllerBase +{ + private readonly IAdminPanelService _adminPanelService; + + private readonly IMedicineService _medicineService; + //private readonly ILogger _loggerADMPanel; + + + public MedicinesController(IMedicineService medicineService, + IAdminPanelService adminPanelService) //, ILogger loggerADMPanel) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + _medicineService = medicineService; + _adminPanelService = adminPanelService; + //this._loggerADMPanel = loggerADMPanel; + } + + [HttpGet] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetAllMedicines() + { + var activeMedicines = await _medicineService.GetAll(); + return Ok(activeMedicines); + } + + + [HttpGet("{patientId}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetActiveMedicines(string patientId) + { + if (!ObjectId.TryParse(patientId, out var objectId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var activeMedicines = await _medicineService.GetActiveMedicinesByPatient(objectId); + return Ok(activeMedicines); + } + + + #region ADMPanel + + [HttpPost("paginated")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetPaginatedMedicines([FromBody] PaginationFilter request) + { + if (request == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var result = await _medicineService.GetPaginatedMedicines(request); + return Ok(result); + } + + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task PostMedicine([FromBody] Medicine medicine) + { + if (medicine == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var insertedMedicine = await _adminPanelService.PostMedicine(medicine); + return Ok(insertedMedicine); + } + + [HttpPut("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateMedicine(string id, [FromBody] Medicine medicine) + { + if (!ObjectId.TryParse(id, out var objectId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var med = await _adminPanelService.GetMedicineById(objectId); + medicine.Id = med!.Id; + return Ok(await _adminPanelService.UpdateMedicine(medicine)); + } + + [HttpDelete("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DeleteMedicine(string id) + { + await _adminPanelService.DeleteMedicineById(id); + return Ok(); + } + + [HttpGet("groups")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetAllGroups() + { + return Ok(await _medicineService.GetAllGroups()); + } + + [HttpGet("types")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetTypes() + { + return Ok(await _medicineService.GetAllTypes()); + } + + [HttpGet("names")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetNames() + { + return Ok(await _medicineService.GetAllNames()); + } + + [HttpGet("codes")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetCodes() + { + return Ok(await _medicineService.GetAllCodes()); + } + + #endregion +} \ No newline at end of file diff --git a/adas-core/Controllers/NoticeController.cs b/adas-core/Controllers/NoticeController.cs new file mode 100644 index 00000000..f76f85b2 --- /dev/null +++ b/adas-core/Controllers/NoticeController.cs @@ -0,0 +1,88 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.MongoModels; +using adas_core.Logging; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; + +namespace adas_core.Controllers; + +[Route("notices")] +public class NoticeController : ControllerBase +{ + private readonly INoticeService _noticeService; + + + public NoticeController(INoticeService noticeService) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + _noticeService = noticeService; + } + + + [HttpGet] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetAllNotices() + { + var result = await _noticeService.GetNoticesAsync(); + return Ok(result); + } + + [HttpGet("{type}/type")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetNoticesByType(string type) + { + var result = await _noticeService.GetNoticeByTypeAsync(type); + return Ok(result); + } + + [HttpGet("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetNoticesById(string id) + { + var result = await _noticeService.GetNoticeByTypeAsync(id); + + return Ok(result); + } + + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task InsertNotice([FromBody] Notice notice, [FromHeader] string displayId) + { + if (!ObjectId.TryParse(displayId, out var displayIdParsed)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + notice.DisplayId = displayIdParsed; + var result = await _noticeService.InsertNotice(notice); + return Ok(result); + } + + [HttpPut("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateNotice(string id, [FromBody] Notice notice) + { + if (!ObjectId.TryParse(id, out var noticeId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + notice.Id = noticeId; + await _noticeService.UpdateNoticeAsync(notice); + return Ok(); + } + + [HttpDelete("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DeleteNotice(string id) + { + if (!ObjectId.TryParse(id, out var noticeId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + await _noticeService.DeleteNoticeByIdAsync(noticeId); + return Ok(); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/ObservationsController.cs b/adas-core/Controllers/ObservationsController.cs new file mode 100644 index 00000000..e483a570 --- /dev/null +++ b/adas-core/Controllers/ObservationsController.cs @@ -0,0 +1,160 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.Observations; +using adas_core.Logging; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; + +namespace adas_core.Controllers; + +[Route("observations")] +[ApiController] +public class ObservationController : ControllerBase +{ + //private readonly IConfigObservationService _configObservation; + private readonly IObservationService _observationService; + private readonly IPatientService _patientService; + + + public ObservationController(IObservationService observationService, IPatientService patientService) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + _observationService = observationService; + + _patientService = patientService; + } + + + [HttpPost] + [Route("")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthAdmin)] + public IActionResult ProcessRequest([FromBody] ObservationDto observationDto) + { + var request = new ApiRequest + { + TraceId = Guid.NewGuid().ToString(), + Type = "ORU_R01", + PatientNumber = observationDto.PatientNumber, + Location = observationDto.Location, + ObservationData = observationDto.ObservationData, + Observations = observationDto.Observations, + MessageTime = observationDto.MessageTime, + WaitResult = observationDto.WaitResult + }; + + + if (request.WaitResult) _observationService.SaveRequest(request); + + else _observationService.SaveRequestAsync(request); + return NoContent(); + } + + [HttpPost] + [Route("NurseObservations")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthAdmin)] + public IActionResult ProcessRequestObsNurse([FromBody] ObservationDto observationDto) + { + //if (request == null) return BadRequest("Request is null"); + var request = new ApiRequest + { + TraceId = Guid.NewGuid().ToString(), + Type = "ORU_R01", + PatientNumber = observationDto.PatientNumber, + Location = observationDto.Location, + ObservationData = observationDto.ObservationData, + Observations = observationDto.Observations, + MessageTime = observationDto.MessageTime, + WaitResult = observationDto.WaitResult + }; + + _observationService.SaveRequestNurseObsAsync(request); + return NoContent(); + } + + // TESTING: Dejar comentado al terminar las pruebas + [HttpPost] + [Route("apiRequest")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthAdmin)] + public IActionResult ProcessRequest([FromBody] ApiRequest apiRequest) + { + //if (request == null) return BadRequest("Request is null"); + + if (apiRequest.WaitResult) _observationService.SaveRequest(apiRequest); + + else _observationService.SaveRequestAsync(apiRequest); + return Ok(); + } + + [HttpPost("expired")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public IActionResult ExpireAllExpired() + { + _observationService.ExpireObservations(); + return Ok(); + } + + + [HttpPost("paginated")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetPaginatedObservations([FromBody] PaginationFilter request) + { + if (request == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var result = await _observationService.GetPaginatedObservations(request); + return Ok(result); + } + + [HttpGet] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetObservations([FromBody] ObservationsRequest request) + { + if (request.PatientNumber == null) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + + var archivedObservation = false; + var patient = await _patientService.FindByPatientNumber(request.PatientNumber); + if (patient == null) + { + archivedObservation = true; + patient = await _patientService.FindByPatientNumberArchived(request.PatientNumber); + } + + if (patient == null) throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + //if (request.ObservationNames?.Count == 0) return BadRequest("The request need almost one type"); + + return Ok(await _observationService.FindAllBetweenDates(patient.Id, request.StartTime, request.EndTime, + request.ObservationNames, archivedObservation)); + } + + [HttpPost("{patientId}/force-news/{value}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ForceNews(string patientid, string value) + { + var parsed = ObjectId.TryParse(patientid, out var paObjectId); + if (!parsed) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + if (!int.TryParse(value, out _)) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var patient = await _patientService.FindById(paObjectId); + + var newObs = new PatientObservation + { + Name = "NEWS", + CodingSystem = "ADAS", + Expires = 15, + Value = value, + PatientId = patient!.Id, + Time = DateTime.Now + }; + await _observationService.InsertObservation(newObs); + + return Ok(); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/PatientAppointmentController.cs b/adas-core/Controllers/PatientAppointmentController.cs new file mode 100644 index 00000000..36fc3702 --- /dev/null +++ b/adas-core/Controllers/PatientAppointmentController.cs @@ -0,0 +1,43 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.DTO; +using adas_core.Logging; +using Microsoft.AspNetCore.Mvc; + +namespace adas_core.Controllers; + +[Route("appointments")] +[ApiController] +public class PatientAppointmentController : ControllerBase +{ + private readonly IAppointmentService _appointmentService; + + + public PatientAppointmentController(IAppointmentService appointmentService) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + _appointmentService = appointmentService; + } + + + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthAdmin)] + public IActionResult ProcessRequest([FromBody] AppointmentDto appointmentDto) + { + var request = new ApiRequest + { + TraceId = Guid.NewGuid().ToString(), + Type = "SIU_S12", + PatientNumber = appointmentDto.PatientNumber, + Location = appointmentDto.Location, + Appointments = appointmentDto.Appointments, + MessageTime = appointmentDto.MessageTime, + WaitResult = appointmentDto.WaitResult + }; + _appointmentService.SaveRequestAsync(request); + return Ok(); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/PatientController.cs b/adas-core/Controllers/PatientController.cs new file mode 100644 index 00000000..6b91d5ff --- /dev/null +++ b/adas-core/Controllers/PatientController.cs @@ -0,0 +1,438 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Authentication.Interfaces; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using adas_core.Logging; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; + +namespace adas_core.Controllers; + +[Route("persons")] +[ApiController] +public class PatientController : ControllerBase +{ + private readonly IAdminPanelService _adminPanelService; + private readonly IObservationService _observationService; + private readonly IPatientService _patientService; + private readonly IUnitService _unitService; + private readonly IUserService _userService; + + + public PatientController( + IPatientService patientService, + IObservationService observationService, + IAdminPanelService adminPanelService, + IUserService userService, IUnitService unitService) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + _observationService = observationService; + _patientService = patientService; + _adminPanelService = adminPanelService; + _userService = userService; + _unitService = unitService; + } + + [HttpGet] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetAllPatients() + { + return Ok(await _patientService.FindAll()); + } + + [HttpGet("find-by-patientnumber/{patientNumber}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task GetByPatientNumber(string patientNumber) + { + return Ok(await _patientService.FindByPatientNumber(patientNumber, true)); + } + + [HttpGet("{patientId}/last-observation")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetLastObservation(string patientId) + { + if (!ObjectId.TryParse(patientId, out var id)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var obs = await _observationService.FindLastObservations(id, 1); + return Ok(obs); + } + + [HttpGet("active-poc")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetActivePatients() + { + var patients = await _patientService.FindInActivePoC(); + return Ok(patients); + } + + [HttpPost("paginated")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetPaginatedPatients([FromBody] PaginationFilter request) + { + if (request == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var patients = await _patientService.GetPaginatedPatients(request); + return Ok(patients); + } + + [HttpGet("basic")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetPatientsBasic() + { + var patients = await _patientService.FindAll(); + var persons = patients.Select(p => p.Person).ToList(); + return Ok(persons); + } + + + [HttpPost("request")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ProcessRequest([FromBody] PatientDto patientDto) + { + var request = new ApiRequest + { + TraceId = Guid.NewGuid().ToString(), + Type = "ADT_A01", + PatientNumber = patientDto.PatientNumber, + Location = patientDto.Location, + Patient = patientDto.Patient, + + MessageTime = patientDto.MessageTime, + WaitResult = patientDto.WaitResult, + RequestFromPanel = true + }; + await _patientService.SaveRequestAsync(request); + var patient = await _patientService.FindByPatientNumber(patientDto.PatientNumber, true); + + return Ok(patient); + } + + [HttpPost("api-request")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ProcessRequestApiRequest([FromBody] ApiRequest apiRequest) + { + await _patientService.SaveRequestAsync(apiRequest); + return Ok(); + } + + [HttpGet("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetPatientById(string id) + { + if (!ObjectId.TryParse(id, out var patientId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + var patient = await _patientService.FindById(patientId, true); + return Ok(patient); + } + + /// + /// Salida de un paciente + /// + /// + /// + [HttpPost("{id}/exit")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ExitPatientById(string id) + { + if (!ObjectId.TryParse(id, out var patientId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var patient = await _patientService.FindById(patientId); + + var unitConfig = await _unitService.FindById(patient!.UnitId); + switch (unitConfig) + { + case null: + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + case { Configuration.ManualDischarge: false }: + throw new ConflictException(HttpEnum.ErrorMessage.ServiceUnavailableCensusInAutomaticMode); + default: + await _patientService.ExitPatientById(patientId); + return Ok(); + } + } + + [HttpPost("{id}/archived-procedure")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ArchivePatientProcedure(string id, [FromBody] List options) + { + var userNameContext = HttpContext.User.Identity?.Name ?? + throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedNoPermission); + + var user = await _userService.GetUserByUserName(userNameContext); + + if (!ObjectId.TryParse(id, out var patientId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var patient = await _patientService.FindById(patientId); + var optionLists = patient!.Procedures?.Where(c => c.Name != options.First().Name).ToList(); + options.First().Description ??= "Deleted"; + var patientToReturn = + await _patientService.UpdatePatientMasterList(patientId, MasterListType.ProcedureList, optionLists!, user, + options); + return Ok(patientToReturn); + } + + [HttpPost("{id}/archived-test")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ArchivePatientTest(string id, [FromBody] List options) + { + var userNameContext = HttpContext.User.Identity?.Name ?? + throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedNoPermission); + + var user = await _userService.GetUserByUserName(userNameContext); + + if (!ObjectId.TryParse(id, out var patientId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var patient = await _patientService.FindById(patientId); + var optionLists = patient!.Tests?.Where(c => c.Name != options.First().Name).ToList(); + options.First().Description ??= "Deleted"; + var patientToReturn = + await _patientService.UpdatePatientMasterList(patientId, MasterListType.TestList, optionLists!, user, + options); + return Ok(patientToReturn); + } + + [HttpPost("{id}/archived-treatment")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ArchivePatientTreatment(string id, [FromBody] List options) + { + var userNameContext = HttpContext.User.Identity?.Name ?? + throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedNoPermission); + + var user = await _userService.GetUserByUserName(userNameContext); + if (!ObjectId.TryParse(id, out var patientId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var patient = await _patientService.FindById(patientId); + //await _archivePatientProcedureService.UpdateTreatment(patient, options); + //patient.Treatment = patient.Treatment?.Where(c=>c.Name!=options.First().Name).ToList(); + var optionLists = patient!.Treatment?.Where(c => c.Name != options.First().Name).ToList()!; + options.First().Description ??= "Deleted"; + //await _patientService.GenerateNurseCarePlanAndInsert(MasterListType.TreatmentList, (archiveData ?? new List())!,patient, user); + var patientToReturn = + await _patientService.UpdatePatientMasterList(patientId, MasterListType.TreatmentList, optionLists!, user, + options); + return Ok(patientToReturn); + } + + [HttpPut("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdatePatient(string id, [FromBody] Patient newPatient) + { + if (!ObjectId.TryParse(id, out _)) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + await _patientService.Update(newPatient); + return Ok(newPatient); + } + + [HttpGet("{patientId}/{oldPoc}/new-location/{newPoc}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task MovePatient(string patientId, string newPoc, string oldPoc) + { + if (!ObjectId.TryParse(patientId, out var pId) || !ObjectId.TryParse(newPoc, out var newPocId) + || !ObjectId.TryParse(oldPoc, out var oldPocId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + var oldPatient = await _patientService.FindById(pId); + + var unitConfig = await _unitService.FindById(oldPatient!.UnitId); + switch (unitConfig) + { + case null: + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + case { Configuration.ManualMove: false }: + throw new ConflictException(HttpEnum.ErrorMessage.ServiceUnavailableCensusInAutomaticMode); + } + + + var response = await _patientService.Move(oldPatient, newPocId, oldPocId); + //await _archivePatientProcedureService.Update(oldPatient, newPatient); + if (response) + return Ok(response); + throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + } + + [HttpPut("{id}/{patientNumber}/data")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdatePerson(string id, [FromBody] Person person, string? patientNumber) + { + if (!ObjectId.TryParse(id, out var patientId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var patient = await _patientService.FindById(patientId); + + var unitConfig = await _unitService.FindById(patient!.UnitId); + switch (unitConfig) + { + case null: + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + case { Configuration.ManualEdit: false }: + throw new ConflictException(HttpEnum.ErrorMessage.ServiceUnavailableCensusInAutomaticMode); + } + + + var number = patientNumber ?? patient.PatientNumber ?? string.Empty; + await _patientService.UpdatePatientData(patientId, number, person); + return Ok(); + } + + [HttpPut("{id}/incoming-data")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdatePatientIncomingData(string id, [FromBody] Patient person) + { + var userNameContext = HttpContext.User.Identity?.Name ?? + throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedNoPermission); + if (!ObjectId.TryParse(id, out var patientId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var user = await _userService.GetUserByUserName(userNameContext); + + var patient = await _patientService.FindById(patientId); + var personDataChange = new PatientIncomeData(); + if ((patient!.Origin != null && !patient.Origin.Name.Equals(person.Origin?.Name)) || + (patient.Origin == null && person.Origin != null)) + personDataChange.Origin = person.Origin; + if (patient.OriginAux != person.OriginAux) + personDataChange.OriginAux = person.OriginAux; + if (patient.DiagnosisAux != person.DiagnosisAux) + personDataChange.DiagnosisAux = person.DiagnosisAux; + if ((patient.Diagnosis != null && !patient.Diagnosis.Name.Equals(person.Diagnosis?.Name)) || + (patient.Diagnosis == null && person.Diagnosis != null)) + personDataChange.Diagnosis = person.Diagnosis; + if (patient.AdmTime != person.AdmTime) + personDataChange.AdmTime = person.AdmTime; + + await _patientService.UpdatePatientIncomingData(patientId, person, personDataChange, user!); + + return Ok(); + } + + [HttpPut("{id}/demographic-data")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdatePatientDemographicData(string id, [FromBody] Patient person) + { + var userNameContext = HttpContext.User.Identity?.Name ?? + throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedNoPermission); + var user = await _userService.GetUserByUserName(userNameContext); + + if (!ObjectId.TryParse(id, out var patientId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + await _patientService.FindById(patientId); + await _patientService.UpdatePatientDemographicData(patientId, person, user); + + return Ok(); + } + + [HttpPost("{id}/master-list/{typeName}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdatePatientMasterList([FromBody] List options, string typeName, + string id) + { + var userNameContext = HttpContext.User.Identity?.Name ?? + throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedNoPermission); + if (!ObjectId.TryParse(id, out var patientId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var user = await _userService.GetUserByUserName(userNameContext); + + if (!Enum.TryParse(typeName, out MasterListType masterListType)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestIncorrectType); + + var patient = await _patientService.UpdatePatientMasterList(patientId, masterListType, options, user, options); + + return Ok(patient); + } + + #region ADMPanel + + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task PostNewPatient([FromBody] AdmPanelRequest request) + { + if (string.IsNullOrEmpty(request.PatientNumber)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var patient = await _adminPanelService.FindPatientByPatientNumber(request.PatientNumber); + if (patient != null) return Conflict("person number already exist"); + var newPatient = await _adminPanelService.CreatePatient(request); + return Ok(newPatient); + } + + [HttpPut] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdatePatientData([FromBody] AdmPanelRequest request) + { + var patient = await _adminPanelService.FindPatient(request); + if (patient == null) throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + return Ok(await _adminPanelService.UpdatePatientData(request, patient)); + } + + [HttpPut("entry-date")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdatePatientEntryDate([FromBody] Patient patient) + { + var newPatient = await _patientService.FindById(patient.Id); + newPatient!.AdmTime = patient.AdmTime; + await _patientService.Update(newPatient); + return Ok(newPatient); + } + + [HttpPut("location")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public IActionResult UpdatePatientLocation([FromBody] AdmPanelRequest request) + { + return Ok(_adminPanelService.UpdatePatientLocation(request)); + } + + [HttpPut("{id}/altable")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdatePatientAltable(string id, [FromBody] OptionList altable) + { + var userNameContext = HttpContext.User.Identity?.Name ?? + throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedNoPermission); + if (!ObjectId.TryParse(id, out var patientId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var user = await _userService.GetUserByUserName(userNameContext); + var patient = await _patientService.UpdatePatientAltable(patientId, altable, user); + + return Ok(patient); + } + + [HttpDelete("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DeletePatientById(string id) + { + if (!ObjectId.TryParse(id, out var patientId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var patient = await _adminPanelService.FindPatientById(patientId); + if (patient == null) throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + + return Ok(await _adminPanelService.ArchivePatient(patient)); + } + + #endregion +} \ No newline at end of file diff --git a/adas-core/Controllers/PointOfCareController.cs b/adas-core/Controllers/PointOfCareController.cs new file mode 100644 index 00000000..dc0511cc --- /dev/null +++ b/adas-core/Controllers/PointOfCareController.cs @@ -0,0 +1,153 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; + +namespace adas_core.Controllers; + +[Route("point-of-cares")] +[ApiController] +public class PointOfCareController(IPointOfCareService pointOfCareService) + : ControllerBase +{ + /// + /// GetByCodeSysAndCode all PoC list + /// + /// + /// List + /// + [HttpGet] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task List() + { + return Ok(await pointOfCareService.GetAll()); + } + + /// + /// GetByCodeSysAndCode all Viirtual PoC list + /// + /// + /// List + /// + [HttpGet("get-all-virtual")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public IActionResult VirtualPocList() + { + var virtualPocNames = Enum.GetNames(typeof(VirtualPointOfCare)).ToList(); + return Ok(virtualPocNames); + } + + [HttpPost("paginated")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetPaginatedPoCs([FromBody] PaginationFilter request) + { + if (request == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + + var pocs = await pointOfCareService.GetPaginatedPoCs(request); + return Ok(pocs); + } + + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task CreateNewPointOfCare([FromBody] PointOfCare pointOfCare) + { + var result = await pointOfCareService.InsertPointOfCare(pointOfCare); + return Ok(result); + } + + + [HttpPut("{id}/{status}/status")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task SetPointOfCareStatus(string id, string status) + { + if (!Enum.TryParse(status, out StatusEnum.PointOfCare pocStatus) || !ObjectId.TryParse(id, out var pocId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + await pointOfCareService.SetPointOfCareStatus(pocId, pocStatus); + + return Ok(); + } + + [HttpGet("{id}/unit-id")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetAllPoCByUnitId(string id) + { + if (!ObjectId.TryParse(id, out var unitId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + var result = await pointOfCareService.FindAllByUnitId(unitId); + return Ok(result?.ToList()); + } + + [HttpGet("{unitId}/unit-id/{status}/status")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetPoCByUnitIdAndStatus(string unitId, string status) + { + if (!ObjectId.TryParse(unitId, out var unitIdParsed) || + !Enum.TryParse(status, out StatusEnum.PointOfCare pocStatus)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + var result = await pointOfCareService.FindByUnitAndStatus(unitIdParsed, pocStatus); + return Ok(result); + } + + [HttpPut] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdatePoC([FromBody] PointOfCare poc) + { + var result= await pointOfCareService.Update(poc); + return Ok(result); + } + + [HttpPut("{id}/config")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdatePoCConfiguration(string id, [FromBody] PointOfCareConfiguration pocConfig) + { + if (!ObjectId.TryParse(id, out var pocId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + await pointOfCareService.UpdateConfiguration(pocId, pocConfig); + return Ok(); + } + + [HttpPut("relay")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdatePoCRelay([FromBody] PointOfCare poc) + { + await pointOfCareService.UpdateRelayConfig(poc); + return Ok(); + } + + [HttpPut("{id}/unit")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdatePoCConfiguration([FromBody] Unit unit, string id) + { + if (!ObjectId.TryParse(id, out var pocId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + await pointOfCareService.UpdateUnit(pocId, unit); + return Ok(); + } + + [HttpDelete("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DeletePoC(string id) + { + if (!ObjectId.TryParse(id, out var pocId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + await pointOfCareService.Delete(pocId); + return Ok(); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/ProxyDeviceController.cs b/adas-core/Controllers/ProxyDeviceController.cs new file mode 100644 index 00000000..547e88df --- /dev/null +++ b/adas-core/Controllers/ProxyDeviceController.cs @@ -0,0 +1,75 @@ +using System.Net; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.module.ProxyDevices.Services; +using Microsoft.AspNetCore.Mvc; + +namespace adas_core.Controllers; + +[Route("proxy-devices")] +[ApiController] +public class ProxyDeviceController(IProxyDeviceService proxyDeviceService) : ControllerBase +{ + [HttpGet("{deviceId}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task GetStreamById(string deviceId) + { + var response = await proxyDeviceService.Process(deviceId); + if (!response.IsSuccessStatusCode) return new StatusCodeResult((int)response.StatusCode); + var stream = await response.Content.ReadAsStreamAsync(); + if (response.Content.Headers.ContentLength > 0) Response.ContentLength = response.Content.Headers.ContentLength; + return new FileStreamResult(stream, + response.Content.Headers.ContentType?.ToString() ?? "multipart/x-mixed-replace"); // Set MIME type as needed + } + + [HttpGet("{deviceId}/content")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task GetContentById(string deviceId) + { + var response = await proxyDeviceService.Process(deviceId); + var content = await response.Content.ReadAsStreamAsync(); + if (response.Content.Headers.ContentLength > 0) Response.ContentLength = response.Content.Headers.ContentLength; + if (response.Content.Headers.ContentType != null) + Response.ContentType = response.Content.Headers.ContentType?.ToString() ?? ""; + return Ok(content); + } + + [HttpGet("{deviceId}/mjpeg-stream")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task GetAsMpegStreamById(string deviceId) + { + try + { + + using var cts = CancellationTokenSource.CreateLinkedTokenSource(HttpContext.RequestAborted); + //cts.CancelAfter(TimeSpan.FromMinutes(5)); + + var response = await proxyDeviceService.Stream(deviceId); + + if (!response.IsSuccessStatusCode) + { + Response.StatusCode = (int)response.StatusCode; + return; + } + + if (response.Content.Headers.ContentType != null) + Response.ContentType = response.Content.Headers.ContentType?.ToString() ?? + "multipart/x-mixed-replace;boundary=my-boundary"; + await using var stream = await response.Content.ReadAsStreamAsync(cts.Token); + // Buffer for reading the stream + var buffer = new byte[4096]; + int bytesRead; + + + while (!cts.Token.IsCancellationRequested && (bytesRead = await stream.ReadAsync(buffer, cts.Token)) > 0) + { + await Response.Body.WriteAsync(buffer.AsMemory(0, bytesRead), cts.Token); + await Response.Body.FlushAsync(cts.Token); + } + } + catch (HttpRequestException e) + { + Response.StatusCode = (int)(e.StatusCode ?? HttpStatusCode.NotFound); + } + } +} \ No newline at end of file diff --git a/adas-core/Controllers/PumpsController.cs b/adas-core/Controllers/PumpsController.cs new file mode 100644 index 00000000..ed30ca6c --- /dev/null +++ b/adas-core/Controllers/PumpsController.cs @@ -0,0 +1,122 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using adas_core.Logging; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; + +namespace adas_core.Controllers; + +[Route("pumps")] +public class PumpController : ControllerBase +{ + private readonly IHistoricalConfigChangesService _historicalConfigChangesService; + private readonly IPumpService _pumpService; + + + public PumpController(IPumpService pumpService, IHistoricalConfigChangesService historicalConfigChangesService) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + _pumpService = pumpService; + _historicalConfigChangesService = historicalConfigChangesService; + } + + [HttpPost("request")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public IActionResult ProcessRequest([FromBody] PumpDto pumpDto) + { + var request = new ApiRequest + { + TraceId = Guid.NewGuid().ToString(), + Type = "AlarisPump", + PatientNumber = pumpDto.PatientNumber, + Location = pumpDto.Location, + PumpObservations = pumpDto.PumpObservations, + MessageTime = pumpDto.MessageTime, + WaitResult = pumpDto.WaitResult + }; + + + if (request.WaitResult) _pumpService.SaveRequest(request); + + else _pumpService.SaveRequestAsync(request); + return Ok(); + } + + [HttpGet] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetAllPumpConfig() + { + return Ok(await _pumpService.GetAllPumpConfig()); + } + + [HttpGet("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetPumpConfigById(string id) + { + return Ok(await _pumpService.GetPumpConfigsById(id)); + } + + [HttpPut] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdatePumpConfig([FromBody] ConfigPumps configPumps) + { + if (configPumps == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var oldConfig = await _pumpService.GetPumpConfigsById(configPumps.Id); + + var result = await _pumpService.UpdatePumpConfig(configPumps); + await LogHistoricalConfigChanged(result, oldConfig); + return Ok(result); + } + + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task InsertPumpConfig([FromBody] ConfigPumps config) + { + if (config == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var result = await _pumpService.InsertPumpConfig(config); + await LogHistoricalConfigChanged(config, null); + return Ok(result); + } + + [HttpDelete("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DeletePumpConfig(string id) + { + var config = await _pumpService.GetPumpConfigsById(id); + var result = await _pumpService.DeletePumpConfig(config!); + + await LogHistoricalConfigChanged(null, config); + return Ok(result); + } + + [HttpPost("paginated")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task PaginatedPump([FromBody] PaginationFilter config) + { + if (config == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var result = await _pumpService.GetPaginatedPump(config); + return Ok(result); + } + + + private async Task LogHistoricalConfigChanged(ConfigPumps? newConfigO, ConfigPumps? oldConfig) + { + var user = HttpContext.User.Identity?.Name ?? HttpContext.Connection.RemoteIpAddress?.ToString(); + await _historicalConfigChangesService.LogConfigChanged(user!, DisplayConfigEnums.ConfigTypes.PumpCfg, + newConfigO?.ToJson() ?? string.Empty, oldConfig?.ToJson() ?? string.Empty); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/QXController.cs b/adas-core/Controllers/QXController.cs new file mode 100644 index 00000000..51397f85 --- /dev/null +++ b/adas-core/Controllers/QXController.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Mvc; + +namespace adas_core.Controllers; + +[Route("QX")] +public class QxController : ControllerBase +{ + // TODO: ver donde ses usa este controlador para version 3 del modelo + + // private readonly IBoxService _boxService; + // private readonly ILogger _logger; + // + // public QxController(IBoxService boxService, ILogger logger) + // { + // LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + // + // _boxService = boxService; + // _logger = logger; + // } + // + // [HttpPost] + // [Route("alert")] + // public IActionResult SendAlert([FromBody] ApiRequest request) + // { + // if (request != null) + // { + // try + // { + // _boxService.SendSurgeryRoomAlert(request); + // } + // catch (Exception ex) + // { + // _logger.LogError("Error saving the request: {exMessage} ", ex); + // return BadRequest(); + // } + // return Ok(); + // } + // return BadRequest("Request is null"); + // } + // + // [HttpPost] + // [Route("chat")] + // public IActionResult SendChatMessage([FromBody] ApiRequest request) + // { + // if (request != null) + // { + // try + // { + // if (request.ChatMessage == null) + // return BadRequest($"Request ChatMessage: {request} is null"); + // + // //_boxService.SendChatMessage(request.chatMessage); + // + // } + // catch (Exception ex) + // { + // _logger.LogError("Error saving the request: {exMessage} ", ex); + // return BadRequest(); + // } + // return Ok(); + // } + // return BadRequest("Request is null"); + // } +} \ No newline at end of file diff --git a/adas-core/Controllers/RabbitController.cs b/adas-core/Controllers/RabbitController.cs new file mode 100644 index 00000000..ddb399d7 --- /dev/null +++ b/adas-core/Controllers/RabbitController.cs @@ -0,0 +1,33 @@ +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Infrastructure.Services; +using adas_core.Logging; +using Microsoft.AspNetCore.Mvc; + +namespace adas_core.Controllers; + +[Route("rabbit")] +[ApiController] +public class RabbitController(ReceiverService receiverService) : ControllerBase +{ + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthAdmin)] + [Route("start-consume/{queueName}")] + public async Task ConsumeErrorQueue(string queueName) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + await receiverService.ProcessErrorQueue(queueName); + return Ok(); + } + + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthAdmin)] + [Route("stop-consume/{queueName}")] + public async Task ConsumeStopErrorQueue(string queueName) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + await receiverService.ProcessErrorQueue(queueName); + return Ok(); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/RecordingAlertController.cs b/adas-core/Controllers/RecordingAlertController.cs new file mode 100644 index 00000000..2bc45d59 --- /dev/null +++ b/adas-core/Controllers/RecordingAlertController.cs @@ -0,0 +1,49 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.DTO; +using adas_core.Logging; +using Microsoft.AspNetCore.Mvc; + +namespace adas_core.Controllers; + +[Route("alerts")] +[ApiController] +public class RecordingAlertController : ControllerBase +{ + private readonly IRecordingAlertService _recordingAlertService; + + + public RecordingAlertController(IRecordingAlertService recordingAlertService) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + _recordingAlertService = recordingAlertService; + } + + + [HttpPost] + [Route("")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public IActionResult ProcessRequest([FromBody] RecordingAlertDto recordingAlertDto) + { + var request = new ApiRequest + { + TraceId = Guid.NewGuid().ToString(), + Type = "RecordingAlert", + PatientNumber = recordingAlertDto.PatientNumber, + Location = recordingAlertDto.Location, + RecordingAlerts = recordingAlertDto.RecordingAlerts, + Recording = recordingAlertDto.RecordingData, + MessageTime = recordingAlertDto.MessageTime, + WaitResult = recordingAlertDto.WaitResult + }; + + if (request.WaitResult) _recordingAlertService.SaveRequest(request); + else _recordingAlertService.SaveRequestAsync(request); + + return Ok(); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/RecordingController.cs b/adas-core/Controllers/RecordingController.cs new file mode 100644 index 00000000..91e06ad4 --- /dev/null +++ b/adas-core/Controllers/RecordingController.cs @@ -0,0 +1,140 @@ +using Microsoft.AspNetCore.Mvc; + +namespace adas_core.Controllers; + +[Route("Recording")] +[ApiController] +public class RecordingController : ControllerBase +{ + // TODO: ver donde ses usa este controlador para version 3 del modelo + + // private readonly IRecordingService _recordingService; + // private readonly IPatientService _patientService; + // private readonly ISectionService _sectionService; + // private readonly ILogger _logger; + // + // private readonly string? _url = string.Empty; + // + // public RecordingController(IPatientService patientService, + // IRecordingService recordingService, + // ISectionService sectionService, + // ILogger logger) + // { + // LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + // + // _recordingService = recordingService; + // _patientService = patientService; + // _sectionService = sectionService; + // _logger = logger; + // } + // + // [HttpPost] + // [Route("{patientId}/stop")] + // public async Task StopRecording([FromBody] ManualRecording manualRecording, string patientId) + // { + // try + // { + // if (!ObjectId.TryParse(patientId, out var patientIdParsed)) return BadRequest("patientId is not ObjectId"); + // + // var patient = await _patientService.FindById(patientIdParsed); + // if (patient == null) return BadRequest($"User not found:{patientId}"); + // var boxConfig = await _sectionService.FindBox(patient.Location); + // if (boxConfig == null) return BadRequest($"boxConfig not found:{patient.Location}"); + // + // //await _recordingService.SendRecordingData(patient, boxConfig, manualRecording, start: false); + // + // return Ok($"[{_url}] -- {JsonConvert.SerializeObject(manualRecording)}"); + // } + // catch (Exception ex) + // { + // _logger.LogError("Error saving the request: {exMessage} ", ex); + // return BadRequest($"ERROR:{ex.Message}"); + // } + // + // } + // + // [HttpPost] + // [Route("{patientId}/start")] + // public async Task StartRecording([FromBody] ManualRecording manualRecording, string patientId) //TODO PASAR API REQUEST + // { + // try + // { + // if (!ObjectId.TryParse(patientId, out var patientIdParsed)) return BadRequest("patientId is not ObjectId"); + // + // var patient = await _patientService.FindById(patientIdParsed); + // if (patient == null) return BadRequest($"User not found:{patientId}"); + // var boxConfig = await _sectionService.FindBox(patient.Location); + // if (boxConfig == null) return BadRequest($"boxConfig not found:{patient.Location}"); + // + // //encola el mensaje + // //await _recordingService.SendRecordingData(patient, boxConfig, manualRecording, start: true); + // + // + // + // return Ok($"[{_url}] -- {JsonConvert.SerializeObject(manualRecording)}"); + // } + // catch (Exception ex) + // { + // Log.Warning("Error saving the request: " + ex); + // return BadRequest($"ERROR:{ex.Message}"); + // } + // + // + // } + // + // [HttpPost] + // [Route("{patientId}/cancel")] + // public async Task CancelRecording(string patientId) + // { + // try + // { + // + // if (!ObjectId.TryParse(patientId, out var patientIdParsed)) return BadRequest("patientId is not ObjectId"); + // + // var patient = await _patientService.FindById(patientIdParsed); + // if (patient == null) return BadRequest($"User not found:{patientId}"); + // var boxConfig = await _sectionService.FindBox(patient.Location); + // if (boxConfig == null) return BadRequest($"boxConfig not found:{patient.Location}"); + // + // // var cancelRecordingSucessfully = await _recordingService.SendCancelRecordingToRecordingApi(patient, boxConfig); + // // return cancelRecordingSucessfully ? Ok() : StatusCode(500, "error canceling recording"); + // return Ok(); + // + // } + // catch (Exception ex) + // { + // _logger.LogError("Error saving the request: {exMessage} ", ex); + // throw; + // } + // + // } + // + // [HttpPost] + // [Route("{patientId}/auto")] + // public async Task AutomaticRecording([FromBody] RecordingData automaticRecording, string patientId) + // { + // try + // { + // if (!ObjectId.TryParse(patientId, out var patientIdParsed)) return BadRequest("patientId is not ObjectId"); + // + // var patient = await _patientService.FindById(patientIdParsed); + // if (patient == null) return BadRequest($"User not found:{patientId}"); + // var boxConfig = await _sectionService.FindBox(patient.Location); + // if (boxConfig == null) return BadRequest($"boxConfig not found:{patient.Location}"); + // + // // var result = await _recordingService.SendAutoRecordingData(patient, boxConfig, automaticRecording); + // + // // if (result) + // // return Ok($"[{_url}] -- {JsonConvert.SerializeObject(automaticRecording)}"); + // // else + // // return BadRequest($"[{_url}] -- {JsonConvert.SerializeObject(automaticRecording)}"); + // return Ok(); + // } + // catch (Exception ex) + // { + // _logger.LogError("Error saving the request: {exMessage} ", ex); + // throw; + // } + // + // } +} \ No newline at end of file diff --git a/adas-core/Controllers/RelayController.cs b/adas-core/Controllers/RelayController.cs new file mode 100644 index 00000000..64617e3c --- /dev/null +++ b/adas-core/Controllers/RelayController.cs @@ -0,0 +1,252 @@ +using System.Net; +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Utils; +using adas_core.Logging; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; +using MongoDB.Bson; + +namespace adas_core.Controllers; + +[ApiController] +[Route("Relay")] +public class RelayController : ControllerBase +{ + // TODO: ver donde ses usa este controlador para version 3 del modelo + + private string? _url = string.Empty; + private readonly IPointOfCareService _pointOfCareService; + private readonly IRelayService _relayService; + private readonly ILogger _logger; + + public RelayController( + IOptions recordingSettings, + IRelayService relayService, + ILogger logger, IPointOfCareService pointOfCareService) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + _relayService = relayService; + _logger = logger; + _pointOfCareService = pointOfCareService; + } + + + [HttpGet] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [Route("status")] + public async Task CheckSectionStatus() + { + var section = await _pointOfCareService.GetAllConfigs(); + if (section.IsNullOrEmpty()) return NotFound(); + var boxes = section.Where(b =>b.Configuration != null && !b.Configuration.RelayList.IsNullOrEmpty()).ToList(); + var dict = new List(); + foreach (var box in boxes) + { + foreach (var relay in box.Configuration!.RelayList!) + { + var status = await _relayService.CheckRelayStatus(relay); + dict.Add(new { Unit = box.Unit?.Name??box.UnitId.ToString(), bed = box.Bed, status}); + } + } + + return Ok(dict); + } + + + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [Route("powerOn")] + public async Task PowerOnPoC([FromBody] PowerActionRequest action) + { + return await PowerAction(action.UnitId, action.PointOfCareId, action.Bed, async (relayConfig) => + { + await _relayService.PowerOn(relayConfig); + }); + } + + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [Route("powerOff")] + public async Task PowerOffPoC([FromBody] PowerActionRequest action) + { + return await PowerAction(action.UnitId, action.PointOfCareId, action.Bed, async (relayConfig) => + { + await _relayService.PowerOff(relayConfig); + }); + } + + private async Task PowerAction(string unitId, string pointOfCareId, string bed, Func action) + { + if(!ObjectId.TryParse(pointOfCareId, out var poc)) return Conflict("Cannot parse to OBjectId"); + + var box = await _pointOfCareService.FindByIdAllConfig(poc); + if (box == null) return NotFound("No box found"); + + if (box.Configuration == null || box.Configuration.RelayList.IsNullOrEmpty()) return NotFound("No relay configuration found"); + + foreach (var relay in box.Configuration.RelayList!) + { + await action(relay); + } + + return Ok(); + } + + + + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthAdmin)] + [Route("{id}/status")] + public IActionResult CheckRelayStatus(int id, string driver, string host, int port, string relayName, int total, string username, string password) + { + Relay relayConfig = new() + { + Driver = driver, + Ip = host, + Port = port, + RelayNumber = id, + RelayName = relayName, + Total = total, + Username = username, + Password = password + }; + var strConfig = relayConfig.ToString() ?? "null"; + _logger.LogDebug("Config:{strConfig}", strConfig); + + var result = _relayService.CheckRelayStatus(relayConfig); + + var resultStr = result.ToString()??"null"; + _logger.LogDebug("Relay Status {resultStr}", resultStr); + + return Ok(result); + } + + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [Route("{id}/power/on")] + public IActionResult PowerOn(int id, string driver, string host, int port, string relayName, int total, string username, string? password) + { + Relay relayConfig = new() + { + Driver = driver, + Ip = host, + Port = port, + RelayNumber = id, + RelayName = relayName, + Total = total, + Username = username, + Password = password + }; + + var strConfig = relayConfig.ToString() ?? "null"; + _logger.LogDebug("Config: {strConfig}", strConfig); + + + try + { + _relayService.PowerOn(relayConfig); + } + catch (Exception) + { + return StatusCode(int.Parse(HttpStatusCode.InternalServerError.ToString())); + + } + + + return Ok(true); + } + + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [Route("{id}/power/off")] + public IActionResult PowerOff(int id, string driver, string host, int port, string relayName, int total, string username, string? password) + { + Relay relayConfig = new () + { + Driver = driver, + Ip = host, + Port = port, + RelayNumber = id, + RelayName = relayName, + Total = total, + Username = username, + Password = password + }; + + var strConfig = relayConfig.ToString() ?? "null"; + _logger.LogDebug("Config: {strConfig}", strConfig); + + try + { + _relayService.PowerOff(relayConfig); + } + catch (Exception) + { + return StatusCode(int.Parse(HttpStatusCode.InternalServerError.ToString())); + } + return Ok(false); + } + + + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [Route("manual/{status}")] + public IActionResult ManualSwitch(string status, string pocId, string type) + { + try + { + if (!Enum.TryParse( status, out RelayEnum.Status relayStatus)) + return BadRequest(); + if (!Enum.TryParse( type, out RelayEnum.Type relayType)) + return BadRequest(); + if (!ObjectId.TryParse(pocId, out var poc)) + return BadRequest(); + + _= _relayService.SetManualRelay( relayStatus, poc, relayType); + } + catch (Exception) + { + return StatusCode(int.Parse(HttpStatusCode.InternalServerError.ToString())); + } + return Ok(false); + } + [HttpPost("paginated")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task GetPaginatedUnits([FromBody] PaginationFilter request, bool withPoCs) + { + if (request == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var relays = await _relayService.GetPaginatedRelays(request); + return Ok(relays); + } + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task CreateRelay([FromBody] Relay? request) + { + if (request == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var relays = await _relayService.InsertRelay(request); + return Ok(relays); + } + [HttpPut("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + public async Task UpdateRelay([FromBody] Relay relay, string id) + { + if (!ObjectId.TryParse(id, out var idParsed)) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var relayUpdated = await _relayService.UpdateRelayById(idParsed, relay); + return Ok(relayUpdated); + } + +} + +public class PowerActionRequest +{ + public string UnitId { get; set; } = null!; + public string PointOfCareId { get; set; } = null!; + public string Bed { get; set; } = null!; +} \ No newline at end of file diff --git a/adas-core/Controllers/SendAlertController.cs b/adas-core/Controllers/SendAlertController.cs new file mode 100644 index 00000000..96efcb81 --- /dev/null +++ b/adas-core/Controllers/SendAlertController.cs @@ -0,0 +1,91 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Logging; +using Microsoft.AspNetCore.Mvc; +using Newtonsoft.Json; +using WebPush; + +namespace adas_core.Controllers; + +[Route("send-alerts")] +[ApiController] +public class SendAlertController : ControllerBase +{ + private readonly ISendAlertService _sendAlertService; + + public SendAlertController(ISendAlertService sendAlertService) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + _sendAlertService = sendAlertService; + } + + public static List Subscriptions { get; set; } = []; + + [HttpGet("rabbit")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthAdmin)] + public async Task GetErrorQueues() + { + var errorQueues = await _sendAlertService.GetQueues(); + return Ok(errorQueues); + } + + [HttpGet("performance")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthAdmin)] + public IActionResult GetPerformance() + { + var performance = _sendAlertService.GetPerformance(); + return Ok(performance); + } + + [HttpGet("api-clients")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthAdmin)] + public async Task GetApiClients() + { + var clients = await _sendAlertService.GetApiClients(); + return Ok(clients); + } + + [HttpPost("web-subscribe")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthAdmin)] + public IActionResult WebSubscribe([FromBody] PushSubscription sub) + { + Subscriptions.Add(sub); + return Ok("correct subscription"); + } + + [HttpPost("web-unsubscribe")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthAdmin)] + public IActionResult WebUnSubscribe([FromBody] PushSubscription sub) + { + var item = Subscriptions.FirstOrDefault(s => s.Endpoint == sub.Endpoint); + if (item != null) + { + Subscriptions.Remove(item); + return Ok("subscripcion correcta"); + } + + return Ok("failed subscription"); + } + + [HttpPost("broadcast/{sub}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthAdmin)] + public IActionResult Broadcast(string sub, [FromBody] object message) + { + var vapidDetails = new VapidDetails("mailto:example@example.com", + "BJk9MGeoFy4npXPA6PQIgZYW5oxhiiBm_t2pB08FiotYQ4UvSaibG3T946Oqrc_nP17tdONbx0O4k9dWnt16TMM", + "21YckusFRv00z1nOTpLZK1NlMXSZxZECnpwbsaKTjOE"); + + var client = new WebPushClient(); + var serializedMessage = JsonConvert.SerializeObject(message); + var pushSubscription = Subscriptions.FirstOrDefault(s => s.Endpoint == sub); + + if (pushSubscription == null) + return NotFound("Subscription not found"); + + client.SendNotification(pushSubscription, serializedMessage, vapidDetails); + + return Ok("correct subscription"); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/SensorController.cs b/adas-core/Controllers/SensorController.cs new file mode 100644 index 00000000..742159a9 --- /dev/null +++ b/adas-core/Controllers/SensorController.cs @@ -0,0 +1,95 @@ +using Microsoft.AspNetCore.Mvc; + +namespace adas_core.Controllers; + +[Route("Sensor")] +[ApiController] +public class SensorController : ControllerBase +{ + // private readonly IObservationService _observationService; + // private readonly ISectionService _sectionService; + // private readonly ILightBeaconService _lightBeaconService; + // private readonly IPatientService _patientService; + // private readonly ILogger _logger; + // private readonly IClientMessageService _clientMessageService; + // private readonly ISubscribersService _subscribersService; + // + // public SensorController(IObservationService observationService, ISectionService sectionService, IPatientService patientService, ILightBeaconService lightBeaconService, ILogger logger, IClientMessageService clientMessageService, ISubscribersService subscribersService) + // { + // LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + // + // _observationService = observationService; + // _sectionService = sectionService; + // _patientService = patientService; + // _lightBeaconService = lightBeaconService; + // _logger = logger; + // _clientMessageService = clientMessageService; + // _subscribersService = subscribersService; + // } + // + // [HttpPost] + // [Route("")] + // public async Task ProcessRequest([FromBody] ApiRequest request) + // { + // if (request != null) + // { + // try + // { + // await ProcessSensorData(request); + // } + // catch (Exception ex) + // { + // _logger.LogError("Error processing sensor request: {exMessage}", ex); + // return BadRequest(ex.Message); + // } + // } + // return Ok(); + // } + // + // private async Task ProcessSensorData(ApiRequest request) + // { + // if (request.Observations == null) + // { + // _logger.LogError("Error processing sensor data. Request observations null. ApiRequest: {request}", request); + // return; + // } + // + // var patient = await _patientService.FindByLocation(request.Location); + // + // if (patient != null) + // { + // foreach (var sensorData in request.Observations) + // { + // var sensorMappedData = await _observationService.MapObservationsByName(sensorData); + // var section = await _sectionService.FindByPointOfCare(patient.UnitString); + // if (section == null) return; + // + // var subscribers = _subscribersService.GetSubscribers().Where(s => + // (s.SubscriptionType == SubscriptionType.Box && s.Box == patient.Bed && + // s.Section == section.Id) || + // (s.SubscriptionType == SubscriptionType.Section && s.Section == section.Id)).ToList(); + // if (sensorMappedData == null) continue; + // + // sensorMappedData.PatientId = patient.Id; + // + // foreach (var subscriber in subscribers) + // { + // await _clientMessageService.SendAsync(subscriber.Id, OperationType.Observation, sensorMappedData); + // } + // } + // } + // + // + // var obsWithAlert = request.Observations.FirstOrDefault(o => o.Status != ObservationStatus.Ok); + // + // if (obsWithAlert == null) obsWithAlert = request.Observations.FirstOrDefault(); + // else + // { + // obsWithAlert = request.Observations.FirstOrDefault(o => o.Status == ObservationStatus.Alert); + // obsWithAlert ??= request.Observations.FirstOrDefault(o => o.Status == ObservationStatus.Warning); + // } + // + // if (obsWithAlert != null) + // _lightBeaconService.GenerateColorAlert(obsWithAlert); + // } +} \ No newline at end of file diff --git a/adas-core/Controllers/ServiceConfigController.cs b/adas-core/Controllers/ServiceConfigController.cs new file mode 100644 index 00000000..5fad7644 --- /dev/null +++ b/adas-core/Controllers/ServiceConfigController.cs @@ -0,0 +1,29 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Logging; +using Microsoft.AspNetCore.Mvc; + +namespace adas_core.Controllers; + +[Route("service-configs")] +[ApiController] +public class ServiceConfigController : ControllerBase +{ + private readonly IServiceConfigService _serviceConfigService; + + public ServiceConfigController(IServiceConfigService serviceConfigService) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + _serviceConfigService = serviceConfigService; + } + + [HttpGet("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthAdmin)] + public async Task Find(string id) + { + var serviceConfig = await _serviceConfigService.Get(id); + return Ok(serviceConfig); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/TreatmentsController.cs b/adas-core/Controllers/TreatmentsController.cs new file mode 100644 index 00000000..6ae0116f --- /dev/null +++ b/adas-core/Controllers/TreatmentsController.cs @@ -0,0 +1,132 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Filter; +using adas_core.Logging; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; + +namespace adas_core.Controllers; + +[Route("treatments")] +[ApiController] +public class TreatmentsController : ControllerBase +{ + private readonly IPatientService _patientService; + private readonly ITreatmentService _treatmentService; + + public TreatmentsController(ITreatmentService treatmentService, IPatientService patientService) + { + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString(), true); + + _treatmentService = treatmentService; + _patientService = patientService; + } + + + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ProcessRequest([FromBody] TreatmentDto treatmentDto) + { + var request = new ApiRequest + { + TraceId = Guid.NewGuid().ToString(), + Type = "RAS_O17", + PatientNumber = treatmentDto.PatientNumber, + Location = treatmentDto.Location, + MessageTime = treatmentDto.MessageTime, + WaitResult = treatmentDto.WaitResult, + RequestFromPanel = true, + Treatments = [] + }; + + + if (treatmentDto.Treatment != null) + request.Treatments.Add(treatmentDto.Treatment); + + var patient = await _patientService.FindByPatientNumber(treatmentDto.PatientNumber, true) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + + request.Location ??= patient.Location; + + await _treatmentService.SaveRequestAsync(request); + + //var treatList = await _treatmentService.GetTreatmentsByPatientId(patient.Id); + + //if (!treatList.Any(t => t.MessageTime == treatmentDto.MessageTime)) + //{ + // return NotFound($"Treatment not inserted"); + //} + + //var treat = treatList.FirstOrDefault(t => t.MessageTime == treatmentDto.MessageTime); + //if (treat == null) + //{ + // return BadRequest("Error inserting treatment"); + //} + + //return Ok(treat); + + return Ok(); + } + + [HttpGet("{patientid}/patient-id")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetTreatmentsByPatientId(string patientid) + { + if (!ObjectId.TryParse(patientid, out var validId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var treatments = await _treatmentService.GetTreatmentsByPatientId(validId); + return Ok(treatments.OrderBy(p => p.OrderTime).Take(50)); + } + + + [HttpPut] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateTreatment([FromBody] PatientTreatment request) + { + var treatment = await _treatmentService.UpdateTreatment(request); + return Ok(treatment); + } + + //Archive + + [HttpDelete("{patientid}/patient-id")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DeleteTreatment(string patientId) + { + if (!ObjectId.TryParse(patientId, out var id)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + await _treatmentService.DeleteByPatientId(id); + return Ok(); + } + + [HttpDelete("{treatmentId}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DeleteTreatmentById(string treatmentId) + { + if (!ObjectId.TryParse(treatmentId, out var id)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + await _treatmentService.DeleteById(id); + return Ok(); + } + + [HttpPost("paginated")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetPaginatedTreatments([FromBody] PaginationFilter request) + { + if (request == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var treatments = await _treatmentService.GetPaginatedTreatments(request); + + return Ok(treatments); + } +} \ No newline at end of file diff --git a/adas-core/Controllers/UnitController.cs b/adas-core/Controllers/UnitController.cs new file mode 100644 index 00000000..8452d23c --- /dev/null +++ b/adas-core/Controllers/UnitController.cs @@ -0,0 +1,281 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; + +namespace adas_core.Controllers; + +[Route("units")] +[ApiController] +public class UnitController( + ILogger logger, + IUnitService unitService, + IAdminPanelService adminPanelService) + : ControllerBase +{ + /// + /// GetByCodeSysAndCode all unit list + /// + /// + /// List + /// + [HttpGet("{withPoCs}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task List(bool withPoCs) + { + var units = await unitService.GetAll(withPoCs); + return Ok(units); + } + + [HttpGet("compact")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task ListCompact() + { + var units = await unitService.GetAllCompact(); + return Ok(units); + } + + [HttpGet("compact/{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task Compact(string id) + { + if (!ObjectId.TryParse(id, out var idConvertd)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var unit = await unitService.GetOneCompact(idConvertd); + return Ok(unit); + } + + [HttpPost("paginated/{withPoCs}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetPaginatedUnits([FromBody] PaginationFilter request, bool withPoCs) + { + if (request == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var units = await unitService.GetPaginatedUnits(request, withPoCs); + return Ok(units); + } + + + /// + /// GetByCodeSysAndCode unit by id + /// + /// + /// + /// + [HttpGet("{id}/{listFilled}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task Find(string id, bool listFilled = true) + { + if (!ObjectId.TryParse(id, out var unitId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + //TODO: ADAPTAR A LOCALE + var unit = await unitService.GetInfo(unitId, fillLists: listFilled, withPoCs: true, + dataLocale: LocaleEnum.Default); + return Ok(unit); + } + + /// + /// GetByCodeSysAndCode unit status + /// + /// + /// + [HttpGet("status/{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task Status(string id) + { + if (!ObjectId.TryParse(id, out var unitId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + //TODO: ADAPTAR A LOCALE + var section = await unitService.GetInfo(unitId, LocaleEnum.Default); + + return Ok(section!.Status); + } + + /// + /// GetByCodeSysAndCode unit config by unit name + /// + /// + /// + [HttpGet("{unitName}/config")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task Config(string unitName) + { + //SEND SECTION INFO + var section = await unitService.GetByName(unitName); + return Ok(section!.Configuration); + } + + /// + /// + /// + /// + [HttpGet("{patientId}/patient-id")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task FindByPatientId(string patientId) + { + if (!ObjectId.TryParse(patientId, out var objPatientId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + var sections = await unitService.FindByPatientId(objPatientId); + + return Ok(sections); + } + + /// + /// Unit PointOfCares + /// + /// + /// + [HttpGet("{id}/point-of-cares")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetUnitPointOfCares(string id) + { + if (!ObjectId.TryParse(id, out var unitId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + //TODO: ADAPTAR A LOCALE + var sec = await unitService.GetInfo(unitId); + return Ok(sec?.PointOfCares); + } + + #region UciPanel + + /// + /// Post unit + /// + /// + /// + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task PostUnit(Unit unit) + { + var sec = await unitService.FindById(unit.Id); + if (sec != null) throw new ConflictException(HttpEnum.ErrorMessage.ConflictResourceAlreadyExists); + sec = await adminPanelService.InsertUnit(unit); + return Ok(sec); + } + + /// + /// Post unit + /// + /// + /// + [HttpDelete("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DeleteUnit(string id) + { + if (!ObjectId.TryParse(id, out var unitId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + + var unit = await unitService.FindById(unitId) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + var deleted = await adminPanelService.DeleteUnitById(unit.Id); + if (deleted) + return Ok(); + throw new BadRequestException(HttpEnum.ErrorMessage.ConflictDeleteFailed); + } + + /// + /// Update Unit + /// + /// + /// + /// + [HttpPut("{id}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateSection(string id, [FromBody] Unit unit) + { + if (!ObjectId.TryParse(id, out var unitId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + //TODO: ADAPTAR A LOCALE + await unitService.GetInfo(unitId, LocaleEnum.Default, false); + var secUpdated = await unitService.UpdateUnit(unit); + logger.LogInformation("Section {sectionid} updated at UTC {DateTimeUtcNow}", unit.Id, DateTime.UtcNow); + return Ok(secUpdated); + } + + /// + /// Update Unit master list Id + /// + /// + /// + [HttpPut("master-list-id")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateMasterListUnit([FromBody] UpdateUnitIdListDto updateUnitListDto) + { + //TODO: ADAPTAR A LOCALE + await unitService.GetInfo(updateUnitListDto.UnitId, LocaleEnum.Default, false); + + var secUpdated = await unitService.UpdateUnitMasterList(updateUnitListDto); + logger.LogInformation("Unit {sectionid} updated at UTC {DateTimeUtcNow} for master list {master}", + updateUnitListDto.UnitId, DateTime.UtcNow, updateUnitListDto.MasterListData.ToString()); + return Ok(secUpdated); + } + + [HttpPut("{unitId}/config")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateConfiguration(string unitId, [FromBody] UnitConfiguration unitConfiguration) + { + var isParsed = ObjectId.TryParse(unitId, out var unitIdParsed); + if (!isParsed) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var secUpdated = await unitService.UpdateConfiguration(unitIdParsed, unitConfiguration); + return Ok(secUpdated); + } + + /// + /// Update Unit + /// + /// + /// + /// + [HttpPut("{id}/info")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task UpdateUnitInfo([FromBody] UnitInfoDto request, string id) + { + if (!ObjectId.TryParse(id, out var unitId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var unitUpdated = + await unitService.UpdateUnitInfo(unitId, request.Name, request.Title, request.ConfigObservationId); + + logger.LogInformation("Section info {sectionid} updated at UTC {DateTimeUtcNow}", unitId, DateTime.UtcNow); + return Ok(unitUpdated); + } + + [HttpGet("{id}/dependency")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetUnitDependency(string id) + { + if (!ObjectId.TryParse(id, out var unitId)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var unit = await unitService.FindById(unitId) ?? + throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing); + + + var result = await adminPanelService.GetUnitDependencyDto(unit) ?? + throw new BadRequestException(HttpEnum.ErrorMessage.InternalServerErrorUnexpected); + + return Ok(result); + } + + #endregion +} \ No newline at end of file diff --git a/adas-core/Controllers/UserController.cs b/adas-core/Controllers/UserController.cs new file mode 100644 index 00000000..b24f3629 --- /dev/null +++ b/adas-core/Controllers/UserController.cs @@ -0,0 +1,301 @@ +using adas_core.Application.Exceptions; +using adas_core.Application.Services.Interfaces; +using adas_core.Authentication.Attributes; +using adas_core.Authentication.Interfaces; +using adas_core.Authentication.Models; +using adas_core.Domain.Enums; +using adas_core.Domain.Models.DTO; +using adas_core.Domain.Models.Filter; +using adas_core.Domain.Models.MongoModels; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using MongoDB.Bson; + +namespace adas_core.Controllers; + +[ApiController] +[Route("users")] +public class UserController( + IUserService userService, + IAuthorityService authorityService, + Lazy permissionService) + : ControllerBase +{ + [AllowAnonymous] + [HttpPost("login")] + public async Task Login([FromBody] LoginInfo loginInfo) + { + var token = await userService.Login(loginInfo.Username, loginInfo.Password); + var authorities = await authorityService.GetUserAuthorities(token.User.Id); + token.User.Authorization = authorities; + token.User.Password = ""; + return Ok(UciResponse.FromSuccess(token)); + } + [AllowAnonymous] + [HttpGet("token")] + public async Task LoginWithBearer() + { + var user = (User?)HttpContext.Items["User"] ?? + throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedUserNotRegistered); + var newJwtToken = await userService.GenerateJwt(user); + var authorities = await authorityService.GetUserAuthorities(newJwtToken.User.Id); + newJwtToken.User.Authorization = authorities; + return Ok(UciResponse.FromSuccess(newJwtToken)); + } + + [AllowAnonymous] + [HttpGet("login")] + public async Task LoginWithAccessToken([FromQuery] string token) + { + var accessToken = await userService.LoginWithAccessToken(token); + accessToken.AccessToken = token; + userService.ValidateToken(token, "user", out var tokenRead); + accessToken.AccessTokenExpiryTime = tokenRead.ValidTo; + return Ok(UciResponse.FromSuccess(accessToken)); + } + + [AllowAnonymous] + [HttpPost("refresh-token")] + public async Task RefreshToken([FromBody] string refreshToken) + { + var token = await userService.RefreshToken(refreshToken); + return Ok(UciResponse.FromSuccess(token)); + } + + [AllowAnonymous] + [HttpGet("login-cas")] + public async Task LoginWithCas([FromQuery] string service, [FromQuery] string ticket) + { + try + { + var user = await userService.GetUserByCasTicket(service, ticket); + if (user != null) + { + var newJwtToken = await userService.GenerateJwt(user); + return Ok(UciResponse.FromSuccess(newJwtToken)); + } + } + catch (Exception ex) + { + Console.WriteLine(ex); + throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedNoPermission); + } + + return NotFound(); + } + + [HttpGet("me")] + public IActionResult Me() + { + var user = (User?)HttpContext.Items["User"] ?? + throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedUserNotRegistered); + return Ok(UciResponse.FromSuccess(user)); + } + + [HttpGet] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetAll() + { + var users = await userService.GetAll(); + return Ok(UciResponse>.FromSuccess(users)); + } + + + [HttpGet("authorities")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetAllAuthorities() + { + var authorities = await authorityService.GetAllAuthorities(); + return Ok(UciResponse>.FromSuccess(authorities)); + } + + [HttpPost("paginated")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task PaginatedUsers([FromBody] PaginationFilter config) + { + if (config == null) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestMissingParameters); + var result = await userService.GetPaginatedUsers(config); + return Ok(result); + } + + #region UciPanel + [AllowAnonymous] + [HttpPost("login/adm-panel")] + public async Task LoginPanel([FromBody] LoginInfo loginInfo) + { + var token = await userService.Login(loginInfo.Username, loginInfo.Password); + var authorities = await authorityService.GetUserAuthorities(token.User.Id); + token.User.Authorization = authorities; + token.User.Password = ""; + var result = new TokenResultPanel + { + TokenResult = token, + Permissions = permissionService.Value.GetPermissionsForPanel(token.User) + }; + return Ok(UciResponse.FromSuccess(result)); + } + [AllowAnonymous] + [HttpGet("token/adm-panel")] + public async Task LoginWithBearerPanel() + { + var user = (User?)HttpContext.Items["User"] ?? + throw new UnauthorizedException(HttpEnum.ErrorMessage.UnauthorizedUserNotRegistered); + var newJwtToken = await userService.GenerateJwt(user); + var authorities = await authorityService.GetUserAuthorities(newJwtToken.User.Id); + newJwtToken.User.Authorization = authorities; + var result = new TokenResultPanel + { + TokenResult = newJwtToken, + Permissions = permissionService.Value.GetPermissionsForPanel(newJwtToken.User) + }; + return Ok(UciResponse.FromSuccess(result)); + } + [AllowAnonymous] + [HttpGet("login/adm-panel")] + public async Task LoginWithAccessTokenPanel([FromQuery] string token) + { + var accessToken = await userService.LoginWithAccessToken(token); + var result = new TokenResultPanel + { + TokenResult = accessToken, + Permissions = permissionService.Value.GetPermissionsForPanel(accessToken.User) + }; + return Ok(UciResponse.FromSuccess(result)); + } + + [HttpPost("refresh-token/adm-panel")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task RefreshTokenPanel([FromBody] string refreshToken) + { + var token = await userService.RefreshToken(refreshToken); + var result = new TokenResultPanel + { + TokenResult = token, + Permissions = permissionService.Value.GetPermissionsForPanel(token.User) + }; + return Ok(UciResponse.FromSuccess(result)); + } + + [HttpPost] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task NewUser(User u) + { + var newUser = await userService.CreateNewUserByRequest(u); + return Ok(newUser); + } + + [HttpPost("authorities")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task NewUserWithAuthorities([FromBody] CreateUserWithAuthDto createUserWithAuthDto) + { + var user = await userService.CreateNewUserWithAuthorities(createUserWithAuthDto); + return Ok(user); + } + + [HttpDelete("{userId}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DeleteUser(string userId) + { + var isParsed = ObjectId.TryParse(userId, out var userIdParsed); + if (!isParsed) throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var isDeleted = await userService.DeleteUser(userIdParsed); + + if (isDeleted) + { + await authorityService.DeleteAuthoritiesForUser(userIdParsed); + return Ok(); + } + + throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed); + } + + [HttpPut("{updatePass}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task EditUser(User u, bool updatePass) + { + _ = await userService.UpdateUsersByRequest(u, updatePass) ?? + throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed); + return Ok(); + } + + [HttpPut("{updatePass}/authorities")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task EditUserWithAuthorities(UpdateUserWithAuthDto updateUserWithAuthDto, + bool updatePass) + { + await userService.UpdateUserWithAuthorities(updateUserWithAuthDto, updatePass); + return Ok(); + } + + [HttpPost("authority")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task NewUserAuthority(Authorization authorization) + { + var auth = await userService.CreateNewAuthority(authorization); + return Ok(auth); + } + + [HttpDelete("authority/{authorityId}")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DeleteUserAuthority(string authorityId) + { + await userService.DeleteAuthority(authorityId); + return Ok(); + } + + [HttpPut("authorities")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DeleteUserAuthorities([FromBody] List auths) + { + foreach (var auth in auths) await userService.DeleteAuthority(auth); + return Ok(); + } + + [HttpPost("{id}/password")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task DeleteUserAuthorities(string id, UpdatePasswordDto updatePasswordDto) + { + if (!ObjectId.TryParse(id, out var userIdParsed)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var result = await userService.UpdateUserPassword(userIdParsed, updatePasswordDto.OldPassword, + updatePasswordDto.NewPassword); + + return Ok(result); + } + + [HttpPut("authority")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task EditUserAuthority(Authorization authorization) + { + await userService.UpdateAuthority(authorization); + return Ok(); + } + + [HttpGet("{userId}/authority")] + [AuthorizeRoles(PermissionEnum.RolesType.AuthSome)] + [PermissionAuthorize("Source", "DisplayId")] + public async Task GetAuthorityByUserId(string userId) + { + if (!ObjectId.TryParse(userId, out var userIdParsed)) + throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat); + var authorities = await authorityService.GetUserAuthorities(userIdParsed); + + return Ok(UciResponse>.FromSuccess(authorities)); + } + + #endregion +} \ No newline at end of file diff --git a/adas-core/Dockerfile b/adas-core/Dockerfile new file mode 100644 index 00000000..7a3befe4 --- /dev/null +++ b/adas-core/Dockerfile @@ -0,0 +1,23 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +WORKDIR /src +COPY ["adas-core.csproj", "adas-core/"] +WORKDIR adas-core +RUN dotnet restore "adas-core.csproj" +COPY . . +WORKDIR "/src/adas-core" +RUN dotnet build "adas-core.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "adas-core.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "adas-core.dll"] \ No newline at end of file diff --git a/adas-core/ErrorMiddleware/ErrorHandlerMiddleware.cs b/adas-core/ErrorMiddleware/ErrorHandlerMiddleware.cs new file mode 100644 index 00000000..56384898 --- /dev/null +++ b/adas-core/ErrorMiddleware/ErrorHandlerMiddleware.cs @@ -0,0 +1,56 @@ +using adas_core.Application.Exceptions; +using adas_core.Domain.Exceptions; +using Microsoft.AspNetCore.Mvc; + +namespace adas_core.ErrorMiddleware; +public class ErrorHandlerMiddleware(RequestDelegate next, ILogger logger) +{ + public async Task InvokeAsync(HttpContext context) + { + try + { + await next(context); + } + catch (Exception ex) + { + var traceId = context.TraceIdentifier; + + logger.LogError(ex, "Unhandled exception. TraceId: {TraceId}", traceId); + + context.Response.ContentType = "application/json"; + + var (statusCode, title) = ex switch + { + NotFoundException => (StatusCodes.Status404NotFound, "Resource not found"), + CustomArgumentException => (StatusCodes.Status400BadRequest, "Invalid parameters"), + InvalidFormatException => (StatusCodes.Status400BadRequest, "Invalid format"), + BadRequestException => (StatusCodes.Status400BadRequest, "Bad request"), + ConflictException => (StatusCodes.Status409Conflict, "Conflict"), + ForbbidenException => (StatusCodes.Status403Forbidden, "Forbidden"), + UnauthorizedAccessException => (StatusCodes.Status401Unauthorized, "Unauthorized"), + LoginServicesNotFoundException => (StatusCodes.Status401Unauthorized, "Unauthorized"), + UnauthorizedException => (StatusCodes.Status401Unauthorized, "Unauthorized"), + UnprocessableEntityException => (StatusCodes.Status422UnprocessableEntity, "Unprocessable entity"), + TokenException => (StatusCodes.Status401Unauthorized, "Invalid token"), + _ => (StatusCodes.Status500InternalServerError, "Internal server error") + }; + + context.Response.StatusCode = statusCode; + + var problem = new ProblemDetails + { + Status = statusCode, + Title = title, + Detail = statusCode == 500 + ? "An unexpected error occurred" + : null, // no exponer detalles + Instance = context.Request.Path + }; + + // correlation id para soporte/debug + problem.Extensions["traceId"] = traceId; + + await context.Response.WriteAsJsonAsync(problem); + } + } +} \ No newline at end of file diff --git a/adas-core/Logging/LogExecutionContext.cs b/adas-core/Logging/LogExecutionContext.cs new file mode 100644 index 00000000..96873a44 --- /dev/null +++ b/adas-core/Logging/LogExecutionContext.cs @@ -0,0 +1,45 @@ +using Serilog; + +namespace adas_core.Logging; + +public static class LogExecutionContext +{ + private static readonly AsyncLocal AsyncLocalTraceIdentifier = new(); + + public static string? TraceIdentifier => AsyncLocalTraceIdentifier.Value; + + /// Tries to set the trace identifier. + /// + /// Trace identifier. + /// If existing trace ID should be replaced (set to true ONLY if you receive and handle traced entities in a constant context)! + public static bool TrySetTraceIdentifier(string traceIdentifier, bool force = false) + { + return TrySetValue(nameof(TraceIdentifier), traceIdentifier, AsyncLocalTraceIdentifier, string.IsNullOrEmpty, + force); + } + + private static bool TrySetValue( + string contextPropertyName, + string newValue, + AsyncLocal ambientHolder, + Func valueInvalidator, + bool force) + + { + if (newValue.Equals(null) || valueInvalidator.Invoke(newValue)) return false; + + var currentValue = ambientHolder.Value; + if (force || currentValue is null || currentValue.Equals(null) || valueInvalidator.Invoke(currentValue)) + { + ambientHolder.Value = newValue; + return true; + } + + if (!currentValue.Equals(newValue)) + Log.Error( + "Tried to set different value for {ctx}, but it is already set for this execution flow -{contextPropertyName} please, check the execution context logic! Current value: {currentValue} ; rejected value: {newValue}" + , contextPropertyName, contextPropertyName, currentValue, newValue); + + return false; + } +} \ No newline at end of file diff --git a/adas-core/Logging/LogProvider.cs b/adas-core/Logging/LogProvider.cs new file mode 100644 index 00000000..b9c1c9ea --- /dev/null +++ b/adas-core/Logging/LogProvider.cs @@ -0,0 +1,39 @@ +using Microsoft.Extensions.Logging.Abstractions; + +namespace adas_core.Logging; + +public static class LogProvider +{ + private static ILoggerFactory? _loggerFactory; + + /// + /// Sets the current log provider based on logger factory. + /// + /// The logger factory. + public static void SetLogProvider(ILoggerFactory loggerFactory) + { + _loggerFactory = loggerFactory; + } + + public static ILogger CreateLogger(string category) + { + if (_loggerFactory != null) return _loggerFactory.CreateLogger(category); + + return NullLogger.Instance; + } + + public static ILogger CreateLogger() + { + if (_loggerFactory != null) return _loggerFactory.CreateLogger(); + + return NullLogger.Instance; + } + +#if DIAGNOSTICS_SOURCE + internal static class Cached + { + internal static readonly System.Lazy Default = + new(() => new System.Diagnostics.DiagnosticListener(DiagnosticHeaders.DefaultListenerName)); + } +#endif +} \ No newline at end of file diff --git a/adas-core/Program.cs b/adas-core/Program.cs new file mode 100644 index 00000000..091629eb --- /dev/null +++ b/adas-core/Program.cs @@ -0,0 +1,494 @@ +using adas_core.Application.Repositories.Interfaces; +using adas_core.Application.Services; +using adas_core.Application.Services.Interfaces; +using adas_core.Application.Subscriptions; +using adas_core.Authentication.RegistrationExtensions; +using adas_core.Configurations; +using adas_core.Domain.Models; +using adas_core.Domain.Models.AppSettings; +using adas_core.Domain.Models.Masters; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Utils; +using adas_core.ErrorMiddleware; +using adas_core.Infrastructure.Repositories; +using adas_core.Infrastructure.Services; +using adas_core.Infrastructure.Utils; +using adas_core.LdapLogin.RegistrationExtension; +using adas_core.LocalLogin.RegistrationExtension; +using adas_core.Logging; +using adas_core.module.LightBeacons.Services; +using adas_core.WebSocket; +using adas_core.WebSocket.Hubs; +using audit_logs.Extensions; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi.Models; +using MongoDB.Bson; +using MongoDB.Driver; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +using Serilog; +using Serilog.Settings.Configuration; +using ServiceConfigService = adas_core.Domain.Models.MongoModels.ServiceConfigService; + +var builder = WebApplication.CreateBuilder(args); + +var configuration = + new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", false) + .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", + true) + .Build(); + + +//SERILOG +builder.Host.UseSerilog((_, loggerConfig) => +{ + if (configuration.GetSection("Serilog").Exists()) + { + ConfigurationReaderOptions options = new() { SectionName = "Serilog" }; + loggerConfig.ReadFrom.Configuration(configuration, options); + } + else + { + loggerConfig.MinimumLevel.Information().WriteTo.Console(); + } + + LogExecutionContext.TrySetTraceIdentifier(Guid.NewGuid().ToString()); + + + loggerConfig.Enrich.WithDynamicProperty("TraceIdentifier", () => LogExecutionContext.TraceIdentifier); + + +}); + + + + +builder.Services.AddSingleton(typeof(ILogger<>), typeof(Logger<>)); + + +builder.Services.Configure(options => +{ + options.MaxRequestBodySize = int.MaxValue; // or your desired value +}); + +// Json +builder.Services + .AddControllers(o => + { + + var policy = new Microsoft.AspNetCore.Authorization.AuthorizationPolicyBuilder() + .RequireAuthenticatedUser() + .Build(); + + o.Filters.Add(new Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter(policy)); + + + }) + .AddNewtonsoftJson(options => + { + options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + options.SerializerSettings.Converters.Add(new StringEnumConverter()); + options.SerializerSettings.Converters.Add(new ObjectIdConverter()); + options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; + options.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver(); + }); + +// Configuraci�n de servicios de auditor�a +builder.Services.AddAuditServices(builder.Configuration); + +// Registro de servicios generales +builder.Services.AddHttpContextAccessor(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSignalR(); +builder.Services.AddEndpointsApiExplorer(); + + +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new() + { + Title = "adas-core", + Version = "v1" + }); + + c.CustomSchemaIds(type => type.FullName); + + c.MapType(() => new OpenApiSchema + { + Type = "string", + Format = "objectid" + }); +}); + +builder.Services.AddSwaggerGenNewtonsoftSupport(); + + + +// MongoDB +builder.Host.UseMongo(); +// 1. Create the Client once +builder.Services.AddSingleton(sp => +{ + var settings = sp.GetRequiredService>().Value; + // USAMOS EL NOMBRE COMPLETO PARA EVITAR TU CLASE + var mongoSettings = MongoClientSettings.FromConnectionString(settings.ConnectionString); + mongoSettings.MaxConnectionPoolSize = settings.MaxConnectionPoolSize; + mongoSettings.MinConnectionPoolSize = settings.MinConnectionPoolSize; + mongoSettings.MaxConnectionIdleTime = TimeSpan.FromSeconds(settings.MaxConnectionIdleTime); + return new MongoClient(mongoSettings); +}); + +// 2. Register the Database once +builder.Services.AddSingleton(sp => +{ + var client = sp.GetRequiredService(); + var settings = sp.GetRequiredService>().Value; + return client.GetDatabase(settings.DatabaseName); +}); +builder.Services.AddAuth(); +builder.Services.AddLocalLogin(); +builder.Services.AddLdapAuth(configuration); +builder.ConfigureJwtAuthentication(); +builder.UseProxyDevices(); +builder.CreateConfigEntitiesFromAppSettingsSections(); + + + +// Cache +builder.Host.UseCache(); + +//First repos because dont have dependencies on construct +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +// Register MasterListRepository for each specific type +builder.Services.AddSingleton, MasterListRepository>(); +builder.Services.AddSingleton, MasterListRepository>(); +builder.Services.AddSingleton, MasterListRepository>(); +builder.Services.AddSingleton, MasterListRepository>(); +builder.Services.AddSingleton, MasterListRepository>(); +builder.Services.AddSingleton, MasterListRepository>(); +builder.Services.AddSingleton, MasterListRepository>(); +builder.Services + .AddSingleton, MasterListRepository>(); +builder.Services.AddSingleton, MasterListRepository>(); +builder.Services.AddSingleton, MasterListRepository>(); +builder.Services.AddSingleton, MasterListRepository>(); +builder.Services.AddSingleton, MasterListRepository>(); +builder.Services.AddSingleton, MasterListRepository>(); +builder.Services.AddSingleton, MasterListRepository>(); +builder.Services.AddSingleton, MasterListRepository>(); +builder.Services.AddSingleton, MasterListRepository>(); +builder.Services.AddSingleton, MasterListRepository>(); +builder.Services + .AddSingleton, MasterListRepository>(); +builder.Services.AddSingleton, MasterListRepository>(); +builder.Services.AddSingleton, MasterListRepository>(); +builder.Services.AddSingleton, MasterListRepository>(); +builder.Services.AddSingleton, MasterListRepository>(); +builder.Services.AddSingleton, MasterListRepository>(); + + +builder.Services.AddSingleton>(serviceProvider => + serviceProvider.GetRequiredService>()); +builder.Services.AddSingleton>(serviceProvider => + serviceProvider.GetRequiredService>()); +builder.Services.AddSingleton>(serviceProvider => + serviceProvider.GetRequiredService>()); +builder.Services.AddSingleton>(serviceProvider => + serviceProvider.GetRequiredService>()); +builder.Services.AddSingleton>(serviceProvider => + serviceProvider.GetRequiredService>()); +builder.Services.AddSingleton>(serviceProvider => + serviceProvider.GetRequiredService>()); +builder.Services.AddSingleton>(serviceProvider => + serviceProvider.GetRequiredService>()); +builder.Services.AddSingleton>(serviceProvider => + serviceProvider.GetRequiredService>()); + +builder.Services.AddSingleton>(serviceProvider => + serviceProvider.GetRequiredService>()); +builder.Services.AddSingleton>(serviceProvider => serviceProvider + .GetRequiredService>()); +builder.Services.AddSingleton>(serviceProvider => + serviceProvider.GetRequiredService>()); +builder.Services.AddSingleton>(serviceProvider => + serviceProvider.GetRequiredService>()); +builder.Services.AddSingleton>(serviceProvider => + serviceProvider.GetRequiredService>()); +builder.Services.AddSingleton>(serviceProvider => + serviceProvider.GetRequiredService>()); +builder.Services.AddSingleton>(serviceProvider => + serviceProvider.GetRequiredService>()); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +// builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +// builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +// builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +//MasterLists +builder.Services.AddSingleton(provider => provider); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped, MasterListService>(); +builder.Services.AddScoped(typeof(IMasterListService<>), typeof(MasterListService<>)); + +builder.Services.AddSingleton(); + +builder.Services.AddTransient(typeof(Lazy<>)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +// builder.Services.AddSingleton>(provider => +// new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddScoped>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddScoped>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddScoped>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddScoped>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddScoped>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); +builder.Services.AddSingleton>(provider => + new Lazy(provider.GetRequiredService)); + + +//as singleton to cache mapping +builder.Services.AddSingleton(); + +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); +//builder.Services.AddSingleton(x => new MongoClient(x.GetRequiredService>())); +builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + +builder.Services.AddHttpClient(); + +builder.Services.AddCors(options => +{ + options.AddPolicy("OpenCors", policy => + { + policy + .AllowAnyOrigin() + .AllowAnyMethod() + .AllowAnyHeader(); + }); +}); + +var app = builder.Build(); + +// MongoDB Migration +app.RunMongoMigrations(); + +app.Services.GetRequiredService(); +app.Services.GetRequiredService(); + +app.Services.GetRequiredService(); +app.Services.GetRequiredService(); +app.Services.GetRequiredService(); +app.Services.GetRequiredService(); +app.Services.GetRequiredService(); + + +app.UseSwagger(); +app.UseSwaggerUI(); + + +app.UseStaticFiles(); + +//ws config +var webSocketOptions = new WebSocketOptions +{ + KeepAliveInterval = TimeSpan.FromSeconds(120) +}; +// Auth + +app.UseAuthentication(); +app.UseAuthorization(); + +// Websockets +app.UseWebSockets(webSocketOptions); +app.MapHub("/subscribe"); +app.MapHub("/chatMessage"); + +//app.UseCors(c => c.AllowAnyMethod().AllowAnyHeader().SetIsOriginAllowed(_ => true).AllowCredentials()); +app.UseCors("OpenCors"); +app.UseMiddleware(); +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/adas-core/Properties/launchSettings.json b/adas-core/Properties/launchSettings.json new file mode 100644 index 00000000..5272b375 --- /dev/null +++ b/adas-core/Properties/launchSettings.json @@ -0,0 +1,148 @@ +{ + "profiles": { + "LOCAL": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Local" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7006;http://localhost:5006" + }, + "HPAZ UCIP": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "HPAZ" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7006;http://localhost:5006" + }, + "HRYCM UCIP": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "HRYCM" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://0.0.0.0:7006;http://0.0.0.0:5006" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Local" + } + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "publishAllPorts": true, + "useSSL": true + }, + "H12O": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "H12O" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://localhost:7006;http://localhost:5006" + }, + "HUVH-UCIN": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "HUVH-UCIN" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://0.0.0.0:7006;http://0.0.0.0:5006" + }, + "CHUO": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "https://localhost:7006/swagger/index.html", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "CHUO" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://0.0.0.0:7006;http://0.0.0.0:5006" + }, + "NursePlan": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "https://localhost:7006/swagger/index.html", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "NursePlan" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://0.0.0.0:7006;http://0.0.0.0:5006" + }, + "BombasBD": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "https://localhost:7006/swagger/index.html", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "BombasBD" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://0.0.0.0:7006;http://0.0.0.0:5006" + }, + "Auditoria": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "https://localhost:7006/swagger/index.html", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Auditoria" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://0.0.0.0:7006;http://0.0.0.0:5006" + }, + "HUVH-UCIA": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "HUVH-UCIA" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://0.0.0.0:7006;http://0.0.0.0:5006" + }, + "LocalTest": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "https://localhost:7006/swagger/index.html", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "LocalTest" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://0.0.0.0:7006;http://0.0.0.0:5006" + }, + "Develop": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "https://localhost:7006/swagger/index.html", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Develop" + }, + "dotnetRunMessages": true, + "applicationUrl": "https://0.0.0.0:7006;http://0.0.0.0:5006" + } + }, + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:60532/", + "sslPort": 44358 + } + } +} \ No newline at end of file diff --git a/adas-core/WebSocket/Hubs/UciHub.cs b/adas-core/WebSocket/Hubs/UciHub.cs new file mode 100644 index 00000000..8ae72269 --- /dev/null +++ b/adas-core/WebSocket/Hubs/UciHub.cs @@ -0,0 +1,149 @@ +using adas_core.Application.Services.Interfaces; +using adas_core.Application.Subscriptions; +using adas_core.Domain.Models.SignalR; +using Microsoft.AspNetCore.SignalR; +using Newtonsoft.Json; +using Serilog; +using ILogger = Serilog.ILogger; + +namespace adas_core.WebSocket.Hubs; + +//[Authorize] +public class UciHub( + IClientMessageService clientMessageService, + ISubscribersService subscribersService, + ISubscriberGroupedService subscriberGroupedService) + : Hub +{ + private static readonly ILogger Logger = Log.ForContext(); + + + public override async Task OnConnectedAsync() + { + try + { + Logger.Debug("OnConnectedAsync new connection id {ContextConnectionId}", Context.ConnectionId); + + var subscriberSameId = subscribersService.GetById(Context.ConnectionId); + if (subscriberSameId != null) + { + Logger.Debug( + "OnConnectedAsync connection id {ContextConnectionId} exists in subscribers list, removing it before inserting new one", + Context.ConnectionId); + subscribersService.RemoveConnectionById(subscriberSameId.Id); + } + + subscribersService.AddSubscriber(new WsSubscriber(Context.ConnectionId)); + + await base.OnConnectedAsync(); + } + catch (Exception ex) + { + Logger.Error("error on new connection {exMessage} trace: {exStackTrace}", ex.Message, ex.StackTrace); + await Task.FromException(ex); + } + } + + public override async Task OnDisconnectedAsync(Exception? exception) + { + if (exception != null) + Logger.Error(exception, + "Unexpected onDisconnectedAsync received for connection id {ContextConnectionId} with exception {exceptionMessage}", + Context.ConnectionId, exception.Message); + else + Logger.Debug("onDisconnectedAsync received for connection id {ContextConnectionId}", Context.ConnectionId); + + Logger.Debug("Removing from subscribers grouped with ConnectionId {ContextConnectionId}", Context.ConnectionId); + var grouped = subscriberGroupedService.GetGrouped(); + foreach (var wsl in grouped) + { + if (wsl.WsSubscriber.Any()) + { + var removedEs = wsl.WsSubscriber.RemoveAll(ws => ws.Equals(Context.ConnectionId)); + if (removedEs > 0) + Logger.Verbose( + "Removed {removedEs} grouped subscribers with connectionId: {ContextConnectionId}", removedEs, + Context.ConnectionId); + } + + var removed = subscriberGroupedService.CheckEmptySubscriberGroup(wsl); + if (removed.Any()) Logger.Verbose($"CheckEmptySubscriberGroup: {string.Join(", ", removed)}"); + } + + + var total = subscribersService.RemoveConnectionById(Context.ConnectionId); + Logger.Debug("Removing {total} from subscribers with ConnectionId {ContextConnectionId}", total, + Context.ConnectionId); + Logger.Debug("Removed id: {ContextConnectionId}", Context.ConnectionId); + + await base.OnDisconnectedAsync(exception); + } + + + [HubMethodName("subscribe")] + public async Task OnSubscribe(string subMessage) + { + try + { + var msg = JsonConvert.DeserializeObject(subMessage); + if (msg == null) + { + Logger.Warning("Error deserializing subscription message {subMessage}", subMessage); + return; + } + + var subs = subscribersService.GetById(Context.ConnectionId); + + if (subs == null) + { + Logger.Warning("Subscription received from unknown connection id {ContextConnectionId}", + Context.ConnectionId); + return; + } + + subs.Section = msg.Data?.Section; + subs.Version = msg.Version ?? "Not a squirrel app"; + subs.GroupedFields = msg.Data?.GroupedFields; + + await clientMessageService.ProcessMessage(msg, Context.ConnectionId); + } + catch (Exception ex) + { + Logger.Error("Error on subscribe {exMessage}", ex); + } + } + + [HubMethodName("unsubscribe")] + public void UnSubscribe() + { + try + { + subscribersService.RemoveConnectionById(Context.ConnectionId); + } + catch (Exception ex) + { + Logger.Error("error on unsubscribe {exMessage}", ex.Message); + } + } + + //TODO revisar esto, si va a acabar llamando al mismo sitio y procesando el mensaje igual que OnSuscribe, realmente sobra. + [HubMethodName("chatMessage")] + public async Task OnChatMessage(string message) + { + try + { + var msg = JsonConvert.DeserializeObject(message); + if (msg == null) + { + Logger.Error("Error deserializing Chat message: {message}", message); + return; + } + + await clientMessageService.ProcessMessage(msg, Context.ConnectionId); + } + catch (Exception ex) + { + Logger.Error("error on subscribe {exMessage} trace: {exStackTrace}", ex.Message, ex.StackTrace); + } + } +} \ No newline at end of file diff --git a/adas-core/WebSocket/WebSocketMessageService.cs b/adas-core/WebSocket/WebSocketMessageService.cs new file mode 100644 index 00000000..a89306c0 --- /dev/null +++ b/adas-core/WebSocket/WebSocketMessageService.cs @@ -0,0 +1,1126 @@ +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.Masters; +using adas_core.Domain.Models.MongoModels; +using adas_core.Domain.Models.Recording; +using adas_core.Domain.Models.Responses; +using adas_core.Domain.Models.SignalR; +using adas_core.Domain.Utils; +using adas_core.WebSocket.Hubs; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.Options; +using MongoDB.Bson; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +using Serilog; +using ILogger = Serilog.ILogger; +using Patient = adas_core.Domain.Models.MongoModels.Patient; + +namespace adas_core.WebSocket; + +public class WebSocketMessageService( + Lazy patientService, + Lazy observationService, + Lazy pumpService, + Lazy treatmentService, + IGroupedObservationService groupedObservationService, + IConfigObservationService configObservationService, + Lazy appointmentService, + IRelayService relayService, + IOptions apiSettings, + IOptions recordingSettings, + Lazy recordingService, + IHubContext hub, + Lazy lightBeaconService, + Lazy diagnosisService, + Lazy alarmService, + ISubscribersService subscribersService, + ISubscriberGroupedService subscriberGroupedService, + Lazy displayService, + Lazy unitService, + Lazy admissionService, + Lazy dischargeService, + Lazy noticeService, + IObservationDemoService observationDemoService +) + : IClientMessageService +{ + #region Properties + + private readonly ILogger _logger = Log.ForContext(); + + private readonly bool _sendPumpsZero = apiSettings.Value.SendPumpsZero; + + private IHubContext Hub { get; } = hub; + + #endregion + + #region Methods + + #region Subscriptions + + private async Task ProcessDemoRequest(Message msg, string connectionId) + { + var sub = subscribersService.GetById(connectionId); + if (sub is { DisplayId: not null }) + { + var display = await displayService.Value.GetInfo(sub.DisplayId!.Value, msg.Data?.UserName, null, + msg.Data?.Locale, true, true, false); + if (display == null) + { + _ = SendErrorMessage("Display " + sub.DisplayId.Value + " not found", connectionId); + return; + } + + if (display.DisplayConfig == null) + { + _ = SendErrorMessage("Display " + sub.DisplayId.Value + " has not config", connectionId); + return; + } + + switch (display.DisplayConfig?.Type) + { + case DisplayConfigEnums.DisplayType.SmartDisplay: + var smartDisplayCast = display.DisplayConfig as SmartDisplay; + msg.Data ??= new MessageData(); + msg.Data.GroupedFields = smartDisplayCast?.GroupedFieldList; + msg.Data.Alarmfields = smartDisplayCast?.AlarmFieldList; + msg.Data.Fields = smartDisplayCast?.FieldList; + break; + case DisplayConfigEnums.DisplayType.DisplayNurse: + var nurseDisplayCast = display.DisplayConfig as DisplayNurse; + msg.Data ??= new MessageData(); + msg.Data.Fields = nurseDisplayCast?.FieldList; + break; + case DisplayConfigEnums.DisplayType.PumpDisplay: + var pumpDisplayCast = display.DisplayConfig as PumpDisplay; + msg.Data ??= new MessageData(); + msg.Data.GroupedFields = pumpDisplayCast?.GroupedFieldList; + msg.Data.Fields = pumpDisplayCast?.FieldList; + break; + default: + _ = SendErrorMessage( + "Display " + sub.DisplayId.Value + " with type: " + display.DisplayConfig?.Type + "not mapped", + connectionId); + break; + } + + foreach (var displayPointOfCare in display.PointOfCares) + { + //var patient = await _patientService.Value.FindByLocation(displayPointOfCare.Location); + var patient = await patientService.Value.FindByPointOfCareId(displayPointOfCare.Id); + if (msg.Data is { IsDemo: true }) + { + var rnd = new Random(); + var rndLang = rnd.Next(0, 5); + List languages = ["Español", "Ingles", "Portugués", "Aleman", "Catalan"]; + if (patient?.Person != null) patient.Person.Language = languages[rndLang]; + } + + if (patient != null) + _ = SendPatientData(patient, msg, connectionId); + } + } + } + private void ProcessAdminSubscriber(Message msg, string connectionId) + { + if(msg.Data?.UserName == null) return; + AddMessageDisplayToSubcriber(msg, connectionId, SubscriptionEnum.WsType.Admin); + } + private async Task ProcessDisplaySubscriber(Message msg, string connectionId) + { + var id = msg.Data?.DisplayId; + if (id == null || !ObjectId.TryParse(id, out var idParsed)) + { + _ = SendErrorMessage("Message Display " + id + " null or not an ObjectId", connectionId); + return; + } + + #region [DISPLAY INFO] + + var display = + await displayService.Value.GetInfo(idParsed, msg.Data?.UserName, null, msg.Data?.Locale, true, true, false); + if (display == null) + { + _ = SendErrorMessage("Display " + id + " not found", connectionId); + return; + } + + if (display.DisplayConfig == null) + { + _ = SendErrorMessage("Display " + id + " has not config", connectionId); + return; + } + + display.PointOfCares.ForEach(poc => msg.Data?.Locations.Add(poc.Location)); + display.PointOfCares.ForEach(poc => msg.Data?.LocationIds.Add(poc.Id)); + + switch (display.DisplayConfig?.Type) + { + case DisplayConfigEnums.DisplayType.SmartDisplay: + var smartDisplayCast = display.DisplayConfig as SmartDisplay; + msg.Data ??= new MessageData(); + msg.Data.GroupedFields = smartDisplayCast?.GroupedFieldList; + var subs = subscribersService.GetById(connectionId); + if (subs != null) subs.GroupedFields = smartDisplayCast?.GroupedFieldList; + msg.Data.Alarmfields = smartDisplayCast?.AlarmFieldList; + msg.Data.Fields = smartDisplayCast?.FieldList; + // Sensors? + // Send PoC List + await ProcessPocList(display.PointOfCares, connectionId, msg); + + // Send Unit MasterList + ProcessMasterList(display.UnitId, connectionId, msg.Data.Locale); + + _ = ProcessDisplayList(display.Type, display.Id, msg.Data?.UserName, connectionId); + + break; + case DisplayConfigEnums.DisplayType.StandarDisplay: + var standarDisplayCast = display.DisplayConfig as StandarDisplay; + msg.Data ??= new MessageData(); + msg.Data.GroupedFields = standarDisplayCast?.GroupedFieldList; + var subsStand = subscribersService.GetById(connectionId); + if (subsStand != null) subsStand.GroupedFields = standarDisplayCast?.GroupedFieldList; + msg.Data.Alarmfields = standarDisplayCast?.AlarmFieldList; + msg.Data.Fields = standarDisplayCast?.FieldList; + msg.Data.SectionConfig = standarDisplayCast?.SectionConfig; + + await ProcessStandard(msg.Operation, display, connectionId); + // Sensors? + break; + case DisplayConfigEnums.DisplayType.DisplayNurse: + var nurseDisplayCast = display.DisplayConfig as DisplayNurse; + msg.Data ??= new MessageData(); + msg.Data.Fields = nurseDisplayCast?.FieldList; + msg.Data.GroupedFields = nurseDisplayCast?.GroupedFieldList; + // Send PoC List + await ProcessPocList(display.PointOfCares, connectionId, msg); + + // Send Unit MasterList + ProcessMasterList(display.UnitId, connectionId, msg.Data.Locale); + + _ = ProcessDisplayList(display.Type, display.Id, msg.Data?.UserName, connectionId); + + break; + case DisplayConfigEnums.DisplayType.PumpDisplay: + var pumpDisplayCast = display.DisplayConfig as PumpDisplay; + msg.Data ??= new MessageData(); + msg.Data.GroupedFields = pumpDisplayCast?.GroupedFieldList; + msg.Data.Fields = pumpDisplayCast?.FieldList; + // Send PoC List + await ProcessPocList(display.PointOfCares, connectionId, msg); + + // Send Unit MasterList + ProcessMasterList(display.UnitId, connectionId, msg.Data.Locale); + + _ = ProcessDisplayList(display.Type, display.Id, msg.Data?.UserName, connectionId); + break; + default: + _ = SendErrorMessage("Display " + id + " with type: " + display.DisplayConfig?.Type + "not mapped", + connectionId); + break; + } + + #endregion + + #region [CONFIG OBSERVATIONS] + + //Solo mandamos el configObservations en este punto cuando NO es de tipo Standard + var configObservations = await configObservationService.GetAllConfigs(); + if (configObservations.Count > 0 && + display.DisplayConfig?.Type != DisplayConfigEnums.DisplayType.StandarDisplay) + _ = SendAsync(connectionId, OperationType.ConfigObservation, configObservations); + + #endregion + + #region APPOINTMENTS + + //Mandamos todos los appointments de la unidad + List unitAppointments = []; + + if (msg.Data?.Appointments is true) + { + foreach (var poc in display.PointOfCares) + { + var location = new PatientLocation(poc.UnitName, poc.Bed, poc.Room); + unitAppointments.AddRange(await appointmentService.Value.FindByLocation(location)); + } + + if(unitAppointments.Count > 0) + _ = SendAsync(connectionId, OperationType.NewAppointment, unitAppointments); + } + + #endregion + + #region [PATIENT DATA] + + foreach (var displayPointOfCare in display.PointOfCares) + { + //var patient = await _patientService.Value.FindByLocation(displayPointOfCare.Location); + var patient = await patientService.Value.FindByPointOfCareId(displayPointOfCare.Id); + if (msg.Data is { IsDemo: true }) + { + var rnd = new Random(); + var rndLang = rnd.Next(0, 5); + List languages = ["Español", "Ingles", "Portugués", "Aleman", "Catalan"]; + if (patient?.Person != null) patient.Person.Language = languages[rndLang]; + } + + if (patient != null) + _ = SendPatientData(patient, msg, connectionId, configObservations); + } + + + if (msg.Data != null && + !msg.Data.LocationIds.IsNullOrEmpty() && + display.DisplayConfig?.Type == DisplayConfigEnums.DisplayType.DisplayNurse) + { + // Send Admission + ProcessAdmission(msg.Data.LocationIds, display.UnitId, connectionId, msg.Data.Locale); + + // Send Discharger + ProcessDischarge(msg.Data.LocationIds, connectionId, msg.Data.Locale); + } + + #endregion + + #region [NOTICES] + + if (msg.Data?.DisplayId != null && + display.DisplayConfig?.Type == DisplayConfigEnums.DisplayType.DisplayNurse) + ProcessNotice(msg.Data.DisplayId, connectionId); + + #endregion + + #region [RECORDING] + + if (!string.IsNullOrEmpty(recordingSettings.Value.RecordingApiUrl)) + _ = Task.Run(async () => + { + if (msg.Data != null && (msg.Data.RecordingAlerts || msg.Data.Recordings)) + foreach (var displayPointOfCare in display.PointOfCares) + if (displayPointOfCare.Configuration is { Id: not null }) + { + var recording = + await recordingService.Value.GetRecordings(displayPointOfCare.Configuration.Id + .Value); + + if (recording != null) + { + if (recording.Any(r => r.AlarmType == AlarmEnum.Type.Manual)) + _ = SendAsync(connectionId, OperationType.RecordingStatus, + new List + { recording.First(r => r.AlarmType == AlarmEnum.Type.Manual) }); + else if (recording.Any(r => r.AlarmType == AlarmEnum.Type.Auto)) + _ = SendAsync(connectionId, OperationType.RecordingAlert, + recording.Where(r => r.AlarmType == AlarmEnum.Type.Auto).ToList()); + } + } + }); + + #endregion + + #region [RELAY] + + if (msg.Data is { Relay: true } && !string.IsNullOrEmpty(recordingSettings.Value.RecordingApiUrl)) + _ = Task.Run(async () => + { + foreach (var displayPointOfCare in display.PointOfCares) + { + if (displayPointOfCare.Configuration?.RelayIdList == null) + continue; + + foreach (var relay in displayPointOfCare.Configuration.RelayIdList) + { + var relayStatus = + await relayService.CheckRelayStatus(relay); + var relayResult = await relayService.GetById(relay); + if(relayResult != null) + { + relayResult.Status = relayStatus; + + _ = SendAsync(connectionId, OperationType.Relay, relay); + } + } + } + }); + + #endregion + + #region [BEACONS] + + if (msg.Data is { Beacons: true }) + foreach (var displayPointOfCare in display.PointOfCares) + _ = Task.Run(() => { _ = SendBeaconsData(displayPointOfCare, connectionId); }); + + #endregion + + AddMessageDisplayToSubcriber(msg, connectionId, SubscriptionEnum.WsType.Display); + } + + private async Task ProcessStandard(SubscriptionEnum.Type suscriptionType, Display display, string connectionId) + { + var standarDisplayCast = display.DisplayConfig as StandarDisplay; + if (standarDisplayCast?.SectionConfig == null) + return; + + var configObservations = await configObservationService.GetAllConfigs(); + _ = SendAsync(connectionId, OperationType.ConfigObservation, configObservations); + + foreach (var poc in display.PointOfCares) + { + var patient = await patientService.Value.FindByPointOfCareId(poc.Id); + if (patient == null) + { + poc.HasPatient = false; + continue; + } + + poc.HasPatient = true; + poc.Patient = patient; + } + + var section = new Section + { + SectionConfig = standarDisplayCast.SectionConfig, + SectionTitle = display.Name, + PointOfCareList = new Dictionary> + { + { "pasillo 1", display.PointOfCares } + } //TODO no se ha tenido en cuenta las agrupaciones de boxes (Hall?) + }; + await SendAsync(connectionId, OperationType.Section, section); + + + if (suscriptionType == SubscriptionEnum.Type.BoxSubscribe) + { + var box = display.PointOfCares.First(); + + var boxStatus = await patientService.Value.GetBox(box, true); + + _ = SendAsync(connectionId, OperationType.Box, boxStatus); + } + } + + private async Task ProcessDisplayList( + DisplayConfigEnums.DisplayType displayType, + ObjectId displayId, + string? dataUserName, + string connectionId) + { + var listToSend = await displayService.Value.GetDisplaySectionByUser(displayType, displayId, dataUserName, null); + if (listToSend.Count > 0) + await SendAsync(connectionId, OperationType.DisplayList, listToSend); + } + + private async Task ProcessPocList(List displayPointOfCares, string connectionId, Message msg) + { + //TODO revisar si es necesario que se envíen los pocs aquí si no está en modo demo ¿redundante? + foreach (var poc in displayPointOfCares) + { + if (msg.Data is { IsDemo: true }) + { + var rnd = new Random(); + var rndLang = rnd.Next(0, 5); + List languages = ["Español", "Ingles", "Portugués", "Aleman", "Catalan"]; + if (poc.Patient?.Person != null) poc.Patient.Person.Language = languages[rndLang]; + } + await SendAsync(connectionId, OperationType.NewPointOfCare, poc); + } + } + + private async void ProcessMasterList(ObjectId id, string connectionId, LocaleEnum? dataLocale) + { + try + { + var unit = await unitService.Value.GetInfo(id, dataLocale); + if (unit == null) + { + _logger.Error("Error processing Master List id {id}", id); + return; + } + + _ = SendAsync(connectionId, OperationType.UpdateUnit, unit); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.AllergyList); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.AltableOptionList); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.DestinationList); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.DiagnosisList); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.DischargeStatusList); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.DoctorList); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.DoctorTypeList); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.InsulationList); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.MobilityOptionList); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.OriginList); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.PatientStatusList); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.ProcedureList); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.TestList); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.ServiceList); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.TherapeuticCeilingList); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.TreatmentList); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.VisitOptionList); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.AccessControlList); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.LanguageBarrierList); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.PassiveSittingList); + _ = SendAsync(connectionId, OperationType.NewMasterList, unit.GenericList); + } + catch (Exception ex) + { + _logger.Error("Exception processing mas. Exception: {ex}", ex); + } + } + + private async void ProcessNotice(string displayId, string connectionId) + { + try + { + if (!ObjectId.TryParse(displayId, out var id)) + return; + + var notice = await noticeService.Value.GetNoticesByDisplayId(id); + if (notice != null) + { + var noticeList = notice.ToList(); + if (!noticeList.IsNullOrEmpty()) + noticeList.ForEach(c => _ = SendAsync(connectionId, OperationType.NewNotice, c)); + } + else + { + _logger.Information($"Display id: {displayId} has not notices"); + } + } + catch (Exception ex) + { + _logger.Error("Exception processing Notice. Exception: {ex}", ex); + } + } + + private async void ProcessDischarge(List dataLocations, string connectionId, LocaleEnum dataLocale) + { + try + { + foreach (var location in dataLocations) + { + var discharge = + await dischargeService.Value.GetDischargeByPointOfCareIdWithLocale(location, dataLocale); + + if (discharge != null) + { + var patient = await patientService.Value.FindById(discharge.PatientId); + discharge.Patient = patient; + _ = SendAsync(connectionId, OperationType.NewDischarge, discharge); + } + else + { + _logger.Information($"Location {location} has no discharge"); + } + } + } + catch (Exception e) + { + _logger.Error("Exception processing Discharge. Exception: {ex}", e); + } + } + + private async void ProcessAdmission(List dataLocations, ObjectId? displayUnitId, + string connectionId, LocaleEnum dataLocale) + { + try + { + foreach (var location in dataLocations) + { + var admission = await admissionService.Value.GetAdmissionByPointOfCareIdAndLocale(location, dataLocale); + foreach (var adm in admission) await SendAsync(connectionId, OperationType.NewAdmission, adm); + } + + if (displayUnitId.HasValue) + { + var admWithNoPoc = await admissionService.Value.GetAdmissionByUnitIdWithOutPoC(displayUnitId.Value); + foreach (var adm in admWithNoPoc) _ = SendAsync(connectionId, OperationType.NewAdmission, adm); + } + } + catch (Exception e) + { + _logger.Error("Exception processing Admission. Exception: {ex}", e); + } + } + + #endregion + + #region Process Messages + + public async Task ProcessMessage(Message msg, string connectionId) + { + try + { + switch (msg.Operation) + { + #region Operation V1 + + case SubscriptionEnum.Type.ChatMessage: + await ProcessChatMessage(msg, connectionId); + break; + case SubscriptionEnum.Type.RecordingSuscribe: + await ProcessRecordingMessage(msg, connectionId); + break; + case SubscriptionEnum.Type.PatientData: + await ProcessPatientData(msg, connectionId); + break; + case SubscriptionEnum.Type.Idle: + break; + case SubscriptionEnum.Type.GroupedObservations: + _ = ProcessGroupedObservations(msg, connectionId); + break; + + #endregion + + case SubscriptionEnum.Type.Admin: + ProcessAdminSubscriber(msg, connectionId); + break; + case SubscriptionEnum.Type.Display: + case SubscriptionEnum.Type.BoxSubscribe: + _ = ProcessDisplaySubscriber(msg, connectionId); + break; + case SubscriptionEnum.Type.Demo: + _ = ProcessDemoRequest(msg, connectionId); + break; + } + } + catch (Exception ex) + { + _logger.Error("Error processing message: {msg}. Message: {exMessage}. Exception: {ex}", msg, ex.Message, + ex); + throw; + } + } + + private async Task ProcessGroupedObservations(Message msg, string connectionId) + { + if (msg.Data is { GroupedFields: not null } && msg.Data.GroupedFields.Any()) + { + if (msg.Data.Location == null) return; + + List patients = []; + if (msg.Data.Location.Bed != null) + { + var patient = await patientService.Value.FindByLocation(msg.Data.Location); + if (patient != null) + patients.Add(patient); + } + else if (msg.Data.Location.UnitName != null) + { + // var patientList = await _patientService.Value.FindByUnit(msg.Data.Location.UnitName); + // if (patientList != null) + // patients.AddRange(patientList); + } + + + foreach (var patient in patients) + foreach (var groupedField in msg.Data.GroupedFields) + try + { + var groupedObservation = msg.Data.TimeZoneId != null + ? await groupedObservationService.GenerateGroupedObservation(patient.Id, groupedField, + msg.Data.TimeZoneId) + : await groupedObservationService.GenerateGroupedObservation(patient.Id, groupedField); + + + await SendAsync(connectionId, OperationType.GroupedObservation, groupedObservation); + } + catch (Exception e) + { + _logger.Error( + "Exception sending Grouped Observations: person: {patient} DTOMessage: {msg} Exception {eMessage}", + patient, msg, e); + } + } + } + + private async Task ProcessRecordingMessage(Message msg, string connectionId) + { + //var subscribers = Subscribers.Instance.GetSubscribers(); + + AddMessagetoSubcriber(msg, connectionId); + + var subscriber = subscribersService.GetById(connectionId); + if (subscriber == null) + return; + var recordings = msg.Data?.CurrentRecordings; + if (recordings is not { Count: > 0 }) return; + + await SendAsync(connectionId, OperationType.RecordingAlert, recordings); + } + + private async Task ProcessPatientData(Message msg, string connectionId) + { + Patient? patient = new(); + var patientId = msg.Data?.PatientId; + + if (patientId != null) + { + patient = await patientService.Value.FindById(ObjectId.Parse(patientId)); + } + else + { + var patientLocation = msg.Data?.Location; + if (patientLocation != null) + patient = await patientService.Value.FindByLocation(patientLocation); + } + + if (patient != null) + await SendPatientData(patient, msg, connectionId); + } + + private async Task ProcessChatMessage(Message msg, string connectionId) + { + var fromLocation = msg.Data?.Location; + var toLocation = msg.Data?.ChatMessageToLocation; + + if (fromLocation == null || toLocation == null) + return; + + var subscriber = subscribersService.GetSubscribers() + .FirstOrDefault(s => + s.Box != null && s.Box == toLocation.Bed && s.Section != null && s.Section == msg.Data?.Section); + if (subscriber == null) + return; + + ChatMessage chatMessage = new() + { + LocationFrom = fromLocation, + LocationTo = msg.Data?.ChatMessageToLocation, + MessageTime = msg.Data?.ChatMessageDate, + Message = msg.Data?.ChatMessage + }; + + await SendAsync(connectionId, OperationType.ChatMessage, chatMessage); + } + + #endregion + + #region Send Data + + public void SendUpdateMessageToBoxes(List patientLocations) + { + var subscribers = new List(); + + foreach (var patientLocation in patientLocations) + if (string.IsNullOrEmpty(patientLocation.Bed)) + { + var subs = subscribersService.GetSubscribers().Where(s => + s.SubscriptionType == SubscriptionEnum.WsType.Section && s.Section == patientLocation.UnitName && + !subscribers.Contains(s)).ToList(); + subscribers.AddRange(subs); + } + else + { + var subs = subscribersService.GetSubscribers().Where(s => + s.SubscriptionType == SubscriptionEnum.WsType.Box && s.Box == patientLocation.Bed && + s.Section == patientLocation.UnitName && !subscribers.Contains(s)).ToList(); + subscribers.AddRange(subs); + } + + foreach (var subscriber in subscribers) _ = SendAsync(subscriber.Id, OperationType.UpdateApplication, null); + } + + + private async Task SendPatientData( + Patient patient, + Message msg, + string connectionId, + ICollection? configObservations = null, + CancellationToken ct = default) + { + var tasks = new List(); + + if (msg.Data?.Fields?.Any() == true) + tasks.Add(SendLatestObservationsAsync(patient, msg.Data, connectionId, ct)); + + if(msg.Data?.Appointments == true) + tasks.Add(SendPatientAppointmentsAsync(patient, msg.Data, connectionId, ct)); + + if (msg.Data?.Alarmfields?.Any() == true) + tasks.Add(SendLatestAlarmsAsync(patient, msg.Data, configObservations, connectionId)); + + if (msg.Data?.Pumps == true) + tasks.Add(SendLatestPumpsAsync(patient, msg.Data, connectionId, ct)); + + if (msg.Data?.Treatments == true) + tasks.Add(SendTreatmentsAsync(patient, msg.Data, connectionId)); + + if (msg.Data?.Diagnosis == true) + tasks.Add(SendDiagnosisAsync(patient, msg.Data, connectionId)); + + if (msg.Data?.GroupedFields?.Any() == true) + tasks.Add(SendGroupedObsAsync(patient, msg.Data, connectionId, ct)); + + await Task.WhenAll(tasks); + } + + + private async Task SendLatestObservationsAsync( + Patient patient, + MessageData data, + string connectionId, + CancellationToken ct = default) + { + try + { + if (data.Fields != null) + { + var observations = data is { IsDemo: true } + ? await observationDemoService.GenerateObservationByField(patient, data.Fields) + : await observationService.Value.FindLastObservationsByField(patient.Id, data.Fields, ct: ct); + if (observations.Count > 0) + { + observations = observations.OrderBy(p => p.Time).ToList(); + observations.ForEach(o => _ = SendAsync(connectionId, OperationType.Observation, o)); + } + } + } + catch (OperationCanceledException) { } + catch (Exception e) + { + _logger.Error( + "Exception sending person Data: {eMessage} person: {patient} Message Data: {data} {eStackTrace} ", + e.Message, patient, data, e.StackTrace); + } + } + + private async Task SendPatientAppointmentsAsync( + Patient patient, + MessageData msgData, + string subscriberId, + CancellationToken ct = default) + { + // SEND APPOINTMENTS OF BOXES + try + { + var appointments = await appointmentService.Value.GetTodayByPatient(patient.Id, ct); + foreach (var appointment in appointments) + _ = SendAsync(subscriberId, OperationType.NewAppointment, appointment); + } + catch (OperationCanceledException) { } + catch (Exception e) + { + _logger.Error( + "Exception sending patient appointments Data: {eMessage} PointOfCare: {location} DTOMessage: {msg} ", + e.Message, patient, msgData); + } + } + + private async Task SendLatestAlarmsAsync( + Patient patient, + MessageData data, + ICollection? configObservations, + string connectionId) + { + try + { + data.Alarmfields ??= []; + + var configAlarm = configObservations? + .Where(c => data.Alarmfields.Any(h => h.Name == c.Name)) + .ToList(); + var observations = data.IsDemo + ? + await observationDemoService.GenerateAlarmByField(patient, data.Alarmfields) + : configAlarm != null && configAlarm.Count == data.Alarmfields.Count + ? await alarmService.Value.FindLastValuesNotExpired(patient.Id, data.Alarmfields, + configAlarm) + : + await alarmService.Value.FindLastObservationsByField(patient.Id, data.Alarmfields); + + if (observations.Count > 0) + { + observations = observations.OrderBy(p => p.Time).ToList(); + foreach (var observation in observations) + { + var obs = await alarmService.Value.MapObservationsByName(observation); + if (obs != null) _ = SendAsync(connectionId, OperationType.Alarm, obs); + } + } + } + catch (Exception e) + { + _logger.Error( + "Exception sending person Data: {eMessage} person: {patient} Message Data: {data} {eStackTrace} ", + e.Message, patient, data, e.StackTrace); + } + } + + + private async Task SendLatestPumpsAsync( + Patient patient, + MessageData data, + string connectionId, + CancellationToken ct = default) + { + try + { + var observations = await pumpService.Value.FindLastPumpObservations(patient.Id, data.Last); + if (observations.Count > 0) + foreach (var observation in observations) + { + if (!_sendPumpsZero && observation.Number == 0) + continue; + + var obs = await pumpService.Value.MapPumpObservation(observation); + _ = SendAsync(connectionId, OperationType.Pump, obs); + } + } + catch (Exception e) + { + _logger.Error( + "Exception sending pump Data: {eMessage} person: {patient} Message Data: {data} ", + e.Message, patient, data); + } + } + + + private async Task SendTreatmentsAsync( + Patient patient, + MessageData data, + string connectionId) + { + try + { + var treatments = await treatmentService.Value.GetTreatmentsByPatientId(patient.Id); + var patientTreatments = treatments.ToList(); + if (patientTreatments.Count != 0) + foreach (var treatment in patientTreatments.OrderByDescending(t => t.OrderTime) + .Take(50)) + _ = SendAsync(connectionId, OperationType.Treatment, treatment); + } + catch (Exception e) + { + _logger.Error( + "Exception sending treatments Data. connection id {connectionId}, person: {patient} Exception: {eMessage} ", + connectionId, patient, e); + } + } + + + private async Task SendDiagnosisAsync( + Patient patient, + MessageData data, + string connectionId) + { + try + { + var diagnosis = await diagnosisService.Value.GetByPatientId(patient.Id); + var patientDiagnosis = diagnosis.ToList(); + if (patientDiagnosis.Any()) + foreach (var diag in patientDiagnosis.OrderByDescending(t => t.Time).Take(50)) + _ = SendAsync(connectionId, OperationType.Diagnosis, diag); + } + catch (Exception e) + { + _logger.Error( + "Exception sending Diagnosis Data: {eMessage} person: {patient} message data: {data} ", + e.Message, patient, data); + } + } + + + private async Task SendGroupedObsAsync( + Patient patient, + MessageData data, + string connectionId, + CancellationToken ct = default) + { + var groupedFields = data.GroupedFields; + if(groupedFields == null) + return; + foreach (var groupedField in groupedFields) + try + { + var groupedObservation = data.IsDemo + ? await observationDemoService.GenerateGroupedObservation(patient, groupedField) + : data.TimeZoneId != null + ? await groupedObservationService.GenerateGroupedObservation(patient.Id, + groupedField, data.TimeZoneId, ct: ct) + : await groupedObservationService.GenerateGroupedObservation(patient.Id, + groupedField, ct: ct); + subscriberGroupedService.CheckOnSubscriptionGroup(groupedField, patient.Id, + data.TimeZoneId ?? "Romance Standard Time", + connectionId, groupedObservation); + + + await SendAsync(connectionId, OperationType.GroupedObservation, groupedObservation); + } + catch (Exception e) + { + _logger.Error( + "Exception sending Grouped Data: person: {patient} message data: {data} Exception {eMessage}", + patient, data, e); + } + } + + private async Task SendBeaconsData( + PointOfCare poc, + string connectionId) + { + var beaconColor = await lightBeaconService.Value.GetColor(poc); + BeaconResponse bc = new(beaconColor.ToString(), poc.Id, poc.UnitId); + _ = SendAsync(connectionId, OperationType.Beacon, bc); + } + + #endregion + + #region Add Subscriber + + private void AddMessagetoSubcriber(Message msg, string connectionId) + { + SubscriptionEnum.WsType? subType = null; + switch (msg.Operation) + { + case SubscriptionEnum.Type.SectionSubscribe: + subType = SubscriptionEnum.WsType.Section; + break; + case SubscriptionEnum.Type.PatientData: + // subscriptionType = SubscriptionType.person; + break; + case SubscriptionEnum.Type.Idle: + // subscriptionType = SubscriptionType.Idle; + break; + case SubscriptionEnum.Type.BoxSubscribe: + subType = SubscriptionEnum.WsType.Box; + break; + case SubscriptionEnum.Type.ChatMessage: + break; + case SubscriptionEnum.Type.RecordingSuscribe: + break; + case SubscriptionEnum.Type.GroupedObservations: + break; + case SubscriptionEnum.Type.Sensors: + break; + default: + subType = SubscriptionEnum.WsType.Section; + break; + } + + var subscriber = subscribersService.GetSubscribers().FirstOrDefault(s => s.Id == connectionId); + + if (subscriber != null) + { + _logger.Debug($"Ws ConnectionId {connectionId} exists, " + + (subscriber.Box != msg.Data?.Location?.Bed + ? $"updated bed and box from {subscriber.Box ?? "null"} to {msg.Data?.Location?.Bed ?? "null"} section: {msg.Data?.Section} " + : $"bed and box {subscriber.Box ?? "null"} not updated ") + + (subscriber.Section != msg.Data?.Section + ? $"updated section from {subscriber.Section ?? "null"} to {msg.Data?.Section ?? "null"} " + : $"section {subscriber.Box ?? "null"} not updated ") + ); + subscriber.Box = msg.Data?.Location?.Bed; + subscriber.Section = msg.Data?.Section; + subscriber.SubscriptionType = subType; + } + else + { + subscriber = new WsSubscriber(connectionId) + { + Box = msg.Data?.Location?.Bed, + Section = msg.Data?.Section, + SubscriptionType = subType + }; + + subscribersService.AddSubscriber(subscriber); + _logger.Debug("Ws ConnectionId NOT exists, created new WebSocket subscriber {subscriber}", + JsonConvert.SerializeObject(subscriber)); + } + } + + private void AddMessageDisplayToSubcriber(Message msg, string connectionId,SubscriptionEnum.WsType? subType) + { + var subscriber = subscribersService.GetSubscribers().FirstOrDefault(s => s.Id == connectionId); + + if (subscriber != null) + { + _logger.Debug($"Ws ConnectionId {connectionId} exists, " + + (subscriber.Locations != msg.Data?.Locations + ? $"updated Locations from {JsonConvert.SerializeObject(subscriber.Locations)} to {JsonConvert.SerializeObject(msg.Data?.Locations)}" + : $"Locations: {JsonConvert.SerializeObject(subscriber.Locations)} not updated ")); + subscriber.Locations = msg.Data?.Locations ?? []; + subscriber.LocationIds = msg.Data?.LocationIds ?? []; + subscriber.SubscriptionType = subType; + _ = msg.Data?.DisplayId != null ? subscriber.DisplayId = ObjectId.Parse(msg.Data?.DisplayId) : subscriber.DisplayId = null; + subscriber.Locale = msg.Data?.Locale; + subscriber.UserName = msg.Data?.UserName; + } + else + { + subscriber = new WsSubscriber(connectionId) + { + Locations = msg.Data?.Locations ?? [], + LocationIds = msg.Data?.LocationIds ?? [], + SubscriptionType = subType, + DisplayId = msg.Data?.DisplayId != null ? ObjectId.Parse(msg.Data?.DisplayId) : null, + Locale = msg.Data?.Locale, + UserName = msg.Data?.UserName + }; + + subscribersService.AddSubscriber(subscriber); + _logger.Debug("Ws ConnectionId NOT exists, created new WebSocket subscriber {subscriber}", + JsonConvert.SerializeObject(subscriber)); + } + } + + #endregion + + #region Send To Client Hub + + public async Task SendAsync(string subscriberId, OperationType? type, object? msg) + { + try + { + var settings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + Converters = new List { new StringEnumConverter() } + }; + var json = JsonConvert.SerializeObject(msg, Formatting.Indented, settings); + + await Hub.Clients.Client(subscriberId).SendAsync("ReceiveMessage", type?.ToString(), json); + } + catch (Exception ex) + { + _logger.Error("Error serializing message {msg}, Exception: {ex}", msg as MessageData, ex); + throw; + } + } + + public async Task SendToAllAsync(OperationType type, object? msg) + { + var settings = new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + NullValueHandling = NullValueHandling.Ignore, // NULL ARE NOT SHOWED + // REMOVED TO RETURN TO FRONTEND IN SPECIFIED TIME ZONE IF Kind Specified. DateTimeZoneHandling = DateTimeZoneHandling.Utc, // DATE IN UTC TIME ZONE + DateTimeZoneHandling = DateTimeZoneHandling.RoundtripKind, + DateFormatHandling = DateFormatHandling.IsoDateFormat, // DATE IN ISO FORMAT + DefaultValueHandling = DefaultValueHandling.Ignore // IGNORE SPECIFIC DEFAULT VALUES + }; + + await Hub.Clients.All.SendAsync("ReceiveMessage", type.ToString(), + JsonConvert.SerializeObject(msg, Formatting.Indented, settings)); + } + + private async Task SendErrorMessage(string errorMessage, string connectionId) + { + var msg = JsonConvert.SerializeObject(new Dictionary + { + { "error", true }, + { "message", errorMessage } + }); + + await Hub.Clients.Client(connectionId).SendAsync("ReceiveMessage", OperationType.Error, msg); + } + + #endregion + + #endregion +} \ No newline at end of file diff --git a/adas-core/adas-core.csproj b/adas-core/adas-core.csproj new file mode 100644 index 00000000..40769239 --- /dev/null +++ b/adas-core/adas-core.csproj @@ -0,0 +1,99 @@ + + + + net8.0 + enable + enable + adas_core + 32f28f15-4ca6-4522-b80b-06b784cf0c46 + Linux + 2.1 + 2.1 + 2.1 + Epigram Technologies + ADAS Core + + + + True + + + + True + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Always + + + Always + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/adas-core/appsettings.Auditoria.json b/adas-core/appsettings.Auditoria.json new file mode 100644 index 00000000..288904d6 --- /dev/null +++ b/adas-core/appsettings.Auditoria.json @@ -0,0 +1,1518 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Serilog": { + "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File", "Serilog.Exceptions", "Serilog.Expressions", "Serilog.Enrichers.Thread" ], + "Enrich": [ "WithExceptionDetails", "WithDynamicProperty", "WithThreadId" ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "adas_core": "Debug", + "EasyNetQ": "Information", + "Quartz": "Information", + "Microsoft": "Warning", + "System": "Error" + } + }, + "WriteTo": [ + { + "Name": "Console" + }, + { + "Name": "File", + "Args": { + "path": "Logs/.log", + "outputTemplate": "[{Timestamp:o}] [{Level:u3}] [{SourceContext:l}] {Message}{NewLine}{Exception}", + "rollingInterval": 4, //0: Infinite, 1: Year, 2: Month, 3: Day, 4: Hour, 5: Minute + "retainedFileCountLimit": 72, // 3 days + "rollOnFileSizeLimit": true, + "fileSizeLimitBytes": 20971520 // 20Mb + } + } + ], + "Properties": { + "Application": "ADASCORE" + } + }, + "AllowedHosts": "*", + "DatabaseSettings": { + //"ConnectionString": "mongodb://smacsvdh:dE.s2tg3Upz@127.0.0.1:37017/?authSource=admin", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@10.0.20.67:18888/?authSource=admin", + "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@sumo.julianrojas.xyz:27017/?authSource=admin", + "DatabaseName": "adas" + }, + "DatabaseConfigurationAudit": { + "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@sumo.julianrojas.xyz:27017/?authSource=admin", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@10.0.20.67:18888/?authSource=admin", + //"ConnectionString": "mongodb://smacsvdh:dE.s2tg3Upz@127.0.0.1:37017/?authSource=admin", + "DatabaseName": "AuditAdas" + + }, + "RabbitMQSettings": { + "ConnectionString": "host=sumo.julianrojas.xyz;port=5672;username=adas_core;password=adas_core;publisherConfirms=true;timeout=30", + "PatientsQueue": "patients", + "TreatmentsQueue": "treatments", + "ObservationsQueue": "observations", + "PumpsQueue": "pumps", + "MaxRetries": 1 + }, + //ldap://vhebron.net:389 cn=smartDisplayUCI_nounats,ou=Grupos de Seguridad,dc=vhebron,dc=net + "Ldap": { + "Enabled": true, + "Server": "vhebron.net", + "Port": 389, + "SearchBase": "OU=Grupos de Seguridad,DC=vhebron,DC=net", + //"PositionProperty": "CAMSanidadPuesto", + "UserDomainName": "venue", + "AuthoritiesMap": [ + { //Box 29 + "Group": "CN=smartDisplayUCI_nounats,OU=Grupos de Seguridad,DC=vhebron,DC=net", + "Rol": "AuthAdmin", + //"Actions": [ "View" ], + "DisplayId": "67a3405e96eb83abafd7e7e1" + } + //{ + // "Group": "CN=smartDisplayUCI_nounats,OU=Grupos de Seguridad,DC=vhebron,DC=net", + // "Rol": "View", + // "Actions": [ "View" ], + // "DisplayId": "65c114566d5d1ac02b4f2478" + //} + ], + "WhiteList": [ + { //Box 26 + "Name": "Prueba", + "Username": "venue", + "Rol": "AuthAdmin", + //"Actions": [ "View", "Write" ], + "DisplayId": "67a3402596eb83abafd7e7de" + } + ] + }, + "AuthSettings": { + "JwtConfig": { + "Key": "534224WERW�$&/($$ghdTDDTHSSDFVADFTRGW%Y&%�B&CVW$&H&$HWETGWXZ%EGX$%c", + "Issuer": "https://www.issuer.es/healthcare", + "Audience": "https://adas.health", + "ValidateIssuer": false, + "TokenValidityInMinutes": 1200, + "RefreshTokenValidityInMinutes": 14400 + }, + "LoginMethods": [ "Local", "Ldap" ] + }, + "ApiSettings": { + "Customize": "HUVH.UCIN", + "PointOfCareMapping": { + "Key": "PV1", + "Required": false, + "Refresh": 600 + }, + "ConfigObservation": { + "Key": "PV1", + "IgnoreUnknownObservation": true, + "IgnoreUnknownTreatment": false, + "Refresh": 60 + }, + //"SmacsOrApiUrl": "http://localhost:5118", + //"ConnectionString": "host=127.0.0.1;port=5672;username=smacs;password=smacs!;publisherConfirms=true;timeout=30", + //"RabbitConnectionString": "host=127.0.0.1;port=8888;username=smacs;password=2a!nMa;publisherConfirms=true;timeout=30", + //"RabbitConnectionString": "host=127.0.0.1;port=5672;username=mirth;password=lD2dvc.df4-;publisherConfirms=true;timeout=30", + + "RecordingQueue": "recordings", + "RetentionPolicy": "NoDelete", + "RetentionPolicyValue": 15, + "CreatePatientWithORU": true, + "CreatePatientWithSIU": false, + "CreatePatientWithoutLocation": false, + "CreatePatientWithLocation": false, + "UpdatePatientDataWithORU": false, + "UpdatePatientLocationWithORU": false, + "ArchivePatientWithOru": false, + "UpdatePatientNumberWithOru": false, + "OnArchivePatientAction": "Archive", + "ArchivePatientsWithoutObservationsSinceHours": 1200, + "CheckInactivePatientSchedulerIntervalHours": 100, + "CheckActiveTreatmentsSchedulerIntervalMinutes": 10000, + "SinceDischargeTimeToArchive": 48000, + "Shift": [ "08:00", "15:00", "18:00" ], + "FindPatientByLocationWithoutID": false, + "PersistObservationCodes": true, + "IccaFacility": "Philips", + "SendingFacility": [ "obs", "UMS" ], + "AllergiesCode": "473011001", + "DrainageCode": "56868008", + "PositionCode": "386053000", + "IsolationCode": "302147001", + "HighFrequencyVentilation": [ "PC-HFO" ], + "InvasiveVentilation": [ "A/C (S.I.P.P.V. )", "A/C (S.I.P.P.V.) + V.G.", "PRVC", "PC-SIMV", "SIMV-PRVC", "PC-AC", "SIMV", "SIMV-PC", "S.I.M.V.", "PC-CMV", "SIPPV", "PC" ], + "NonInvasiveVentilation": [ "DUOPAP", "NCPAP", "CPAP" ], + "Surgery": [ "138875005", "35631009", "420419008", "370897002", "115959002", "370896006" ], + "SurgeryText": [ "Cardiaca", "Dialisis peritoneal", "Hemodiafiltración", "Digestiva", "Neurologica", "Toracica" ], + "Transcutaneous": [ "151756", "151760" ], + "RegionalBrainSaturation": [ "194908", "194909" ], + "MedicationBolus": [ "000523", "001034", "21011000140105", "322440003", "322761006", "18851000140100", "333919005", "333920004", "333922007" ], + "IntravenousLinesCode": "10546003", + "Electroencephalogram": "42803009", + "MappingInterventions": [ + { + "Type": "Respiratory_Device", + "Category": "Intervention", + "Group": "not defined", + "Name": "Oxigeno (O2)/Alto flujo", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 204.0, + "FinalValue": null + } + ] + }, + { + "Type": "Respiratory_Device", + "Category": "Intervention", + "Group": "not defined", + "Name": "CPAP ", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 202.0, + "FinalValue": null + } + ] + }, + { + "Type": "Respiratory_Device", + "Category": "Intervention", + "Group": "not defined", + "Name": "Ventilaci\u00f3n mec\u00e1nica Invasiva", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 201.0, + "FinalValue": null + } + ] + }, + { + "Type": "Respiratory_Device", + "Category": "Intervention", + "Group": "not defined", + "Name": "VAFO Ventilaci\u00f3n Oscilatoria de alta frecuencia", + "Value": [ + { + "InitialValue": 4.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 200.0, + "FinalValue": null + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VV perfif\u00e9rica", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 103.1, + "FinalValue": 103.5 + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VV epicut\u00e1neo cava", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 102.1, + "FinalValue": 102.5 + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VV umbilical", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 101.1, + "FinalValue": null + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VV central", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 104.1, + "FinalValue": 104.3 + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VV PICC", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 108.1, + "FinalValue": 108.2 + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VA umbilical", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 101.2, + "FinalValue": null + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VA", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 106.1, + "FinalValue": 106.3 + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Digoxina ev", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 118.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Digoxina vo", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 1753.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Amiodarona ev", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 169.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Amiodarona vo", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 16323.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Atropina", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 186.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Esmolol", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 853.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Propranolol", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 7122.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Isoprenalina", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 283.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Flecainida ev", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 1247.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Flecainida vo", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 7055.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Adrenalina", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 281.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Noradrenalina", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 100000.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Milrinona", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 618.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Levosimendan", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 2073.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Dopamina", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 149.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Debutamina", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 1968.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Fenilefrina", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 12319.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Morfina ev", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 472.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Morfina ov", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 7081.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Fentanil", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 434.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Midazolam", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 756.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Dexmedetomidina", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 15910.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Cisatraculi", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 15066.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Propofol", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 314.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Ketamina", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 374.0, + "FinalValue": null + } + ] + }, + { + "Type": "System", + "Category": "Intervention", + "Group": "not defined", + "Name": "Hipotermia", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 100001.0, + "FinalValue": null + } + ] + }, + { + "Type": "System", + "Category": "Intervention", + "Group": "not defined", + "Name": "aEEG", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 100002.0, + "FinalValue": null + } + ] + }, + { + "Type": "System", + "Category": "Intervention", + "Group": "not defined", + "Name": "Drenaje ventricular (DVE)", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 315.1, + "FinalValue": null + }, + { + "InitialValue": 315.2, + "FinalValue": null + } + ] + }, + { + "Type": "System", + "Category": "Intervention", + "Group": "not defined", + "Name": "Drenaje toracico", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 311.0, + "FinalValue": 314.0 + } + ] + }, + { + "Type": "System", + "Category": "Intervention", + "Group": "not defined", + "Name": "Dialisis peritoneal - abdominal", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 306.0, + "FinalValue": 310.0 + } + ] + }, + { + "Type": "Newborn_Weight", + "Category": "Observation", + "Group": "not defined", + "Name": "Newborn_Weight", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": 500.0, + "valueContributed": 5 + }, + { + "InitialValue": 501.0, + "FinalValue": 1000.0, + "valueContributed": 4 + }, + { + "InitialValue": 1001.0, + "FinalValue": 1500.0, + "valueContributed": 3 + }, + { + "InitialValue": 1501.0, + "FinalValue": 2000.0, + "valueContributed": 2 + }, + { + "InitialValue": 2000.0, + "FinalValue": 2500.0, + "valueContributed": 1 + } + ], + "Codes": [ + { + "InitialValue": 10000465.0, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Cardiaca", + "Value": [ + { + "InitialValue": 4.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.1, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Digestiva", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.2, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Neurologica ", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.3, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Toracica", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.4, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "ORL/Maxil\u00b7lo", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.5, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Oftalmo", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.6, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Ortopedia", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.7, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Altres", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.8, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "No", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.9, + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "Parenteral", + "Name": "NUTRICIO PARENTERAL", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "1388", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LM", + "Name": "Llet materna", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIB586", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LM", + "Name": "Llet materna de banc", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIB000589", + "FinalValue": null + }, + { + "InitialValue": "BIB000587", + "FinalValue": null + }, + { + "InitialValue": "BIB000588", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LM", + "Name": "Llet materna a demanda", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "NO_EST2", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LM descremada", + "Name": "LM descremada", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "NO_EST3", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Formula de soja", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBCOM003", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Formula hidrolizada (Alfare)", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBCOM002", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Formula inici", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBNOR003", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Formula prematurs 14,4%", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBCOM017", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Formula prematurs 15,9%", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBCOM018", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Formula sense lactosa", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBCOM01", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Monogen", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBCOM009", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Infatrini", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBNOR001", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Infatini Peptisorb", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBNOR0013", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Neocate 13,8%", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBBAS019", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Neocate Advance 25%", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "NO_EST4", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Energivit", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBBAS021", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Bibero especial", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "ALTESP001", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "Absoluta", + "Name": "Absoluta", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "15002284", + "FinalValue": null + } + ] + } + ], + // "MappingInterventions":[{"Type":"Respiratory_Device","Name":"Ox\u00edgeno (O2)/Alto flujo","Codes":[{"InitialValue":204,"FinalValue":null}]},{"Type":"Respiratory_Device","Name":"CPAP","Codes":[{"InitialValue":202.0,"FinalValue":null}]},{"Type":"Respiratory_Device","Name":"Ventilaci\u00f3n mec\u00e1nica Invasiva","Codes":[{"InitialValue":201.0,"FinalValue":null}]},{"Type":"Respiratory_Device","Name":"VAFO Ventilaci\u00f3n Oscilatoria de alta frecuencia","Codes":[{"InitialValue":204.0,"FinalValue":null}]},{"Type":"Intravenous_Routes","Name":"VV perif\u00e9rica","Codes":[{"InitialValue":103.1,"FinalValue":103.5}]},{"Type":"Intravenous_Routes","Name":"VV epicut\u00e1neo cava","Codes":[{"InitialValue":102.1,"FinalValue":102.5}]},{"Type":"Intravenous_Routes","Name":"VV umbilical","Codes":[{"InitialValue":101.1,"FinalValue":null}]},{"Type":"Intravenous_Routes","Name":"VV central","Codes":[{"InitialValue":104.1,"FinalValue":104.3}]},{"Type":"Intravenous_Routes","Name":"VV PICC","Codes":[{"InitialValue":108.1,"FinalValue":null},{"InitialValue":108.2,"FinalValue":null}]},{"Type":"Intravenous_Routes","Name":"VA umbilical","Codes":[{"InitialValue":101.2,"FinalValue":null}]},{"Type":"Intravenous_Routes","Name":"VA","Codes":[{"InitialValue":106.1,"FinalValue":106.3}]},{"Type":"System","Name":"Hipotermia","Codes":[{"InitialValue":100001,"FinalValue":null}]},{"Type":"System","Name":"aEEG","Codes":[{"InitialValue":100002,"FinalValue":null}]},{"Type":"System","Name":"Drenaje ventricular (DVE)","Codes":[{"InitialValue":315.1,"FinalValue":null},{"InitialValue":315.2,"FinalValue":null}]},{"Type":"System","Name":"Drenaje tor\u00e1cico","Codes":[{"InitialValue":311,"FinalValue":314}]},{"Type":"System","Name":"Di\u00e1lisis peritoneal - abdominal","Codes":[{"InitialValue":306,"FinalValue":310}]}], + "ComplexityMappingObservation": { + "Oxigeno (O2)/Alto flujo": 1, + "CPAP ": 2, + "Ventilación mecánica Invasiva": 3, + "VAFO Ventilación Oscilatoria de alta frecuencia": 4, + "VV perfiférica": 1, + "VV epicutáneo cava": 2, + "VV umbilical": 2, + "VV central": 2, + "VV PICC": 2, + "VA umbilical": 3, + "VA": 3, + "Digoxina ev": 1, + "Digoxina vo": 1, + "Amiodarona ev": 1, + "Amiodarona vo": 1, + "Atropina": 1, + "Esmolol": 1, + "Propranolol": 1, + "Isoprenalina": 1, + "Flecainida ev": 1, + "Flecainida vo": 1, + "Adrenalina": 2, + "Noradrenalina": 2, + "Milrinona": 2, + "Levosimendan": 2, + "Dopamina": 2, + "Debutamina": 2, + "Fenilefrina": 2, + "Hipotermia": 2, + "aEEG": 2, + "Drenaje ventricular (DVE)": 2, + "Drenaje toracico": 2, + "Dialisis peritoneal - abdominal": 2, + "Cardiaca": 4, + "Digestiva": 3, + "Neurologica ": 3, + "Toracica": 3, + "ORL/Maxil·lo": 2, + "Oftalmo": 1, + "Ortopedia": 1, + "Altres": 1, + "No": 0 + }, + //Dejar siempre en ultima posicion el Ino code 84481000140102 + "Respiratory": [ "361110005", "416362002", "84481000140102" ], + "Oni": "84481000140102", + //"BalizaUrl": "http://10.0.10.35/info.html", + //"BalizaPassword": "password", + "ConfigObservationRefresh": 1200, + "ActivateSchedulerCheckExpiredObservation": false, + "ActivateSchedulerCheckActiveTreatmentsJob": false, + "PumpExpiresSeconds": 12000, + "SendPumpsZero": false, + "PumPressureAlarm": [], + "VolumetricAirOcclusionAlarm": [ "Accumulated Air In Line", "Air In Line", "Occlusion", "Upstream Occlusion" ], + "EndContinuousInfusionAlarm": [ "End Of Infusion", "Near End Of Infusion" ], + "InotropicEndInfusionAlarm": [ "Near End Of Infusion", "Occlusion", "Air In Line" ], + "inotropicMedicines": [ "Dopamina", "Dobutamina", "Milrinona", "Noradrenalina", "Levosimendán" ], + "PathUpdateFiles": [ "C:", "Standard_Files", "UploadFromServer" ], + "ActivateCheckInactivePatients": false, + "NoCalculateStatusWithCodingSystem": "SENSOR" + } +} \ No newline at end of file diff --git a/adas-core/appsettings.BombasBD.json b/adas-core/appsettings.BombasBD.json new file mode 100644 index 00000000..0980f03e --- /dev/null +++ b/adas-core/appsettings.BombasBD.json @@ -0,0 +1,283 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "DatabaseSettings": { + //"ConnectionString": "mongodb://admin:password@127.0.0.1:27017?authMechanism=SCRAM-SHA-256&connectTimeoutMS=5000&maxPoolSize=500&retryWrites=true&waitQueueMultiple=15", + //"ConnectionString": "mongodb://localhost:27017", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@127.0.0.1:27017/?authSource=admin", + //develop + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@10.0.20.67:18888/?authSource=admin&maxPoolSize=200&connectTimeoutMS=10000&socketTimeoutMS=30000", + "ConnectionString": "mongodb://adas:adas2025@168.119.160.124:27017/adas?authSource=admin", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@sumo.julianrojas.xyz:27017/?authSource=admin", + //"ConnectionString": "mongodb://smartuci:!H12o2020@10.206.250.147:21103/?authSource=smartuci", + //producción + //"ConnectionString": "mongodb://smartuci:!H12o2020@localhost:27017/?authSource=smartuci", + //"ConnectionString": "mongodb://admin:4q*pHca@10.0.20.78:22007", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@sumo.julianrojas.xyz:27017/?authSource=admin", + "DatabaseName": "adas" + }, + "DatabaseConfigurationAudit": { + "ConnectionString": "mongodb://adas:adas2025@168.119.160.124:27017/?authSource=admin", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@julianrojas.xyz:27017/?authSource=admin", + "DatabaseName": "AuditAdas" + }, + "RabbitMQSettings": { + "ConnectionString": "host=168.119.160.124;port=5672;username=adascore;password=ni2T7s2N6Dby7dNJ;virtualHost=/;publisherConfirms=true;timeout=10", + //"ConnectionString": "amqp://adascore:ni2T7s2N6Dby7dNJ@adas-rabbit:5672", + //"ConnectionString": "host=localhost;port=5672;username=smacs;password=smacs;publisherConfirms=true;timeout=10", + //"ConnectionString": "host=sumo.julianrojas.xyz;port=5672;username=admin;password=R466itMq;publisherConfirms=true;timeout=30", + //"ConnectionString": "host=10.0.20.241;port=8889;username=smacs;password=smacs!;publisherConfirms=true;timeout=10", + //"PatientsQueue": "patients", + //"TreatmentsQueue": "treatments", + //"ObservationsQueue": "observations", + "MaxRetries": 1 + }, + "Ldap": { + "Enabled": true, + "Server": "bastionvpn.epigram", + "Port": 21389, + "SearchBase": "OU=CSCM,DC=salud,DC=madrid,DC=org", + "PositionProperty": "CAMSanidadPuesto", + "UserDomainName": "SALUD", + "AuthoritiesMap": [ + { + "Group": "CN=gHDOC_T_Philips,OU=Hospital Doce de Octubre,OU=Hospitales,OU=CSCM,DC=salud,DC=madrid,DC=org", + "Rol": "AuthAdmin", + "DisplayId": "65c114566d5d1ac02b4f2478" + } + ], + "WhiteList": [ + { + "Name": "Fernando Igual Novella", + "Username": "53091058C", + "Rol": "AuthAdmin", + "DisplayId": "65c114566d5d1ac02b4f2478" + } + ] + }, + "AuthSettings": { + "JwtConfig": { + "Key": "534224WERW�$&/($$ghdTDDTHSSDFVADFTRGW%Y&%�B&CVW$&H&$HWETGWXZ%EGX$%c", + "Issuer": "https://www.philips.es/healthcare", + "Audience": "Madrid Salud", + "TokenValidityInMinutes": 1440, + "RefreshTokenValidityInMinutes": 10 + }, + "LoginMethods": [ + "Local", + "Ldap" + ] + }, + "ApiSettings": { + "HL7RestrictionByConfigAlwaysDisabled": true, + "Customize": "BD", + "PointOfCareMapping": { + "Key": "PV1", + "Required": false, + "Refresh": 600 + }, + "ConfigObservation": { + "Key": "PV1", + "IgnoreUnknownObservation": true, + "IgnoreUnknownTreatment": false, + "Refresh": 60 + }, + "ArchiveNurseData": { + "Active": true, + "IntervalMinutes": 1, + "ArchiveTest": { + //pruebas + "Active": true, + "EndDateAfterMinutes": 60 + }, + "ArchiveProcedure": { + //intervenciones + "Active": true, + "EndDateAfterMinutes": 60 + }, + "ArchiveTreatment": { + //terapias + "Active": true, + "EndDateAfterMinutes": 60 + } + }, + "SmacsOrApiUrl": "http://localhost:5118", + "RecordingQueue": "recordings", + "RetentionPolicy": "NoDelete", + "RetentionPolicyValue": 0, + "CreatePatientWithORU": true, + "CreatePatientWithSIU": true, + "CreatePatientWithoutLocation": false, + "UpdatePatientDataWithORU": true, + "updatePatientDataWithAdt_A02": true, + "UpdatePatientLocationWithORU": false, + "ArchivePatientWithOru": false, + "UpdatePatientNumberWithOru": false, + "OnArchivePatientAction": "Archive", + "ArchivePatientsWihtoutObservationsSinceDays": 10, + "CheckInactivePatientSchedulerIntervalHours": 24, + "CheckActiveTreatmentsSchedulerIntervalMinutes": 2, + "SinceDischargeTimeToArchive": 48, + "Shift": [ + "08:00", + "15:00", + "22:00" + ], + "FindPatientByLocationWithoutID": false, + "PersistObservationCodes": true, + "IccaFacility": "Philips.CIS.CVC", + "SendingFacility": [ + "LABORATORIO" + ], + "AllergiesCode": [ + "473011001" + ], + "DrainageCode": [ + "56868008" + ], + "PositionCode": [ + "386053000" + ], + "IsolationCode": [ + "302147001" + ], + "HighFrequencyVentilation": [ + "PC-HFO", + "Alto Flujo" + ], + "InvasiveVentilation": [ + "A/C (S.I.P.P.V. )", + "A/C (S.I.P.P.V.) + V.G.", + "PRVC", + "PC-SIMV", + "SIMV-PRVC", + "PC-AC", + "SIMV", + "SIMV-PC", + "S.I.M.V.", + "PC-CMV", + "SIPPV", + "PC" + ], + "NonInvasiveVentilation": [ + "DUOPAP", + "NCPAP", + "CPAP", + "V.N.I. Ciclada" + ], + "Surgery": [ + "138875005", + "370896006", + "370896006", + "35631009", + "420419008", + "370897002", + "115959002", + "370896006" + ], + "SurgeryText": [ + "Cirugía láser of", + "Cirugía", + "Cirugía sentidos", + "Cirugía respirat", + "Cirugía locomoto", + "Cirugía renal", + "Cirugía digestiv", + "Cirugía neuro" + ], + "Transcutaneous": [ + "151756", + "151760" + ], + "RegionalBrainSaturation": [ + "194908", + "194909" + ], + "MedicationBolus": [ + "000523", + "001034", + "21011000140105", + "322440003", + "322761006", + "18851000140100", + "333919005", + "333920004", + "333922007", + "18791000140100", + "18801000140104", + "18811000140101" + ], + "IntravenousLinesCode": [ + "10546003" + ], + "Electroencephalogram": [ + "42803009", + "408987002" + ], + "Ecmo": [ + "705923009" + ], + "NotesIndicatingMedication": [ + "SUEROS Y HEMODER,Perfusiones,Medicación,NPT" + ], + //Dejar siempre en ultima posicion el Ino code 84481000140102 + "Respiratory": [ + "361110005", + "416362002", + "84481000140102" + ], + "Oni": [ + "84481000140102", + "416362002" + ], + "BalizaUrl": "http://10.0.10.35/info.html", + "BalizaPassword": "password", + "ConfigObservationRefresh": 120, + "ConfigUnitsRequired": true, + "ActivateSchedulerCheckExpiredObservation": false, + "schedulerCheckExpiredObservationsIntervalMinutes": 5, + "diagnosisSystem": "Snomed-CT", + "DiagnosisCode": [ + "55607006" + ], + "PumpExpiresSeconds": 120, + "SendPumpsZero": false, + "PumPressureAlarm": [], + "VolumetricAirOcclusionAlarm": [ + "Accumulated Air In Line", + "Air In Line", + "Occlusion", + "Upstream Occlusion" + ], + "EndContinuousInfusionAlarm": [ + "End Of Infusion", + "Near End Of Infusion" + ], + "InotropicEndInfusionAlarm": [ + "Near End Of Infusion", + "Occlusion", + "Air In Line" + ], + "inotropicMedicines": [ + "Dopamina", + "Dobutamina", + "Milrinona", + "Noradrenalina", + "Levosimendán" + ], + "ActivateCheckInactivePatients": false, + "PathToDisplayAssets": [ + "D:", + "Web", + "smacs-uci-webdisplay", + "src", + "assets", + "icon" + ] + } +} diff --git a/adas-core/appsettings.CENTRAL_PHILIPS.json b/adas-core/appsettings.CENTRAL_PHILIPS.json new file mode 100644 index 00000000..3fb8f745 --- /dev/null +++ b/adas-core/appsettings.CENTRAL_PHILIPS.json @@ -0,0 +1,273 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Serilog": { + "Using": [ + "Serilog.Sinks.Console", + "Serilog.Sinks.File", + "Serilog.Exceptions", + "Serilog.Expressions", + "Serilog.Enrichers.Thread" + ], + "Enrich": [ + "WithExceptionDetails", + "WithDynamicProperty", + "WithThreadId" + ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "adas_core": "Debug", + "EasyNetQ": "Information", + "Quartz": "Information", + "Microsoft": "Warning", + "System": "Error" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "formatter": { + "type": "Serilog.Templates.ExpressionTemplate, Serilog.Expressions", + "template": "{@t:yyyy-MM-dd HH:mm:ss} [{@l:u3}]{#if SourceContext is not null} ({SourceContext,35}){#end}: {@m}\n{@x}" + }, + "theme": "Serilog.Sinks.SystemConsole.Themes.SystemConsoleTheme::Grayscale, Serilog.Sinks.Console" + } + }, + { + "Name": "File", + "Args": { + "path": "Logs/.log", + "formatter": { + "type": "Serilog.Templates.ExpressionTemplate, Serilog.Expressions", + "template": "{@t:yyyy-MM-dd HH:mm:ss} [{@l:u3}]{#if SourceContext is not null} ({#if Length(SourceContext) > 35}{Substring(SourceContext, Length(SourceContext) - 35)}{#else}{SourceContext,35}{#end}){#end}: {@m}{#if TraceId is not null} TraceId: {TraceId}{#end}{#if TraceMessageId is not null} TraceMessageId: {TraceMessageId}{#end} TraceIdentifier: {TraceIdentifier} Thread: {ThreadId} \n{@x}" + }, + "rollingInterval": 4, + //0: Infinite, 1: Year, 2: Month, 3: Day, 4: Hour, 5: Minute + "retainedFileCountLimit": 72, + // 3 days + "rollOnFileSizeLimit": true + } + } + ], + "Properties": { + "Application": "ADASCORE" + } + }, + "AllowedHosts": "*", + "DatabaseSettings": { + "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@10.0.20.67:18888/?authSource=admin", + "DatabaseName": "Central_Philips" + }, + "RabbitMQSettings": { + "ConnectionString": "host=localhost;port=8888;username=smacs;password=smacs!;publisherConfirms=true;timeout=10", + "PatientsQueue": "patients", + "TreatmentsQueue": "treatments", + "ObservationsQueue": "observations", + "PumpsQueue": "pumps", + "RecordingQueue": "recordings", + "AppointmentsQueue": "appointments", + "MaxRetries": 1 + }, + "AutoRecordingSettings": { + "AutoStart": { + "BlueCode": 30, + "HemoFilterAlert": 30, + "RespirationAlert": 15 + }, + "AutoStop": { + "BlueCode": 30, + "HemoFilterAlert": 30, + "RespirationAlert": 15 + }, + "MustSendAlert": { + "BlueCode": false, + "HemoFilterAlert": false, + "RespirationAlert": false + } + }, + "ApiSettings": { + "Customize": "HPAZ", + "PointOfCareMapping": { + "Key": "PV1", + "Required": false, + "Refresh": 600 + }, + "ConfigObservation": { + "Key": "PV1", + "IgnoreUnknownObservation": true, + "IgnoreUnknownTreatment": false, + "Refresh": 60 + }, + "RetentionPolicy": "NoDelete", + "RetentionPolicyValue": 15, + "CreatePatientWithORU": true, + "CreatePatientWithSIU": true, + "CreatePatientWithoutLocation": false, + "UpdatePatientDataWithORU": false, + "UpdatePatientLocationWithORU": false, + "ArchivePatientWithOru": false, + "UpdatePatientNumberWithOru": false, + "OnArchivePatientAction": "Archive", + "ArchivePatientsWithoutObservationsSinceHours": 10, + "CheckInactivePatientSchedulerIntervalHours": 24, + "CheckActiveTreatmentsSchedulerIntervalMinutes": 10, + "SinceDischargeTimeToArchive": 120, + "Shift": [ + "08:00", + "15:00", + "18:00" + ], + "FindPatientByLocationWithoutID": false, + "PersistObservationCodes": true, + "IccaFacility": "Philips", + "SendingFacility": [ + "LABORATORIO" + ], + "AllergiesCode": [ + "473011001" + ], + "DrainageCode": [ + "56868008" + ], + "PositionCode": [ + "386053000" + ], + "IsolationCode": [ + "302147001" + ], + "HighFrequencyVentilation": [ + "PC-HFO" + ], + "InvasiveVentilation": [ + "A/C (S.I.P.P.V. )", + "A/C (S.I.P.P.V.) + V.G.", + "PRVC", + "PC-SIMV", + "SIMV-PRVC", + "PC-AC", + "SIMV", + "SIMV-PC", + "S.I.M.V.", + "PC-CMV", + "SIPPV", + "PC" + ], + "NonInvasiveVentilation": [ + "DUOPAP", + "NCPAP" + ], + "Surgery": [ + "138875005", + "35631009", + "420419008", + "370897002", + "115959002", + "370896006" + ], + "SurgeryText": [ + "Cirugía láser of", + "Cirugía", + "Cirugía", + "sentidos", + "Cirugía", + "respirat", + "Cirugía", + "locomoto", + "Cirugía", + "renal", + "Cirugía", + "digestiv", + "Cirugía", + "neuro" + ], + "Transcutaneous": [ + "151756", + "151760" + ], + "RegionalBrainSaturation": [ + "194908", + "194909" + ], + "MedicationBolus": [ + "000523", + "001034", + "21011000140105", + "322440003", + "322761006", + "18851000140100", + "333919005", + "333920004", + "333922007" + ], + "IntravenousLinesCode": [ + "10546003" + ], + "Electroencephalogram": [ + "42803009" + ], + //Dejar siempre en ultima posicion el Ino code 84481000140102 + "Respiratory": [ + "361110005", + "416362002", + "84481000140102" + ], + "Oni": [ + "84481000140102" + ], + "BalizaUrl": "http://10.0.10.35/info.html", + "BalizaPassword": "password", + "ConfigObservationRefresh": 150, + "ConfigUnitsRequired": true, + "ActivateSchedulerCheckExpiredObservation": true, + "SchedulerCheckExpiredObservationsIntervalMinutes": 5, + "PumpExpiresSeconds": 30, + "SendPumpsZero": false, + "VolumetricAirOcclusionAlarm": [ + "Occlusion", + "Upstream Occlusion" + ], + "VolumetricAirInLineAlarm": [ + "Accumulated Air In Line", + "Air In Line", + "Air In Line Now" + ], + "EndContinuousInfusionAlarm": [ + "End Of Infusion", + "Near End Of Infusion", + "End Of Infusion - KVO Stop", + "End Of Infusion - Stop" + ], + "InotropicEndInfusionAlarm": [ + "Near End Of Infusion", + "End Of Infusion", + "End Of Infusion - KVO Stop", + "End Of Infusion - Stop" + ], + "InotropicMedicines": [ + "Dopamina", + "Dobutamina", + "Milrinona", + "Noradrenalina", + "Levosimendán", + "Alteplasa-rTPA", + "Esmolol", + "Milrinona", + "Nitroprusiato", + "Norad BASE" + ], + "ActivateCheckInactivePatients": true, + "PathUpdateFiles": [ + "C:", + "MeddisPicu_Deploy", + "configs", + "updateFiles" + ], + "IgnoreCalcObservationsOlderInMinutesThan": 5 + } +} diff --git a/adas-core/appsettings.CHUO.json b/adas-core/appsettings.CHUO.json new file mode 100644 index 00000000..09e8f282 --- /dev/null +++ b/adas-core/appsettings.CHUO.json @@ -0,0 +1,356 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Serilog": { + "Using": [ + "Serilog.Sinks.Console", + "Serilog.Sinks.File", + "Serilog.Exceptions", + "Serilog.Expressions", + "Serilog.Enrichers.Thread" + ], + "Enrich": [ + "WithExceptionDetails", + "WithDynamicProperty", + "WithThreadId" + ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "adas_core": "Debug", + "EasyNetQ": "Information", + "Quartz": "Information", + "Microsoft": "Warning", + "System": "Error" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + /*"formatter": { + "type": "Serilog.Templates.ExpressionTemplate, Serilog.Expressions", + "template": "{@t:yyyy-MM-dd HH:mm:ss} [{@l:u3}]{#if SourceContext is not null} ({SourceContext,35}){#end}: {@m}\n{@x}" + },*/ + "theme": "Serilog.Sinks.SystemConsole.Themes.SystemConsoleTheme::Grayscale, Serilog.Sinks.Console" + } + }, + { + "Name": "File", + "Args": { + "path": "Logs/.log", + "formatter": { + "type": "Serilog.Templates.ExpressionTemplate, Serilog.Expressions", + "template": "{@t:yyyy-MM-dd HH:mm:ss} [{@l:u3}]{#if SourceContext is not null} ({#if Length(SourceContext) > 35}{Substring(SourceContext, Length(SourceContext) - 35)}{#else}{SourceContext,35}{#end}){#end}: {@m}{#if TraceId is not null} TraceId: {TraceId}{#end}{#if TraceMessageId is not null} TraceMessageId: {TraceMessageId}{#end} TraceIdentifier: {TraceIdentifier} Thread: {ThreadId} \n{@x}" + }, + //"outputTemplate": "[{Timestamp:o}] [{Level:u}] [{SourceContext:l}] {Message}{NewLine}{Exception}", + "rollingInterval": 4, + //0: Infinite, 1: Year, 2: Month, 3: Day, 4: Hour, 5: Minute + "retainedFileCountLimit": 72, + // 3 days + "rollOnFileSizeLimit": true + //"fileSizeLimitBytes": 20971520 // 20Mb + } + } + ], + "Properties": { + "Application": "ADASCORE" + } + }, + "AllowedHosts": "*", + "DatabaseSettings": { + //develop + "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@10.0.20.67:18888/?authSource=admin&maxPoolSize=200&connectTimeoutMS=10000&socketTimeoutMS=30000", + //prod + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@localhost:27017/?maxConnectionPoolSize=1000&waitQueueSize=100000&connectTimeoutMS=10000", + //"DatabaseName": "meddiscore" + "DatabaseName": "CHUO_dev" + //"DatabaseName": "MedDisCore_CHUO" + }, + "RabbitMQSettings": { + //dev + "ConnectionString": "host=10.0.20.241;port=8889;username=smacs;password=smacs!;publisherConfirms=true;timeout=30", + //prod + //"ConnectionString": "host=localhost;port=5672;username=smacsuci;password=2(R*aQpu2r;publisherConfirms=true;timeout=30", + "PatientsQueue": "patients", + "ObservationsQueue": "observations", + "AlarmObservationQueue": "alarms", + "MaxRetries": 1 + }, + "ApiSettings": { + "Customize": "CHUO", + "PointOfCareMapping": { + "Key": "PV1", + "Required": true, + "Refresh": 600 + }, + "ConfigObservation": { + "Key": "PV1", + "IgnoreUnknownObservation": true, + "IgnoreUnknownTreatment": false, + "Refresh": 60 + }, + "RetentionPolicy": "NoDelete", + "RetentionPolicyValue": 0, + "CreatePatientWithORU": true, + "CreatePatientWithSIU": true, + "CreatePatientWithoutLocation": false, + "CreatePatientWithADT_A08": true, + "UpdatePatientDataWithORU": false, + "UpdatePatientLocationWithORU": false, + "ArchivePatientWithOru": true, + "UpdatePatientNumberWithOru": false, + "OnArchivePatientAction": "Archive", + "ActivateCheckInactivePatients": true, + "ArchivePatientsWithoutObservationsSinceHours": 24, + "CheckInactivePatientSchedulerIntervalHours": 24, + "ActivateSchedulerCheckNewsJob": false, + "SinceDischargeTimeToArchive": 48, + "FindPatientByLocationWithoutID": false, + "PersistObservationCodes": true, + "IccaFacility": "Philips.CIS.CVC", + "SendingFacility": [ + "obs", + "UMS" + ], + //"SendingFacility": [ "LABORATORIO" ], + "AllergiesCode": [ + "473011001" + ], + "DrainageCode": [ + "56868008" + ], + "PositionCode": [ + "386053000" + ], + "IsolationCode": [ + "302147001" + ], + "HighFrequencyVentilation": [ + "HNF", + "HIGH FLOW" + ], + "InvasiveVentilation": [ + "67303831", + "67303912" + ], + "NonInvasiveVentilation": [ + "BIPAP", + "CPAP" + ], + "ConfigObservationRefresh": 60, + "ConfigUnitsRequired": false, + "ActivateSchedulerCheckExpiredObservation": false, + "PathUpdateFiles": [ + "C:", + "Standard_Files", + "UploadFromServer" + ] + }, + "ProxyDevicesSettings": [ + { + "id": "cam2", + "type": "Http", + "parameters": { + "url": "http://69.136.185.70/axis-cgi/jpg/image.cgi?camera=1", + "name": "Camera 2", + "username": "adas", + "password": "2cD1.743$g0*" + } + }, + { + "id": "cam4", + "type": "Http", + "parameters": { + "url": "http://69.136.185.69/axis-cgi/jpg/image.cgi?camera=1", + "name": "Camera 4", + "username": "adas", + "password": "2cD1.743$g0*" + } + }, + { + "id": "cam6", + "type": "Http", + "parameters": { + "url": "http://69.136.185.68/axis-cgi/jpg/image.cgi?camera=1", + "name": "Camera 6", + "username": "adas", + "password": "2cD1.743$g0*" + } + }, + { + "id": "cam8", + "type": "Http", + "parameters": { + "url": "http://69.136.185.67/axis-cgi/jpg/image.cgi?camera=1", + "name": "Camera 8", + "username": "adas", + "password": "2cD1.743$g0*" + } + }, + { + "id": "cam10", + "type": "Http", + "parameters": { + "url": "http://69.136.185.66/axis-cgi/jpg/image.cgi?camera=1", + "name": "Camera 10", + "username": "adas", + "password": "2cD1.743$g0*" + } + }, + { + "id": "cam12", + "type": "Http", + "parameters": { + "url": "http://69.136.185.65/axis-cgi/jpg/image.cgi?camera=1", + "name": "Camera 12", + "username": "adas", + "password": "2cD1.743$g0*" + } + }, + { + "id": "cam14", + "type": "Http", + "parameters": { + "url": "http://69.136.185.64/axis-cgi/jpg/image.cgi?camera=1", + "name": "Camera 14", + "username": "adas", + "password": "2cD1.743$g0*" + } + }, + { + "id": "cam16", + "type": "Http", + "parameters": { + "url": "http://69.136.185.63/axis-cgi/jpg/image.cgi?camera=1", + "name": "Camera 16", + "username": "adas", + "password": "2cD1.743$g0*" + } + }, + { + "id": "cam18", + "type": "Http", + "parameters": { + "url": "http://69.136.185.61/axis-cgi/jpg/image.cgi?camera=1", + "name": "Camera 18", + "username": "adas", + "password": "2cD1.743$g0*" + } + }, + { + "id": "cam20", + "type": "Http", + "parameters": { + "url": "http://69.136.185.62/axis-cgi/jpg/image.cgi?camera=1", + "name": "Camera 20", + "username": "adas", + "password": "2cD1.743$g0*" + } + }, + { + "id": "cam2-mjpg", + "type": "Http", + "parameters": { + "url": "http://69.136.185.70/axis-cgi/mjpg/video.cgi?camera=1", + "name": "Camera 2", + "username": "adas", + "password": "2cD1.743$g0*" + } + }, + { + "id": "cam4-mjpg", + "type": "Http", + "parameters": { + "url": "http://69.136.185.69/axis-cgi/mjpg/video.cgi?camera=1", + "name": "Camera 4", + "username": "adas", + "password": "2cD1.743$g0*" + } + }, + { + "id": "cam6-mjpg", + "type": "Http", + "parameters": { + "url": "http://69.136.185.68/axis-cgi/mjpg/video.cgi?camera=1", + "name": "Camera 6", + "username": "adas", + "password": "2cD1.743$g0*" + } + }, + { + "id": "cam8-mjpg", + "type": "Http", + "parameters": { + "url": "http://69.136.185.67/axis-cgi/mjpg/video.cgi?camera=1", + "name": "Camera 8", + "username": "adas", + "password": "2cD1.743$g0*" + } + }, + { + "id": "cam10-mjpg", + "type": "Http", + "parameters": { + "url": "http://69.136.185.66/axis-cgi/mjpg/video.cgi?camera=1", + "name": "Camera 10", + "username": "adas", + "password": "2cD1.743$g0*" + } + }, + { + "id": "cam12-mjpg", + "type": "Http", + "parameters": { + "url": "http://69.136.185.65/axis-cgi/mjpg/video.cgi?camera=1", + "name": "Camera 12", + "username": "adas", + "password": "2cD1.743$g0*" + } + }, + { + "id": "cam14-mjpg", + "type": "Http", + "parameters": { + "url": "http://69.136.185.64/axis-cgi/mjpg/video.cgi?camera=1", + "name": "Camera 14", + "username": "adas", + "password": "2cD1.743$g0*" + } + }, + { + "id": "cam16-mjpg", + "type": "Http", + "parameters": { + "url": "http://69.136.185.63/axis-cgi/mjpg/video.cgi?camera=1", + "name": "Camera 16", + "username": "adas", + "password": "2cD1.743$g0*" + } + }, + { + "id": "cam18-mjpg", + "type": "Http", + "parameters": { + "url": "http://69.136.185.61/axis-cgi/mjpg/video.cgi?camera=1", + "name": "Camera 18", + "username": "adas", + "password": "2cD1.743$g0*" + } + }, + { + "id": "cam20-mjpg", + "type": "Http", + "parameters": { + "url": "http://69.136.185.62/axis-cgi/mjpg/video.cgi?camera=1", + "name": "Camera 20", + "username": "adas", + "password": "2cD1.743$g0*" + } + } + ] +} diff --git a/adas-core/appsettings.Develop.json b/adas-core/appsettings.Develop.json new file mode 100644 index 00000000..4a58e73a --- /dev/null +++ b/adas-core/appsettings.Develop.json @@ -0,0 +1,1674 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Serilog": { + "Using": [ + "Serilog.Sinks.Console", + "Serilog.Sinks.File", + "Serilog.Exceptions", + "Serilog.Expressions", + "Serilog.Enrichers.Thread" + ], + "Enrich": [ + "WithExceptionDetails", + "WithDynamicProperty", + "WithThreadId" + ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "adas_core": "Debug", + "EasyNetQ": "Information", + "Quartz": "Information", + "Microsoft": "Warning", + "System": "Error" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "formatter": { + "type": "Serilog.Templates.ExpressionTemplate, Serilog.Expressions", + "template": "{@t:yyyy-MM-dd HH:mm:ss} [{@l:u3}]{#if SourceContext is not null} ({SourceContext,35}){#end}: {@m}\n{@x}" + }, + "theme": "Serilog.Sinks.SystemConsole.Themes.SystemConsoleTheme::Grayscale, Serilog.Sinks.Console" + } + }, + { + "Name": "File", + "Args": { + "path": "Logs/.log", + "formatter": { + "type": "Serilog.Templates.ExpressionTemplate, Serilog.Expressions", + "template": "{@t:yyyy-MM-dd HH:mm:ss} [{@l:u3}]{#if SourceContext is not null} ({#if Length(SourceContext) > 35}{Substring(SourceContext, Length(SourceContext) - 35)}{#else}{SourceContext,35}{#end}){#end}: {@m}{#if TraceId is not null} TraceId: {TraceId}{#end}{#if TraceMessageId is not null} TraceMessageId: {TraceMessageId}{#end} TraceIdentifier: {TraceIdentifier} Thread: {ThreadId} \n{@x}" + }, + //"outputTemplate": "[{Timestamp:o}] [{Level:u}] [{SourceContext:l}] {Message}{NewLine}{Exception}", + "rollingInterval": 4, + //0: Infinite, 1: Year, 2: Month, 3: Day, 4: Hour, 5: Minute + "retainedFileCountLimit": 72, + // 3 days + "rollOnFileSizeLimit": true + //"fileSizeLimitBytes": 20971520 // 20Mb + } + } + ], + "Properties": { + "Application": "ADASCORE" + } + }, + "AllowedHosts": "*", + "DatabaseSettings": { + //"ConnectionString": "mongodb://localhost:27017", + "ConnectionString": "mongodb://adas:adas2025@168.119.160.124:37017/adas?authSource=admin", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@localhost:27017/?authSource=admin", + //"ConnectionString": "mongodb://admin:4q*pHca@localhost:27017", + //"ConnectionString": "mongodb://admin:4q*pHca@bastionvpn.epigram:22007", + "DatabaseName": "adas" + }, + "RabbitMQSettings": { + //"ConnectionString": "host=localhost;port=8891;username=smacs;password=smacs;publisherConfirms=true;timeout=10", + //"ConnectionString": "host=localhost;port=5672;username=smacs;password=2a!nMa;publisherConfirms=true;timeout=10", + //"ConnectionString": "host=localhost;port=8888;username=smacs;password=smacs!;publisherConfirms=true;timeout=10", + "ConnectionString": "host=168.119.160.124;port=6672;username=adascore;password=ni2T7s2N6Dby7dNJ;virtualHost=/;publisherConfirms=true;timeout=10", + //"ConnectionString": "host=rabbitmqdev;port=5672;username=smacs;password=smacs!;publisherConfirms=true;timeout=10", + //PROD + // "ConnectionString": "host=10.0.20.78;port=22672;username=smacs;password=2a!nMa;publisherConfirms=true;timeout=10", + + "PatientsQueue": "patients", + "TreatmentsQueue": "treatments", + "ObservationsQueue": "observations", + "PumpsQueue": "pumps", + "RecordingQueue": "recordings", + "AppointmentsQueue": "appointments", + "MaxRetries": 1 + }, + /*"KafkaSettings": { + "ConnectionString": "10.0.20.67:9094", + "patientsTopic":"SMACS_PATIENTS", + "treatmentTopic":"SMACS_TREATMENTS", + "observationTopic":"SMACS_OBSERVATIONS", + "diagnosisTopic": "SMACS_DIAGNOSIS" + }*/ + "RecordingSettings": { + "SmacsOrApiUrl": "http://localhost:5118", + "SmacsOrApiClientId": "testadmin", + "SmacsOrApiClientSecret": "!SM4CS2024", + //"ec7e7e454a45d74a16de5a002e4d67cu", + "HttpClientTimeout": 40 + }, + "AutoRecordingSettings": { + "AutoStart": { + "BlueCode": 30, + "HemoFilterAlert": 30, + "RespirationAlert": 15 + }, + "AutoStop": { + "BlueCode": 30, + "HemoFilterAlert": 30, + "RespirationAlert": 15 + }, + "MustSendAlert": { + "BlueCode": false, + "HemoFilterAlert": false, + "RespirationAlert": false + } + }, + "AuthSettings": { + "JwtConfig": { + "Key": "534224WERW�$&/($$ghdTDDTHSSDFVADFTRGW%Y&%�B&CVW$&H&$HWETGWXZ%EGX$%c", + "Issuer": "https://www.adas.es/healthcare", + "Audience": "Adas healthcare", + "TokenValidityInMinutes": 1440, + "RefreshTokenValidityInMinutes": 10 + }, + "LoginMethods": [ + "Local", + "Ldap" + ] + }, + "ApiSettings": { + "Customize": "HUVH.UCIN", + "PointOfCareMapping": { + "Key": "PV1", + "Required": false, + "Refresh": null + }, + "ConfigObservation": { + "Key": "PV1", + "IgnoreUnknownObservation": true, + "IgnoreUnknownTreatment": false, + "Refresh": null + }, + "RetentionPolicy": "NoDelete", + "RetentionPolicyValue": 15, + "CreatePatientWithORU": false, + "CreatePatientWithSIU": false, + "CreatePatientWithoutLocation": false, + "UpdatePatientDataWithORU": false, + "UpdatePatientLocationWithORU": false, + "ArchivePatientWithOru": false, + "UpdatePatientNumberWithOru": false, + "OnArchivePatientAction": "Archive", + "ArchivePatientsWithoutObservationsSinceHours": 4800, + "SinceDischargeTimeToArchive ": 48, + "CheckInactivePatientSchedulerIntervalHours": 24, + "CheckActiveTreatmentsSchedulerIntervalMinutes": 10, + "ActivateCheckExpiredObservations": true, + "ActivateSchedulerGetProvidersObservations": true, + "ActivateSchedulerCheckNewsJob": true, + "ActivateSchedulerCheckExpiredObservation": true, + "ActivateCheckInactivePatients": false, + "ActivateCheckExpiredAlerts": false, + "SchedulerCheckExpiredAlertsIntervalSeconds": 1, + "ActivateSchedulerCheckHydricBalance": false, + "ActivateSchedulerCheckOpiateBoluses": false, + "ActivateSchedulerCheckActiveTreatmentsJob": false, + "StartRecordingWithoutPatientNumber": true, + "SinceDischargeTimeToArchive": 120, + "Shift": [ + "08:00", + "15:00", + "18:00" + ], + "FindPatientByLocationWithoutID": true, + "PersistObservationCodes": true, + //"IccaFacility": "Philips", + "IccaFacility": "obs", + "SendingFacility": [ + "LABORATORIO" + ], + "AllergiesCode": [ + "473011001" + ], + "DrainageCode": [ + "56868008" + ], + "PositionCode": [ + "386053000" + ], + "IsolationCode": [ + "302147001" + ], + "HighFrequencyVentilation": [ + "PC-HFO" + ], + "InvasiveVentilation": [ + "A/C (S.I.P.P.V. )", + "A/C (S.I.P.P.V.) + V.G.", + "PRVC", + "PC-SIMV", + "SIMV-PRVC", + "PC-AC", + "SIMV", + "SIMV-PC", + "S.I.M.V.", + "PC-CMV", + "SIPPV", + "PC" + ], + "NonInvasiveVentilation": [ + "DUOPAP", + "NCPAP" + ], + "Surgery": [ + "138875005", + "35631009", + "420419008", + "370897002", + "115959002", + "370896006" + ], + "SurgeryText": [ + "Cirugía láser of", + "Cirugía", + "Cirugía", + "sentidos", + "Cirugía", + "respirat", + "Cirugía", + "locomoto", + "Cirugía", + "renal", + "Cirugía", + "digestiv", + "Cirugía", + "neuro" + ], + "Transcutaneous": [ + "151756", + "151760" + ], + "RegionalBrainSaturation": [ + "194908", + "194909" + ], + "MedicationBolus": [ + "000523", + "001034", + "21011000140105", + "322440003", + "322761006", + "18851000140100", + "333919005", + "333920004", + "333922007" + ], + "IntravenousLinesCode": [ + "10546003" + ], + "Electroencephalogram": [ + "42803009" + ], + "MappingInterventions": [ + { + "Type": "Respiratory_Device", + "Category": "Intervention", + "Group": "not defined", + "Name": "Oxigeno (O2)/Alto flujo", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 204.0, + "FinalValue": null + } + ] + }, + { + "Type": "Respiratory_Device", + "Category": "Intervention", + "Group": "not defined", + "Name": "CPAP ", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 202.0, + "FinalValue": null + } + ] + }, + { + "Type": "Respiratory_Device", + "Category": "Intervention", + "Group": "not defined", + "Name": "Ventilaci\u00f3n mec\u00e1nica Invasiva", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 201.0, + "FinalValue": null + } + ] + }, + { + "Type": "Respiratory_Device", + "Category": "Intervention", + "Group": "not defined", + "Name": "VAFO Ventilaci\u00f3n Oscilatoria de alta frecuencia", + "Value": [ + { + "InitialValue": 4.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 200.0, + "FinalValue": null + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VV perfif\u00e9rica", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 103.1, + "FinalValue": 103.5 + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VV epicut\u00e1neo cava", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 102.1, + "FinalValue": 102.5 + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VV umbilical", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 101.1, + "FinalValue": null + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VV central", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 104.1, + "FinalValue": 104.3 + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VV PICC", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 108.1, + "FinalValue": 108.2 + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VA umbilical", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 101.2, + "FinalValue": null + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VA", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 106.1, + "FinalValue": 106.3 + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Digoxina ev", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 118.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Digoxina vo", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 1753.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Amiodarona ev", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 169.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Amiodarona vo", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 16323.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Atropina", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 186.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Esmolol", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 853.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Propranolol", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 7122.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Isoprenalina", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 283.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Flecainida ev", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 1247.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Flecainida vo", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 7055.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Adrenalina", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 281.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Noradrenalina", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 100000.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Milrinona", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 618.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Levosimendan", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 2073.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Dopamina", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 149.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Debutamina", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 1968.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Fenilefrina", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 12319.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Morfina ev", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 472.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Morfina ov", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 7081.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Fentanil", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 434.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Midazolam", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 756.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Dexmedetomidina", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 15910.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Cisatraculi", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 15066.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Propofol", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 314.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Ketamina", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 374.0, + "FinalValue": null + } + ] + }, + { + "Type": "System", + "Category": "Intervention", + "Group": "not defined", + "Name": "Hipotermia", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 100001.0, + "FinalValue": null + } + ] + }, + { + "Type": "System", + "Category": "Intervention", + "Group": "not defined", + "Name": "aEEG", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 100002.0, + "FinalValue": null + } + ] + }, + { + "Type": "System", + "Category": "Intervention", + "Group": "not defined", + "Name": "Drenaje ventricular (DVE)", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 315.1, + "FinalValue": null + }, + { + "InitialValue": 315.2, + "FinalValue": null + } + ] + }, + { + "Type": "System", + "Category": "Intervention", + "Group": "not defined", + "Name": "Drenaje toracico", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 311.0, + "FinalValue": 314.0 + } + ] + }, + { + "Type": "System", + "Category": "Intervention", + "Group": "not defined", + "Name": "Dialisis peritoneal - abdominal", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 306.0, + "FinalValue": 310.0 + } + ] + }, + { + "Type": "Newborn_Weight", + "Category": "Observation", + "Group": "not defined", + "Name": "Newborn_Weight", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": 500.0, + "valueContributed": 5 + }, + { + "InitialValue": 501.0, + "FinalValue": 1000.0, + "valueContributed": 4 + }, + { + "InitialValue": 1001.0, + "FinalValue": 1500.0, + "valueContributed": 3 + }, + { + "InitialValue": 1501.0, + "FinalValue": 2000.0, + "valueContributed": 2 + }, + { + "InitialValue": 2000.0, + "FinalValue": 2500.0, + "valueContributed": 1 + } + ], + "Codes": [ + { + "InitialValue": 10000465.0, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Cardiaca", + "Value": [ + { + "InitialValue": 4.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.1, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Digestiva", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.2, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Neurologica ", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.3, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Toracica", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.4, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "ORL/Maxil\u00b7lo", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.5, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Oftalmo", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.6, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Ortopedia", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.7, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Altres", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.8, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "No", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.9, + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "Parenteral", + "Name": "NUTRICIO PARENTERAL", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "1388", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LM", + "Name": "Llet materna", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIB586", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LM", + "Name": "Llet materna de banc", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIB000589", + "FinalValue": null + }, + { + "InitialValue": "BIB000587", + "FinalValue": null + }, + { + "InitialValue": "BIB000588", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LM", + "Name": "Llet materna a demanda", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "NO_EST2", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LM descremada", + "Name": "LM descremada", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "NO_EST3", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Formula de soja", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBCOM003", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Formula hidrolizada (Alfare)", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBCOM002", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Formula inici", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBNOR003", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Formula prematurs 14,4%", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBCOM017", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Formula prematurs 15,9%", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBCOM018", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Formula sense lactosa", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBCOM01", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Monogen", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBCOM009", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Infatrini", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBNOR001", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Infatini Peptisorb", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBNOR0013", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Neocate 13,8%", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBBAS019", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Neocate Advance 25%", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "NO_EST4", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Energivit", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBBAS021", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Bibero especial", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "ALTESP001", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "Absoluta", + "Name": "Absoluta", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "15002284", + "FinalValue": null + } + ] + } + ], + "ComplexityMappingObservation": { + "Oxigeno (O2)/Alto flujo": 1, + "CPAP ": 2, + "Ventilación mecánica Invasiva": 3, + "VAFO Ventilación Oscilatoria de alta frecuencia": 4, + "VV perfiférica": 1, + "VV epicutáneo cava": 2, + "VV umbilical": 2, + "VV central": 2, + "VV PICC": 2, + "VA umbilical": 3, + "VA": 3, + "Digoxina ev": 1, + "Digoxina vo": 1, + "Amiodarona ev": 1, + "Amiodarona vo": 1, + "Atropina": 1, + "Esmolol": 1, + "Propranolol": 1, + "Isoprenalina": 1, + "Flecainida ev": 1, + "Flecainida vo": 1, + "Adrenalina": 2, + "Noradrenalina": 2, + "Milrinona": 2, + "Levosimendan": 2, + "Dopamina": 2, + "Debutamina": 2, + "Fenilefrina": 2, + "Hipotermia": 2, + "aEEG": 2, + "Drenaje ventricular (DVE)": 2, + "Drenaje toracico": 2, + "Dialisis peritoneal - abdominal": 2, + "Cardiaca": 4, + "Digestiva": 3, + "Neurologica ": 3, + "Toracica": 3, + "ORL/Maxil·lo": 2, + "Oftalmo": 1, + "Ortopedia": 1, + "Altres": 1, + "No": 0 + }, + //Dejar siempre en ultima posicion el Ino code 84481000140102 + "Respiratory": [ + "361110005", + "416362002", + "84481000140102" + ], + "Oni": [ + "84481000140102" + ], + "BalizaUrl": "http://10.0.10.98/info.html", + "BalizaPassword": "password", + "ConfigObservationRefresh": 150, + "ConfigUnitsRequired": false, + "SchedulerCheckExpiredObservationsIntervalMinutes": 5, + "PumpExpiresSeconds": 30, + "SendPumpsZero": false, + "PumPressureAlarm": [], + "VolumetricAirOcclusionAlarm": [ + "Occlusion", + "Upstream Occlusion", + "Downstream Occlusion" + ], + "VolumetricAirInLineAlarm": [ + "Accumulated Air In Line", + "Air In Line", + "Air In Line Now" + ], + "EndContinuousInfusionAlarm": [ + "End Of Infusion", + "Near End Of Infusion", + "End Of Infusion - KVO Stop", + "End Of Infusion - KVO", + "End Of Infusion - Stop", + "VTBI Done - Stop", + "KVO Stop" + ], + "InotropicEndInfusionAlarm": [ + "End Of Infusion", + "Near End Of Infusion", + "End Of Infusion - KVO Stop", + "End Of Infusion - KVO", + "End Of Infusion - Stop", + "VTBI Done - Stop", + "KVO Stop" + ], + "InotropicMedicines": [ + "Dopamina", + "Dobutamina", + "Milrinona", + "Noradrenalina", + "Levosimendán", + "Alteplasa-rTPA", + "Esmolol", + "Milrinona", + "Nitroprusiato", + "Norad BASE" + ], + "PathUpdateFiles": [ + "C:", + "MeddisPicu_Deploy", + "configs", + "updateFiles" + ], + "IgnoreCalcObservationsOlderInMinutesThan": 5, + "CalculateExtremeEvents": true, + "ActivateExpireAlertsTimer": true, + "ExpireAlertIntervalSeconds": 1 + } +} diff --git a/adas-core/appsettings.H12O.json b/adas-core/appsettings.H12O.json new file mode 100644 index 00000000..6fcf384c --- /dev/null +++ b/adas-core/appsettings.H12O.json @@ -0,0 +1,206 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "DatabaseSettings": { + //"ConnectionString": "mongodb://admin:password@127.0.0.1:27017?authMechanism=SCRAM-SHA-256&connectTimeoutMS=5000&maxPoolSize=500&retryWrites=true&waitQueueMultiple=15", + //"ConnectionString": "mongodb://localhost:27017", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@127.0.0.1:27017/?authSource=admin", + //develop + "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@10.0.20.67:18888/?authSource=admin", + //"ConnectionString": "mongodb://smartuci:!H12o2020@10.206.250.147:21103/?authSource=smartuci", + //producción + //"ConnectionString": "mongodb://smartuci:!H12o2020@localhost:27017/?authSource=smartuci", + //"ConnectionString": "mongodb://admin:4q*pHca@10.0.20.78:22007", + "DatabaseName": "H12O_20240118" + }, + "RabbitMQSettings": { + "ConnectionString": "host=localhost;port=9999;username=smacs;password=smacs!;publisherConfirms=true;timeout=30", + "PatientsQueue": "patients", + "TreatmentsQueue": "treatments", + "ObservationsQueue": "observations", + "PumpsQueue": "pumps", + "MaxRetries": 1 + }, + "ApiSettings": { + "Customize": "H12O.UCIN", + "PointOfCareMapping": { + "Key": "PV1", + "Required": false, + "Refresh": 600 + }, + "ConfigObservation": { + "Key": "PV1", + "IgnoreUnknownObservation": true, + "IgnoreUnknownTreatment": false, + "Refresh": 60 + }, + "RecordingQueue": "recordings", + "RetentionPolicy": "NoDelete", + "RetentionPolicyValue": 0, + "CreatePatientWithORU": true, + "CreatePatientWithSIU": true, + "CreatePatientWithoutLocation": false, + "UpdatePatientDataWithORU": true, + "UpdatePatientLocationWithORU": false, + "ArchivePatientWithOru": false, + "UpdatePatientNumberWithOru": false, + "OnArchivePatientAction": "Archive", + "ArchivePatientsWihtoutObservationsSinceDays": 10, + "CheckInactivePatientSchedulerIntervalHours": 24, + "CheckActiveTreatmentsSchedulerIntervalMinutes": 2, + "SinceDischargeTimeToArchive": 48, + "Shift": [ + "08:00", + "15:00", + "22:00" + ], + "FindPatientByLocationWithoutID": false, + "PersistObservationCodes": true, + "IccaFacility": "Philips.CIS.CVC", + "SendingFacility": [ + "LABORATORIO" + ], + "AllergiesCode": [ + "473011001" + ], + "DrainageCode": [ + "56868008" + ], + "PositionCode": [ + "386053000" + ], + "IsolationCode": [ + "302147001" + ], + "HighFrequencyVentilation": [ + "PC-HFO", + "Alto Flujo" + ], + "InvasiveVentilation": [ + "A/C (S.I.P.P.V. )", + "A/C (S.I.P.P.V.) + V.G.", + "PRVC", + "PC-SIMV", + "SIMV-PRVC", + "PC-AC", + "SIMV", + "SIMV-PC", + "S.I.M.V.", + "PC-CMV", + "SIPPV", + "PC" + ], + "NonInvasiveVentilation": [ + "DUOPAP", + "NCPAP", + "CPAP", + "V.N.I. Ciclada" + ], + "Surgery": [ + "138875005", + "370896006", + "370896006", + "35631009", + "420419008", + "370897002", + "115959002", + "370896006" + ], + "SurgeryText": [ + "Cirugía láser of", + "Cirugía", + "Cirugía sentidos", + "Cirugía respirat", + "Cirugía locomoto", + "Cirugía renal", + "Cirugía digestiv", + "Cirugía neuro" + ], + "Transcutaneous": [ + "151756", + "151760" + ], + "RegionalBrainSaturation": [ + "194908", + "194909" + ], + "MedicationBolus": [ + "000523", + "001034", + "21011000140105", + "322440003", + "322761006", + "18851000140100", + "333919005", + "333920004", + "333922007", + "18791000140100", + "18801000140104", + "18811000140101" + ], + "IntravenousLinesCode": [ + "10546003" + ], + "Electroencephalogram": [ + "42803009", + "408987002" + ], + "Ecmo": [ + "705923009" + ], + "NotesIndicatingMedication": [ + "SUEROS Y HEMODER,Perfusiones,Medicación,NPT" + ], + //Dejar siempre en ultima posicion el Ino code 84481000140102 + "Respiratory": [ + "361110005", + "416362002", + "84481000140102" + ], + "Oni": [ + "84481000140102", + "416362002" + ], + "BalizaUrl": "http://10.0.10.35/info.html", + "BalizaPassword": "password", + "ConfigObservationRefresh": 120, + "ConfigUnitsRequired": true, + "ActivateSchedulerCheckExpiredObservation": false, + "schedulerCheckExpiredObservationsIntervalMinutes": 5, + "diagnosisSystem": "Snomed-CT", + "DiagnosisCode": [ + "55607006" + ], + "PumpExpiresSeconds": 120, + "SendPumpsZero": false, + "PumPressureAlarm": [], + "VolumetricAirOcclusionAlarm": [ + "Accumulated Air In Line", + "Air In Line", + "Occlusion", + "Upstream Occlusion" + ], + "EndContinuousInfusionAlarm": [ + "End Of Infusion", + "Near End Of Infusion" + ], + "InotropicEndInfusionAlarm": [ + "Near End Of Infusion", + "Occlusion", + "Air In Line" + ], + "inotropicMedicines": [ + "Dopamina", + "Dobutamina", + "Milrinona", + "Noradrenalina", + "Levosimendán" + ], + "ActivateCheckInactivePatients": false + } +} diff --git a/adas-core/appsettings.HGMM.json b/adas-core/appsettings.HGMM.json new file mode 100644 index 00000000..bae587ec --- /dev/null +++ b/adas-core/appsettings.HGMM.json @@ -0,0 +1,273 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Serilog": { + "Using": [ + "Serilog.Sinks.Console", + "Serilog.Sinks.File", + "Serilog.Exceptions", + "Serilog.Expressions", + "Serilog.Enrichers.Thread" + ], + "Enrich": [ + "WithExceptionDetails", + "WithDynamicProperty", + "WithThreadId" + ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "adas_core": "Debug", + "EasyNetQ": "Information", + "Quartz": "Information", + "Microsoft": "Warning", + "System": "Error" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "formatter": { + "type": "Serilog.Templates.ExpressionTemplate, Serilog.Expressions", + "template": "{@t:yyyy-MM-dd HH:mm:ss} [{@l:u3}]{#if SourceContext is not null} ({SourceContext,35}){#end}: {@m}\n{@x}" + }, + "theme": "Serilog.Sinks.SystemConsole.Themes.SystemConsoleTheme::Grayscale, Serilog.Sinks.Console" + } + }, + { + "Name": "File", + "Args": { + "path": "Logs/.log", + "formatter": { + "type": "Serilog.Templates.ExpressionTemplate, Serilog.Expressions", + "template": "{@t:yyyy-MM-dd HH:mm:ss} [{@l:u3}]{#if SourceContext is not null} ({#if Length(SourceContext) > 35}{Substring(SourceContext, Length(SourceContext) - 35)}{#else}{SourceContext,35}{#end}){#end}: {@m}{#if TraceId is not null} TraceId: {TraceId}{#end}{#if TraceMessageId is not null} TraceMessageId: {TraceMessageId}{#end} TraceIdentifier: {TraceIdentifier} Thread: {ThreadId} \n{@x}" + }, + //"outputTemplate": "[{Timestamp:o}] [{Level:u}] [{SourceContext:l}] {Message}{NewLine}{Exception}", + "rollingInterval": 4, + //0: Infinite, 1: Year, 2: Month, 3: Day, 4: Hour, 5: Minute + "retainedFileCountLimit": 72, + // 3 days + "rollOnFileSizeLimit": true + //"fileSizeLimitBytes": 20971520 // 20Mb + } + } + ], + "Properties": { + "Application": "ADASCORE" + } + }, + "AllowedHosts": "*", + "DatabaseSettings": { + //"ConnectionString": "mongodb://admin:password@127.0.0.1:27017?authMechanism=SCRAM-SHA-256&connectTimeoutMS=5000&maxPoolSize=500&retryWrites=true&waitQueueMultiple=15", + //"ConnectionString": "mongodb://smacs:!HRYCM21@10.0.20.78:20907", + //develop + "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@10.0.20.67:18888/?authSource=admin", + //producción + //"ConnectionString": "mongodb://admin:4q*pHca@10.0.20.78:22007", + //"ConnectionString": "mongodb://localhost:27017", + "DatabaseName": "hgugmm" + }, + "RabbitMQSettings": { + "ConnectionString": "host=localhost;port=9999;username=smacs;password=smacs!;publisherConfirms=true;timeout=30", + //"ConnectionString": "host=localhost;port=18672;username=smacs;password=smacs;publisherConfirms=true;timeout=10", + "PatientsQueue": "patients", + "TreatmentsQueue": "treatments", + "ObservationsQueue": "observations", + "PumpsQueue": "pumps", + "MaxRetries": 1 + }, + //"ProvidersSettings": [ + // { + // "Name": "Adas", + // "Url": "http://185.208.206.12:8080/" + // } + //], + + "ApiSettings": { + "Customize": "HGMM", + "PointOfCareMapping": { + "Key": "PV1", + "Required": false, + "Refresh": 600 + }, + "ConfigObservation": { + "Key": "PV1", + "IgnoreUnknownObservation": true, + "IgnoreUnknownTreatment": false, + "Refresh": 60 + }, + "RetentionPolicy": "NoDelete", + "RetentionPolicyValue": 15, + "CreatePatientWithORU": true, + "CreatePatientWithSIU": true, + "CreatePatientWithoutLocation": false, + "CreatePatientWithADT_A08": false, + "UpdatePatientDataWithORU": true, + "UpdatePatientLocationWithORU": false, + "ArchivePatientWithOru": false, + "UpdatePatientNumberWithOru": false, + "OnArchivePatientAction": "Archive", + "ArchivePatientsWithoutObservationsSinceHours": 48, + "SinceDischargeTimeToArchive ": 48, + "CheckInactivePatientSchedulerIntervalHours": 24, + "CheckActiveTreatmentsSchedulerIntervalMinutes": 10, + "ActivateSchedulerGetProvidersObservations": false, + "SinceDischargeTimeToArchive": 48, + "Shift": [ + "08:00", + "15:00", + "18:00" + ], + "FindPatientByLocationWithoutID": false, + "PersistObservationCodes": true, + "IccaFacility": "Philips", + "SendingFacility": [ + "LABORATORIO" + ], + "AllergiesCode": [ + "473011001" + ], + "DrainageCode": [ + "56868008" + ], + "PositionCode": [ + "386053000" + ], + "IsolationCode": [ + "302147001" + ], + "HighFrequencyVentilation": [ + "PC-HFO" + ], + "InvasiveVentilation": [ + "A/C (S.I.P.P.V. )", + "A/C (S.I.P.P.V.) + V.G.", + "PRVC", + "PC-SIMV", + "SIMV-PRVC", + "PC-AC", + "SIMV", + "SIMV-PC", + "S.I.M.V.", + "PC-CMV", + "SIPPV", + "PC" + ], + "NonInvasiveVentilation": [ + "DUOPAP", + "NCPAP" + ], + "Surgery": [ + "138875005", + "35631009", + "420419008", + "370897002", + "115959002", + "370896006" + ], + "SurgeryText": [ + "Cirugía láser of", + "Cirugía", + "Cirugía sentidos", + "Cirugía respirat", + "Cirugía locomoto", + "Cirugía renal", + "Cirugía digestiv", + "Cirugía neuro" + ], + "Transcutaneous": [ + "151756", + "151760" + ], + "RegionalBrainSaturation": [ + "194908", + "194909" + ], + "MedicationBolus": [ + "000523", + "001034", + "21011000140105", + "322440003", + "322761006", + "18851000140100", + "333919005", + "333920004", + "333922007" + ], + "IntravenousLinesCode": [ + "10546003" + ], + "Electroencephalogram": [ + "42803009" + ], + //Dejar siempre en ultima posicion el Ino code 84481000140102 + "Respiratory": [ + "361110005", + "416362002", + "84481000140102" + ], + "Oni": "84481000140102", + "BalizaUrl": "http://10.0.10.35/info.html", + "BalizaPassword": "password", + "TimeToSendGroupedObs": 20, + "ActivateSchedulerCheckNewsJob": true, + "ConfigObservationRefresh": 150, + "ConfigUnitsRequired": true, + "ActivateSchedulerCheckExpiredObservation": true, + "SchedulerCheckExpiredObservationsIntervalMinutes": 5, + "PumpExpiresSeconds": 100, + "SendPumpsZero": false, + "PumPressureAlarm": [ + "Downstream Occlusion" + ], + "VolumetricAirOcclusionAlarm": [ + "Occlusion", + "Upstream Occlusion" + ], + "VolumetricAirInLineAlarm": [ + "Accumulated Air In Line", + "Air In Line", + "Air In Line Now" + ], + "EndContinuousInfusionAlarm": [ + "End Of Infusion", + "Near End Of Infusion", + "End Of Infusion - KVO Stop", + "End Of Infusion - KVO", + "End Of Infusion - Stop" + ], + "InotropicEndInfusionAlarm": [ + "End Of Infusion", + "Near End Of Infusion", + "End Of Infusion - KVO Stop", + "End Of Infusion - KVO", + "End Of Infusion - Stop" + ], + "InotropicMedicines": [ + "Dopamina", + "Dobutamina", + "Milrinona", + "Noradrenalina", + "Levosimendán", + "Alteplasa-rTPA", + "Esmolol", + "Milrinona", + "Nitroprusiato", + "Norad BASE" + ], + "ActivateCheckInactivePatients": true, + "PathUpdateFiles": [ + "C:", + "MeddisPicu_Deploy", + "configs", + "updateFiles" + ], + "CalculateExtremeEvents": true, + "PushPatientWithORU": true + } +} diff --git a/adas-core/appsettings.HPAZ.json b/adas-core/appsettings.HPAZ.json new file mode 100644 index 00000000..a92c604e --- /dev/null +++ b/adas-core/appsettings.HPAZ.json @@ -0,0 +1,327 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Serilog": { + "Using": [ + "Serilog.Sinks.Console", + "Serilog.Sinks.File", + "Serilog.Exceptions", + "Serilog.Expressions", + "Serilog.Enrichers.Thread" + ], + "Enrich": [ + "WithExceptionDetails", + "WithDynamicProperty", + "WithThreadId" + ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "adas_core": "Debug", + "EasyNetQ": "Information", + "Quartz": "Information", + "Microsoft": "Warning", + "System": "Error" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "formatter": { + "type": "Serilog.Templates.ExpressionTemplate, Serilog.Expressions", + "template": "{@t:yyyy-MM-dd HH:mm:ss} [{@l:u3}]{#if SourceContext is not null} ({SourceContext,35}){#end}: {@m}\n{@x}" + }, + "theme": "Serilog.Sinks.SystemConsole.Themes.SystemConsoleTheme::Grayscale, Serilog.Sinks.Console" + } + }, + { + "Name": "File", + "Args": { + "path": "Logs/.log", + "formatter": { + "type": "Serilog.Templates.ExpressionTemplate, Serilog.Expressions", + "template": "{@t:yyyy-MM-dd HH:mm:ss} [{@l:u3}]{#if SourceContext is not null} ({#if Length(SourceContext) > 35}{Substring(SourceContext, Length(SourceContext) - 35)}{#else}{SourceContext,35}{#end}){#end}: {@m}{#if TraceId is not null} TraceId: {TraceId}{#end}{#if TraceMessageId is not null} TraceMessageId: {TraceMessageId}{#end} TraceIdentifier: {TraceIdentifier} Thread: {ThreadId} \n{@x}" + }, + //"outputTemplate": "[{Timestamp:o}] [{Level:u}] [{SourceContext:l}] {Message}{NewLine}{Exception}", + "rollingInterval": 4, + //0: Infinite, 1: Year, 2: Month, 3: Day, 4: Hour, 5: Minute + "retainedFileCountLimit": 72, + // 3 days + "rollOnFileSizeLimit": true + //"fileSizeLimitBytes": 20971520 // 20Mb + } + } + ], + "Properties": { + "Application": "ADASCORE" + } + }, + "AllowedHosts": "*", + "DatabaseSettings": { + //"ConnectionString": "mongodb://localhost:27017", + "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@10.0.20.67:18888/?authSource=admin", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@localhost:27017/?authSource=admin", + //"ConnectionString": "mongodb://admin:4q*pHca@localhost:27017", + //"ConnectionString": "mongodb://admin:4q*pHca@bastionvpn.epigram:22007", + //"DatabaseName": "paz_preprod" + "DatabaseName": "paz_v3" + }, + "DatabaseConfigurationAudit": { + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@julianrojas.xyz:27017/?authSource=admin", + "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@10.0.20.67:18888/?authSource=admin", + "DatabaseName": "Audit_paz" + }, + "RabbitMQSettings": { + //"ConnectionString": "host=localhost;port=8891;username=smacs;password=smacs;publisherConfirms=true;timeout=10", + //"ConnectionString": "host=localhost;port=5672;username=smacs;password=2a!nMa;publisherConfirms=true;timeout=10", + //"ConnectionString": "host=localhost;port=8888;username=smacs;password=smacs!;publisherConfirms=true;timeout=10", + //"ConnectionString": "host=localhost;port=9999;username=smacs;password=smacs!;publisherConfirms=true;timeout=10", + //"ConnectionString": "host=hyde-runner2.epigram/;port=5672;username=smacs;password=smacs!;publisherConfirms=true;timeout=10", + //"ConnectionString": "host=10.0.20.241;port=8889;username=smacs;password=smacs!;publisherConfirms=true;timeout=10", + + //PROD + // "ConnectionString": "host=10.0.20.78;port=22672;username=smacs;password=2a!nMa;publisherConfirms=true;timeout=10", + + //"PatientsQueue": "patients", + //"TreatmentsQueue": "treatments", + //"ObservationsQueue": "observations", + //"PumpsQueue": "pumps", + //"RecordingQueue": "recordings", + //"AppointmentsQueue": "appointments", + //"MaxRetries": 1 + }, + "RecordingSettings": { + "SmacsOrApiUrl": "http://localhost:5118", + "SmacsOrApiClientId": "testadmin", + "SmacsOrApiClientSecret": "!SM4CS2024", + //"ec7e7e454a45d74a16de5a002e4d67cu", + "HttpClientTimeout": 40 + }, + "AutoRecordingSettings": { + "AutoStart": { + "BlueCode": 30, + "HemoFilterAlert": 30, + "RespirationAlert": 15 + }, + "AutoStop": { + "BlueCode": 30, + "HemoFilterAlert": 30, + "RespirationAlert": 15 + }, + "MustSendAlert": { + "BlueCode": false, + "HemoFilterAlert": false, + "RespirationAlert": false + } + }, + "RelaySettings": { + "Cache": false, + "RefreshTime": 0 + }, + "ApiSettings": { + "Customize": "HPAZ", + "PointOfCareMapping": { + "Key": "PV1", + "Required": false, + "Refresh": null + }, + "ConfigObservation": { + "Key": "PV1", + "IgnoreUnknownObservation": true, + "IgnoreUnknownTreatment": false, + "Refresh": null + }, + "RetentionPolicy": "NoDelete", + "RetentionPolicyValue": 15, + "CreatePatientWithORU": true, + "CreatePatientWithSIU": true, + "CreatePatientWithoutLocation": false, + "UpdatePatientDataWithORU": false, + "UpdatePatientLocationWithORU": false, + "ArchivePatientWithOru": false, + "UpdatePatientNumberWithOru": false, + "OnArchivePatientAction": "Archive", + "ArchivePatientsWithoutObservationsSinceHours": 48000, + "SinceDischargeTimeToArchive ": 48000, + "CheckInactivePatientSchedulerIntervalHours": 24, + "CheckActiveTreatmentsSchedulerIntervalMinutes": 10, + "ActivateCheckExpiredObservations": false, + "ActivateSchedulerGetProvidersObservations": false, + "ActivateSchedulerCheckNewsJob": false, + "ActivateSchedulerCheckExpiredObservation": false, + "ActivateCheckInactivePatients": false, + "ActivateCheckExpiredAlerts": false, + "ActivateSchedulerCheckHydricBalance": false, + "ActivateSchedulerCheckOpiateBoluses": false, + "ActivateSchedulerCheckActiveTreatmentsJob": false, + "StartRecordingWithoutPatientNumber": true, + "SinceDischargeTimeToArchive": 120, + "Shift": [ + "08:00", + "15:00", + "18:00" + ], + "FindPatientByLocationWithoutID": false, + "PersistObservationCodes": true, + "IccaFacility": "Philips", + "SendingFacility": [ + "LABORATORIO" + ], + "AllergiesCode": [ + "473011001" + ], + "DrainageCode": [ + "56868008" + ], + "PositionCode": [ + "386053000" + ], + "IsolationCode": [ + "302147001" + ], + "HighFrequencyVentilation": [ + "PC-HFO" + ], + "InvasiveVentilation": [ + "A/C (S.I.P.P.V. )", + "A/C (S.I.P.P.V.) + V.G.", + "PRVC", + "PC-SIMV", + "SIMV-PRVC", + "PC-AC", + "SIMV", + "SIMV-PC", + "S.I.M.V.", + "PC-CMV", + "SIPPV", + "PC" + ], + "NonInvasiveVentilation": [ + "DUOPAP", + "NCPAP" + ], + "Surgery": [ + "138875005", + "35631009", + "420419008", + "370897002", + "115959002", + "370896006" + ], + "SurgeryText": [ + "Cirugía láser of", + "Cirugía", + "Cirugía", + "sentidos", + "Cirugía", + "respirat", + "Cirugía", + "locomoto", + "Cirugía", + "renal", + "Cirugía", + "digestiv", + "Cirugía", + "neuro" + ], + "Transcutaneous": [ + "151756", + "151760" + ], + "RegionalBrainSaturation": [ + "194908", + "194909" + ], + "MedicationBolus": [ + "000523", + "001034", + "21011000140105", + "322440003", + "322761006", + "18851000140100", + "333919005", + "333920004", + "333922007" + ], + "IntravenousLinesCode": [ + "10546003" + ], + "Electroencephalogram": [ + "42803009" + ], + //Dejar siempre en ultima posicion el Ino code 84481000140102 + "Respiratory": [ + "361110005", + "416362002", + "84481000140102" + ], + "Oni": [ + "84481000140102" + ], + "BalizaUrl": "http://10.0.10.98/info.html", + "BalizaPassword": "password", + "ConfigObservationRefresh": 150, + "ConfigUnitsRequired": true, + "SchedulerCheckExpiredObservationsIntervalMinutes": 5, + "PumpExpiresSeconds": 30, + "SendPumpsZero": false, + "PumPressureAlarm": [ + "Downstream Occlusion" + ], + "VolumetricAirOcclusionAlarm": [ + "Occlusion", + "Upstream Occlusion" + ], + "VolumetricAirInLineAlarm": [ + "Accumulated Air In Line", + "Air In Line", + "Air In Line Now" + ], + "EndContinuousInfusionAlarm": [ + "End Of Infusion", + "Near End Of Infusion", + "End Of Infusion - KVO Stop", + "End Of Infusion - KVO", + "End Of Infusion - Stop", + "VTBI Done - Stop", + "KVO Stop" + ], + "InotropicEndInfusionAlarm": [ + "End Of Infusion", + "Near End Of Infusion", + "End Of Infusion - KVO Stop", + "End Of Infusion - KVO", + "End Of Infusion - Stop", + "VTBI Done - Stop", + "KVO Stop" + ], + "InotropicMedicines": [ + "Dopamina", + "Dobutamina", + "Milrinona", + "Noradrenalina", + "Levosimendán", + "Alteplasa-rTPA", + "Esmolol", + "Milrinona", + "Nitroprusiato", + "Norad BASE" + ], + "PathUpdateFiles": [ + "C:", + "MeddisPicu_Deploy", + "configs", + "updateFiles" + ], + "IgnoreCalcObservationsOlderInMinutesThan": 5, + "CalculateExtremeEvents": true, + "SchedulerCheckExpiredAlertsIntervalSeconds": 1, + "ActivateExpireAlertsTimer": true, + "ExpireAlertIntervalSeconds": 1 + } +} diff --git a/adas-core/appsettings.HPAZBastion.json b/adas-core/appsettings.HPAZBastion.json new file mode 100644 index 00000000..2fede8ba --- /dev/null +++ b/adas-core/appsettings.HPAZBastion.json @@ -0,0 +1,147 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "DatabaseSettings": { + //"ConnectionString": "mongodb://localhost:27017", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@10.0.20.67:18888/?authSource=admin", + "ConnectionString": "mongodb://admin:4q*pHca@10.0.20.78:22007", + "DatabaseName": "paz" + }, + "RabbitMQSettings": { + //"ConnectionString": "host=localhost;port=5672;username=smacs;password=smacs!;publisherConfirms=true;timeout=10", + "ConnectionString": "host=10.0.20.78;port=22672;username=smacs;password=2a!nMa;publisherConfirms=true;timeout=10", + //"PatientsQueue": "patients", + "TreatmentsQueue": "treatments", + //"ObservationsQueue": "observations", + "PumpsQueue": "pumps", + "RecordingQueue": "recordings", + "AppointmentsQueue": "appointments", + "MaxRetries": 1 + }, + "ApiSettings": { + "Customize": "H12O", + "PointOfCareMapping": { + "Key": "PV1", + "Required": false, + "Refresh": 600 + }, + "ConfigObservation": { + "Key": "PV1", + "IgnoreUnknownObservation": true, + "IgnoreUnknownTreatment": false, + "Refresh": 60 + }, + "RetentionPolicy": "NoDelete", + "RetentionPolicyValue": 15, + "CreatePatientWithORU": true, + "CreatePatientWithSIU": true, + "CreatePatientWithoutLocation": false, + "UpdatePatientDataWithORU": false, + "UpdatePatientLocationWithORU": false, + "ArchivePatientWithOru": false, + "UpdatePatientNumberWithOru": false, + "OnArchivePatientAction": "Archive", + "ArchivePatientsWithoutObservationsSinceHours": 10, + "CheckInactivePatientSchedulerIntervalHours": 24, + "CheckActiveTreatmentsSchedulerIntervalMinutes": 10, + "SinceDischargeTimeToArchive": 48, + "Shift": [ + "08:00", + "15:00", + "18:00" + ], + "FindPatientByLocationWithoutID": false, + "PersistObservationCodes": true, + "IccaFacility": "Philips", + "SendingFacility": "LABORATORIO", + "AllergiesCode": "473011001", + "DrainageCode": "56868008", + "PositionCode": "386053000", + "IsolationCode": "302147001", + "HighFrequencyVentilation": [ + "PC-HFO" + ], + "InvasiveVentilation": [ + "A/C (S.I.P.P.V. )", + "A/C (S.I.P.P.V.) + V.G.", + "PRVC", + "PC-SIMV", + "SIMV-PRVC", + "PC-AC", + "SIMV", + "SIMV-PC", + "S.I.M.V.", + "PC-CMV", + "SIPPV", + "PC" + ], + "NonInvasiveVentilation": [ + "DUOPAP", + "NCPAP" + ], + "Surgery": [ + "138875005", + "35631009", + "420419008", + "370897002", + "115959002", + "370896006" + ], + "SurgeryText": [ + "Cirugía láser of", + "Cirugía", + "Cirugía", + "sentidos", + "Cirugía", + "respirat", + "Cirugía", + "locomoto", + "Cirugía", + "renal", + "Cirugía", + "digestiv", + "Cirugía", + "neuro" + ], + "Transcutaneous": [ + "151756", + "151760" + ], + "RegionalBrainSaturation": [ + "194908", + "194909" + ], + "MedicationBolus": [ + "000523", + "001034", + "21011000140105", + "322440003", + "322761006", + "18851000140100", + "333919005", + "333920004", + "333922007" + ], + "IntravenousLinesCode": "10546003", + "Electroencephalogram": "42803009", + //Dejar siempre en ultima posicion el Ino code 84481000140102 + "Respiratory": [ + "361110005", + "416362002", + "84481000140102" + ], + "Oni": "84481000140102", + "BalizaUrl": "http://10.0.10.35/info.html", + "BalizaPassword": "password", + "ConfigObservationRefresh": 120, + "ConfigUnitsRequired": false, + "ActivateSchedulerCheckExpiredObservation": true, + "PumpExpiresSeconds": 120, + "SendPumpsZero": false + } +} diff --git a/adas-core/appsettings.HRYCM.json b/adas-core/appsettings.HRYCM.json new file mode 100644 index 00000000..194d52ce --- /dev/null +++ b/adas-core/appsettings.HRYCM.json @@ -0,0 +1,226 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Serilog": { + "Using": [ + "Serilog.Sinks.Console", + "Serilog.Sinks.File", + "Serilog.Exceptions", + "Serilog.Expressions", + "Serilog.Enrichers.Thread" + ], + "Enrich": [ + "WithExceptionDetails", + "WithDynamicProperty", + "WithThreadId" + ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "adas_core": "Debug", + "EasyNetQ": "Information", + "Quartz": "Information", + "Microsoft": "Warning", + "System": "Error" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "formatter": { + "type": "Serilog.Templates.ExpressionTemplate, Serilog.Expressions", + "template": "{@t:yyyy-MM-dd HH:mm:ss} [{@l:u3}]{#if SourceContext is not null} ({SourceContext,35}){#end}: {@m}\n{@x}" + }, + "theme": "Serilog.Sinks.SystemConsole.Themes.SystemConsoleTheme::Grayscale, Serilog.Sinks.Console" + } + }, + { + "Name": "File", + "Args": { + "path": "Logs/.log", + "formatter": { + "type": "Serilog.Templates.ExpressionTemplate, Serilog.Expressions", + "template": "{@t:yyyy-MM-dd HH:mm:ss} [{@l:u3}]{#if SourceContext is not null} ({#if Length(SourceContext) > 35}{Substring(SourceContext, Length(SourceContext) - 35)}{#else}{SourceContext,35}{#end}){#end}: {@m}{#if TraceId is not null} TraceId: {TraceId}{#end}{#if TraceMessageId is not null} TraceMessageId: {TraceMessageId}{#end} TraceIdentifier: {TraceIdentifier} Thread: {ThreadId} \n{@x}" + }, + //"outputTemplate": "[{Timestamp:o}] [{Level:u}] [{SourceContext:l}] {Message}{NewLine}{Exception}", + "rollingInterval": 4, + //0: Infinite, 1: Year, 2: Month, 3: Day, 4: Hour, 5: Minute + "retainedFileCountLimit": 72, + // 3 days + "rollOnFileSizeLimit": true + //"fileSizeLimitBytes": 20971520 // 20Mb + } + } + ], + "Properties": { + "Application": "ADASCORE" + } + }, + "AllowedHosts": "*", + "RedisSettings": { + "ConnectionString": "localhost:6379", + "EnableGroupedObservationsCache": true, + "ExpireKeyInSeconds": 7200 + // 2h + }, + "CacheSettings": { + "IsEnabled": true, + "Service": "Redis", + "GroupedObservations": { + "IsEnabled": true + } + }, + "DatabaseSettings": { + //Producción + //"ConnectionString": "mongodb://smacs:!HRYCM21@10.0.20.78:20907/?maxConnectionPoolSize=1000&waitQueueSize=100000&connectTimeoutMS=10000", + //"ConnectionString": "mongodb://smacs:!HRYCM21@localhost:27017", + //develop + "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@10.0.20.67:18888/?maxConnectionPoolSize=1000&waitQueueSize=100000&connectTimeoutMS=10000", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@10.0.20.67:18888/?authSource=admin", + //"DatabaseName": "smartuci" + "DatabaseName": "MeddisPlanUciPanel" + }, + "RabbitMQSettings": { + "ConnectionString": "host=localhost;port=5672;username=smacs;password=smacs;publisherConfirms=true;timeout=30", + "PatientsQueue": "patients", + "TreatmentsQueue": "treatments", + "ObservationsQueue": "observations", + "PumpsQueue": "pumps", + "RecordingQueue": "recordings", + "AppointmentsQueue": "appointments", + "MaxRetries": 1 + }, + "Ldap": { + "Enabled": true, + "Server": "bastionvpn.epigram", + "Port": 21389, + "SearchBase": "OU=CSCM,DC=salud,DC=madrid,DC=org", + "PositionProperty": "CAMSanidadPuesto", + "UserDomainName": "SALUD", + "AuthoritiesMap": [ + { + "Group": "CN=gHDOC_T_Philips,OU=Hospital Doce de Octubre,OU=Hospitales,OU=CSCM,DC=salud,DC=madrid,DC=org", + "Rol": "View", + "Actions": [ + "View" + ], + "DisplayId": "65c114566d5d1ac02b4f2478" + } + ], + "WhiteList": [ + { + "Name": "Fernando Igual Novella", + "Username": "53091058C", + "Rol": "Edit", + "Actions": [ + "View", + "Write" + ], + "DisplayId": "65c114566d5d1ac02b4f2478" + } + ] + }, + "AuthSettings": { + "JwtConfig": { + "Key": "534224WERW�$&/($$ghdTDDTHSSDFVADFTRGW%Y&%�B&CVW$&H&$HWETGWXZ%EGX$%c", + "Issuer": "https://www.issuer.es/healthcare", + "Audience": "https://adas.health", + "ValidateIssuer": false, + "TokenValidityInMinutes": 1200, + "RefreshTokenValidityInMinutes": 14400 + }, + "LoginMethods": [ + "Local", + "Ldap" + ] + }, + "ApiSettings": { + "Customize": "HRYC", + "PointOfCareMapping": { + "Key": "PV1", + "Required": true, + "Refresh": 600 + }, + "ConfigObservation": { + "Key": "PV1", + "IgnoreUnknownObservation": true, + "IgnoreUnknownTreatment": false, + "Refresh": 60 + }, + "RetentionPolicy": "NoDelete", + "RetentionPolicyValue": 0, + "CreatePatientWithORU": true, + "CreatePatientWithSIU": true, + "CreatePatientWithoutLocation": false, + "CreatePatientWithADT_A08": true, + "UpdatePatientDataWithORU": false, + "UpdatePatientLocationWithORU": false, + "ArchivePatientWithOru": false, + "UpdatePatientNumberWithOru": false, + "OnArchivePatientAction": "Archive", + "ArchivePatientsWithoutObservationsSinceHours": 120000000, + "CheckInactivePatientSchedulerIntervalHours": 240000000, + "CheckActiveTreatmentsSchedulerIntervalMinutes": 10, + "ActivateSchedulerCheckNewsJob": false, + "SinceDischargeTimeToArchive": 48, + //"Shift": [ "08:00", "15:00", "18:00" ], + "FindPatientByLocationWithoutID": false, + "PersistObservationCodes": true, + "IccaFacility": "Philips.CIS.CVC", + "SendingFacility": [ + "LABORATORIO" + ], + "AllergiesCode": [ + "473011001" + ], + "DrainageCode": [ + "56868008" + ], + "PositionCode": [ + "386053000" + ], + "IsolationCode": [ + "302147001" + ], + "HighFrequencyVentilation": [ + "HNF", + "HIGH FLOW" + ], + "InvasiveVentilation": [ + "67303831", + "67303912" + ], + "NonInvasiveVentilation": [ + "BIPAP", + "CPAP" + ], + //"Surgery": [ "138875005", "35631009", "420419008", "370897002", "115959002", "370896006" ], + //"SurgeryText": [ "Cirugía láser of", "Cirugía", "Cirugía", "sentidos", "Cirugía", "respirat", "Cirugía", "locomoto", "Cirugía", "renal", "Cirugía", "digestiv", "Cirugía", "neuro" ], + //"Transcutaneous": [ "151756", "151760" ], + //"RegionalBrainSaturation": [ "194908", "194909" ], + //"MedicationBolus": [ "000523", "001034", "21011000140105", "322440003", "322761006", "18851000140100", "333919005", "333920004", "333922007" ], + //"IntravenousLinesCode": "10546003", + //"Electroencephalogram": "42803009", + //Dejar siempre en ultima posicion el Ino code 84481000140102 + //"Respiratory": [ "361110005", "416362002", "84481000140102" ], + //"Oni": "84481000140102", + //"BalizaUrl": "http://10.0.10.35/info.html", + //"BalizaPassword": "password", + "ConfigObservationRefresh": 60, + "ConfigUnitsRequired": false, + "ActivateSchedulerCheckExpiredObservation": false, + "PathUpdateFiles": [ + "C:", + "Standard_Files", + "UploadFromServer" + ], + "ActivateCheckInactivePatients": true + + //"PumpExpiresSeconds": 120 + } +} diff --git a/adas-core/appsettings.HUVH-UCIA.json b/adas-core/appsettings.HUVH-UCIA.json new file mode 100644 index 00000000..b5c5198a --- /dev/null +++ b/adas-core/appsettings.HUVH-UCIA.json @@ -0,0 +1,1347 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Serilog": { + "Using": [ + "Serilog.Sinks.Console", + "Serilog.Sinks.File", + "Serilog.Exceptions", + "Serilog.Expressions", + "Serilog.Enrichers.Thread" + ], + "Enrich": [ + "WithExceptionDetails", + "WithDynamicProperty", + "WithThreadId" + ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "smacs_uci_api": "Debug", + "EasyNetQ": "Information", + "Quartz": "Information", + "Microsoft": "Warning", + "System": "Error" + } + }, + "WriteTo": [ + { + "Name": "Console" + }, + { + "Name": "File", + "Args": { + "path": "Logs/.log", + "outputTemplate": "[{Timestamp:o}] [{Level:u3}] [{SourceContext:l}] {Message}{NewLine}{Exception}", + "rollingInterval": 4, + //0: Infinite, 1: Year, 2: Month, 3: Day, 4: Hour, 5: Minute + "retainedFileCountLimit": 72, + // 3 days + "rollOnFileSizeLimit": true, + "fileSizeLimitBytes": 20971520 + // 20Mb + } + } + ], + "Properties": { + "Application": "SmacsUCIAPI" + } + }, + "AllowedHosts": "*", + "DatabaseSettings": { + "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@10.0.20.67:18888/?authSource=admin", + //"ConnectionString": "mongodb://localhost:27017", + "DatabaseName": "vdh_ucia_v3_nueva" + //"ConnectionString": "mongodb://smacsvdh:dE.s2tg3Upz@172.17.53.91:27017/?authSource=admin", + }, + "DatabaseConfigurationAudit": { + "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@julianrojas.xyz:27017/?authSource=admin", + "DatabaseName": "AuditMeddis_vdha" + }, + //"RabbitMQSettings": { + // "ConnectionString": "host=10.0.20.241;port=8889;username=smacs;password=smacs!;publisherConfirms=true;timeout=30", + // //"ConnectionString": "host=localhost;port=9999;username=smacs;password=smacs!;publisherConfirms=true;timeout=30", + // //"ConnectionString": "host=127.0.0.1;port=5672;username=mirth;password=lD2dvc.df4-;publisherConfirms=true;timeout=30", + // //"ConnectionString": "amqp://admin:R466itMq@julianrojas.xyz:5672", + // "PatientsQueue": "patients", + // "TreatmentsQueue": "treatments", + // "ObservationsQueue": "observations", + // "PumpsQueue": "pumps", + // "MaxRetries": 1 + //}, + "Ldap": { + "Enabled": true, + "Server": "bastionvpn.epigram", + "Port": 21389, + "SearchBase": "OU=CSCM,DC=salud,DC=madrid,DC=org", + "PositionProperty": "CAMSanidadPuesto", + "UserDomainName": "SALUD", + "AuthoritiesMap": [ + { + "Group": "CN=gHDOC_T_Philips,OU=Hospital Doce de Octubre,OU=Hospitales,OU=CSCM,DC=salud,DC=madrid,DC=org", + "Rol": "View", + "Actions": [ + "View" + ], + "DisplayId": "65c114566d5d1ac02b4f2478" + } + ], + "WhiteList": [ + { + "Name": "Fernando Igual Novella", + "Username": "53091058C", + "Rol": "Edit", + "Actions": [ + "View", + "Write" + ], + "DisplayId": "65c114566d5d1ac02b4f2478" + } + ] + }, + "AuthSettings": { + "JwtConfig": { + "Key": "534224WERW�$&/($$ghdTDDTHSSDFVADFTRGW%Y&%�B&CVW$&H&$HWETGWXZ%EGX$%c", + "Issuer": "https://www.philips.es/healthcare", + "Audience": "https://www.smacs.es/", + "ValidateIssuer": false, + "TokenValidityInMinutes": 1200, + "RefreshTokenValidityInMinutes": 14400 + }, + "LoginMethods": [ + "Local", + "Ldap" + ] + }, + "ApiSettings": { + "Customize": "HUVH.UCIA", + "PointOfCareMapping": { + "Key": "PV1", + "Required": false, + "Refresh": 600 + }, + "ConfigObservation": { + "Key": "PV1", + "IgnoreUnknownObservation": true, + "IgnoreUnknownTreatment": false, + "Refresh": 60 + }, + //"SmacsOrApiUrl": "http://localhost:5118", + //"ConnectionString": "host=127.0.0.1;port=5672;username=smacs;password=smacs!;publisherConfirms=true;timeout=30", + //"RabbitConnectionString": "host=127.0.0.1;port=8888;username=smacs;password=2a!nMa;publisherConfirms=true;timeout=30", + //"RabbitConnectionString": "host=127.0.0.1;port=5672;username=mirth;password=lD2dvc.df4-;publisherConfirms=true;timeout=30", + + "RecordingQueue": "recordings", + "RetentionPolicy": "NoDelete", + "RetentionPolicyValue": 15, + "CreatePatientWithORU": false, + "CreatePatientWithSIU": true, + "CreatePatientWithoutLocation": true, + "CreatePatientWithLocation": false, + "UpdatePatientDataWithORU": true, + "UpdatePatientLocationWithORU": true, + "ArchivePatientWithOru": true, + "UpdatePatientNumberWithOru": true, + "OnArchivePatientAction": "Archive", + "ArchivePatientsWithoutObservationsSinceHours": 12, + "CheckInactivePatientSchedulerIntervalHours": 1, + "CheckActiveTreatmentsSchedulerIntervalMinutes": 10, + "SinceDischargeTimeToArchive": 48, + "Shift": [ + "08:00", + "15:00", + "18:00" + ], + "FindPatientByLocationWithoutID": true, + "PersistObservationCodes": true, + "IccaFacility": "Philips", + "SendingFacility": [ + "obs", + "UMS" + ], + "AllergiesCode": "473011001", + "DrainageCode": "56868008", + "PositionCode": "386053000", + "IsolationCode": "302147001", + "HighFrequencyVentilation": [ + "PC-HFO" + ], + "InvasiveVentilation": [ + "A/C (S.I.P.P.V. )", + "A/C (S.I.P.P.V.) + V.G.", + "PRVC", + "PC-SIMV", + "SIMV-PRVC", + "PC-AC", + "SIMV", + "SIMV-PC", + "S.I.M.V.", + "PC-CMV", + "SIPPV", + "PC" + ], + "NonInvasiveVentilation": [ + "DUOPAP", + "NCPAP", + "CPAP" + ], + "Surgery": [ + "138875005", + "35631009", + "420419008", + "370897002", + "115959002", + "370896006" + ], + "SurgeryText": [ + "Cardiaca", + "Dialisis peritoneal", + "Hemodiafiltración", + "Digestiva", + "Neurologica", + "Toracica" + ], + "Transcutaneous": [ + "151756", + "151760" + ], + "RegionalBrainSaturation": [ + "194908", + "194909" + ], + "MedicationBolus": [ + "000523", + "001034", + "21011000140105", + "322440003", + "322761006", + "18851000140100", + "333919005", + "333920004", + "333922007" + ], + "IntravenousLinesCode": "10546003", + "Electroencephalogram": "42803009", + "Sedation": [ + "Propofol", + "Midazolam", + "Isoflorano", + "Ketamina sedación" + ], + "inotropicMedicines": [ + "Noradrenalina", + "Vasopresina", + "Milrinona", + "Adrenalina", + "Dopamina", + "Levosimendam", + "Dobutamina", + "Terlipresina", + "Angiotensina II" + ], + "antibioticList": [ + "Amoxicil.lina+clavulanic", + "Piperacil.lina+tazobactam", + "Penicil.lina", + "Cloxacil.lina", + "Ampicil.lina", + "Cefadroxilo", + "Cefalexina", + "Cefazolina", + "Cefuroxima", + "Cefoxitina", + "Cefixima", + "Cefditoren", + "Ceftriaxona", + "Cefotaxima", + "Ceftazidima", + "Cefepime", + "Ceftarolina", + "Ceftobiprol", + "Ceftazidima-avibactam", + "Ceftolozano-tazobactam", + "Imipenem", + "Ertapenem", + "Meropenem", + "Aztreonam", + "Sulbactam", + "Avibactam", + "Cloranfenicol", + "Vancomicina", + "Teicoplanina", + "Dalvabancina", + "Clindamicina", + "Lincomicina", + "Claritromicina", + "Azitromicina", + "Roxitromicina", + "Espiramicina", + "Josamicina", + "Midecamicina", + "Metronidazol", + "Tinidazol", + "Linezolid", + "Tedizolid", + "Ácido", + "nalidíxico", + "Ciprofloxacino", + "Norfloxacino", + "Moxifloxacino", + "Levofloxacino", + "Ofloxacino", + "Ozenoxacino", + "Nadifloxacino", + "Cotrimoxazole (trimetoprim-sulfametoxazole)", + "Sulfadiazina", + "Tetraciclina", + "Doxiciclina", + "Minociclina", + "Oxitetraciclina", + "Tigeciclina", + "Rifabutina", + "Rifampicina", + "Rifaximina", + "Daptomicina", + "Fosfomicina", + "Isoniazida", + "Etambutol", + "Pirazinamida", + "Tobramicina", + "Colistina", + "Cefiderocol", + "Pentamidina", + "Atovacuona" + ], + "anxiolyticList": [ + "Midazolam", + "Diazepam", + "Lorazepam", + "Lormetazepam", + "Clonidina", + "Clorazepato", + "Clonazepam", + "Clometiazol", + "Clotiapina", + "Alprazolam", + "Bromazepam", + "Levomepromazina", + "Zolpidem", + "Temazepam" + ], + "antipsychoticList": [ + "Haloperidol", + "Quetiapina", + "Olanzapina", + "Clorpromazina", + "Aripiprazol", + "Cloziapina", + "Risperidona", + "Ziprasidona" + ], + "antidepressantsList": [ + "Sertralina", + "Amitriptilina", + "Mirtazapina", + "Venlafaxina", + "Citalopram", + "Escitalopram", + "Fluoxetina", + "Desvenlafaxina", + "Fluvoxamina", + "Duloxetina", + "Trazodona", + "Bupropion" + ], + "neuroMedicationList": [ + "Propofol", + "Midazolam", + "Ketamina", + "sedación", + "Isoflorano", + "Remifentanilo", + "Morfina", + "Ketamina", + "analgésica", + "Fentanilo", + "Rocuroni", + "Cisatracuri" + ], + "serumCrystalloidList": [ + "Isofundin", + "Suero", + "fisiológico", + "Ringer-lactato", + "Glucosado 5%", + "Glucosado 20%", + "Glucosado 30%", + "Glucosalino" + ], + "serumColloidList": [ + "Albúmina 5%", + "Albúmina 20%", + "Gelafundina" + ], + "antihypertensivesList": [ + "Urapidilo", + "Hidralazina", + "Clevidipino", + "Amlodipino", + "Nimodipino", + "Nifedipino", + "Diltiazem", + "Doxazosina", + "Labetalol", + "Esmolol", + "Bisprolol", + "Carvedilol", + "Landiolol", + "Captopril", + "Enalapril", + "Sacubitril/valsartan", + "Lisinopril", + "Losartan", + "Candesartan", + "Valsartan", + "Nitroprusiato", + "Nitroglicerina", + "Propranolol", + "Clonidina", + "Atenolol", + "Olmesartan" + ], + "MappingInterventions": [ + { + "Type": "Respiratorio", + "Group": "not defined", + "Name": "Oxigeno (O2)/Alto flujo", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 204.0, + "FinalValue": null + } + ] + }, + { + "Type": "Respiratorio", + "Group": "not defined", + "Name": "CPAP ", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 202.0, + "FinalValue": null + } + ] + }, + { + "Type": "Respiratorio", + "Group": "not defined", + "Name": "Ventilaci\u00f3n mec\u00e1nica Invasiva", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 201.0, + "FinalValue": null + } + ] + }, + { + "Type": "Respiratorio", + "Group": "not defined", + "Name": "VAFO Ventilaci\u00f3n Oscilatoria de alta frecuencia", + "Value": [ + { + "InitialValue": 4.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 200.0, + "FinalValue": null + } + ] + }, + { + "Type": "Vias", + "Group": "not defined", + "Name": "VV perfif\u00e9rica", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 103.1, + "FinalValue": 103.5 + } + ] + }, + { + "Type": "Vias", + "Group": "not defined", + "Name": "VV epicut\u00e1neo cava", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 102.1, + "FinalValue": 102.5 + } + ] + }, + { + "Type": "Vias", + "Group": "not defined", + "Name": "VV umbilical", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 101.1, + "FinalValue": null + } + ] + }, + { + "Type": "Vias", + "Group": "not defined", + "Name": "VV central", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 104.1, + "FinalValue": 104.3 + } + ] + }, + { + "Type": "Vias", + "Group": "not defined", + "Name": "VV PICC", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 108.1, + "FinalValue": 108.2 + } + ] + }, + { + "Type": "Vias", + "Group": "not defined", + "Name": "VA umbilical", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 101.2, + "FinalValue": null + } + ] + }, + { + "Type": "Vias", + "Group": "not defined", + "Name": "VA", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 106.1, + "FinalValue": 106.3 + } + ] + }, + { + "Type": "Medicacion", + "Group": "Antiarr\u00edtmicos", + "Name": "Digoxina ev", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 118.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Antiarr\u00edtmicos", + "Name": "Digoxina vo", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 1753.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Antiarr\u00edtmicos", + "Name": "Amiodarona ev", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 169.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Antiarr\u00edtmicos", + "Name": "Amiodarona vo", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 16323.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Antiarr\u00edtmicos", + "Name": "Atropina", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 186.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Antiarr\u00edtmicos", + "Name": "Esmolol", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 853.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Antiarr\u00edtmicos", + "Name": "Propranolol", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 7122.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Antiarr\u00edtmicos", + "Name": "Isoprenalina", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 283.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Antiarr\u00edtmicos", + "Name": "Flecainida ev", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 1247.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Antiarr\u00edtmicos", + "Name": "Flecainida vo", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 7055.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Drogas vasoactivas", + "Name": "Adrenalina", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 281.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Drogas vasoactivas", + "Name": "Noradrenalina", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 100000.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Drogas vasoactivas", + "Name": "Milrinona", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 618.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Drogas vasoactivas", + "Name": "Levosimendan", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 2073.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Drogas vasoactivas", + "Name": "Dopamina", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 149.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Drogas vasoactivas", + "Name": "Debutamina", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 1968.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Drogas vasoactivas", + "Name": "Fenilefrina", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 12319.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Sedaci\u00f3n", + "Name": "Morfina ev", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 472.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Sedaci\u00f3n", + "Name": "Morfina ov", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 7081.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Sedaci\u00f3n", + "Name": "Fentanil", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 434.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Sedaci\u00f3n", + "Name": "Midazolam", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 756.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Sedaci\u00f3n", + "Name": "Dexmedetomidina", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 15910.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Sedaci\u00f3n", + "Name": "Cisatraculi", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 15066.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Sedaci\u00f3n", + "Name": "Propofol", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 314.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medicacion", + "Group": "Sedaci\u00f3n", + "Name": "Ketamina", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 374.0, + "FinalValue": null + } + ] + }, + { + "Type": "Sistemas", + "Group": "not defined", + "Name": "Hipotermia", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 100001.0, + "FinalValue": null + } + ] + }, + { + "Type": "Sistemas", + "Group": "not defined", + "Name": "aEEG", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 100002.0, + "FinalValue": null + } + ] + }, + { + "Type": "Sistemas", + "Group": "not defined", + "Name": "Drenaje ventricular (DVE)", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 315.1, + "FinalValue": null + }, + { + "InitialValue": 315.2, + "FinalValue": null + } + ] + }, + { + "Type": "Sistemas", + "Group": "not defined", + "Name": "Drenaje toracico", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 311.0, + "FinalValue": 314.0 + } + ] + }, + { + "Type": "Sistemas", + "Group": "not defined", + "Name": "Dialisis peritoneal - abdominal", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 306.0, + "FinalValue": 310.0 + } + ] + }, + { + "Type": "Peso nacimiento", + "Group": "not defined", + "Name": "Peso nacimiento", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": 500.0, + "valueContributed": 5 + }, + { + "InitialValue": 501.0, + "FinalValue": 1000.0, + "valueContributed": 4 + }, + { + "InitialValue": 1001.0, + "FinalValue": 1500.0, + "valueContributed": 3 + }, + { + "InitialValue": 1501.0, + "FinalValue": 2000.0, + "valueContributed": 2 + }, + { + "InitialValue": 2000.0, + "FinalValue": 2500.0, + "valueContributed": 1 + } + ], + "Codes": [ + { + "InitialValue": 10000465.0, + "FinalValue": null + } + ] + }, + { + "Type": "Cirugia", + "Group": "not defined", + "Name": "Cardiaca", + "Value": [ + { + "InitialValue": 4.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.1, + "FinalValue": null + } + ] + }, + { + "Type": "Cirugia", + "Group": "not defined", + "Name": "Digestiva", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.2, + "FinalValue": null + } + ] + }, + { + "Type": "Cirugia", + "Group": "not defined", + "Name": "Neurologica ", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.3, + "FinalValue": null + } + ] + }, + { + "Type": "Cirugia", + "Group": "not defined", + "Name": "Toracica", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.4, + "FinalValue": null + } + ] + }, + { + "Type": "Cirugia", + "Group": "not defined", + "Name": "ORL/Maxil\u00b7lo", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.5, + "FinalValue": null + } + ] + }, + { + "Type": "Cirugia", + "Group": "not defined", + "Name": "Oftalmo", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.6, + "FinalValue": null + } + ] + }, + { + "Type": "Cirugia", + "Group": "not defined", + "Name": "Ortopedia", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.7, + "FinalValue": null + } + ] + }, + { + "Type": "Cirugia", + "Group": "not defined", + "Name": "Altres", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.8, + "FinalValue": null + } + ] + }, + { + "Type": "Cirugia", + "Group": "not defined", + "Name": "No", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.9, + "FinalValue": null + } + ] + } + ], + //Dejar siempre en ultima posicion el Ino code 84481000140102 + "Respiratory": [ + "361110005", + "416362002", + "84481000140102" + ], + "Oni": "84481000140102", + //"BalizaUrl": "http://10.0.10.35/info.html", + //"BalizaPassword": "password", + "ConfigObservationRefresh": 120, + "ActivateSchedulerCheckExpiredObservation": true, + "PumpExpiresSeconds": 120, + "SendPumpsZero": false, + "PumPressureAlarm": [], + "VolumetricAirOcclusionAlarm": [ + "Accumulated Air In Line", + "Air In Line", + "Occlusion", + "Upstream Occlusion" + ], + "EndContinuousInfusionAlarm": [ + "End Of Infusion", + "Near End Of Infusion" + ], + "InotropicEndInfusionAlarm": [ + "Near End Of Infusion", + "Occlusion", + "Air In Line" + ], + "PathUpdateFiles": [ + "C:", + "Standard_Files", + "UploadFromServer" + ], + "ActivateCheckInactivePatients": false, + "NoCalculateStatusWithCodingSystem": "SENSOR" + } +} diff --git a/adas-core/appsettings.HUVH-UCIN.json b/adas-core/appsettings.HUVH-UCIN.json new file mode 100644 index 00000000..2a7b5d71 --- /dev/null +++ b/adas-core/appsettings.HUVH-UCIN.json @@ -0,0 +1,1621 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Serilog": { + "Using": [ + "Serilog.Sinks.Console", + "Serilog.Sinks.File", + "Serilog.Exceptions", + "Serilog.Expressions", + "Serilog.Enrichers.Thread" + ], + "Enrich": [ + "WithExceptionDetails", + "WithDynamicProperty", + "WithThreadId" + ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "adas_core": "Debug", + "EasyNetQ": "Information", + "Quartz": "Information", + "Microsoft": "Warning", + "System": "Error" + } + }, + "WriteTo": [ + { + "Name": "Console" + }, + { + "Name": "File", + "Args": { + "path": "Logs/.log", + "outputTemplate": "[{Timestamp:o}] [{Level:u3}] [{SourceContext:l}] {Message}{NewLine}{Exception}", + "rollingInterval": 4, + //0: Infinite, 1: Year, 2: Month, 3: Day, 4: Hour, 5: Minute + "retainedFileCountLimit": 72, + // 3 days + "rollOnFileSizeLimit": true, + "fileSizeLimitBytes": 20971520 + // 20Mb + } + } + ], + "Properties": { + "Application": "ADASCORE" + } + }, + "AllowedHosts": "*", + "DatabaseSettings": { + "ConnectionString": "mongodb://adas:adas2025@168.119.160.124:37017/?authSource=admin", + //"ConnectionString": "mongodb://smacsvdh:dE.s2tg3Upz@127.0.0.1:37017/?authSource=admin", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@10.0.20.67:18888/?authSource=admin", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@julianrojas.xyz:27017/?authSource=admin", + "DatabaseName": "adas" + }, + "DatabaseConfigurationAudit": { + "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@julianrojas.xyz:27017/?authSource=admin", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@10.0.20.67:18888/?authSource=admin", + //"ConnectionString": "mongodb://smacsvdh:dE.s2tg3Upz@127.0.0.1:37017/?authSource=admin", + "DatabaseName": "AuditMeddis_vdhn" + }, + //"RabbitMQSettings": { + // //"ConnectionString": "host=10.0.20.241;port=8889;username=smacs;password=smacs!;publisherConfirms=true;timeout=30", + // //"ConnectionString": "host=julianrojas.xyz;port=5672;username=admin;password=R466itMqY;publisherConfirms=true;timeout=30", + // //"ConnectionString": "host=localhost;port=9999;username=smacs;password=smacs!;publisherConfirms=true;timeout=30", + // "ConnectionString": "host=127.0.0.1;port=25672;username=mirth;password=lD2dvc.df4-;publisherConfirms=true;timeout=30", + // "PatientsQueue": "patients", + // "TreatmentsQueue": "treatments", + // "ObservationsQueue": "observations", + // "PumpsQueue": "pumps", + // "MaxRetries": 1 + //}, + //ldap://vhebron.net:389 cn=smartDisplayUCI_nounats,ou=Grupos de Seguridad,dc=vhebron,dc=net + "Ldap": { + "Enabled": true, + "Server": "vhebron.net", + "Port": 389, + "SearchBase": "OU=Grupos de Seguridad,DC=vhebron,DC=net", + //"PositionProperty": "CAMSanidadPuesto", + "UserDomainName": "venue", + "AuthoritiesMap": [ + { + //Box 29 + "Group": "CN=smartDisplayUCI_nounats,OU=Grupos de Seguridad,DC=vhebron,DC=net", + "Rol": "AuthAdmin", + //"Actions": [ "View" ], + "DisplayId": "67a3405e96eb83abafd7e7e1" + } + //{ + // "Group": "CN=smartDisplayUCI_nounats,OU=Grupos de Seguridad,DC=vhebron,DC=net", + // "Rol": "View", + // "Actions": [ "View" ], + // "DisplayId": "65c114566d5d1ac02b4f2478" + //} + ], + "WhiteList": [ + { + //Box 26 + "Name": "Prueba", + "Username": "venue", + "Rol": "AuthAdmin", + //"Actions": [ "View", "Write" ], + "DisplayId": "67a3402596eb83abafd7e7de" + } + ] + }, + "AuthSettings": { + "JwtConfig": { + "Key": "534224WERW�$&/($$ghdTDDTHSSDFVADFTRGW%Y&%�B&CVW$&H&$HWETGWXZ%EGX$%c", + "Issuer": "https://www.issuer.es/healthcare", + "Audience": "https://adas.health", + "ValidateIssuer": false, + "TokenValidityInMinutes": 1200, + "RefreshTokenValidityInMinutes": 14400 + }, + "LoginMethods": [ + "Local", + "Ldap" + ] + }, + "ApiSettings": { + "Customize": "HUVH.UCIN", + "PointOfCareMapping": { + "Key": "PV1", + "Required": false, + "Refresh": 600 + }, + "ConfigObservation": { + "Key": "PV1", + "IgnoreUnknownObservation": true, + "IgnoreUnknownTreatment": false, + "Refresh": 60 + }, + //"SmacsOrApiUrl": "http://localhost:5118", + //"ConnectionString": "host=127.0.0.1;port=5672;username=smacs;password=smacs!;publisherConfirms=true;timeout=30", + //"RabbitConnectionString": "host=127.0.0.1;port=8888;username=smacs;password=2a!nMa;publisherConfirms=true;timeout=30", + //"RabbitConnectionString": "host=127.0.0.1;port=5672;username=mirth;password=lD2dvc.df4-;publisherConfirms=true;timeout=30", + + "RecordingQueue": "recordings", + "RetentionPolicy": "NoDelete", + "RetentionPolicyValue": 15, + "CreatePatientWithORU": false, + "CreatePatientWithSIU": false, + "CreatePatientWithoutLocation": false, + "CreatePatientWithLocation": false, + "UpdatePatientDataWithORU": false, + "UpdatePatientLocationWithORU": false, + "ArchivePatientWithOru": false, + "UpdatePatientNumberWithOru": false, + "OnArchivePatientAction": "Archive", + "ArchivePatientsWithoutObservationsSinceHours": 1200, + "CheckInactivePatientSchedulerIntervalHours": 100, + "CheckActiveTreatmentsSchedulerIntervalMinutes": 10000, + "SinceDischargeTimeToArchive": 48000, + "Shift": [ + "08:00", + "15:00", + "18:00" + ], + "FindPatientByLocationWithoutID": false, + "PersistObservationCodes": true, + "IccaFacility": "Philips", + "SendingFacility": [ + "obs", + "UMS" + ], + "AllergiesCode": "473011001", + "DrainageCode": "56868008", + "PositionCode": "386053000", + "IsolationCode": "302147001", + "HighFrequencyVentilation": [ + "PC-HFO" + ], + "InvasiveVentilation": [ + "A/C (S.I.P.P.V. )", + "A/C (S.I.P.P.V.) + V.G.", + "PRVC", + "PC-SIMV", + "SIMV-PRVC", + "PC-AC", + "SIMV", + "SIMV-PC", + "S.I.M.V.", + "PC-CMV", + "SIPPV", + "PC" + ], + "NonInvasiveVentilation": [ + "DUOPAP", + "NCPAP", + "CPAP" + ], + "Surgery": [ + "138875005", + "35631009", + "420419008", + "370897002", + "115959002", + "370896006" + ], + "SurgeryText": [ + "Cardiaca", + "Dialisis peritoneal", + "Hemodiafiltración", + "Digestiva", + "Neurologica", + "Toracica" + ], + "Transcutaneous": [ + "151756", + "151760" + ], + "RegionalBrainSaturation": [ + "194908", + "194909" + ], + "MedicationBolus": [ + "000523", + "001034", + "21011000140105", + "322440003", + "322761006", + "18851000140100", + "333919005", + "333920004", + "333922007" + ], + "IntravenousLinesCode": "10546003", + "Electroencephalogram": "42803009", + "MappingInterventions": [ + { + "Type": "Respiratory_Device", + "Category": "Intervention", + "Group": "not defined", + "Name": "Oxigeno (O2)/Alto flujo", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 204.0, + "FinalValue": null + } + ] + }, + { + "Type": "Respiratory_Device", + "Category": "Intervention", + "Group": "not defined", + "Name": "CPAP ", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 202.0, + "FinalValue": null + } + ] + }, + { + "Type": "Respiratory_Device", + "Category": "Intervention", + "Group": "not defined", + "Name": "Ventilaci\u00f3n mec\u00e1nica Invasiva", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 201.0, + "FinalValue": null + } + ] + }, + { + "Type": "Respiratory_Device", + "Category": "Intervention", + "Group": "not defined", + "Name": "VAFO Ventilaci\u00f3n Oscilatoria de alta frecuencia", + "Value": [ + { + "InitialValue": 4.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 200.0, + "FinalValue": null + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VV perfif\u00e9rica", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 103.1, + "FinalValue": 103.5 + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VV epicut\u00e1neo cava", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 102.1, + "FinalValue": 102.5 + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VV umbilical", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 101.1, + "FinalValue": null + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VV central", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 104.1, + "FinalValue": 104.3 + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VV PICC", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 108.1, + "FinalValue": 108.2 + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VA umbilical", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 101.2, + "FinalValue": null + } + ] + }, + { + "Type": "Intravenous_Routes", + "Category": "Intervention", + "Group": "not defined", + "Name": "VA", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 106.1, + "FinalValue": 106.3 + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Digoxina ev", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 118.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Digoxina vo", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 1753.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Amiodarona ev", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 169.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Amiodarona vo", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 16323.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Atropina", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 186.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Esmolol", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 853.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Propranolol", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 7122.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Isoprenalina", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 283.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Flecainida ev", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 1247.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Antiarr\u00edtmicos", + "Name": "Flecainida vo", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 7055.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Adrenalina", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 281.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Noradrenalina", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 100000.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Milrinona", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 618.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Levosimendan", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 2073.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Dopamina", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 149.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Debutamina", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 1968.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Drogas vasoactivas", + "Name": "Fenilefrina", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 12319.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Morfina ev", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 472.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Morfina ov", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 7081.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Fentanil", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 434.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Midazolam", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 756.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Dexmedetomidina", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 15910.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Cisatraculi", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 15066.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Propofol", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 314.0, + "FinalValue": null + } + ] + }, + { + "Type": "Medication", + "Category": "Treatment", + "Group": "Sedaci\u00f3n", + "Name": "Ketamina", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 374.0, + "FinalValue": null + } + ] + }, + { + "Type": "System", + "Category": "Intervention", + "Group": "not defined", + "Name": "Hipotermia", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 100001.0, + "FinalValue": null + } + ] + }, + { + "Type": "System", + "Category": "Intervention", + "Group": "not defined", + "Name": "aEEG", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 100002.0, + "FinalValue": null + } + ] + }, + { + "Type": "System", + "Category": "Intervention", + "Group": "not defined", + "Name": "Drenaje ventricular (DVE)", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 315.1, + "FinalValue": null + }, + { + "InitialValue": 315.2, + "FinalValue": null + } + ] + }, + { + "Type": "System", + "Category": "Intervention", + "Group": "not defined", + "Name": "Drenaje toracico", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 311.0, + "FinalValue": 314.0 + } + ] + }, + { + "Type": "System", + "Category": "Intervention", + "Group": "not defined", + "Name": "Dialisis peritoneal - abdominal", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 306.0, + "FinalValue": 310.0 + } + ] + }, + { + "Type": "Newborn_Weight", + "Category": "Observation", + "Group": "not defined", + "Name": "Newborn_Weight", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": 500.0, + "valueContributed": 5 + }, + { + "InitialValue": 501.0, + "FinalValue": 1000.0, + "valueContributed": 4 + }, + { + "InitialValue": 1001.0, + "FinalValue": 1500.0, + "valueContributed": 3 + }, + { + "InitialValue": 1501.0, + "FinalValue": 2000.0, + "valueContributed": 2 + }, + { + "InitialValue": 2000.0, + "FinalValue": 2500.0, + "valueContributed": 1 + } + ], + "Codes": [ + { + "InitialValue": 10000465.0, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Cardiaca", + "Value": [ + { + "InitialValue": 4.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.1, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Digestiva", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.2, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Neurologica ", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.3, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Toracica", + "Value": [ + { + "InitialValue": 3.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.4, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "ORL/Maxil\u00b7lo", + "Value": [ + { + "InitialValue": 2.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.5, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Oftalmo", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.6, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Ortopedia", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.7, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "Altres", + "Value": [ + { + "InitialValue": 1.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.8, + "FinalValue": null + } + ] + }, + { + "Type": "surgery", + "Category": "Observation", + "Group": "not defined", + "Name": "No", + "Value": [ + { + "InitialValue": 0.0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": 14000070.9, + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "Parenteral", + "Name": "NUTRICIO PARENTERAL", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "1388", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LM", + "Name": "Llet materna", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIB586", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LM", + "Name": "Llet materna de banc", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIB000589", + "FinalValue": null + }, + { + "InitialValue": "BIB000587", + "FinalValue": null + }, + { + "InitialValue": "BIB000588", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LM", + "Name": "Llet materna a demanda", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "NO_EST2", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LM descremada", + "Name": "LM descremada", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "NO_EST3", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Formula de soja", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBCOM003", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Formula hidrolizada (Alfare)", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBCOM002", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Formula inici", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBNOR003", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Formula prematurs 14,4%", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBCOM017", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Formula prematurs 15,9%", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBCOM018", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Formula sense lactosa", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBCOM01", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Monogen", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBCOM009", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Infatrini", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBNOR001", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Infatini Peptisorb", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBNOR0013", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Neocate 13,8%", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBBAS019", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Neocate Advance 25%", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "NO_EST4", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Energivit", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "BIBBAS021", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "LA", + "Name": "Bibero especial", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "ALTESP001", + "FinalValue": null + } + ] + }, + { + "Type": "Nutrition", + "Category": "Treatment", + "Group": "Absoluta", + "Name": "Absoluta", + "Value": [ + { + "InitialValue": 0, + "FinalValue": null + } + ], + "Codes": [ + { + "InitialValue": "15002284", + "FinalValue": null + } + ] + } + ], + // "MappingInterventions":[{"Type":"Respiratory_Device","Name":"Ox\u00edgeno (O2)/Alto flujo","Codes":[{"InitialValue":204,"FinalValue":null}]},{"Type":"Respiratory_Device","Name":"CPAP","Codes":[{"InitialValue":202.0,"FinalValue":null}]},{"Type":"Respiratory_Device","Name":"Ventilaci\u00f3n mec\u00e1nica Invasiva","Codes":[{"InitialValue":201.0,"FinalValue":null}]},{"Type":"Respiratory_Device","Name":"VAFO Ventilaci\u00f3n Oscilatoria de alta frecuencia","Codes":[{"InitialValue":204.0,"FinalValue":null}]},{"Type":"Intravenous_Routes","Name":"VV perif\u00e9rica","Codes":[{"InitialValue":103.1,"FinalValue":103.5}]},{"Type":"Intravenous_Routes","Name":"VV epicut\u00e1neo cava","Codes":[{"InitialValue":102.1,"FinalValue":102.5}]},{"Type":"Intravenous_Routes","Name":"VV umbilical","Codes":[{"InitialValue":101.1,"FinalValue":null}]},{"Type":"Intravenous_Routes","Name":"VV central","Codes":[{"InitialValue":104.1,"FinalValue":104.3}]},{"Type":"Intravenous_Routes","Name":"VV PICC","Codes":[{"InitialValue":108.1,"FinalValue":null},{"InitialValue":108.2,"FinalValue":null}]},{"Type":"Intravenous_Routes","Name":"VA umbilical","Codes":[{"InitialValue":101.2,"FinalValue":null}]},{"Type":"Intravenous_Routes","Name":"VA","Codes":[{"InitialValue":106.1,"FinalValue":106.3}]},{"Type":"System","Name":"Hipotermia","Codes":[{"InitialValue":100001,"FinalValue":null}]},{"Type":"System","Name":"aEEG","Codes":[{"InitialValue":100002,"FinalValue":null}]},{"Type":"System","Name":"Drenaje ventricular (DVE)","Codes":[{"InitialValue":315.1,"FinalValue":null},{"InitialValue":315.2,"FinalValue":null}]},{"Type":"System","Name":"Drenaje tor\u00e1cico","Codes":[{"InitialValue":311,"FinalValue":314}]},{"Type":"System","Name":"Di\u00e1lisis peritoneal - abdominal","Codes":[{"InitialValue":306,"FinalValue":310}]}], + "ComplexityMappingObservation": { + "Oxigeno (O2)/Alto flujo": 1, + "CPAP ": 2, + "Ventilación mecánica Invasiva": 3, + "VAFO Ventilación Oscilatoria de alta frecuencia": 4, + "VV perfiférica": 1, + "VV epicutáneo cava": 2, + "VV umbilical": 2, + "VV central": 2, + "VV PICC": 2, + "VA umbilical": 3, + "VA": 3, + "Digoxina ev": 1, + "Digoxina vo": 1, + "Amiodarona ev": 1, + "Amiodarona vo": 1, + "Atropina": 1, + "Esmolol": 1, + "Propranolol": 1, + "Isoprenalina": 1, + "Flecainida ev": 1, + "Flecainida vo": 1, + "Adrenalina": 2, + "Noradrenalina": 2, + "Milrinona": 2, + "Levosimendan": 2, + "Dopamina": 2, + "Debutamina": 2, + "Fenilefrina": 2, + "Hipotermia": 2, + "aEEG": 2, + "Drenaje ventricular (DVE)": 2, + "Drenaje toracico": 2, + "Dialisis peritoneal - abdominal": 2, + "Cardiaca": 4, + "Digestiva": 3, + "Neurologica ": 3, + "Toracica": 3, + "ORL/Maxil·lo": 2, + "Oftalmo": 1, + "Ortopedia": 1, + "Altres": 1, + "No": 0 + }, + //Dejar siempre en ultima posicion el Ino code 84481000140102 + "Respiratory": [ + "361110005", + "416362002", + "84481000140102" + ], + "Oni": "84481000140102", + //"BalizaUrl": "http://10.0.10.35/info.html", + //"BalizaPassword": "password", + "ConfigObservationRefresh": 1200, + "ActivateSchedulerCheckExpiredObservation": false, + "ActivateSchedulerCheckActiveTreatmentsJob": false, + "PumpExpiresSeconds": 12000, + "SendPumpsZero": false, + "PumPressureAlarm": [], + "VolumetricAirOcclusionAlarm": [ + "Accumulated Air In Line", + "Air In Line", + "Occlusion", + "Upstream Occlusion" + ], + "EndContinuousInfusionAlarm": [ + "End Of Infusion", + "Near End Of Infusion" + ], + "InotropicEndInfusionAlarm": [ + "Near End Of Infusion", + "Occlusion", + "Air In Line" + ], + "inotropicMedicines": [ + "Dopamina", + "Dobutamina", + "Milrinona", + "Noradrenalina", + "Levosimendán" + ], + "PathUpdateFiles": [ + "C:", + "Standard_Files", + "UploadFromServer" + ], + "ActivateCheckInactivePatients": false, + "NoCalculateStatusWithCodingSystem": "SENSOR" + } +} diff --git a/adas-core/appsettings.Local.json b/adas-core/appsettings.Local.json new file mode 100644 index 00000000..e1d34922 --- /dev/null +++ b/adas-core/appsettings.Local.json @@ -0,0 +1,297 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Serilog": { + "Using": [ + "Serilog.Sinks.Console", + "Serilog.Sinks.File", + "Serilog.Exceptions", + "Serilog.Expressions", + "Serilog.Enrichers.Thread" + ], + "Enrich": [ + "WithExceptionDetails", + "WithDynamicProperty", + "WithThreadId" + ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "adas_core": "Debug", + "EasyNetQ": "Information", + "Quartz": "Information", + "Microsoft": "Warning", + "System": "Error" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "theme": "Serilog.Sinks.SystemConsole.Themes.SystemConsoleTheme::Grayscale, Serilog.Sinks.Console" + } + }, + { + "Name": "File", + "Args": { + "path": "Logs/.log", + "formatter": { + "type": "Serilog.Templates.ExpressionTemplate, Serilog.Expressions", + "template": "{@t:yyyy-MM-dd HH:mm:ss} [{@l:u3}]{#if SourceContext is not null} ({#if Length(SourceContext) > 35}{Substring(SourceContext, Length(SourceContext) - 35)}{#else}{SourceContext,35}{#end}){#end}: {@m}{#if TraceId is not null} TraceId: {TraceId}{#end}{#if TraceMessageId is not null} TraceMessageId: {TraceMessageId}{#end} TraceIdentifier: {TraceIdentifier} Thread: {ThreadId} \n{@x}" + }, + "rollingInterval": 4, + "retainedFileCountLimit": 72, + "rollOnFileSizeLimit": true + } + } + ], + "Properties": { + "Application": "ADASCORE" + } + }, + "AllowedHosts": "*", + "DatabaseSettings": { + "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@10.0.20.67:18888/?authSource=admin&maxPoolSize=200&connectTimeoutMS=10000&socketTimeoutMS=30000", + "DatabaseName": "nursePlanDev" + }, + "RabbitMQSettings": { + "ConnectionString": "host=localhost;port=5672;username=smacs;password=smacs!;publisherConfirms=true;timeout=10", + "PatientsQueue": "patients", + "TreatmentsQueue": "treatments", + "ObservationsQueue": "observations", + "RecordingQueue": "recordings", + "PumpsQueue": "pumps", + "AlarmObservationQueue": "alarms", + "MaxRetries": 1 + }, + "CacheSettings": { + "IsEnabled": true, + "Patients": "Redis", + "ConfigDisplays": "Cache", + "PumpObservations": "None", + "Appointments": "Cache", + "GroupedObservations": "Redis", + + "InMemory": { + "Ttl": { + "GlobalSeconds": 3600, + "PatientsSeconds": 600, + "ConfigDisplaysSeconds": 1200, + "PumpObservationsSeconds": 300, + "AppointmentsSeconds": 900, + "GroupedObservationsSeconds": 300 + } + }, + + "Redis": { + "ConnectionString": "localhost:6379", + "EnableScalable": false, + "Ssl": false, + "Password": null, + "ChannelPrefix": "AdasCore", + "Endpoints": [ + { "Host": "localhost", "Port": 6379 } + ], + "Ttl": { + "GlobalSeconds": 3600, + "PatientsSeconds": 600, + "ConfigDisplaysSeconds": 1200, + "PumpObservationsSeconds": 300, + "AppointmentsSeconds": 900, + "GroupedObservationsSeconds": 300 + } + } + }, + + "AuthSettings": { + "JwtConfig": { + "Key": "534224WERW�$&/($$ghdTDDTHSSDFVADFTRGW%Y&%�B&CVW$&H&$HWETGWXZ%EGX$%c", + "Issuer": "https://www.philips.es/healthcare", + "Audience": "Madrid Salud", + "TokenValidityInMinutes": 120, + "RefreshTokenValidityInMinutes": 1440 + }, + "LoginMethods": [ + "Local", + "Ldap" + ] + }, + "ApiSettings": { + "Customize": "H12O.UCIN", + "PointOfCareMapping": { + "Key": "PV1", + "Required": false, + "Refresh": 600 + }, + "ConfigObservation": { + "Key": "PV1", + "IgnoreUnknownObservation": true, + "IgnoreUnknownTreatment": false, + "Refresh": 60 + }, + "RetentionPolicy": "NoDelete", + "RetentionPolicyValue": 1500, + "CreatePatientWithORU": true, + "CreatePatientWithSIU": true, + "CreatePatientWithoutLocation": false, + "CreatePatientWithADT_A08": false, + "UpdatePatientDataWithORU": true, + "UpdatePatientLocationWithORU": true, + "ArchivePatientWithOru": false, + "UpdatePatientNumberWithOru": false, + "OnArchivePatientAction": "Archive", + "CretatePatientWithoutPatientNumber": false, + "ArchivePatientsWithoutObservationsSinceHours": 4800, + "SinceDischargeTimeToArchive ": 48, + "CheckInactivePatientSchedulerIntervalHours": 2400, + "CheckActiveTreatmentsSchedulerIntervalMinutes": 1000, + "ActivateCheckExpiredObservations": false, + "ActivateSchedulerGetProvidersObservations": false, + "ActivateSchedulerCheckNewsJob": false, + "ActivateSchedulerCheckExpiredObservation": false, + "ActivateCheckInactivePatients": true, + "ActivateCheckExpiredAlerts": false, + "ActivateSchedulerCheckHydricBalance": false, + "ActivateSchedulerCheckOpiateBoluses": false, + "ActivateSchedulerCheckActiveTreatmentsJob": false, + "Shift": [ + "08:00", + "15:00", + "18:00" + ], + "FindPatientByLocationWithoutID": false, + "PersistObservationCodes": true, + "IccaFacility": "Philips.CIS.CVC", + "SendingFacility": [ + "obs", + "UMS" + ], + "AllergiesCode": [ + "473011001" + ], + "DrainageCode": [ + "56868008" + ], + "PositionCode": [ + "386053000" + ], + "IsolationCode": [ + "302147001" + ], + "HighFrequencyVentilation": [ + "PC-HFO" + ], + "InvasiveVentilation": [ + "A/C (S.I.P.P.V. )", + "A/C (S.I.P.P.V.) + V.G.", + "PRVC", + "PC-SIMV", + "SIMV-PRVC", + "PC-AC", + "SIMV", + "SIMV-PC", + "S.I.M.V.", + "PC-CMV", + "SIPPV", + "PC" + ], + "NonInvasiveVentilation": [ + "DUOPAP", + "NCPAP" + ], + "Surgery": [ + "138875005", + "35631009", + "420419008", + "370897002", + "115959002", + "370896006" + ], + "SurgeryText": [ + "Cirugía láser of", + "Cirugía", + "Cirugía sentidos", + "Cirugía respirat", + "Cirugía locomoto", + "Cirugía renal", + "Cirugía digestiv", + "Cirugía neuro" + ], + "Transcutaneous": [ + "151756", + "151760" + ], + "RegionalBrainSaturation": [ + "194908", + "194909" + ], + "MedicationBolus": [ + "000523", + "001034", + "21011000140105", + "322440003", + "322761006", + "18851000140100", + "333919005", + "333920004", + "333922007" + ], + "IntravenousLinesCode": [ + "10546003" + ], + "Electroencephalogram": [ + "42803009" + ], + "Respiratory": [ + "361110005", + "416362002", + "84481000140102" + ], + "Oni": "84481000140102", + "BalizaUrl": "http://10.0.10.35/info.html", + "BalizaPassword": "password", + "ConfigObservationRefresh": 120, + "ConfigUnitsRequired": false, + "PumpExpiresSeconds": 120, + "SendPumpsZero": false, + "PumPressureAlarm": [], + "VolumetricAirOcclusionAlarm": [ + "Occlusion", + "Upstream Occlusion" + ], + "VolumetricAirInLineAlarm": [ + "Accumulated Air In Line", + "Air In Line", + "Air In Line Now" + ], + "EndContinuousInfusionAlarm": [ + "End Of Infusion", + "Near End Of Infusion", + "End Of Infusion - KVO Stop", + "End Of Infusion - Stop" + ], + "InotropicEndInfusionAlarm": [ + "Near End Of Infusion", + "End Of Infusion", + "End Of Infusion - KVO Stop", + "End Of Infusion - Stop" + ], + "InotropicMedicines": [ + "Dopamina", + "Dobutamina", + "Milrinona", + "Noradrenalina", + "Levosimendán" + ], + "PathUpdateFiles": [ + "C:", + "Standard_Files", + "UploadFromServer" + ], + "TimeToSendGroupedObs": 20 + } +} diff --git a/adas-core/appsettings.LocalTest.json b/adas-core/appsettings.LocalTest.json new file mode 100644 index 00000000..3b308a43 --- /dev/null +++ b/adas-core/appsettings.LocalTest.json @@ -0,0 +1,312 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Serilog": { + "Using": [ + "Serilog.Sinks.Console", + "Serilog.Sinks.File", + "Serilog.Exceptions", + "Serilog.Expressions", + "Serilog.Enrichers.Thread" + ], + "Enrich": [ + "WithExceptionDetails", + "WithDynamicProperty", + "WithThreadId" + ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "adas_core": "Debug", + "EasyNetQ": "Information", + "Quartz": "Information", + "Microsoft": "Warning", + "System": "Error" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "theme": "Serilog.Sinks.SystemConsole.Themes.SystemConsoleTheme::Grayscale, Serilog.Sinks.Console" + } + }, + { + "Name": "File", + "Args": { + "path": "Logs/.log", + "formatter": { + "type": "Serilog.Templates.ExpressionTemplate, Serilog.Expressions", + "template": "{@t:yyyy-MM-dd HH:mm:ss} [{@l:u3}]{#if SourceContext is not null} ({#if Length(SourceContext) > 35}{Substring(SourceContext, Length(SourceContext) - 35)}{#else}{SourceContext,35}{#end}){#end}: {@m}{#if TraceId is not null} TraceId: {TraceId}{#end}{#if TraceMessageId is not null} TraceMessageId: {TraceMessageId}{#end} TraceIdentifier: {TraceIdentifier} Thread: {ThreadId} \n{@x}" + }, + "rollingInterval": 4, + "retainedFileCountLimit": 72, + "rollOnFileSizeLimit": true + } + } + ], + "Properties": { + "Application": "ADASCORE" + } + }, + "AllowedHosts": "*", + "DatabaseSettings": { + "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@sumo.julianrojas.xyz:27017/?authSource=admin", + "DatabaseName": "adas" + }, + "RabbitMQSettings": { + "ConnectionString": "host=sumo.julianrojas.xyz;port=5672;username=adas_core;password=adas_core;publisherConfirms=true;timeout=30", + "PatientsQueue": "patients", + "TreatmentsQueue": "treatments", + "ObservationsQueue": "observations", + "RecordingQueue": "recordings", + "PumpsQueue": "pumps", + "AlarmObservationQueue": "alarms", + "MaxRetries": 1 + }, + "DatabaseConfigurationAudit": { + "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@sumo.julianrojas.xyz:27017/?authSource=admin", + "DatabaseName": "AuditAdas" + + }, + + "CacheSettings": { + "IsEnabled": true, + "Patients": "Redis", + "Displays": "Redis", + "PumpObservations": "None", + "Appointments": "Cache", + "PointOfCares": "Redis", + "GroupedObservations": "Redis", + "PatientObservations": "Cache", + "ConfigObservations": "Redis", + + "InMemory": { + "Ttl": { + "GlobalSeconds": 3600, + "PatientsSeconds": 600, + "DisplaysSeconds": 1200, + "PumpObservationsSeconds": 300, + "AppointmentsSeconds": 900, + "PointOfCaresSeconds": 1200, + "GroupedObservationsSeconds": 300, + "ConfigObservationsSeconds": 1200, + "PatientObservationsSeconds": 300 + } + }, + + "Redis": { + "ConnectionString": "localhost:6379", + "EnableScalable": false, + "Ssl": false, + "Password": null, + "ChannelPrefix": "AdasCore", + "Endpoints": [ + { "Host": "localhost", "Port": 6379 } + ], + "Ttl": { + "GlobalSeconds": 3600, + "PatientsSeconds": 600, + "DisplaysSeconds": 1200, + "PumpObservationsSeconds": 300, + "AppointmentsSeconds": 900, + "PointOfCaresSeconds": 1200, + "GroupedObservationsSeconds": 300, + "ConfigObservationsSeconds": 1200, + "PatientObservationsSeconds": 300 + } + } + }, + + "AuthSettings": { + "JwtConfig": { + "Key": "534224WERW�$&/($$ghdTDDTHSSDFVADFTRGW%Y&%�B&CVW$&H&$HWETGWXZ%EGX$%c", + "Issuer": "https://www.philips.es/healthcare", + "Audience": "Madrid Salud", + "TokenValidityInMinutes": 120, + "RefreshTokenValidityInMinutes": 1440 + }, + "LoginMethods": [ + "Local", + "Ldap" + ] + }, + "ApiSettings": { + "Customize": "BD", + "PointOfCareMapping": { + "Key": "PV1", + "Required": false, + "Refresh": 600 + }, + "ConfigObservation": { + "Key": "PV1", + "IgnoreUnknownObservation": true, + "IgnoreUnknownTreatment": false, + "Refresh": 60 + }, + "RetentionPolicy": "NoDelete", + "RetentionPolicyValue": 1500, + "CreatePatientWithORU": true, + "CreatePatientWithSIU": true, + "CreatePatientWithoutLocation": false, + "CreatePatientWithADT_A08": false, + "UpdatePatientDataWithORU": true, + "UpdatePatientLocationWithORU": true, + "ArchivePatientWithOru": false, + "UpdatePatientNumberWithOru": false, + "OnArchivePatientAction": "Archive", + "CretatePatientWithoutPatientNumber": false, + "ArchivePatientsWithoutObservationsSinceHours": 4800, + "SinceDischargeTimeToArchive ": 48, + "CheckInactivePatientSchedulerIntervalHours": 2400, + "CheckActiveTreatmentsSchedulerIntervalMinutes": 1000, + "ActivateCheckExpiredObservations": false, + "ActivateSchedulerGetProvidersObservations": false, + "ActivateSchedulerCheckNewsJob": false, + "ActivateSchedulerCheckExpiredObservation": false, + "ActivateCheckInactivePatients": true, + "ActivateCheckExpiredAlerts": false, + "ActivateSchedulerCheckHydricBalance": false, + "ActivateSchedulerCheckOpiateBoluses": false, + "ActivateSchedulerCheckActiveTreatmentsJob": false, + "Shift": [ + "08:00", + "15:00", + "18:00" + ], + "FindPatientByLocationWithoutID": false, + "PersistObservationCodes": true, + "IccaFacility": "Philips.CIS.CVC", + "SendingFacility": [ + "obs", + "UMS" + ], + "AllergiesCode": [ + "473011001" + ], + "DrainageCode": [ + "56868008" + ], + "PositionCode": [ + "386053000" + ], + "IsolationCode": [ + "302147001" + ], + "HighFrequencyVentilation": [ + "PC-HFO" + ], + "InvasiveVentilation": [ + "A/C (S.I.P.P.V. )", + "A/C (S.I.P.P.V.) + V.G.", + "PRVC", + "PC-SIMV", + "SIMV-PRVC", + "PC-AC", + "SIMV", + "SIMV-PC", + "S.I.M.V.", + "PC-CMV", + "SIPPV", + "PC" + ], + "NonInvasiveVentilation": [ + "DUOPAP", + "NCPAP" + ], + "Surgery": [ + "138875005", + "35631009", + "420419008", + "370897002", + "115959002", + "370896006" + ], + "SurgeryText": [ + "Cirugía láser of", + "Cirugía", + "Cirugía sentidos", + "Cirugía respirat", + "Cirugía locomoto", + "Cirugía renal", + "Cirugía digestiv", + "Cirugía neuro" + ], + "Transcutaneous": [ + "151756", + "151760" + ], + "RegionalBrainSaturation": [ + "194908", + "194909" + ], + "MedicationBolus": [ + "000523", + "001034", + "21011000140105", + "322440003", + "322761006", + "18851000140100", + "333919005", + "333920004", + "333922007" + ], + "IntravenousLinesCode": [ + "10546003" + ], + "Electroencephalogram": [ + "42803009" + ], + "Respiratory": [ + "361110005", + "416362002", + "84481000140102" + ], + "Oni": "84481000140102", + "BalizaUrl": "http://10.0.10.35/info.html", + "BalizaPassword": "password", + "ConfigObservationRefresh": 120, + "ConfigUnitsRequired": false, + "PumpExpiresSeconds": 120, + "SendPumpsZero": false, + "PumPressureAlarm": [], + "VolumetricAirOcclusionAlarm": [ + "Occlusion", + "Upstream Occlusion" + ], + "VolumetricAirInLineAlarm": [ + "Accumulated Air In Line", + "Air In Line", + "Air In Line Now" + ], + "EndContinuousInfusionAlarm": [ + "End Of Infusion", + "Near End Of Infusion", + "End Of Infusion - KVO Stop", + "End Of Infusion - Stop" + ], + "InotropicEndInfusionAlarm": [ + "Near End Of Infusion", + "End Of Infusion", + "End Of Infusion - KVO Stop", + "End Of Infusion - Stop" + ], + "InotropicMedicines": [ + "Dopamina", + "Dobutamina", + "Milrinona", + "Noradrenalina", + "Levosimendán" + ], + "PathUpdateFiles": [ + "C:", + "Standard_Files", + "UploadFromServer" + ], + "TimeToSendGroupedObs": 20 + } +} diff --git a/adas-core/appsettings.NursePlan.json b/adas-core/appsettings.NursePlan.json new file mode 100644 index 00000000..daab64e9 --- /dev/null +++ b/adas-core/appsettings.NursePlan.json @@ -0,0 +1,355 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "DatabaseSettings": { + //"ConnectionString": "mongodb://adas:adas2025@168.119.160.124:27017/adas?authSource=admin", + "ConnectionString": "mongodb://adas:adas2025@168.119.160.124:37017", + //"ConnectionString": "mongodb://localhost:27017", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@127.0.0.1:27017/?authSource=admin", + //develop + // "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@10.0.20.67:18888/?authSource=admin&maxPoolSize=200&connectTimeoutMS=10000&socketTimeoutMS=30000", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@julianrojas.xyz:27017/?authSource=admin", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@sumo.julianrojas.xyz:27017/?authSource=admin", + //"ConnectionString": "mongodb://smartuci:!H12o2020@10.206.250.147:21103/?authSource=smartuci", + //producción + //"ConnectionString": "mongodb://smartuci:!H12o2020@localhost:27017/?authSource=smartuci", + //"ConnectionString": "mongodb://admin:4q*pHca@10.0.20.78:22007", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@sumo.julianrojas.xyz:27017/?authSource=admin", + "DatabaseName": "adasDevNewStandard" + }, + "DatabaseConfigurationAudit": { + "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@julianrojas.xyz:27017/?authSource=admin", + "DatabaseName": "AuditMeddisPlanUciPanel" + }, + "RabbitMQSettings": { + "ConnectionString": "host=localhost;port=5672;username=adas;password=adas;publisherConfirms=true;timeout=30", + // "ConnectionString": "host=sumo.julianrojas.xyz;port=5672;username=adas_core;password=adas_core;publisherConfirms=true;timeout=30", + //"ConnectionString": "host=sumo.julianrojas.xyz;port=5672;username=admin;password=R466itMq;publisherConfirms=true;timeout=30", + //"ConnectionString": "host=10.0.20.241;port=8889;username=smacs;password=smacs!;publisherConfirms=true;timeout=10",amqp://admin:R466itMq@sumo.julianrojas.xyz:5672 + "PatientsQueue": "patients", + "TreatmentsQueue": "treatments", + "ObservationsQueue": "observations", + "PumpsQueue": "pumps", + "MaxRetries": 1 + }, + "Ldap": { + "Enabled": true, + + // Servidor OpenLDAP + "Server": "168.119.160.124", + "Port": 389, + + // Si se usa LDAPS o StartTLS en el servidor + "UseLdaps": false, + "UseStartTls": false, + + // Patrón DN para autenticar usuarios + // Resultado final: uid=USERNAME,ou=IT,dc=adas,dc=health + "BindDnPattern": "uid={0},ou=IT,dc=adas,dc=health", + + // Sólo para AD + "UserDomainName": null, + + //Bindear con Admin + "BindAdminDn": "cn=admin,dc=adas,dc=health", + "BindAdminPassword": "admin", + + // DN raíz desde donde buscar usuarios + "SearchBase": "ou=IT,dc=adas,dc=health", + + // atributo principal del usuario OpenLDAP + "UserNameProperty": "uid", + + "FirstNameProperty": "givenName", + "LastNameProperty": "sn", + + // memberOf + "GroupsProperty": "memberOf", + + // Mapeo automático de grupos LDAP → Authorities locales + "AuthoritiesMap": [ + { + "Group": "cn=admins", + "AuthorityName": "Admin" + }, + { + "Group": "cn=isp", + "AuthorityName": "Admin" + } + ], + + // Lista blanca: usuarios con permisos especiales + "WhiteList": [ + { + "Username": "admin", + "AuthorityName": "Admin" + } + ] + }, + "CacheSettings": { + "IsEnabled": true, + "Patients": "Redis", + "ConfigDisplays": "Cache", + "PumpObservations": "None", + "Appointments": "Cache", + "GroupedObservations": "Redis", + + "InMemory": { + "Ttl": { + "GlobalSeconds": 3600, + "PatientsSeconds": 600, + "ConfigDisplaysSeconds": 1200, + "PumpObservationsSeconds": 300, + "AppointmentsSeconds": 900, + "GroupedObservationsSeconds": 300 + } + }, + + "Redis": { + "ConnectionString": "localhost:6379", + "EnableScalable": false, + "Ssl": false, + "Password": null, + "ChannelPrefix": "AdasCore", + "Endpoints": [ + { + "Host": "localhost", + "Port": 6379 + } + ], + "Ttl": { + "GlobalSeconds": 3600, + "PatientsSeconds": 600, + "ConfigDisplaysSeconds": 1200, + "PumpObservationsSeconds": 300, + "AppointmentsSeconds": 900, + "GroupedObservationsSeconds": 300 + } + } + }, + "AuthSettings": { + "JwtConfig": { + "Key": "534224WERW�$&/($$ghdTDDTHSSDFVADFTRGW%Y&%�B&CVW$&H&$HWETGWXZ%EGX$%c", + "Issuer": "https://www.philips.es/healthcare", + "Audience": "Madrid Salud", + "TokenValidityInMinutes": 1440, + "RefreshTokenValidityInMinutes": 10 + }, + "LoginMethods": [ + "Local", + "Ldap" + ] + }, + "ApiSettings": { + "HL7RestrictionByConfigAlwaysDisabled": true, + "Customize": "NursePlan", + "PointOfCareMapping": { + "Key": "PV1", + "Required": false, + "Refresh": 600 + }, + "ConfigObservation": { + "Key": "PV1", + "IgnoreUnknownObservation": true, + "IgnoreUnknownTreatment": false, + "Refresh": 60 + }, + "ArchiveNurseData": { + "Active": true, + "IntervalMinutes": 1, + "ArchiveTest": { + //pruebas + "Active": true, + "EndDateAfterMinutes": 60 + }, + "ArchiveProcedure": { + //intervenciones + "Active": true, + "EndDateAfterMinutes": 60 + }, + "ArchiveTreatment": { + //terapias + "Active": true, + "EndDateAfterMinutes": 60 + } + }, + "SmacsOrApiUrl": "http://localhost:5118", + "RecordingQueue": "recordings", + "RetentionPolicy": "NoDelete", + "RetentionPolicyValue": 0, + "CreatePatientWithORU": true, + "CreatePatientWithSIU": true, + "CreatePatientWithoutLocation": false, + "UpdatePatientDataWithORU": true, + "updatePatientDataWithAdt_A02": true, + "UpdatePatientLocationWithORU": false, + "ArchivePatientWithOru": false, + "UpdatePatientNumberWithOru": false, + "OnArchivePatientAction": "Archive", + "ArchivePatientsWihtoutObservationsSinceDays": 10, + "CheckInactivePatientSchedulerIntervalHours": 24, + "CheckActiveTreatmentsSchedulerIntervalMinutes": 2, + "SinceDischargeTimeToArchive": 48, + "Shift": [ + "08:00", + "15:00", + "22:00" + ], + "FindPatientByLocationWithoutID": false, + "PersistObservationCodes": true, + "IccaFacility": "Philips.CIS.CVC", + "SendingFacility": [ + "LABORATORIO" + ], + "AllergiesCode": [ + "473011001" + ], + "DrainageCode": [ + "56868008" + ], + "PositionCode": [ + "386053000" + ], + "IsolationCode": [ + "302147001" + ], + "HighFrequencyVentilation": [ + "PC-HFO", + "Alto Flujo" + ], + "InvasiveVentilation": [ + "A/C (S.I.P.P.V. )", + "A/C (S.I.P.P.V.) + V.G.", + "PRVC", + "PC-SIMV", + "SIMV-PRVC", + "PC-AC", + "SIMV", + "SIMV-PC", + "S.I.M.V.", + "PC-CMV", + "SIPPV", + "PC" + ], + "NonInvasiveVentilation": [ + "DUOPAP", + "NCPAP", + "CPAP", + "V.N.I. Ciclada" + ], + "Surgery": [ + "138875005", + "370896006", + "370896006", + "35631009", + "420419008", + "370897002", + "115959002", + "370896006" + ], + "SurgeryText": [ + "Cirugía láser of", + "Cirugía", + "Cirugía sentidos", + "Cirugía respirat", + "Cirugía locomoto", + "Cirugía renal", + "Cirugía digestiv", + "Cirugía neuro" + ], + "Transcutaneous": [ + "151756", + "151760" + ], + "RegionalBrainSaturation": [ + "194908", + "194909" + ], + "MedicationBolus": [ + "000523", + "001034", + "21011000140105", + "322440003", + "322761006", + "18851000140100", + "333919005", + "333920004", + "333922007", + "18791000140100", + "18801000140104", + "18811000140101" + ], + "IntravenousLinesCode": [ + "10546003" + ], + "Electroencephalogram": [ + "42803009", + "408987002" + ], + "Ecmo": [ + "705923009" + ], + "NotesIndicatingMedication": [ + "SUEROS Y HEMODER,Perfusiones,Medicación,NPT" + ], + //Dejar siempre en ultima posicion el Ino code 84481000140102 + "Respiratory": [ + "361110005", + "416362002", + "84481000140102" + ], + "Oni": [ + "84481000140102", + "416362002" + ], + "BalizaUrl": "http://10.0.10.35/info.html", + "BalizaPassword": "password", + "ConfigObservationRefresh": 120, + "ConfigUnitsRequired": true, + "ActivateSchedulerCheckExpiredObservation": false, + "schedulerCheckExpiredObservationsIntervalMinutes": 5, + "diagnosisSystem": "Snomed-CT", + "DiagnosisCode": [ + "55607006" + ], + "PumpExpiresSeconds": 120, + "SendPumpsZero": false, + "PumPressureAlarm": [], + "VolumetricAirOcclusionAlarm": [ + "Accumulated Air In Line", + "Air In Line", + "Occlusion", + "Upstream Occlusion" + ], + "EndContinuousInfusionAlarm": [ + "End Of Infusion", + "Near End Of Infusion" + ], + "InotropicEndInfusionAlarm": [ + "Near End Of Infusion", + "Occlusion", + "Air In Line" + ], + "inotropicMedicines": [ + "Dopamina", + "Dobutamina", + "Milrinona", + "Noradrenalina", + "Levosimendán" + ], + "ActivateCheckInactivePatients": false, + "PathToDisplayAssets": [ + "D:", + "Web", + "smacs-uci-webdisplay", + "src", + "assets", + "icon" + ] + } +} diff --git a/adas-core/appsettings.json b/adas-core/appsettings.json new file mode 100644 index 00000000..fe69934c --- /dev/null +++ b/adas-core/appsettings.json @@ -0,0 +1,281 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "DatabaseSettings": { + //"ConnectionString": "mongodb://admin:password@127.0.0.1:27017?authMechanism=SCRAM-SHA-256&connectTimeoutMS=5000&maxPoolSize=500&retryWrites=true&waitQueueMultiple=15", + //"ConnectionString": "mongodb://localhost:27017", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@127.0.0.1:27017/?authSource=admin", + //develop + "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@10.0.20.67:18888/?authSource=admin&maxPoolSize=200&connectTimeoutMS=10000&socketTimeoutMS=30000", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@julianrojas.xyz:27017/?authSource=admin", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@sumo.julianrojas.xyz:27017/?authSource=admin", + //"ConnectionString": "mongodb://smartuci:!H12o2020@10.206.250.147:21103/?authSource=smartuci", + //producción + //"ConnectionString": "mongodb://smartuci:!H12o2020@localhost:27017/?authSource=smartuci", + //"ConnectionString": "mongodb://admin:4q*pHca@10.0.20.78:22007", + //"ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@sumo.julianrojas.xyz:27017/?authSource=admin", + "DatabaseName": "vdh_ucia_v3_nueva" + }, + "DatabaseConfigurationAudit": { + "ConnectionString": "mongodb://smacsuci:2(R*aQpu2r@julianrojas.xyz:27017/?authSource=admin", + "DatabaseName": "AuditMeddisPlanUciPanel" + }, + //"RabbitMQSettings": { + // "ConnectionString": "host=localhost;port=5672;username=smacs;password=smacs;publisherConfirms=true;timeout=10", + // //"ConnectionString": "host=sumo.julianrojas.xyz;port=5672;username=admin;password=R466itMq;publisherConfirms=true;timeout=30", + // //"ConnectionString": "host=10.0.20.241;port=8889;username=smacs;password=smacs!;publisherConfirms=true;timeout=10", + // "PatientsQueue": "patients", + // "TreatmentsQueue": "treatments", + // "ObservationsQueue": "observations", + // "PumpsQueue": "pumps", + // "MaxRetries": 1 + //}, + "Ldap": { + "Enabled": true, + "Server": "bastionvpn.epigram", + "Port": 21389, + "SearchBase": "OU=CSCM,DC=salud,DC=madrid,DC=org", + "PositionProperty": "CAMSanidadPuesto", + "UserDomainName": "SALUD", + "AuthoritiesMap": [ + { + "Group": "CN=gHDOC_T_Philips,OU=Hospital Doce de Octubre,OU=Hospitales,OU=CSCM,DC=salud,DC=madrid,DC=org", + "Rol": "AuthAdmin", + "DisplayId": "65c114566d5d1ac02b4f2478" + } + ], + "WhiteList": [ + { + "Name": "Fernando Igual Novella", + "Username": "53091058C", + "Rol": "AuthAdmin", + "DisplayId": "65c114566d5d1ac02b4f2478" + } + ] + }, + "AuthSettings": { + "JwtConfig": { + "Key": "534224WERW�$&/($$ghdTDDTHSSDFVADFTRGW%Y&%�B&CVW$&H&$HWETGWXZ%EGX$%c", + "Issuer": "https://www.philips.es/healthcare", + "Audience": "Madrid Salud", + "TokenValidityInMinutes": 1440, + "RefreshTokenValidityInMinutes": 10 + }, + "LoginMethods": [ + "Local", + "Ldap" + ] + }, + "ApiSettings": { + "HL7RestrictionByConfigAlwaysDisabled": true, + "Customize": "NursePlan", + "PointOfCareMapping": { + "Key": "PV1", + "Required": false, + "Refresh": 600 + }, + "ConfigObservation": { + "Key": "PV1", + "IgnoreUnknownObservation": true, + "IgnoreUnknownTreatment": false, + "Refresh": 60 + }, + "ArchiveNurseData": { + "Active": true, + "IntervalMinutes": 1, + "ArchiveTest": { + //pruebas + "Active": true, + "EndDateAfterMinutes": 60 + }, + "ArchiveProcedure": { + //intervenciones + "Active": true, + "EndDateAfterMinutes": 60 + }, + "ArchiveTreatment": { + //terapias + "Active": true, + "EndDateAfterMinutes": 60 + } + }, + "SmacsOrApiUrl": "http://localhost:5118", + "RecordingQueue": "recordings", + "RetentionPolicy": "NoDelete", + "RetentionPolicyValue": 0, + "CreatePatientWithORU": true, + "CreatePatientWithSIU": true, + "CreatePatientWithoutLocation": false, + "UpdatePatientDataWithORU": true, + "updatePatientDataWithAdt_A02": true, + "UpdatePatientLocationWithORU": false, + "ArchivePatientWithOru": false, + "UpdatePatientNumberWithOru": false, + "OnArchivePatientAction": "Archive", + "ArchivePatientsWihtoutObservationsSinceDays": 10, + "CheckInactivePatientSchedulerIntervalHours": 24, + "CheckActiveTreatmentsSchedulerIntervalMinutes": 2, + "SinceDischargeTimeToArchive": 48, + "Shift": [ + "08:00", + "15:00", + "22:00" + ], + "FindPatientByLocationWithoutID": false, + "PersistObservationCodes": true, + "IccaFacility": "Philips.CIS.CVC", + "SendingFacility": [ + "LABORATORIO" + ], + "AllergiesCode": [ + "473011001" + ], + "DrainageCode": [ + "56868008" + ], + "PositionCode": [ + "386053000" + ], + "IsolationCode": [ + "302147001" + ], + "HighFrequencyVentilation": [ + "PC-HFO", + "Alto Flujo" + ], + "InvasiveVentilation": [ + "A/C (S.I.P.P.V. )", + "A/C (S.I.P.P.V.) + V.G.", + "PRVC", + "PC-SIMV", + "SIMV-PRVC", + "PC-AC", + "SIMV", + "SIMV-PC", + "S.I.M.V.", + "PC-CMV", + "SIPPV", + "PC" + ], + "NonInvasiveVentilation": [ + "DUOPAP", + "NCPAP", + "CPAP", + "V.N.I. Ciclada" + ], + "Surgery": [ + "138875005", + "370896006", + "370896006", + "35631009", + "420419008", + "370897002", + "115959002", + "370896006" + ], + "SurgeryText": [ + "Cirugía láser of", + "Cirugía", + "Cirugía sentidos", + "Cirugía respirat", + "Cirugía locomoto", + "Cirugía renal", + "Cirugía digestiv", + "Cirugía neuro" + ], + "Transcutaneous": [ + "151756", + "151760" + ], + "RegionalBrainSaturation": [ + "194908", + "194909" + ], + "MedicationBolus": [ + "000523", + "001034", + "21011000140105", + "322440003", + "322761006", + "18851000140100", + "333919005", + "333920004", + "333922007", + "18791000140100", + "18801000140104", + "18811000140101" + ], + "IntravenousLinesCode": [ + "10546003" + ], + "Electroencephalogram": [ + "42803009", + "408987002" + ], + "Ecmo": [ + "705923009" + ], + "NotesIndicatingMedication": [ + "SUEROS Y HEMODER,Perfusiones,Medicación,NPT" + ], + //Dejar siempre en ultima posicion el Ino code 84481000140102 + "Respiratory": [ + "361110005", + "416362002", + "84481000140102" + ], + "Oni": [ + "84481000140102", + "416362002" + ], + "BalizaUrl": "http://10.0.10.35/info.html", + "BalizaPassword": "password", + "ConfigObservationRefresh": 120, + "ConfigUnitsRequired": true, + "ActivateSchedulerCheckExpiredObservation": false, + "schedulerCheckExpiredObservationsIntervalMinutes": 5, + "diagnosisSystem": "Snomed-CT", + "DiagnosisCode": [ + "55607006" + ], + "PumpExpiresSeconds": 120, + "SendPumpsZero": false, + "PumPressureAlarm": [], + "VolumetricAirOcclusionAlarm": [ + "Accumulated Air In Line", + "Air In Line", + "Occlusion", + "Upstream Occlusion" + ], + "EndContinuousInfusionAlarm": [ + "End Of Infusion", + "Near End Of Infusion" + ], + "InotropicEndInfusionAlarm": [ + "Near End Of Infusion", + "Occlusion", + "Air In Line" + ], + "inotropicMedicines": [ + "Dopamina", + "Dobutamina", + "Milrinona", + "Noradrenalina", + "Levosimendán" + ], + "ActivateCheckInactivePatients": false, + "PathToDisplayAssets": [ + "D:", + "Web", + "smacs-uci-webdisplay", + "src", + "assets", + "icon" + ] + } +}