Skip to content

Commit

Permalink
refactor: move script logic to a use case
Browse files Browse the repository at this point in the history
  • Loading branch information
deeonwuli committed Oct 9, 2024
1 parent 1122a06 commit c946d9e
Show file tree
Hide file tree
Showing 25 changed files with 700 additions and 511 deletions.
32 changes: 13 additions & 19 deletions src/data/repositories/AlertD2Repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;
};
import { OutbreakData } from "../../domain/entities/alert/AlertData";

export class AlertD2Repository implements AlertRepository {
constructor(private api: D2Api) {}

updateAlerts(alertOptions: AlertOptions): FutureData<Alert[]> {
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 || "",
Expand Down Expand Up @@ -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<D2TrackerTrackedEntity[]> {
const { program, orgUnit, ouMode, filter } = options;
const d2TrackerTrackedEntities: D2TrackerTrackedEntity[] = [];
Expand Down Expand Up @@ -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<D2TrackerTrackedEntity[]> {
return Future.fromPromise(this.getTrackedEntitiesByTEACodeAsync(options));
}

private getAlertFilter(
private getAlertOutbreakData(
dataSource: DataSource,
suspectedDiseaseCode: Maybe<string>,
hazardTypeCode: Maybe<string>
): Filter {
outbreakValue: Maybe<string>
): 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 };
}
}
}
151 changes: 151 additions & 0 deletions src/data/repositories/AlertDataD2Repository.ts
Original file line number Diff line number Diff line change
@@ -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<AlertData[]> {
return this.getAlertTrackedEntities().flatMap(alertTEIs => {
const alertsWithNoEventId = this.getAlertData(alertTEIs);

return alertsWithNoEventId;
});
}

private getAlertData(alertTrackedEntities: D2TrackerTrackedEntity[]): FutureData<AlertData[]> {
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<D2TrackerTrackedEntity[]> {
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<D2TrackerTrackedEntity[]> {
return Future.fromPromise(this.getTrackedEntitiesByTEACodeAsync(options));
}

private getAlertTrackedEntities(): FutureData<D2TrackerTrackedEntity[]> {
return this.getTrackedEntitiesByTEACode({
program: RTSL_ZEBRA_ALERTS_PROGRAM_ID,
orgUnit: RTSL_ZEBRA_ORG_UNIT_ID,
ouMode: "DESCENDANTS",
});
}
}
22 changes: 9 additions & 13 deletions src/data/repositories/AlertSyncDataStoreRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -24,14 +27,7 @@ export class AlertSyncDataStoreRepository implements AlertSyncRepository {
}

saveAlertSyncData(options: AlertSyncOptions): FutureData<void> {
const {
alert,
dataSource,
hazardTypeCode,
suspectedDiseaseCode,
hazardTypes,
suspectedDiseases,
} = options;
const { alert, outbreakValue, dataSource, hazardTypes, suspectedDiseases } = options;

return this.getAlertTrackedEntity(alert).flatMap(alertTrackedEntity => {
const verificationStatus = getAlertValueFromMap(
Expand All @@ -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,
});
Expand Down Expand Up @@ -72,7 +68,7 @@ export class AlertSyncDataStoreRepository implements AlertSyncRepository {
});
}

public getAlertTrackedEntity(alert: Alert): FutureData<D2TrackerTrackedEntity> {
private getAlertTrackedEntity(alert: Alert): FutureData<D2TrackerTrackedEntity> {
return apiToFuture(
this.api.tracker.trackedEntities.get({
program: RTSL_ZEBRA_ALERTS_PROGRAM_ID,
Expand Down Expand Up @@ -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";

Expand Down
18 changes: 18 additions & 0 deletions src/data/repositories/DiseaseOutbreakEventD2Repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {}
Expand Down Expand Up @@ -42,6 +43,23 @@ export class DiseaseOutbreakEventD2Repository implements DiseaseOutbreakEventRep
});
}

getEventByDiseaseOrHazardType(
filter: OutbreakData
): FutureData<DiseaseOutbreakEventBaseAttrs[]> {
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<Id> {
return getProgramTEAsMetadata(this.api, RTSL_ZEBRA_PROGRAM_ID).flatMap(
teasMetadataResponse => {
Expand Down
38 changes: 32 additions & 6 deletions src/data/repositories/NotificationD2Repository.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
return apiToFuture(this.api.messageConversations.post(notification)).flatMap(() =>
Future.success(undefined)
);
notifyNationalWatchStaff(
alertData: AlertData,
outbreakName: string,
userGroups: UserGroup[]
): FutureData<void> {
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}`;
}
26 changes: 26 additions & 0 deletions src/data/repositories/UserGroupD2Repository.ts
Original file line number Diff line number Diff line change
@@ -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<UserGroup> {
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);
}
}
Loading

0 comments on commit c946d9e

Please sign in to comment.