From c946d9e80ea12a9fda7f11b49cf80b4f6639d45f Mon Sep 17 00:00:00 2001 From: Chukwudumebi Onwuli <37223065+deeonwuli@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:39:12 +0100 Subject: [PATCH] refactor: move script logic to a use case --- src/data/repositories/AlertD2Repository.ts | 32 +- .../repositories/AlertDataD2Repository.ts | 151 +++++++++ .../AlertSyncDataStoreRepository.ts | 22 +- .../DiseaseOutbreakEventD2Repository.ts | 18 + .../repositories/NotificationD2Repository.ts | 38 ++- .../repositories/UserGroupD2Repository.ts | 26 ++ .../DiseaseOutbreakEventTestRepository.ts | 67 ++++ .../repositories/utils/AlertOutbreakMapper.ts | 76 +---- src/data/repositories/utils/MetadataHelper.ts | 22 +- .../repositories/utils/NotificationMapper.ts | 22 -- .../utils/getAllTrackedEntities.ts | 29 +- src/domain/entities/UserGroup.ts | 3 + src/domain/entities/alert/AlertData.ts | 29 +- .../alert/AlertSynchronizationData.ts | 41 +++ .../repositories/AlertDataRepository.ts | 6 + src/domain/repositories/AlertRepository.ts | 3 +- .../repositories/AlertSyncRepository.ts | 7 +- .../DiseaseOutbreakEventRepository.ts | 4 + .../repositories/NotificationRepository.ts | 15 +- .../repositories/UserGroupRepository.ts | 6 + .../MapDiseaseOutbreakToAlertsUseCase.ts | 6 +- src/domain/usecases/MappingScriptUseCase.ts | 214 ++++++++++++ .../usecases/NotifyWatchStaffUseCase.ts | 37 --- src/scripts/common.ts | 28 ++ src/scripts/mapDiseaseOutbreakToAlerts.ts | 309 ++---------------- 25 files changed, 700 insertions(+), 511 deletions(-) create mode 100644 src/data/repositories/AlertDataD2Repository.ts create mode 100644 src/data/repositories/UserGroupD2Repository.ts delete mode 100644 src/data/repositories/utils/NotificationMapper.ts create mode 100644 src/domain/entities/UserGroup.ts create mode 100644 src/domain/entities/alert/AlertSynchronizationData.ts create mode 100644 src/domain/repositories/AlertDataRepository.ts create mode 100644 src/domain/repositories/UserGroupRepository.ts create mode 100644 src/domain/usecases/MappingScriptUseCase.ts delete mode 100644 src/domain/usecases/NotifyWatchStaffUseCase.ts diff --git a/src/data/repositories/AlertD2Repository.ts b/src/data/repositories/AlertD2Repository.ts index 3bd299d7..2d014a7d 100644 --- a/src/data/repositories/AlertD2Repository.ts +++ b/src/data/repositories/AlertD2Repository.ts @@ -19,25 +19,20 @@ import { import { Maybe } from "../../utils/ts-utils"; import { DataSource } from "../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent"; import { Alert } from "../../domain/entities/alert/Alert"; - -export type Filter = { - id: Id; - value: Maybe; -}; +import { OutbreakData } from "../../domain/entities/alert/AlertData"; export class AlertD2Repository implements AlertRepository { constructor(private api: D2Api) {} updateAlerts(alertOptions: AlertOptions): FutureData { - const { dataSource, eventId, hazardTypeCode, incidentStatus, suspectedDiseaseCode } = - alertOptions; - const filter = this.getAlertFilter(dataSource, suspectedDiseaseCode, hazardTypeCode); + const { dataSource, eventId, incidentStatus, outbreakValue } = alertOptions; + const outbreakData = this.getAlertOutbreakData(dataSource, outbreakValue); return this.getTrackedEntitiesByTEACode({ program: RTSL_ZEBRA_ALERTS_PROGRAM_ID, orgUnit: RTSL_ZEBRA_ORG_UNIT_ID, ouMode: "DESCENDANTS", - filter: filter, + filter: outbreakData, }).flatMap(alertTrackedEntities => { const alertsToMap: Alert[] = alertTrackedEntities.map(trackedEntity => ({ id: trackedEntity.trackedEntity || "", @@ -79,11 +74,11 @@ export class AlertD2Repository implements AlertRepository { }); } - async getTrackedEntitiesByTEACodeAsync(options: { + private async getTrackedEntitiesByTEACodeAsync(options: { program: Id; orgUnit: Id; ouMode: "SELECTED" | "DESCENDANTS"; - filter?: Filter; + filter?: OutbreakData; }): Promise { const { program, orgUnit, ouMode, filter } = options; const d2TrackerTrackedEntities: D2TrackerTrackedEntity[] = []; @@ -132,25 +127,24 @@ export class AlertD2Repository implements AlertRepository { } } - getTrackedEntitiesByTEACode(options: { + private getTrackedEntitiesByTEACode(options: { program: Id; orgUnit: Id; ouMode: "SELECTED" | "DESCENDANTS"; - filter?: Filter; + filter?: OutbreakData; }): FutureData { return Future.fromPromise(this.getTrackedEntitiesByTEACodeAsync(options)); } - private getAlertFilter( + private getAlertOutbreakData( dataSource: DataSource, - suspectedDiseaseCode: Maybe, - hazardTypeCode: Maybe - ): Filter { + outbreakValue: Maybe + ): OutbreakData { switch (dataSource) { case DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS: - return { id: RTSL_ZEBRA_ALERTS_DISEASE_TEA_ID, value: suspectedDiseaseCode }; + return { id: RTSL_ZEBRA_ALERTS_DISEASE_TEA_ID, value: outbreakValue }; case DataSource.RTSL_ZEB_OS_DATA_SOURCE_EBS: - return { id: RTSL_ZEBRA_ALERTS_EVENT_TYPE_TEA_ID, value: hazardTypeCode }; + return { id: RTSL_ZEBRA_ALERTS_EVENT_TYPE_TEA_ID, value: outbreakValue }; } } } diff --git a/src/data/repositories/AlertDataD2Repository.ts b/src/data/repositories/AlertDataD2Repository.ts new file mode 100644 index 00000000..fb59344e --- /dev/null +++ b/src/data/repositories/AlertDataD2Repository.ts @@ -0,0 +1,151 @@ +import { D2Api } from "@eyeseetea/d2-api/2.36"; +import { AlertData, OutbreakData } from "../../domain/entities/alert/AlertData"; +import { AlertDataRepository } from "../../domain/repositories/AlertDataRepository"; +import { + D2TrackerTrackedEntity, + TrackedEntitiesGetResponse, +} from "@eyeseetea/d2-api/api/trackerTrackedEntities"; +import { + RTSL_ZEBRA_ALERTS_DISEASE_TEA_ID, + RTSL_ZEBRA_ALERTS_EVENT_TYPE_TEA_ID, + RTSL_ZEBRA_ALERTS_NATIONAL_DISEASE_OUTBREAK_EVENT_ID_TEA_ID, + RTSL_ZEBRA_ALERTS_PROGRAM_ID, + RTSL_ZEBRA_ORG_UNIT_ID, +} from "./consts/DiseaseOutbreakConstants"; +import { Id } from "../../domain/entities/Ref"; +import { FutureData } from "../api-futures"; +import { Future } from "../../domain/entities/generic/Future"; +import _ from "../../domain/entities/generic/Collection"; +import { getTEAttributeById } from "./utils/MetadataHelper"; +import { DataSource } from "../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent"; +import { mapTrackedEntityAttributesToNotificationOptions } from "./utils/AlertOutbreakMapper"; + +export class AlertDataD2Repository implements AlertDataRepository { + constructor(private api: D2Api) {} + + get(): FutureData { + return this.getAlertTrackedEntities().flatMap(alertTEIs => { + const alertsWithNoEventId = this.getAlertData(alertTEIs); + + return alertsWithNoEventId; + }); + } + + private getAlertData(alertTrackedEntities: D2TrackerTrackedEntity[]): FutureData { + const alertsWithNoEventId = _(alertTrackedEntities) + .compactMap(trackedEntity => { + const nationalEventId = getTEAttributeById( + trackedEntity, + RTSL_ZEBRA_ALERTS_NATIONAL_DISEASE_OUTBREAK_EVENT_ID_TEA_ID + ); + const hazardType = getTEAttributeById( + trackedEntity, + RTSL_ZEBRA_ALERTS_EVENT_TYPE_TEA_ID + ); + const diseaseType = getTEAttributeById( + trackedEntity, + RTSL_ZEBRA_ALERTS_DISEASE_TEA_ID + ); + + const notificationOptions = + mapTrackedEntityAttributesToNotificationOptions(trackedEntity); + + const outbreakData = diseaseType + ? { id: diseaseType.attribute, value: diseaseType.value } + : hazardType + ? { id: hazardType.value, value: hazardType.value } + : undefined; + + if (!outbreakData) return undefined; + if (!trackedEntity.trackedEntity || !trackedEntity.orgUnit) + throw new Error("Tracked entity not found"); + + const alertData: AlertData = { + alert: { + id: trackedEntity.trackedEntity, + district: trackedEntity.orgUnit, + }, + outbreakData: outbreakData, + dataSource: diseaseType + ? DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS + : DataSource.RTSL_ZEB_OS_DATA_SOURCE_EBS, + notificationOptions: notificationOptions, + }; + + return !nationalEventId && (hazardType || diseaseType) ? alertData : undefined; + }) + .value(); + + return Future.success(alertsWithNoEventId); + } + + private async getTrackedEntitiesByTEACodeAsync(options: { + program: Id; + orgUnit: Id; + ouMode: "SELECTED" | "DESCENDANTS"; + filter?: OutbreakData; + }): Promise { + const { program, orgUnit, ouMode, filter } = options; + const d2TrackerTrackedEntities: D2TrackerTrackedEntity[] = []; + + const pageSize = 250; + let page = 1; + let result: TrackedEntitiesGetResponse; + + try { + do { + result = await this.api.tracker.trackedEntities + .get({ + program: program, + orgUnit: orgUnit, + ouMode: ouMode, + totalPages: true, + page: page, + pageSize: pageSize, + fields: { + attributes: true, + orgUnit: true, + trackedEntity: true, + trackedEntityType: true, + enrollments: { + events: { + createdAt: true, + dataValues: { + dataElement: true, + value: true, + }, + event: true, + }, + }, + }, + filter: filter ? `${filter.id}:eq:${filter.value}` : undefined, + }) + .getData(); + + d2TrackerTrackedEntities.push(...result.instances); + + page++; + } while (result.page < Math.ceil((result.total as number) / pageSize)); + return d2TrackerTrackedEntities; + } catch { + return []; + } + } + + private getTrackedEntitiesByTEACode(options: { + program: Id; + orgUnit: Id; + ouMode: "SELECTED" | "DESCENDANTS"; + filter?: OutbreakData; + }): FutureData { + return Future.fromPromise(this.getTrackedEntitiesByTEACodeAsync(options)); + } + + private getAlertTrackedEntities(): FutureData { + return this.getTrackedEntitiesByTEACode({ + program: RTSL_ZEBRA_ALERTS_PROGRAM_ID, + orgUnit: RTSL_ZEBRA_ORG_UNIT_ID, + ouMode: "DESCENDANTS", + }); + } +} diff --git a/src/data/repositories/AlertSyncDataStoreRepository.ts b/src/data/repositories/AlertSyncDataStoreRepository.ts index cf054678..787d32d8 100644 --- a/src/data/repositories/AlertSyncDataStoreRepository.ts +++ b/src/data/repositories/AlertSyncDataStoreRepository.ts @@ -6,10 +6,13 @@ import { AlertSyncRepository, } from "../../domain/repositories/AlertSyncRepository"; import { apiToFuture, FutureData } from "../api-futures"; -import { getOutbreakKey, getAlertValueFromMap } from "./utils/AlertOutbreakMapper"; +import { getAlertValueFromMap } from "./utils/AlertOutbreakMapper"; import { Maybe } from "../../utils/ts-utils"; import { DataValue } from "@eyeseetea/d2-api/api/trackerEvents"; -import { AlertSynchronizationData } from "../../domain/entities/alert/AlertData"; +import { + AlertSynchronizationData, + getOutbreakKey, +} from "../../domain/entities/alert/AlertSynchronizationData"; import { DataSource } from "../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent"; import { RTSL_ZEBRA_ALERTS_PROGRAM_ID } from "./consts/DiseaseOutbreakConstants"; import { assertOrError } from "./utils/AssertOrError"; @@ -24,14 +27,7 @@ export class AlertSyncDataStoreRepository implements AlertSyncRepository { } saveAlertSyncData(options: AlertSyncOptions): FutureData { - const { - alert, - dataSource, - hazardTypeCode, - suspectedDiseaseCode, - hazardTypes, - suspectedDiseases, - } = options; + const { alert, outbreakValue, dataSource, hazardTypes, suspectedDiseases } = options; return this.getAlertTrackedEntity(alert).flatMap(alertTrackedEntity => { const verificationStatus = getAlertValueFromMap( @@ -42,7 +38,7 @@ export class AlertSyncDataStoreRepository implements AlertSyncRepository { if (verificationStatus === VerificationStatus.RTSL_ZEB_AL_OS_VERIFICATION_VERIFIED) { const outbreakKey = getOutbreakKey({ dataSource: dataSource, - outbreakValue: suspectedDiseaseCode || hazardTypeCode, + outbreakValue: outbreakValue, hazardTypes: hazardTypes, suspectedDiseases: suspectedDiseases, }); @@ -72,7 +68,7 @@ export class AlertSyncDataStoreRepository implements AlertSyncRepository { }); } - public getAlertTrackedEntity(alert: Alert): FutureData { + private getAlertTrackedEntity(alert: Alert): FutureData { return apiToFuture( this.api.tracker.trackedEntities.get({ program: RTSL_ZEBRA_ALERTS_PROGRAM_ID, @@ -104,7 +100,7 @@ export class AlertSyncDataStoreRepository implements AlertSyncRepository { trackedEntity: D2TrackerTrackedEntity, outbreakKey: string ): AlertSynchronizationData { - const { alert, nationalDiseaseOutbreakEventId, dataSource } = options; + const { alert, dataSource, nationalDiseaseOutbreakEventId } = options; const outbreakType = dataSource === DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS ? "disease" : "hazard"; diff --git a/src/data/repositories/DiseaseOutbreakEventD2Repository.ts b/src/data/repositories/DiseaseOutbreakEventD2Repository.ts index ebb477d8..dc780882 100644 --- a/src/data/repositories/DiseaseOutbreakEventD2Repository.ts +++ b/src/data/repositories/DiseaseOutbreakEventD2Repository.ts @@ -13,6 +13,7 @@ import { getProgramTEAsMetadata } from "./utils/MetadataHelper"; import { assertOrError } from "./utils/AssertOrError"; import { Future } from "../../domain/entities/generic/Future"; import { getAllTrackedEntitiesAsync } from "./utils/getAllTrackedEntities"; +import { OutbreakData } from "../../domain/entities/alert/AlertData"; export class DiseaseOutbreakEventD2Repository implements DiseaseOutbreakEventRepository { constructor(private api: D2Api) {} @@ -42,6 +43,23 @@ export class DiseaseOutbreakEventD2Repository implements DiseaseOutbreakEventRep }); } + getEventByDiseaseOrHazardType( + filter: OutbreakData + ): FutureData { + return Future.fromPromise( + getAllTrackedEntitiesAsync( + this.api, + RTSL_ZEBRA_PROGRAM_ID, + RTSL_ZEBRA_ORG_UNIT_ID, + filter + ) + ).map(trackedEntities => { + return trackedEntities.map(trackedEntity => { + return mapTrackedEntityAttributesToDiseaseOutbreak(trackedEntity); + }); + }); + } + save(diseaseOutbreak: DiseaseOutbreakEventBaseAttrs): FutureData { return getProgramTEAsMetadata(this.api, RTSL_ZEBRA_PROGRAM_ID).flatMap( teasMetadataResponse => { diff --git a/src/data/repositories/NotificationD2Repository.ts b/src/data/repositories/NotificationD2Repository.ts index c63d4429..ed33aea6 100644 --- a/src/data/repositories/NotificationD2Repository.ts +++ b/src/data/repositories/NotificationD2Repository.ts @@ -1,17 +1,43 @@ import { D2Api } from "@eyeseetea/d2-api/2.36"; import { - Notification, + NotificationOptions, NotificationRepository, } from "../../domain/repositories/NotificationRepository"; import { apiToFuture, FutureData } from "../api-futures"; import { Future } from "../../domain/entities/generic/Future"; - +import { UserGroup } from "../../domain/entities/UserGroup"; +import { AlertData } from "../../domain/entities/alert/AlertData"; export class NotificationD2Repository implements NotificationRepository { constructor(private api: D2Api) {} - save(notification: Notification): FutureData { - return apiToFuture(this.api.messageConversations.post(notification)).flatMap(() => - Future.success(undefined) - ); + notifyNationalWatchStaff( + alertData: AlertData, + outbreakName: string, + userGroups: UserGroup[] + ): FutureData { + const { notificationOptions } = alertData; + + return apiToFuture( + this.api.messageConversations.post({ + subject: `New Outbreak Alert: ${outbreakName} in zm Zambia Ministry of Health`, + text: buildNotificationText(outbreakName, notificationOptions), + userGroups: userGroups, + }) + ).flatMap(() => Future.success(undefined)); } } + +function buildNotificationText(outbreakKey: string, notificationData: NotificationOptions): string { + const { detectionDate, emergenceDate, incidentManager, notificationDate, verificationStatus } = + notificationData; + + return `There has been a new Outbreak detected for ${outbreakKey} in zm Zambia Ministry of Health. + +Please see the details of the outbreak below: + +Emergence date: ${emergenceDate} +Detection Date : ${detectionDate} +Notification Date : ${notificationDate} +Incident Manager : ${incidentManager} +Verification Status : ${verificationStatus}`; +} diff --git a/src/data/repositories/UserGroupD2Repository.ts b/src/data/repositories/UserGroupD2Repository.ts new file mode 100644 index 00000000..4f6caa35 --- /dev/null +++ b/src/data/repositories/UserGroupD2Repository.ts @@ -0,0 +1,26 @@ +import { D2Api } from "@eyeseetea/d2-api/2.36"; +import { UserGroupRepository } from "../../domain/repositories/UserGroupRepository"; +import { apiToFuture, FutureData } from "../api-futures"; +import { assertOrError } from "./utils/AssertOrError"; +import { UserGroup } from "../../domain/entities/UserGroup"; + +export class UserGroupD2Repository implements UserGroupRepository { + constructor(private api: D2Api) {} + + getUserGroupByCode(code: string): FutureData { + return apiToFuture( + this.api.metadata.get({ + userGroups: { + fields: { + id: true, + }, + filter: { + code: { eq: code }, + }, + }, + }) + ) + .flatMap(response => assertOrError(response.userGroups[0], `User group ${code}`)) + .map(userGroup => userGroup); + } +} diff --git a/src/data/repositories/test/DiseaseOutbreakEventTestRepository.ts b/src/data/repositories/test/DiseaseOutbreakEventTestRepository.ts index aa12ba4a..ea322509 100644 --- a/src/data/repositories/test/DiseaseOutbreakEventTestRepository.ts +++ b/src/data/repositories/test/DiseaseOutbreakEventTestRepository.ts @@ -1,3 +1,4 @@ +import { OutbreakData } from "../../../domain/entities/alert/AlertData"; import { DataSource, DiseaseOutbreakEvent, @@ -106,6 +107,72 @@ export class DiseaseOutbreakEventTestRepository implements DiseaseOutbreakEventR }, ]); } + getEventByDiseaseOrHazardType( + _filter: OutbreakData + ): FutureData { + return Future.success([ + { + id: "1", + name: "Disease Outbreak 1", + dataSource: DataSource.RTSL_ZEB_OS_DATA_SOURCE_EBS, + created: new Date(), + lastUpdated: new Date(), + createdByName: "createdByName", + hazardType: "Biological:Animal", + mainSyndromeCode: undefined, + suspectedDiseaseCode: undefined, + notificationSourceCode: "1", + areasAffectedDistrictIds: [], + areasAffectedProvinceIds: [], + incidentStatus: IncidentStatus.RTSL_ZEB_OS_INCIDENT_STATUS_WATCH, + emerged: { date: new Date(), narrative: "emerged" }, + detected: { date: new Date(), narrative: "detected" }, + notified: { date: new Date(), narrative: "notified" }, + earlyResponseActions: { + initiateInvestigation: new Date(), + conductEpidemiologicalAnalysis: new Date(), + laboratoryConfirmation: { date: new Date(), na: false }, + appropriateCaseManagement: { date: new Date(), na: false }, + initiatePublicHealthCounterMeasures: { date: new Date(), na: false }, + initiateRiskCommunication: { date: new Date(), na: false }, + establishCoordination: new Date(), + responseNarrative: "responseNarrative", + }, + incidentManagerName: "incidentManager", + notes: undefined, + }, + { + id: "2", + name: "Disease Outbreak 2", + dataSource: DataSource.RTSL_ZEB_OS_DATA_SOURCE_EBS, + created: new Date(), + lastUpdated: new Date(), + createdByName: "createdByName2", + hazardType: "Biological:Human", + mainSyndromeCode: "2", + suspectedDiseaseCode: undefined, + notificationSourceCode: "2", + areasAffectedDistrictIds: [], + areasAffectedProvinceIds: [], + incidentStatus: IncidentStatus.RTSL_ZEB_OS_INCIDENT_STATUS_WATCH, + emerged: { date: new Date(), narrative: "emerged" }, + detected: { date: new Date(), narrative: "detected" }, + notified: { date: new Date(), narrative: "notified" }, + earlyResponseActions: { + initiateInvestigation: new Date(), + conductEpidemiologicalAnalysis: new Date(), + laboratoryConfirmation: { date: new Date(), na: false }, + appropriateCaseManagement: { date: new Date(), na: false }, + initiatePublicHealthCounterMeasures: { date: new Date(), na: false }, + initiateRiskCommunication: { date: new Date(), na: false }, + establishCoordination: new Date(), + responseNarrative: "responseNarrative", + }, + incidentManagerName: "incidentManager", + notes: undefined, + }, + ]); + } save(_diseaseOutbreak: DiseaseOutbreakEvent): FutureData { return Future.success(""); } diff --git a/src/data/repositories/utils/AlertOutbreakMapper.ts b/src/data/repositories/utils/AlertOutbreakMapper.ts index 0ee08d1b..c666c700 100644 --- a/src/data/repositories/utils/AlertOutbreakMapper.ts +++ b/src/data/repositories/utils/AlertOutbreakMapper.ts @@ -1,47 +1,24 @@ import { D2TrackerTrackedEntity } from "@eyeseetea/d2-api/api/trackerTrackedEntities"; -import { DataSource } from "../../../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent"; -import { AlertOptions } from "../../../domain/repositories/AlertRepository"; -import { Maybe } from "../../../utils/ts-utils"; -import { Option } from "../../../domain/entities/Ref"; import { alertOutbreakCodes } from "../consts/AlertConstants"; import { getValueFromMap } from "./DiseaseOutbreakMapper"; -import { - dataSourceMap, - diseaseOutbreakCodes, - incidentStatusMap, -} from "../consts/DiseaseOutbreakConstants"; +import { NotificationOptions } from "../../../domain/repositories/NotificationRepository"; -export function mapTrackedEntityAttributesToAlertOptions( - nationalTrackedEntity: D2TrackerTrackedEntity, - alertTrackedEntity: D2TrackerTrackedEntity -): AlertOptions { - if (!nationalTrackedEntity.trackedEntity) throw new Error("Tracked entity not found"); - - const fromDiseaseOutbreakMap = ( - key: keyof typeof diseaseOutbreakCodes, - trackedEntity: D2TrackerTrackedEntity - ) => getValueFromMap(key, trackedEntity); - - const fromAlertOutbreakMap = ( - key: keyof typeof alertOutbreakCodes, - trackedEntity: D2TrackerTrackedEntity - ) => getAlertValueFromMap(key, trackedEntity); - - const dataSource = dataSourceMap[fromDiseaseOutbreakMap("dataSource", nationalTrackedEntity)]; - const incidentStatus = - incidentStatusMap[fromDiseaseOutbreakMap("incidentStatus", nationalTrackedEntity)]; - - if (!dataSource || !incidentStatus) throw new Error("Data source or incident status not valid"); - - const diseaseOutbreak: AlertOptions = { - eventId: nationalTrackedEntity.trackedEntity, - dataSource: dataSource, - hazardTypeCode: fromAlertOutbreakMap("hazardType", alertTrackedEntity), - suspectedDiseaseCode: fromAlertOutbreakMap("suspectedDisease", alertTrackedEntity), - incidentStatus: incidentStatus, +export function mapTrackedEntityAttributesToNotificationOptions( + trackedEntity: D2TrackerTrackedEntity +): NotificationOptions { + const verificationStatus = getAlertValueFromMap("verificationStatus", trackedEntity); + const incidentManager = getAlertValueFromMap("incidentManager", trackedEntity); + const emergenceDate = getValueFromMap("emergedDate", trackedEntity); + const detectionDate = getValueFromMap("detectedDate", trackedEntity); + const notificationDate = getValueFromMap("notifiedDate", trackedEntity); + + return { + detectionDate: detectionDate, + emergenceDate: emergenceDate, + incidentManager: incidentManager, + notificationDate: notificationDate, + verificationStatus: verificationStatus, }; - - return diseaseOutbreak; } export function getAlertValueFromMap( @@ -53,24 +30,3 @@ export function getAlertValueFromMap( ?.value ?? "" ); } - -export function getOutbreakKey(options: { - dataSource: DataSource; - outbreakValue: Maybe; - hazardTypes: Option[]; - suspectedDiseases: Option[]; -}): string { - const { dataSource, outbreakValue, hazardTypes, suspectedDiseases } = options; - - const diseaseName = suspectedDiseases.find(disease => disease.id === outbreakValue)?.name; - const hazardName = hazardTypes.find(hazardType => hazardType.id === outbreakValue)?.name; - - if (!diseaseName && !hazardName) throw new Error(`Outbreak not found for ${outbreakValue}`); - - switch (dataSource) { - case DataSource.RTSL_ZEB_OS_DATA_SOURCE_EBS: - return hazardName ?? ""; - case DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS: - return diseaseName ?? ""; - } -} diff --git a/src/data/repositories/utils/MetadataHelper.ts b/src/data/repositories/utils/MetadataHelper.ts index 79a6c45f..d5ffef34 100644 --- a/src/data/repositories/utils/MetadataHelper.ts +++ b/src/data/repositories/utils/MetadataHelper.ts @@ -1,8 +1,7 @@ import { D2TrackerTrackedEntity } from "@eyeseetea/d2-api/api/trackerTrackedEntities"; -import { Id, Ref } from "../../../domain/entities/Ref"; +import { Id } from "../../../domain/entities/Ref"; import { D2Api } from "../../../types/d2-api"; -import { apiToFuture, FutureData } from "../../api-futures"; -import { assertOrError } from "./AssertOrError"; +import { apiToFuture } from "../../api-futures"; import { Attribute } from "@eyeseetea/d2-api/api/trackedEntityInstances"; import { Maybe } from "../../../utils/ts-utils"; @@ -26,23 +25,6 @@ export function getProgramTEAsMetadata(api: D2Api, programId: string) { ); } -export function getUserGroupByCode(api: D2Api, code: string): FutureData { - return apiToFuture( - api.metadata.get({ - userGroups: { - fields: { - id: true, - }, - filter: { - code: { eq: code }, - }, - }, - }) - ) - .flatMap(response => assertOrError(response.userGroups[0], `User group ${code}`)) - .map(userGroup => userGroup); -} - export function getTEAttributeById( trackedEntity: D2TrackerTrackedEntity, attributeId: Id diff --git a/src/data/repositories/utils/NotificationMapper.ts b/src/data/repositories/utils/NotificationMapper.ts deleted file mode 100644 index 54a03bd2..00000000 --- a/src/data/repositories/utils/NotificationMapper.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { D2TrackerTrackedEntity } from "@eyeseetea/d2-api/api/trackerTrackedEntities"; -import { getAlertValueFromMap } from "./AlertOutbreakMapper"; -import { NotificationOptions } from "../../../domain/repositories/NotificationRepository"; -import { getValueFromMap } from "./DiseaseOutbreakMapper"; - -export function getNotificationOptionsFromTrackedEntity( - alertTrackedEntity: D2TrackerTrackedEntity -): NotificationOptions { - const verificationStatus = getAlertValueFromMap("verificationStatus", alertTrackedEntity); - const incidentManager = getAlertValueFromMap("incidentManager", alertTrackedEntity); - const emergenceDate = getValueFromMap("emergedDate", alertTrackedEntity); - const detectionDate = getValueFromMap("detectedDate", alertTrackedEntity); - const notificationDate = getValueFromMap("notifiedDate", alertTrackedEntity); - - return { - detectionDate: detectionDate, - emergenceDate: emergenceDate, - incidentManager: incidentManager, - notificationDate: notificationDate, - verificationStatus: verificationStatus, - }; -} diff --git a/src/data/repositories/utils/getAllTrackedEntities.ts b/src/data/repositories/utils/getAllTrackedEntities.ts index 31e26798..503f40fb 100644 --- a/src/data/repositories/utils/getAllTrackedEntities.ts +++ b/src/data/repositories/utils/getAllTrackedEntities.ts @@ -4,11 +4,13 @@ import { TrackedEntitiesGetResponse, } from "@eyeseetea/d2-api/api/trackerTrackedEntities"; import { Id } from "../../../domain/entities/Ref"; +import { OutbreakData } from "../../../domain/entities/alert/AlertData"; export async function getAllTrackedEntitiesAsync( api: D2Api, programId: Id, - orgUnitId: Id + orgUnitId: Id, + filter?: OutbreakData ): Promise { const d2TrackerTrackedEntities: D2TrackerTrackedEntity[] = []; @@ -25,12 +27,8 @@ export async function getAllTrackedEntitiesAsync( totalPages: true, page: page, pageSize: pageSize, - fields: { - attributes: true, - orgUnit: true, - trackedEntity: true, - trackedEntityType: true, - }, + fields: fields, + filter: filter ? `${filter.id}:eq:${filter.value}` : undefined, }) .getData(); @@ -43,3 +41,20 @@ export async function getAllTrackedEntitiesAsync( return []; } } + +const fields = { + attributes: true, + orgUnit: true, + trackedEntity: true, + trackedEntityType: true, + enrollments: { + events: { + createdAt: true, + dataValues: { + dataElement: true, + value: true, + }, + event: true, + }, + }, +}; diff --git a/src/domain/entities/UserGroup.ts b/src/domain/entities/UserGroup.ts new file mode 100644 index 00000000..4049db80 --- /dev/null +++ b/src/domain/entities/UserGroup.ts @@ -0,0 +1,3 @@ +import { Ref } from "./Ref"; + +export type UserGroup = Ref; diff --git a/src/domain/entities/alert/AlertData.ts b/src/domain/entities/alert/AlertData.ts index dade4a44..0a0660bd 100644 --- a/src/domain/entities/alert/AlertData.ts +++ b/src/domain/entities/alert/AlertData.ts @@ -1,30 +1,17 @@ import { Maybe } from "../../../utils/ts-utils"; +import { NotificationOptions } from "../../repositories/NotificationRepository"; import { DataSource } from "../disease-outbreak-event/DiseaseOutbreakEvent"; import { Id } from "../Ref"; import { Alert } from "./Alert"; +export type OutbreakData = { + id: Id; // disease or hazard + value: Maybe; // disease or hazard code +}; + export type AlertData = { alert: Alert; dataSource: DataSource; - outbreakData: { - id: string; - value: string; - }; -}; - -export type AlertSynchronizationData = { - lastSyncTime: string; - type: string; - nationalDiseaseOutbreakEventId: Id; - alerts: { - alertId: string; - eventDate: Maybe; - orgUnit: Maybe; - suspectedCases: string; - probableCases: string; - confirmedCases: string; - deaths: string; - }[]; -} & { - [key in "disease" | "hazard"]?: string; + outbreakData: OutbreakData; + notificationOptions: NotificationOptions; }; diff --git a/src/domain/entities/alert/AlertSynchronizationData.ts b/src/domain/entities/alert/AlertSynchronizationData.ts new file mode 100644 index 00000000..04b947f3 --- /dev/null +++ b/src/domain/entities/alert/AlertSynchronizationData.ts @@ -0,0 +1,41 @@ +import { Maybe } from "../../../utils/ts-utils"; +import { DataSource } from "../disease-outbreak-event/DiseaseOutbreakEvent"; +import { Id, Option } from "../Ref"; + +export type AlertSynchronizationData = { + lastSyncTime: string; + type: string; + nationalDiseaseOutbreakEventId: Id; + alerts: { + alertId: string; + eventDate: Maybe; + orgUnit: Maybe; + suspectedCases: string; + probableCases: string; + confirmedCases: string; + deaths: string; + }[]; +} & { + [key in "disease" | "hazard"]?: string; +}; + +export function getOutbreakKey(options: { + dataSource: DataSource; + outbreakValue: Maybe; + hazardTypes: Option[]; + suspectedDiseases: Option[]; +}): string { + const { dataSource, outbreakValue, hazardTypes, suspectedDiseases } = options; + + const diseaseName = suspectedDiseases.find(disease => disease.id === outbreakValue)?.name; + const hazardName = hazardTypes.find(hazardType => hazardType.id === outbreakValue)?.name; + + if (!diseaseName && !hazardName) throw new Error(`Outbreak not found for ${outbreakValue}`); + + switch (dataSource) { + case DataSource.RTSL_ZEB_OS_DATA_SOURCE_EBS: + return hazardName ?? ""; + case DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS: + return diseaseName ?? ""; + } +} diff --git a/src/domain/repositories/AlertDataRepository.ts b/src/domain/repositories/AlertDataRepository.ts new file mode 100644 index 00000000..b9ea96c9 --- /dev/null +++ b/src/domain/repositories/AlertDataRepository.ts @@ -0,0 +1,6 @@ +import { FutureData } from "../../data/api-futures"; +import { AlertData } from "../entities/alert/AlertData"; + +export interface AlertDataRepository { + get(): FutureData; +} diff --git a/src/domain/repositories/AlertRepository.ts b/src/domain/repositories/AlertRepository.ts index c0805e88..c1d136c8 100644 --- a/src/domain/repositories/AlertRepository.ts +++ b/src/domain/repositories/AlertRepository.ts @@ -14,7 +14,6 @@ export interface AlertRepository { export type AlertOptions = { dataSource: DataSource; eventId: Id; - hazardTypeCode: Maybe; + outbreakValue: Maybe; incidentStatus: IncidentStatus; - suspectedDiseaseCode: Maybe; }; diff --git a/src/domain/repositories/AlertSyncRepository.ts b/src/domain/repositories/AlertSyncRepository.ts index 8148e54c..e2e5d544 100644 --- a/src/domain/repositories/AlertSyncRepository.ts +++ b/src/domain/repositories/AlertSyncRepository.ts @@ -1,8 +1,8 @@ import { FutureData } from "../../data/api-futures"; -import { DataSource } from "../entities/disease-outbreak-event/DiseaseOutbreakEvent"; -import { Id, Option } from "../entities/Ref"; import { Maybe } from "../../utils/ts-utils"; +import { Id, Option } from "../entities/Ref"; import { Alert } from "../entities/alert/Alert"; +import { DataSource } from "../entities/disease-outbreak-event/DiseaseOutbreakEvent"; export interface AlertSyncRepository { saveAlertSyncData(options: AlertSyncOptions): FutureData; @@ -11,9 +11,8 @@ export interface AlertSyncRepository { export type AlertSyncOptions = { alert: Alert; dataSource: DataSource; + outbreakValue: Maybe; nationalDiseaseOutbreakEventId: Id; - hazardTypeCode: Maybe; - suspectedDiseaseCode: Maybe; hazardTypes: Option[]; suspectedDiseases: Option[]; }; diff --git a/src/domain/repositories/DiseaseOutbreakEventRepository.ts b/src/domain/repositories/DiseaseOutbreakEventRepository.ts index 42d3d280..49a03697 100644 --- a/src/domain/repositories/DiseaseOutbreakEventRepository.ts +++ b/src/domain/repositories/DiseaseOutbreakEventRepository.ts @@ -1,10 +1,14 @@ import { FutureData } from "../../data/api-futures"; +import { AttributeFilter } from "../../data/repositories/AlertD2Repository"; import { DiseaseOutbreakEventBaseAttrs } from "../entities/disease-outbreak-event/DiseaseOutbreakEvent"; import { ConfigLabel, Id } from "../entities/Ref"; export interface DiseaseOutbreakEventRepository { get(id: Id): FutureData; getAll(): FutureData; + getEventByDiseaseOrHazardType( + filter: AttributeFilter + ): FutureData; save(diseaseOutbreak: DiseaseOutbreakEventBaseAttrs): FutureData; getConfigStrings(): FutureData; } diff --git a/src/domain/repositories/NotificationRepository.ts b/src/domain/repositories/NotificationRepository.ts index 8f36650f..468605cf 100644 --- a/src/domain/repositories/NotificationRepository.ts +++ b/src/domain/repositories/NotificationRepository.ts @@ -1,16 +1,15 @@ import { FutureData } from "../../data/api-futures"; -import { Ref } from "../entities/Ref"; +import { AlertData } from "../entities/alert/AlertData"; +import { UserGroup } from "../entities/UserGroup"; export interface NotificationRepository { - save(notification: Notification): FutureData; + notifyNationalWatchStaff( + alertData: AlertData, + outbreakName: string, + userGroups: UserGroup[] + ): FutureData; } -export type Notification = { - subject: string; - text: string; - userGroups: Ref[]; -}; - export type NotificationOptions = { detectionDate: string; emergenceDate: string; diff --git a/src/domain/repositories/UserGroupRepository.ts b/src/domain/repositories/UserGroupRepository.ts new file mode 100644 index 00000000..ac92a475 --- /dev/null +++ b/src/domain/repositories/UserGroupRepository.ts @@ -0,0 +1,6 @@ +import { FutureData } from "../../data/api-futures"; +import { UserGroup } from "../entities/UserGroup"; + +export interface UserGroupRepository { + getUserGroupByCode(code: string): FutureData; +} diff --git a/src/domain/usecases/MapDiseaseOutbreakToAlertsUseCase.ts b/src/domain/usecases/MapDiseaseOutbreakToAlertsUseCase.ts index b1aa5b3b..58efc332 100644 --- a/src/domain/usecases/MapDiseaseOutbreakToAlertsUseCase.ts +++ b/src/domain/usecases/MapDiseaseOutbreakToAlertsUseCase.ts @@ -37,9 +37,8 @@ export class MapDiseaseOutbreakToAlertsUseCase { .updateAlerts({ dataSource: dataSource, eventId: diseaseOutbreakEventId, - hazardTypeCode: hazardTypeCode, incidentStatus: incidentStatus, - suspectedDiseaseCode: suspectedDiseaseCode, + outbreakValue: hazardTypeCode ?? suspectedDiseaseCode, }) .flatMap((alerts: Alert[]) => Future.joinObj({ @@ -53,8 +52,7 @@ export class MapDiseaseOutbreakToAlertsUseCase { alert: alert, nationalDiseaseOutbreakEventId: diseaseOutbreakEventId, dataSource: dataSource, - hazardTypeCode: hazardTypeCode, - suspectedDiseaseCode: suspectedDiseaseCode, + outbreakValue: hazardTypeCode ?? suspectedDiseaseCode, hazardTypes: hazardTypes, suspectedDiseases: suspectedDiseases, }) diff --git a/src/domain/usecases/MappingScriptUseCase.ts b/src/domain/usecases/MappingScriptUseCase.ts new file mode 100644 index 00000000..f0228699 --- /dev/null +++ b/src/domain/usecases/MappingScriptUseCase.ts @@ -0,0 +1,214 @@ +import { Future } from "../entities/generic/Future"; +import { Option } from "../entities/Ref"; +import { AlertOptions, AlertRepository } from "../repositories/AlertRepository"; +import { AlertSyncRepository } from "../repositories/AlertSyncRepository"; +import { OptionsRepository } from "../repositories/OptionsRepository"; +import _ from "../entities/generic/Collection"; +import { + DataSource, + DiseaseOutbreakEventBaseAttrs, +} from "../entities/disease-outbreak-event/DiseaseOutbreakEvent"; +import { logger } from "../../utils/logger"; +import { NotificationRepository } from "../repositories/NotificationRepository"; +import { UserGroupRepository } from "../repositories/UserGroupRepository"; +import { AlertData, OutbreakData } from "../entities/alert/AlertData"; +import { AlertDataRepository } from "../repositories/AlertDataRepository"; +import { DiseaseOutbreakEventRepository } from "../repositories/DiseaseOutbreakEventRepository"; +import { getOutbreakKey } from "../entities/alert/AlertSynchronizationData"; + +export class MappingScriptUseCase { + constructor( + private alertRepository: AlertRepository, + private alertDataRepository: AlertDataRepository, + private alertSyncRepository: AlertSyncRepository, + private diseaseOutbreakEventRepository: DiseaseOutbreakEventRepository, + private notificationRepository: NotificationRepository, + private optionsRepository: OptionsRepository, + private userGroupRepository: UserGroupRepository + ) {} + + public async execute(): Promise { + const { hazardTypes, suspectedDiseases } = await this.getOptions(); + const alertData = await this.alertDataRepository.get().toPromise(); + + logger.info( + `${alertData.length} event(s) found in the Zebra Alerts program with no national event id.` + ); + + return _(alertData) + .groupBy(alert => alert.dataSource) + .values() + .forEach(alertsByDataSource => { + const uniqueFilters = getUniqueFilters(alertsByDataSource); + + return uniqueFilters.forEach(filter => { + this.getDiseaseOutbreakEvents({ + id: filter.filterId, + value: filter.filterValue, + }).then(diseaseOutbreakEvents => { + this.handleAlertOutbreakMapping( + diseaseOutbreakEvents, + filter, + alertsByDataSource, + hazardTypes, + suspectedDiseases + ); + }); + }); + }); + } + + private handleAlertOutbreakMapping( + diseaseOutbreakEvents: DiseaseOutbreakEventBaseAttrs[], + filter: { filterId: string; filterValue: string; dataSource: DataSource }, + alertsByDataSource: AlertData[], + hazardTypes: Option[], + suspectedDiseases: Option[] + ) { + if (diseaseOutbreakEvents.length > 1) { + const outbreakKey = getOutbreakKey({ + dataSource: filter.dataSource, + outbreakValue: filter.filterValue, + hazardTypes: hazardTypes, + suspectedDiseases: suspectedDiseases, + }); + + logger.error(`More than 1 National event found for ${outbreakKey} outbreak.`); + + return undefined; + } + + return alertsByDataSource + .filter(alertData => alertData.outbreakData.value === filter.filterValue) + .forEach(alertData => { + this.processAlertData( + alertData, + diseaseOutbreakEvents, + hazardTypes, + suspectedDiseases + ); + }); + } + + private getDiseaseOutbreakEvents( + filter: OutbreakData + ): Promise { + return this.diseaseOutbreakEventRepository + .getEventByDiseaseOrHazardType(filter) + .toPromise(); + } + + private getOptions(): Promise<{ hazardTypes: Option[]; suspectedDiseases: Option[] }> { + return Future.joinObj({ + hazardTypes: this.optionsRepository.getHazardTypesByCode(), + suspectedDiseases: this.optionsRepository.getSuspectedDiseases(), + }).toPromise(); + } + + private processAlertData( + alertData: AlertData, + diseaseOutbreakEvents: DiseaseOutbreakEventBaseAttrs[], + hazardTypes: Option[], + suspectedDiseases: Option[] + ): Promise { + const { dataSource, outbreakData } = alertData; + + const outbreakType = + dataSource === DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS ? "disease" : "hazard"; + const outbreakName = getOutbreakKey({ + dataSource: dataSource, + outbreakValue: outbreakData.value, + hazardTypes: hazardTypes, + suspectedDiseases: suspectedDiseases, + }); + + if (diseaseOutbreakEvents.length === 0) { + return this.notifyNationalWatchStaff(alertData, outbreakType, outbreakName); + } + + const diseaseOutbreakEvent = diseaseOutbreakEvents[0]; + if (!diseaseOutbreakEvent) + throw new Error( + `No disease outbreak event found for ${outbreakType} type ${outbreakName}.` + ); + + return this.updateAlertData({ + alertData: alertData, + diseaseOutbreakEvent: diseaseOutbreakEvent, + hazardTypes: hazardTypes, + suspectedDiseases: suspectedDiseases, + }); + } + + private async notifyNationalWatchStaff( + alertData: AlertData, + alertOutbreakType: string, + outbreakName: string + ): Promise { + logger.debug(`There is no national event with ${outbreakName} ${alertOutbreakType} type.`); + + const userGroup = await this.userGroupRepository + .getUserGroupByCode(RTSL_ZEBRA_NATIONAL_WATCH_STAFF_USER_GROUP_CODE) + .toPromise(); + + return this.notificationRepository + .notifyNationalWatchStaff(alertData, outbreakName, [userGroup]) + .toPromise() + .then(() => logger.success("Successfully notified all national watch staff.")); + } + + private async updateAlertData(options: { + alertData: AlertData; + diseaseOutbreakEvent: DiseaseOutbreakEventBaseAttrs; + hazardTypes: Option[]; + suspectedDiseases: Option[]; + }): Promise { + const { alertData, diseaseOutbreakEvent, hazardTypes, suspectedDiseases } = options; + + const alertOptions: AlertOptions = { + eventId: diseaseOutbreakEvent.id, + dataSource: diseaseOutbreakEvent.dataSource, + outbreakValue: alertData.outbreakData.value, + incidentStatus: diseaseOutbreakEvent.incidentStatus, + }; + + await this.alertRepository + .updateAlerts(alertOptions) + .toPromise() + .then(() => logger.success("Successfully updated alert.")); + + return this.alertSyncRepository + .saveAlertSyncData({ + alert: alertData.alert, + dataSource: alertOptions.dataSource, + outbreakValue: alertOptions.outbreakValue, + nationalDiseaseOutbreakEventId: alertOptions.eventId, + hazardTypes: hazardTypes, + suspectedDiseases: suspectedDiseases, + }) + .toPromise() + .then(() => logger.success("Successfully saved alert sync data.")); + } +} + +function getUniqueFilters(alerts: AlertData[]): { + filterId: string; + filterValue: string; + dataSource: DataSource; +}[] { + return _(alerts) + .uniqBy(filter => filter.outbreakData.value) + .map(filter => ({ + filterId: + filter.dataSource === DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS + ? RTSL_ZEBRA_DISEASE_TEA_ID + : RTSL_ZEBRA_HAZARD_TEA_ID, + filterValue: filter.outbreakData.value ?? "", + dataSource: filter.dataSource, + })) + .value(); +} + +const RTSL_ZEBRA_DISEASE_TEA_ID = "jLvbkuvPdZ6"; +const RTSL_ZEBRA_HAZARD_TEA_ID = "Dzrw3Tf0ukB"; +const RTSL_ZEBRA_NATIONAL_WATCH_STAFF_USER_GROUP_CODE = "RTSL_ZEBRA_NATONAL_WATCH_STAFF"; diff --git a/src/domain/usecases/NotifyWatchStaffUseCase.ts b/src/domain/usecases/NotifyWatchStaffUseCase.ts deleted file mode 100644 index 0eb47c9b..00000000 --- a/src/domain/usecases/NotifyWatchStaffUseCase.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Ref } from "../entities/Ref"; -import { - NotificationOptions, - NotificationRepository, -} from "../repositories/NotificationRepository"; -import { FutureData } from "../../data/api-futures"; - -export class NotifyWatchStaffUseCase { - constructor(private notificationRepository: NotificationRepository) {} - - public execute( - outbreakKey: string, - notificationData: NotificationOptions, - userGroups: Ref[] - ): FutureData { - return this.notificationRepository.save({ - subject: `New Outbreak Alert: ${outbreakKey} in zm Zambia Ministry of Health`, - text: buildNotificationText(outbreakKey, notificationData), - userGroups: userGroups, - }); - } -} - -function buildNotificationText(outbreakKey: string, notificationData: NotificationOptions): string { - const { detectionDate, emergenceDate, incidentManager, notificationDate, verificationStatus } = - notificationData; - - return `There has been a new Outbreak detected for ${outbreakKey} in zm Zambia Ministry of Health. - -Please see the details of the outbreak below: - -Emergence date: ${emergenceDate} -Detection Date : ${detectionDate} -Notification Date : ${notificationDate} -Incident Manager : ${incidentManager} -Verification Status : ${verificationStatus}`; -} diff --git a/src/scripts/common.ts b/src/scripts/common.ts index bce39725..3792b69b 100644 --- a/src/scripts/common.ts +++ b/src/scripts/common.ts @@ -21,3 +21,31 @@ export function getInstance(args: D2ApiArgs): Instance { const instance = new Instance({ url: args.url, ...args.auth }); return instance; } + +export function getApiInstanceFromEnvVariables() { + if (!process.env.VITE_DHIS2_BASE_URL) + throw new Error("VITE_DHIS2_BASE_URL must be set in the .env file"); + + if (!process.env.VITE_DHIS2_AUTH) + throw new Error("VITE_DHIS2_AUTH must be set in the .env file"); + + const username = process.env.VITE_DHIS2_AUTH.split(":")[0] ?? ""; + const password = process.env.VITE_DHIS2_AUTH.split(":")[1] ?? ""; + + if (username === "" || password === "") { + throw new Error("VITE_DHIS2_AUTH must be in the format 'username:password'"); + } + + const envVars = { + url: process.env.VITE_DHIS2_BASE_URL, + auth: { + username: username, + password: password, + }, + }; + + const api = getD2ApiFromArgs(envVars); + const instance = getInstance(envVars); + + return { api: api, instance: instance }; +} diff --git a/src/scripts/mapDiseaseOutbreakToAlerts.ts b/src/scripts/mapDiseaseOutbreakToAlerts.ts index b3b2c4ef..5e22bcd0 100644 --- a/src/scripts/mapDiseaseOutbreakToAlerts.ts +++ b/src/scripts/mapDiseaseOutbreakToAlerts.ts @@ -1,38 +1,16 @@ import { boolean, command, flag, run } from "cmd-ts"; -import { setupLogger, logger } from "../utils/logger"; +import { setupLogger } from "../utils/logger"; import path from "path"; -import { getD2ApiFromArgs, getInstance } from "./common"; -import { - RTSL_ZEBRA_ALERTS_DISEASE_TEA_ID, - RTSL_ZEBRA_ALERTS_EVENT_TYPE_TEA_ID, - RTSL_ZEBRA_ALERTS_NATIONAL_DISEASE_OUTBREAK_EVENT_ID_TEA_ID, - RTSL_ZEBRA_ALERTS_PROGRAM_ID, - RTSL_ZEBRA_ORG_UNIT_ID, - RTSL_ZEBRA_PROGRAM_ID, -} from "../data/repositories/consts/DiseaseOutbreakConstants"; +import { getApiInstanceFromEnvVariables } from "./common"; import _ from "../domain/entities/generic/Collection"; import { AlertD2Repository } from "../data/repositories/AlertD2Repository"; import { NotificationD2Repository } from "../data/repositories/NotificationD2Repository"; import { OptionsD2Repository } from "../data/repositories/OptionsD2Repository"; -import { Future } from "../domain/entities/generic/Future"; -import { getTEAttributeById, getUserGroupByCode } from "../data/repositories/utils/MetadataHelper"; -import { NotifyWatchStaffUseCase } from "../domain/usecases/NotifyWatchStaffUseCase"; -import { - getOutbreakKey, - mapTrackedEntityAttributesToAlertOptions, -} from "../data/repositories/utils/AlertOutbreakMapper"; import { AlertSyncDataStoreRepository } from "../data/repositories/AlertSyncDataStoreRepository"; -import { getNotificationOptionsFromTrackedEntity } from "../data/repositories/utils/NotificationMapper"; -import { AlertData } from "../domain/entities/alert/AlertData"; -import { DataSource } from "../domain/entities/disease-outbreak-event/DiseaseOutbreakEvent"; -import { D2TrackerTrackedEntity } from "@eyeseetea/d2-api/api/trackerTrackedEntities"; -import { Alert } from "../domain/entities/alert/Alert"; -import { Option } from "../domain/entities/Ref"; - -//TO DO : Fetch from metadata on app load -const RTSL_ZEBRA_DISEASE_TEA_ID = "jLvbkuvPdZ6"; -const RTSL_ZEBRA_HAZARD_TEA_ID = "Dzrw3Tf0ukB"; -const RTSL_ZEBRA_NATIONAL_WATCH_STAFF_USER_GROUP_CODE = "RTSL_ZEBRA_NATONAL_WATCH_STAFF"; +import { UserGroupD2Repository } from "../data/repositories/UserGroupD2Repository"; +import { MappingScriptUseCase } from "../domain/usecases/MappingScriptUseCase"; +import { AlertDataD2Repository } from "../data/repositories/AlertDataD2Repository"; +import { DiseaseOutbreakEventD2Repository } from "../data/repositories/DiseaseOutbreakEventD2Repository"; function main() { const cmd = command({ @@ -47,277 +25,32 @@ function main() { }), }, handler: async args => { - if (!process.env.VITE_DHIS2_BASE_URL) - throw new Error("VITE_DHIS2_BASE_URL must be set in the .env file"); - - if (!process.env.VITE_DHIS2_AUTH) - throw new Error("VITE_DHIS2_AUTH must be set in the .env file"); - - const username = process.env.VITE_DHIS2_AUTH.split(":")[0] ?? ""; - const password = process.env.VITE_DHIS2_AUTH.split(":")[1] ?? ""; - - if (username === "" || password === "") { - throw new Error("VITE_DHIS2_AUTH must be in the format 'username:password'"); - } - - const envVars = { - url: process.env.VITE_DHIS2_BASE_URL, - auth: { - username: username, - password: password, - }, - }; - - const api = getD2ApiFromArgs(envVars); - const instance = getInstance(envVars); + const { api, instance } = getApiInstanceFromEnvVariables(); + await setupLogger(instance, { isDebug: args.debug }); const alertRepository = new AlertD2Repository(api); + const alertDataRepository = new AlertDataD2Repository(api); const alertSyncRepository = new AlertSyncDataStoreRepository(api); + const diseaseOutbreakEventRepository = new DiseaseOutbreakEventD2Repository(api); const notificationRepository = new NotificationD2Repository(api); const optionsRepository = new OptionsD2Repository(api); - const notifyWatchStaffUseCase = new NotifyWatchStaffUseCase(notificationRepository); - - await setupLogger(instance, { isDebug: args.debug }); - - const { hazardTypes, suspectedDiseases } = await getOptions(optionsRepository); - const alertTrackedEntities = await getAlertTrackedEntities(); - const alertsWithNoEventId = getAlertsWithNoNationalEventId(alertTrackedEntities); - logger.info( - `${alertsWithNoEventId.length} event(s) found in the Zebra Alerts program with no national event id.` + const userGroupRepository = new UserGroupD2Repository(api); + + const mappingScriptUseCase = new MappingScriptUseCase( + alertRepository, + alertDataRepository, + alertSyncRepository, + diseaseOutbreakEventRepository, + notificationRepository, + optionsRepository, + userGroupRepository ); - return _(alertsWithNoEventId) - .groupBy(alert => alert.dataSource) - .values() - .forEach(alertsByDataSource => { - const uniqueFilters = getUniqueFilters(alertsByDataSource); - - return uniqueFilters.forEach(filter => { - getNationalTrackedEntities(alertRepository, { - id: filter.filterId, - value: filter.filterValue, - }).then(nationalTrackedEntities => { - if (nationalTrackedEntities.length > 1) { - const outbreakKey = getOutbreakKey({ - dataSource: filter.dataSource, - outbreakValue: filter.filterValue, - hazardTypes: hazardTypes, - suspectedDiseases: suspectedDiseases, - }); - - logger.error( - `More than 1 National event found for ${outbreakKey} outbreak.` - ); - - return undefined; - } - - return alertsByDataSource - .filter( - alertData => alertData.outbreakData.value === filter.filterValue - ) - .forEach(alertData => { - const { alert, dataSource, outbreakData } = alertData; - - const outbreakType = - dataSource === DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS - ? "disease" - : "hazard"; - const outbreakName = getOutbreakKey({ - dataSource: dataSource, - outbreakValue: outbreakData.value, - hazardTypes: hazardTypes, - suspectedDiseases: suspectedDiseases, - }); - - alertSyncRepository - .getAlertTrackedEntity(alert) - .toPromise() - .then(alertTrackedEntity => { - if (nationalTrackedEntities.length === 0) { - return notifyNationalWatchStaff( - alertTrackedEntity, - outbreakType, - outbreakName - ); - } - - const nationalTrackedEntity = - nationalTrackedEntities[0]; - if (!nationalTrackedEntity) - throw new Error(`No tracked entity found.`); - - return updateAlertData({ - alert: alert, - alertTrackedEntity: alertTrackedEntity, - nationalTrackedEntity: alertTrackedEntity, - hazardTypes: hazardTypes, - suspectedDiseases: suspectedDiseases, - }); - }); - }); - }); - }); - }); - - async function getAlertTrackedEntities(): Promise { - return alertRepository.getTrackedEntitiesByTEACodeAsync({ - program: RTSL_ZEBRA_ALERTS_PROGRAM_ID, - orgUnit: RTSL_ZEBRA_ORG_UNIT_ID, - ouMode: "DESCENDANTS", - }); - } - - async function notifyNationalWatchStaff( - alertTrackedEntity: D2TrackerTrackedEntity, - alertOutbreakType: string, - outbreakName: string - ): Promise { - logger.debug( - `There is no national event with ${outbreakName} ${alertOutbreakType} type.` - ); - - const userGroup = await getUserGroupByCode( - api, - RTSL_ZEBRA_NATIONAL_WATCH_STAFF_USER_GROUP_CODE - ).toPromise(); - const notificationOptions = - getNotificationOptionsFromTrackedEntity(alertTrackedEntity); - - notifyWatchStaffUseCase - .execute(outbreakName, notificationOptions, [userGroup]) - .toPromise() - .then(() => logger.success("Successfully notified all national watch staff.")); - } - - async function updateAlertData(options: { - alert: Alert; - alertTrackedEntity: D2TrackerTrackedEntity; - nationalTrackedEntity: D2TrackerTrackedEntity; - hazardTypes: Option[]; - suspectedDiseases: Option[]; - }): Promise { - const { - alert, - alertTrackedEntity, - nationalTrackedEntity, - hazardTypes, - suspectedDiseases, - } = options; - - const alertOptions = mapTrackedEntityAttributesToAlertOptions( - nationalTrackedEntity, - alertTrackedEntity - ); - const { dataSource, eventId, hazardTypeCode, suspectedDiseaseCode } = alertOptions; - - await alertRepository - .updateAlerts(alertOptions) - .toPromise() - .then(() => logger.success("Successfully updated alert.")); - - return alertSyncRepository - .saveAlertSyncData({ - dataSource: dataSource, - hazardTypeCode: hazardTypeCode, - suspectedDiseaseCode: suspectedDiseaseCode, - nationalDiseaseOutbreakEventId: eventId, - alert: alert, - hazardTypes: hazardTypes, - suspectedDiseases: suspectedDiseases, - }) - .toPromise() - .then(() => logger.success("Successfully saved alert sync data.")); - } - - async function getNationalTrackedEntities( - alertRepository: AlertD2Repository, - filter: { - id: string; - value: string; - } - ): Promise { - return alertRepository.getTrackedEntitiesByTEACodeAsync({ - program: RTSL_ZEBRA_PROGRAM_ID, - orgUnit: RTSL_ZEBRA_ORG_UNIT_ID, - ouMode: "SELECTED", - filter: filter, - }); - } + return await mappingScriptUseCase.execute(); }, }); run(cmd, process.argv.slice(2)); } -function getOptions( - optionsRepository: OptionsD2Repository -): - | { hazardTypes: any; suspectedDiseases: any } - | PromiseLike<{ hazardTypes: any; suspectedDiseases: any }> { - return Future.joinObj({ - hazardTypes: optionsRepository.getHazardTypesByCode(), - suspectedDiseases: optionsRepository.getSuspectedDiseases(), - }).toPromise(); -} - -function getAlertsWithNoNationalEventId( - alertTrackedEntities: D2TrackerTrackedEntity[] -): AlertData[] { - return _(alertTrackedEntities) - .compactMap(trackedEntity => { - const nationalEventId = getTEAttributeById( - trackedEntity, - RTSL_ZEBRA_ALERTS_NATIONAL_DISEASE_OUTBREAK_EVENT_ID_TEA_ID - ); - const hazardType = getTEAttributeById( - trackedEntity, - RTSL_ZEBRA_ALERTS_EVENT_TYPE_TEA_ID - ); - const diseaseType = getTEAttributeById(trackedEntity, RTSL_ZEBRA_ALERTS_DISEASE_TEA_ID); - - const outbreakData = diseaseType - ? { id: diseaseType.attribute, value: diseaseType.value } - : hazardType - ? { id: hazardType.value, value: hazardType.value } - : undefined; - - if (!outbreakData) return undefined; - if (!trackedEntity.trackedEntity || !trackedEntity.orgUnit) - throw new Error("Tracked entity not found"); - - const alertData: AlertData = { - alert: { - id: trackedEntity.trackedEntity, - district: trackedEntity.orgUnit, - }, - outbreakData: outbreakData, - dataSource: diseaseType - ? DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS - : DataSource.RTSL_ZEB_OS_DATA_SOURCE_EBS, - }; - - return !nationalEventId && (hazardType || diseaseType) ? alertData : undefined; - }) - .value(); -} - -function getUniqueFilters(alerts: AlertData[]): { - filterId: string; - filterValue: string; - dataSource: DataSource; -}[] { - return _(alerts) - .uniqBy(filter => filter.outbreakData.value) - .map(filter => ({ - filterId: - filter.dataSource === DataSource.RTSL_ZEB_OS_DATA_SOURCE_IBS - ? RTSL_ZEBRA_DISEASE_TEA_ID - : RTSL_ZEBRA_HAZARD_TEA_ID, - filterValue: filter.outbreakData.value ?? "", - dataSource: filter.dataSource, - })) - .value(); -} - main();